Commits

bogeymin committed fc300b6

Initial import.

Comments (0)

Files changed (10)

+# Backup files and directories.
+*.b
+*.db
+*.pyc
+*.swp
+.*.swp
+.backup
+b.*
+*~
+.b
+
+# Configuration data will be omitted so that it may be server specific. See
+# README.md for how to set up the config directory.
+config
+
+# Ignore the python directory in the example. But this allows for the example
+# to be tested.
+django
+python
+project
+Copyright (c) 2011-2012, F.S. Davis
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+    * Neither the name of the author nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+include LICENSE.txt
+include README.rst
+============
+Django Audit
+============
+
+A quick and simple app for providing standard audit fields *without*
+the typical inheritance.
+
+Backstory
+=========
+
+As of September 2012, it appears that there is still no clear standard for
+implementing audit data or trails in Django. I created this app because I
+needed an audit tool that provided the most common fields, but I did not want
+or need history or revision tracking.
+
+In addition to the basic audit fields, I also wanted a tool that did *not*
+require me to implement auditing in advance, as when inheriting fields from an
+abstract model. There were a couple of reasons for this:
+
+1. Auditing and audit fields, though common, is not always wanted or needed,
+   and even when audit fields are required, the choice of implementation should
+   rest with the developer.
+2. I often produce small and re-usable Django apps where I prefer to have few
+   or no dependencies, and adding audit fields not only adds a dependency, but
+   also violates the first principal above.
+
+Implementation
+==============
+
+Model Audit approaches auditing as simply as possible, but does take a slightly
+different approach than other packages.
+
+Models
+------
+
+Two, standard models are implemented:
+
+1. ``TimeAudit`` for added/modified timestamps.
+2. ``UserAudit`` for added/modified by user data.
+
+These models are independent of the rest of the data schema and are connected
+to Django's content types.
+
+API
+---
+
+There are two API functions that correspond to each of the models:
+
+1. ``time_audit`` for recording timestamps.
+   added/modified timestamps.
+2. ``user_audit`` for user data.
+
+Mixins
+------
+
+Finally, there are two mixins that provide easy, but optional access to the
+audit data.
+
+1. ``TimeAuditMixin`` gives access to ``added_date_time``,
+   ``modified_date_time`` and the ``TimeAudit`` instance via the
+   ``time_audit()`` method.
+2. ``UserAuditMixin`` gives access to ``added_by``, ``modified_by`` and the
+   ``UserAudit`` instance via the ``user_audit()`` method.
+
+Usage
+=====
+
+Minimal
+-------
+
+The simplest possible implementation involves using the ``time_audit`` and
+``user_audit`` API in your views, or in the ``save_model()`` method of your
+model admin::
+
+    from model_audit.api import time_audit, user_audit
+
+    class MyModelAdmin(admin.ModelAdmin):
+        def save_model(self, request, obj, form, change):
+            obj.save()
+            time_audit(request.user, obj)
+            user_audit(request.user, obj)
+
+
+Advanced
+--------
+
+The minimal implementation is easy and straight-forward, but what if you want
+to access an audit property directly from your model? The mixins allow you to
+do this::
+
+    from model_audit.models import UserAuditMixin, TimeAuditMixin
+    
+    class MyModel(UserAuditMixin, TimeAuditMixin):
+        # ...
+
+Then to access ``added_by`` simply call::
+
+    added_by = MyModelInstance.added_by
+
+Note: In this implementation, each call to a method will result in a database
+query, which may become a performance issue under heavy use. This may be
+reduced a little by calling the ``time_audit()`` or ``user_audit()`` methods,
+and *then* calling the property::
+
+    UserAuditInstance = MyModelInstance.user_audit()
+
+Even so, performance might still be a problem in a large system.
+
+Generic Relations
+-----------------
+
+One might also implement `generic relations`_ to access the related data.
+
+.. _generic relations: https://docs.djangoproject.com/en/dev/ref/contrib/contenttypes/#reverse-generic-relations
+
+Limitations
+===========
+
+Model Audit does come with some limitations.
+
+1. Performance: As mentioned under advanced usage above, slow performance is a
+   possibility for a system under heavy use with lots of audit data. In this
+   case, inheritance from an abstract class that provides the desired fields
+   becomes much more desirable and should be part of the planning and
+   engineering process.
+2. History: The purpose of the app is to provide audit fields, but *not*
+   historical data or revisions.
+3. Existing Records: If records already exist, the "added" audit data will be
+   incorrect. It is best to start a new app using Model Audit in the first
+   place or set this data in advance of Model Audit adoption.
+
+To-do
+=====
+
+A few ideas for improvement:
+
+- Admin access: Implement standard model admin for audit data. Everything would
+  be read-only?
+- Log output: Provide a view which generates log-like output. Perhaps with an
+  export option?
+- Unit tests: What meaningful testing could be implemented to insure flawless
+  operation?
+
+
+Other Audit Tools
+=================
+
+If the Model Audit app doesn't meet your needs, then these resources may be of
+some use:
+
+- Audit Trail: https://code.djangoproject.com/wiki/AuditTrail
+- Django Audit: https://github.com/KanbanSolutions/django-audit
+- Django Audit (NoSQL): https://launchpad.net/django-audit
+- Django Audit Log: https://github.com/Atomidata/django-audit-log
+- Django Packages: http://www.djangopackages.com/grids/g/model-audit/
+- SO Question: http://stackoverflow.com/questions/2007283/how-to-implement-django-model-audit-trail-how-do-you-access-logged-in-user-in-m

model_audit/__init__.py

+# Django Imports
+from django.conf import settings
+
+# Define dependencies as a list of dictionary items. The dictionary should
+# include a name and the module which it represents. The module should be
+# importable, while the name should be human-friendly.
+dependencies = (
+    {'name': 'Django Model Utils', 'module': 'model_utils'},
+)
+
+# Dependency errors are collected into a list for late consumption.
+errors = []
+
+# Check that the required applications are installed.
+for app in dependencies:
+    try:
+        __import__(app['module'])
+    except ImportError:
+        errors.append("The '%s' application is required, but cannot be imported." % app['name'])
+    finally:
+        if not app['module'] in settings.INSTALLED_APPS:
+            errors.append("The '%s' application is required, but is not in INSTALLED_APPS." % app['name'])
+

model_audit/api.py

+"""
+The Model Audit API provides a simple interface for adding audit data from your
+views or model admin.
+"""
+
+# Django Imports
+from django.contrib.contenttypes.models import ContentType
+
+# Local Imports
+from models import TimeAudit, UserAudit
+from utils import now
+
+# Members
+
+
+def time_audit(UserInstance, ContentObject):
+    """Connect ``added_date_time`` and ``modified_date_time`` information to
+    a given object."""
+    ThisContentType = ContentType.objects.get_for_model(ContentObject)
+    try:
+        Audit = TimeAudit.objects.get(content_type__pk=ThisContentType.id, object_id=ContentObject.pk)
+        Audit.modified_date_time = now()
+    except TimeAudit.DoesNotExist:
+        Audit = TimeAudit(content_object=ContentObject)
+        Audit.added_date_time = now()
+        Audit.modified_date_time = now()
+
+    Audit.save()
+    return Audit
+
+
+def user_audit(UserInstance, ContentObject):
+    """Connect ``added_by`` and ``modified_by`` information to a given
+    object.
+    """
+    ThisContentType = ContentType.objects.get_for_model(ContentObject)
+    try:
+        Audit = UserAudit.objects.get(content_type__pk=ThisContentType.id, object_id=ContentObject.pk)
+        Audit.modified_by = UserInstance
+    except UserAudit.DoesNotExist:
+        Audit = UserAudit(content_object=ContentObject)
+        Audit.added_by = UserInstance
+        Audit.modified_by = UserInstance
+
+    Audit.save()
+    return Audit
+

model_audit/fields.py

+# Django Imports
+from django.db import models
+
+# Local Imports
+from utils import now
+
+# Fields
+
+
+class AutoAddDateTimeField(models.DateTimeField):
+    """Standard ``DateTimeField`` which automatically sets itself to the
+    current date and time when the record is first saved.
+    """
+    def __init__(self, *args, **kwargs):
+        kwargs.setdefault('editable', False)
+        kwargs.setdefault('default', now)
+        super(AutoCreatedField, self).__init__(*args, **kwargs)
+
+
+class AutoModifiedDateTimeField(AutoAddDateTimeField):
+    """Extends ``AutoAddDateTimeField`` to automatically set itself to the
+    current date and time whenever the record is saved.
+    """
+    def pre_save(self, model_instance, add):
+        value = now()
+        setattr(model_instance, self.attname, value)
+        return value
+

model_audit/models.py

+"""
+Model Audit provides stand-alone data models that are connected to Django's
+content types. The optional mixins provide easy integration for access to audit
+data directly from your model.
+"""
+# Python Imports
+
+# Django Imports
+from django.contrib.auth.models import User
+from django.contrib.contenttypes import generic
+from django.contrib.contenttypes.models import ContentType
+from django.db import models
+from django.utils.translation import ugettext_lazy as _
+
+# Local Imports
+
+# Choices
+
+# Models
+
+
+
+class AAuditBase(models.Model):
+    """Abstract model that defines the connection to contenttypes for use by
+    other audit models.
+    """
+
+    content_object = generic.GenericForeignKey(
+        ct_field='content_type',
+        fk_field='object_id'
+    )
+
+    content_type = models.ForeignKey(
+        ContentType,
+        help_text=_("The Django content type."),
+        verbose_name=_("content type")
+    )
+
+    object_id = models.IntegerField(
+        _("object id"),
+        help_text=_("ID of the participating object.")
+    )
+
+    class Meta:
+        abstract = True
+
+
+class TimeAudit(AAuditBase):
+    """Provides ``added_date_time`` and ``modified_date_time``."""
+    added_date_time = models.DateTimeField(
+        _('created date and time'),
+        help_text=_("Date and time the record was added.")
+    )
+
+    modified_date_time = models.DateTimeField(
+        _('modified date and time'),
+        help_text=_("Date and time the record was last updated.")
+    )
+
+
+class TimeAuditMixin(models.Model):
+    """Provides access to ``added_date_time`` and ``modified_date_time``
+    properties.
+    """
+
+    class Meta:
+        abstract = True
+
+    @property
+    def added_date_time(self):
+        return self.audit_time().added_date_time
+
+    def audit_time(self):
+        """Return a ``TimeAudit`` instance for the current object."""
+        try:
+            ThisContentType = ContentType.objects.get_for_model(self)
+            return TimeAudit.objects.get(content_type__pk=ThisContentType.id, object_id=self.pk)
+        except TimeAudit.DoesNotExist:
+            return TimeAudit()
+
+    @property
+    def modified_date_time(self):
+        return self.audit_time().modified_date_time
+
+
+class UserAudit(AAuditBase):
+    """Provides a ``added_by`` and ``modified_by`` for the current user."""
+
+    added_by = models.ForeignKey(
+        User,
+        related_name="added_records",
+        verbose_name = _("added by")
+    )
+
+    modified_by = models.ForeignKey(
+        User,
+        related_name="modified_records",
+        verbose_name = _("modified by")
+    )
+
+    class Meta:
+        verbose_name = _("User Audit Record")
+        verbose_name_plural = _("User Audit Records")
+
+
+class UserAuditMixin(models.Model):
+    """Provides access to ``added_by`` and ``modified_by`` properties."""
+
+    class Meta:
+        abstract = True
+
+    @property
+    def added_by(self):
+        return self.audit_user().added_by
+
+    def audit_user(self):
+        """Return a ``UserAudit`` instance for the current object."""
+        try:
+            ThisContentType = ContentType.objects.get_for_model(self)
+            return UserAudit.objects.get(content_type__pk=ThisContentType.id, object_id=self.pk)
+        except UserAudit.DoesNotExist:
+            return UserAudit()
+
+    @property
+    def modified_by(self):
+        return self.audit_user().modified_by
+

model_audit/utils.py

+"""
+Model Audit utilities. Currently, the only feature is to provide access to the
+appropriate ``now()`` function based on timezone settings.
+"""
+
+# Python Imports
+from datetime import datetime
+
+# Django Imports
+from django.conf import settings
+
+if settings.USE_TZ:
+    from django.utils.timezone import now
+else:
+    now = datetime.now
+import os
+from setuptools import setup
+
+def read(file_path):
+    """Shortcut for reading in the long description from a file that is
+    relative to the setup script.."""
+    return open(os.path.join(os.path.dirname(__file__), file_path)).read()
+
+setup(
+    name="django-model-audit",
+    version="0.1.0d",
+    author="F.S. Davis",
+    author_email="consulting@fsdavis.com",
+    description = ("Simple tools for  tracking added/modified user and times."),
+    license="BSD",
+    keywords="django database audit",
+    url="http://bitbucket.org/bogeymin/django-audit",
+    packages=["audit", ],
+    long_description=read('readme.rst'),
+    classifiers=[
+        "Development Status :: 2 - Pre-Alpha",
+        'Environment :: Web Environment',
+        "Framework :: Django",
+        "Intended Audience :: Developers",
+        "License :: OSI Approved :: BSD License",
+        'Operating System :: OS Independent',
+        "Programming Language :: Python",
+        "Topic :: Database",
+    ],
+)