Skip to content

Commit

Permalink
[ci skip] Initial commit. Currently somewhat broken.
Browse files Browse the repository at this point in the history
  • Loading branch information
adam-thomas committed Oct 6, 2015
1 parent ca91e6c commit 75752c8
Show file tree
Hide file tree
Showing 19 changed files with 561 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[run]
source = model_logging

[report]
show_missing = True
13 changes: 13 additions & 0 deletions .travis.yml
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
12 changes: 12 additions & 0 deletions Makefile
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
Empty file added model_logging/__init__.py
Empty file.
32 changes: 32 additions & 0 deletions model_logging/migrations/0001_initial.py
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.
79 changes: 79 additions & 0 deletions model_logging/models.py
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',)
26 changes: 26 additions & 0 deletions model_logging/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import json

from rest_framework.serializers import (
CharField,
Field,
HyperlinkedModelSerializer,
)

from . import models


class JSONField(Field):
def to_representation(self, obj):
return json.loads(obj)


class LogEntrySerializer(HyperlinkedModelSerializer):
data = JSONField()
creator = CharField(source='creator.name')
operation_label = CharField(source='get_operation_display', read_only=True)

class Meta:
model = models.LogEntry
fields = ('url', 'date_created', 'creator', 'operation', 'operation_label', 'data')
read_only_fields = fields
view_name = 'medication-history-detail'
Empty file added model_logging/tests/__init__.py
Empty file.
24 changes: 24 additions & 0 deletions model_logging/tests/factories.py
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
49 changes: 49 additions & 0 deletions model_logging/tests/run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#! /usr/bin/env python
"""From http://stackoverflow.com/a/12260597/400691."""
import sys

import dj_database_url
import django
from colour_runner.django_runner import ColourRunnerMixin
from django.conf import settings
from django.test.runner import DiscoverRunner


settings.configure(
DATABASES={'default': dj_database_url.config(
default='postgres://localhost/model_logging',
)},
DEFAULT_FILE_STORAGE='inmemorystorage.InMemoryStorage',
MIDDLEWARE_CLASSES=(),
PASSWORD_HASHERS=('django.contrib.auth.hashers.MD5PasswordHasher',),
SITE_ID=1,

INSTALLED_APPS=(
'model_logging',

'django',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
),

# Dummy pgcrypto configuration
PGCRYPTO_KEY='so_secure_you_guys',
PUBLIC_PGP_KEY='as_is_this',
PRIVATE_PGP_KEY='ssssshhhh',
)


django.setup()


class TestRunner(ColourRunnerMixin, DiscoverRunner):
"""Enable coloured output for tests."""


test_runner = TestRunner(verbosity=1)
failures = test_runner.run_tests(['model_logging'])
if failures:
sys.exit(1)
64 changes: 64 additions & 0 deletions model_logging/tests/test_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import json

from django.test import TestCase

from . import factories
from model_logging import models


class TestLogEntry(TestCase):
model = models.LogEntry

def test_fields(self):
expected = [
'id',
'date_created',
'creator',
'creator_id',
'operation',
'data',
'user',
'user_id',
'model_path',
]

fields = self.model._meta.get_all_field_names()
self.assertCountEqual(fields, expected)


class TestLogEntryManager(TestCase):
manager = models.LogEntry.objects

def test_log(self):
"""Assert log() creates a LogEntry object correctly."""
user = factories.UserFactory.create()
data = {
'log_entry_creator': user,
'operation': models.LogEntry.OPERATION_ADDED,
'model': models.LogEntry,
'user': user,
'json_data': json.dumps({'item': 42}),
}

self.manager.log(**data)

entry = self.manager.get()
self.assertEqual(entry.creator, data['log_entry_creator'])
self.assertEqual(entry.operation, data['operation'])
self.assertEqual(entry.model_path, 'model_logging.models.LogEntry')
self.assertEqual(entry.user, data['user'])
self.assertEqual(entry.data, data['json_data'])

def test_retrieve_for_user(self):
entry, _ = factories.LogEntryFactory.create_batch(2)
model = models.LogEntry

retrieved = self.manager.retrieve_for_user(model=model, user=entry.user)
self.assertEqual(list(retrieved), [entry])

def test_retrieve_for_creator(self):
entry, _ = factories.LogEntryFactory.create_batch(2)
model = models.LogEntry

retrieved = self.manager.retrieve_for_creator(model=model, creator=entry.creator)
self.assertEqual(list(retrieved), [entry])
27 changes: 27 additions & 0 deletions model_logging/tests/test_serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from incuna_test_utils.testcases.api_request import BaseAPIRequestTestCase

from . import factories
from .utils import get_log_entry_data
from model_logging.serializers import LogEntrySerializer


class TestLogEntrySerializer(BaseAPIRequestTestCase):
user_factory = factories.UserFactory

@classmethod
def setUpTestData(cls):
cls.user = factories.UserFactory.create()
cls.entry = factories.LogEntryFactory.create(user=cls.user, creator=cls.user)

def setUp(self):
self.context = {'request': self.create_request(user=self.user)}

def test_serialize(self):
serializer = LogEntrySerializer(instance=self.entry, context=self.context)
expected = get_log_entry_data(self.entry)
self.assertEqual(serializer.data, expected)

def test_serialize_data_json(self):
"""Ensure `data` is decoded as `json`."""
serializer = LogEntrySerializer(instance=self.entry, context=self.context)
self.assertIsInstance(serializer.data['data'], dict)
Loading

0 comments on commit 75752c8

Please sign in to comment.