-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1 from incuna/it-begins
Add model_logging module and configuration
- Loading branch information
Showing
23 changed files
with
821 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
[run] | ||
source = model_logging | ||
|
||
[report] | ||
show_missing = True |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
language: python | ||
sudo: false | ||
python: | ||
- 3.4 | ||
cache: | ||
directories: | ||
- ~/.cache/pip | ||
script: | ||
make test | ||
notifications: | ||
email: false | ||
install: | ||
pip install -r requirements.txt |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
SHELL := /bin/bash | ||
|
||
help: | ||
@echo "Usage:" | ||
@echo " make release | Release to pypi." | ||
|
||
release: | ||
@python setup.py register sdist bdist_wheel upload | ||
|
||
test: | ||
@coverage run ./model_logging/tests/run.py | ||
@coverage report |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,101 @@ | ||
# django-model-logging | ||
|
||
Logging for changes made to Django model instances. | ||
|
||
## Installation | ||
|
||
`pip install django-model-logging` | ||
|
||
Add `model_logging` to your `INSTALLED_APPS`. | ||
|
||
This library uses [`django-pgcrypto-fields`](https://github.com/incuna/django-pgcrypto-fields), | ||
which means you will need PGP public and private keys. See the `settings.configure()` | ||
block in `model_logging/tests/run.py` for an (extremely insecure) example. | ||
|
||
## Usage | ||
|
||
#### Low-level use | ||
|
||
```python | ||
from model_logging.models import LogEntry | ||
|
||
LogEntry.objects.log( | ||
log_entry_creator, | ||
operation, | ||
model, | ||
user, | ||
json_data, | ||
) | ||
``` | ||
|
||
The parameters are as follows: | ||
|
||
* `log_entry_creator`: The user who made this change. | ||
* `operation`: One of `LogEntry.OPERATION_ADDED`, `LogEntry.OPERATION_REMOVED`, | ||
or `LogEntry.OPERATION_MODIFIED`. | ||
* `model`: The path to the model being logged (e.g. 'users.models.User'). | ||
* `user`: The user to which the model instance belongs. | ||
* `json_data`: A full or partial JSON representation of data on the model instance. | ||
|
||
#### Medium-level use | ||
|
||
To add methods to a view(set) that can be used to straightforwardly log changes: | ||
|
||
```python | ||
from model_logging.views import LoggingMethodMixin | ||
|
||
class AViewOrViewset(LoggingMethodMixin, ModelViewSet): | ||
def _get_logging_user(self): | ||
# Override this method to return a suitable | ||
# value for the `user` parameter above. | ||
return self.instance.user # or similar | ||
|
||
def extra_data(self): | ||
# Overriding this isn't mandatory, it's just a hook | ||
return {'any additional data': 'you wish to log'} | ||
``` | ||
|
||
The class now has access to the following: | ||
|
||
```python | ||
def log(self, operation, data): | ||
# A simplified version of LogEntry.objects.log, | ||
# with some parameters pre-filled. The return | ||
# value of extra_data() will be added to the | ||
# supplied data. | ||
|
||
def _log_on_create(self, serializer): | ||
# Log a LogEntry.OPERATION_ADDED change, using | ||
# the log() method above. | ||
|
||
def _log_on_update(self, serializer): | ||
# Log a LogEntry.OPERATION_MODIFIED change, using | ||
# the log() method above. | ||
|
||
def _log_on_destroy(self, instance): | ||
# Log a LogEntry.OPERATION_DELETED change, using | ||
# the log() method above. | ||
``` | ||
|
||
More abstract still: | ||
|
||
#### High-level use | ||
|
||
A viewset can log its own changes! | ||
|
||
```python | ||
from model_logging.views import LoggingViewSetMixin | ||
|
||
class AVeryShinyViewSet(LoggingViewSetMixin, ModelViewSet): | ||
def _get_logging_user(self): | ||
# Override this method to return a suitable | ||
# value for the `user` parameter above. | ||
return self.instance.user # or similar | ||
|
||
def extra_data(self): | ||
# Overriding this isn't mandatory, it's just a hook | ||
return {'any additional data': 'you wish to log'} | ||
``` | ||
|
||
This mixin is a wrapper around `LoggingMethodMixin` that calls the appropriate logging | ||
methods during `perform_create`, `perform_update` and `perform_destroy`. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
# Changelog | ||
|
||
### v0.1.0 | ||
|
||
* Add LogEntry model, together with a supporting serializer and view mixins. |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
# -*- coding: utf-8 -*- | ||
from __future__ import unicode_literals | ||
|
||
from django.db import models, migrations | ||
import pgcrypto.fields | ||
from django.conf import settings | ||
import django.utils.timezone | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
dependencies = [ | ||
migrations.swappable_dependency(settings.AUTH_USER_MODEL), | ||
] | ||
|
||
operations = [ | ||
migrations.CreateModel( | ||
name='LogEntry', | ||
fields=[ | ||
('id', models.AutoField(serialize=False, verbose_name='ID', auto_created=True, primary_key=True)), | ||
('date_created', models.DateTimeField(default=django.utils.timezone.now)), | ||
('operation', models.CharField(max_length=255, choices=[('added', 'Added'), ('removed', 'Removed'), ('modified', 'Modified')])), | ||
('model_path', models.CharField(max_length=255)), | ||
('data', pgcrypto.fields.TextPGPPublicKeyField(default='')), | ||
('creator', models.ForeignKey(null=True, related_name='log_entries_created', to=settings.AUTH_USER_MODEL)), | ||
('user', models.ForeignKey(null=True, related_name='log_entries', to=settings.AUTH_USER_MODEL)), | ||
], | ||
options={ | ||
'ordering': ('date_created',), | ||
}, | ||
), | ||
] |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
from django.conf import settings | ||
from django.db import models | ||
from django.utils import timezone | ||
from django.utils.translation import ugettext_lazy as _ | ||
from pgcrypto.fields import TextPGPPublicKeyField | ||
|
||
|
||
def get_model_path(model): | ||
"""Return the qualified path to a model class: 'model_logging.models.LogEntry'.""" | ||
return '{}.{}'.format(model.__module__, model.__qualname__) | ||
|
||
|
||
class LogEntryManager(models.Manager): | ||
def log(self, log_entry_creator, operation, model, user, json_data): | ||
""" | ||
Create a log entry representing: | ||
`log_entry_creator` has performed `operation` on an instance of `model` belonging | ||
to `user`. The instance's data is `json_data`. | ||
""" | ||
self.create( | ||
creator=log_entry_creator, | ||
operation=operation, | ||
model_path=get_model_path(model), | ||
user=user, | ||
data=json_data, | ||
) | ||
|
||
def retrieve_for_user(self, model, user): | ||
"""Return all log entries for this user on this model.""" | ||
return self.filter(user=user, model_path=get_model_path(model)) | ||
|
||
def retrieve_for_creator(self, model, creator): | ||
"""Return all log entries logged by this user on this model.""" | ||
return self.filter(creator=creator, model_path=get_model_path(model)) | ||
|
||
|
||
class LogEntry(models.Model): | ||
""" | ||
A change that has been made to an instance of another model. | ||
Stores: | ||
- date_created: The datetime that the change was made. | ||
- creator: Who made the change. | ||
- operation: 'Added', 'Removed' or 'Modified'. | ||
- model_path: The path to the model that's been operated-on. | ||
- data: JSON data representing the model after the operation, or just before it in | ||
the case of a 'Removed' operation. | ||
- user: The user whose model instance it is. | ||
""" | ||
OPERATION_ADDED = 'added' | ||
OPERATION_REMOVED = 'removed' | ||
OPERATION_MODIFIED = 'modified' | ||
OPERATION_CHOICES = ( | ||
(OPERATION_ADDED, _('Added')), | ||
(OPERATION_REMOVED, _('Removed')), | ||
(OPERATION_MODIFIED, _('Modified')), | ||
) | ||
|
||
date_created = models.DateTimeField(default=timezone.now) | ||
creator = models.ForeignKey( | ||
settings.AUTH_USER_MODEL, | ||
null=True, | ||
related_name='log_entries_created', | ||
) | ||
|
||
operation = models.CharField(choices=OPERATION_CHOICES, max_length=255) | ||
model_path = models.CharField(max_length=255) | ||
data = TextPGPPublicKeyField(default='') | ||
user = models.ForeignKey( | ||
settings.AUTH_USER_MODEL, | ||
null=True, | ||
related_name='log_entries', | ||
) | ||
|
||
objects = LogEntryManager() | ||
|
||
class Meta: | ||
ordering = ('date_created',) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import json | ||
|
||
from rest_framework.serializers import ( | ||
CharField, | ||
Field, | ||
ModelSerializer, | ||
) | ||
|
||
from . import models | ||
|
||
|
||
class JSONField(Field): | ||
def to_representation(self, obj): | ||
return json.loads(obj) | ||
|
||
|
||
class LogEntrySerializer(ModelSerializer): | ||
data = JSONField() | ||
creator = CharField(source='creator.email') | ||
operation_label = CharField(source='get_operation_display', read_only=True) | ||
|
||
class Meta: | ||
model = models.LogEntry | ||
fields = ( | ||
'date_created', 'creator', 'operation', 'operation_label', 'data', | ||
) | ||
read_only_fields = fields |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import json | ||
|
||
import factory | ||
from django.contrib.auth.models import User | ||
|
||
from model_logging import models | ||
|
||
|
||
class UserFactory(factory.DjangoModelFactory): | ||
username = factory.Sequence('User {}'.format) | ||
|
||
class Meta: | ||
model = User | ||
|
||
|
||
class LogEntryFactory(factory.DjangoModelFactory): | ||
user = factory.SubFactory(UserFactory) | ||
creator = factory.SubFactory(UserFactory) | ||
operation = models.LogEntry.OPERATION_ADDED | ||
model_path = models.get_model_path(models.LogEntry) | ||
data = json.dumps({'item': 42}) | ||
|
||
class Meta: | ||
model = models.LogEntry |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
-----BEGIN PGP PRIVATE KEY BLOCK----- | ||
Version: GnuPG v1 | ||
|
||
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 | ||
=kJd7 | ||
-----END PGP PRIVATE KEY BLOCK----- |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
-----BEGIN PGP PUBLIC KEY BLOCK----- | ||
Version: GnuPG v1 | ||
|
||
mQENBFRJDkwBCADVJDHy7al1P8urXWDkTSMfb6vGivD6vW0N2dDFixxKHDlOej52 | ||
OB2F/B/WLFpdXFiKxLSTIWEp/d0AEtJQrNMuMCzUKT08h3YU5ypiBdGIY7fqWwi9 | ||
G18ihm29/ygPni1XzG02DCto/pYD++RieIgGPOm8xlLrHwemehxhiOQ1K9eds8wd | ||
d0awXtiKdClaxGi24soyBxBC/tLde2Sgyh5y24RyXsmvZxORwWBM5tRZwOxwsQym | ||
Akt2M7LIGyQVgbLmTe4k5u3Uqv/t5tVK8OdDDbbXVDwJbj4n1Jol68cutHTAahpY | ||
mkOAuqLp7ZWqx/DsYYoJhv/G7dhcpS+CfNs3ABEBAAG0HlRlc3QgS2V5IDxleGFt | ||
cGxlQGV4YW1wbGUuY29tPokBOAQTAQIAIgUCVEkOTAIbAwYLCQgHAwIGFQgCCQoL | ||
BBYCAwECHgECF4AACgkQY3+K26BCfZUZPwgAzM03DFGATxldBcia63gK+vsEoDS+ | ||
xCtHaDpNvestHDlCd3bHU13LcLAXUD9E/s4LpHF+lMACCyscdeTriA/x0IJBIuq9 | ||
4KXMHJPpsh+eyGn09zhSfcfoMnQqgD682tvtBAD+oPb7Z/q+FNbKH26Pq6Daw1Ap | ||
gDEy9AMabBoGx8db878OFk6eNEZliF38QqRplqr/hsjRq4nwkAAT3qj1zufTTN90 | ||
bHhXt1BP7z5bDv0z1fp6duFzPbfyUEjkhbFKhOgJ6p01IOLnnXEn6TGM55sKqW1W | ||
dFX276anyb1gJm79E3gymnuwanC31QNSmlXpSNbiG8rbUfiJ40HfUf/lKrkBDQRU | ||
SQ5MAQgAnztdietX1+rTHqyjK9yDYP+rp5NBL2b98SzTjQpFv68cjevjYy/R7VqI | ||
rRx1qyrit470TYk9iOH2KBYscLLYylNCfpWfv2gIziZfX5Xxb+BxZPXkrq34ux5M | ||
1BHgGvZg8XvtJLSbX5SnkibUHSotCD578MgfWzfTfH4aeVwrZVCKb5BRxZHx0ZA9 | ||
q9tNgYWoadhcKNWmT8yn28JW/ME91v1K/nSDJKd8qG9qOEy0XtGcYOZh6qZZcXI3 | ||
BRpyqSUN4G2TvzccYeEbw6hHH2VjKCetkt9412+2Qg+IR8nTehwPuY3hOZqYghN/ | ||
rT6nuPJSzb/UJ+G6vnb2r0I4To3WUQARAQABiQEfBBgBAgAJBQJUSQ5MAhsMAAoJ | ||
EGN/itugQn2VlBcH/2dRT5yxfS9nTTimfk/wnyXnB+XgqbYOr7H1LFueMulCTSrQ | ||
sIebVUyIKY2Txzm8UxswMBxRzoIMy8g6NxjwuUzm3/w2evdHdO1mwqNMsDNBskPD | ||
CzJE4TLftOeVq7Jh268yLFiAxaAbULyBzr7yLVAHrGBDhjediCoy9KHijIHdM04v | ||
nhgQkV6UpLez7Am5B0Pqd+0kFcoe3IW5cYvphhWVJ4O+w95jujrsvVB/+hg4VPgi | ||
KMgIdXIAmJQuflAU97ilYDIXXmdQV2yUOsnaMW8JBsu2hsxvk3n+M9BNS6GBk40w | ||
HGCOLDrMD1U4Zjwsu58qQGp4nQ9rR54y5El2iJU= | ||
=c2SM | ||
-----END PGP PUBLIC KEY BLOCK----- |
Oops, something went wrong.