diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..373f51d9e --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +*.pyc +dist/ +*.egg-info/ +build/ +MANIFEST +.coverage +.tox/ +htmlcov/ diff --git a/.hgignore b/.hgignore deleted file mode 100644 index 3f9e56383..000000000 --- a/.hgignore +++ /dev/null @@ -1,7 +0,0 @@ -syntax: glob -*.pyc -dist -MANIFEST -simple_history.egg-info -simple_history/.project -simple_history/.pydevproject diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..052ab6d5e --- /dev/null +++ b/.travis.yml @@ -0,0 +1,32 @@ +language: python + +python: + - "2.6" + - "2.7" + +env: + - DJANGO=Django==1.2.7 SOUTH=1 + - DJANGO=Django==1.3.7 SOUTH=1 + - DJANGO=Django==1.4.5 SOUTH=1 + - DJANGO=Django==1.5.1 SOUTH=1 + - DJANGO=https://github.com/django/django/tarball/master SOUTH=1 + - DJANGO=Django==1.3.7 SOUTH=0 + +install: + - pip install coverage $DJANGO --use-mirrors + - sh -c "if [ '$SOUTH' = '1' ]; then pip install South==0.7.6; fi" + - sh -c "if [ '$COVERALLS' != '0' ]; then pip install coveralls; fi" + +script: coverage run -a --branch --include="simple_history/*" --omit="simple_history/tests/*" setup.py test + +matrix: + exclude: + - python: 2.6 + env: DJANGO=Django==1.2.7 SOUTH=1 + include: + - python: 2.5 + env: DJANGO=Django==1.3.7 SOUTH=1 COVERALLS=0 + - python: 2.5 + env: DJANGO=Django==1.4.5 SOUTH=1 COVERALLS=0 + +after_success: coveralls diff --git a/README b/README deleted file mode 100644 index 3a12cfbf2..000000000 --- a/README +++ /dev/null @@ -1,77 +0,0 @@ -django-simple-history is a tool to store state of DB objects on every create/update/delete. It has been tested to work in django 1.X (including 1.2.3 as of 10/25/2010). - -== Install == -Download the tar.gz, extract it and run the following inside the directory: - python setup.py install - -== Basic usage == -Using this package is _really_ simple; you just have to import HistoricalRecords and create an instance of it on every model you want to historically track. - -On your models you need to include the following line at the top: - from simple_history.models import HistoricalRecords - -Then in your model class, include the following line: - history = HistoricalRecords() - -Then from either the model class or from an instance, you can access history.all() which will give you either every history item of the class, or every history item of the specific instance. - -== Example == -class Poll(models.Model): - question = models.CharField(max_length = 200) - pub_date = models.DateTimeField('date published') - - history = HistoricalRecords() - -class Choice(models.Model): - poll = models.ForeignKey(Poll) - choice = models.CharField(max_length=200) - votes = models.IntegerField() - - history = HistoricalRecords() - - -$ ./manage.py shell -In [2]: from poll.models import Poll, Choice - -In [3]: Poll.objects.all() -Out[3]: [] - -In [4]: import datetime - -In [5]: p = Poll(question="what's up?", pub_date=datetime.datetime.now()) - -In [6]: p.save() - -In [7]: p -Out[7]: - -In [9]: p.history.all() -Out[9]: [] - -In [10]: p.pub_date = datetime.datetime(2007,4,1,0,0) - -In [11]: p.save() - -In [13]: p.history.all() -Out[13]: [, ] - -In [14]: p.choice_set.create(choice='Not Much', votes=0) -Out[14]: - -In [15]: p.choice_set.create(choice='The sky', votes=0) -Out[15]: - -In [16]: c = p.choice_set.create(choice='Just hacking again', votes=0) - -In [17]: c.poll -Out[17]: - -In [19]: c.history.all() -Out[19]: [] - -In [20]: Choice.history -Out[20]: - -In [21]: Choice.history.all() -Out[21]: [, , ] - diff --git a/README.rst b/README.rst new file mode 100644 index 000000000..d45c679b3 --- /dev/null +++ b/README.rst @@ -0,0 +1,87 @@ +================== +django-simple-history +================== + +.. image:: https://secure.travis-ci.org/treyhunner/django-simple-history.png?branch=master + :target: http://travis-ci.org/treyhunner/django-simple-history +.. image:: https://coveralls.io/repos/treyhunner/django-simple-history/badge.png?branch=master + :target: https://coveralls.io/r/treyhunner/django-simple-history + +django-simple-history is a tool to store state of DB objects on every create/update/delete. It has been tested to work in django 1.X (including 1.2.3 as of 10/25/2010). + +Install +------- +Download the tar.gz, extract it and run the following inside the directory: + +.. code-block:: bash + + $ python setup.py install + +Basic usage +----------- +Using this package is _really_ simple; you just have to import HistoricalRecords and create an instance of it on every model you want to historically track. + +On your models you need to include the following line at the top: + +.. code-block:: python + + from simple_history.models import HistoricalRecords + +Then in your model class, include the following line: + +.. code-block:: python + + history = HistoricalRecords() + +Then from either the model class or from an instance, you can access history.all() which will give you either every history item of the class, or every history item of the specific instance. + +Example +------- +Models: + +.. code-block:: python + + class Poll(models.Model): + question = models.CharField(max_length = 200) + pub_date = models.DateTimeField('date published') + + history = HistoricalRecords() + + class Choice(models.Model): + poll = models.ForeignKey(Poll) + choice = models.CharField(max_length=200) + votes = models.IntegerField() + + history = HistoricalRecords() + +Usage: + +.. code-block:: pycon + + >>> from poll.models import Poll, Choice + >>> Poll.objects.all() + [] + >>> import datetime + >>> p = Poll(question="what's up?", pub_date=datetime.datetime.now()) + >>> p.save() + >>> p + + >>> p.history.all() + [] + >>> p.pub_date = datetime.datetime(2007,4,1,0,0) + >>> p.save() + >>> p.history.all() + [, ] + >>> p.choice_set.create(choice='Not Much', votes=0) + + >>> p.choice_set.create(choice='The sky', votes=0) + + >>> c = p.choice_set.create(choice='Just hacking again', votes=0) + >>> c.poll + + >>> c.history.all() + [] + >>> Choice.history + + >>> Choice.history.all() + [, , ] diff --git a/runtests.py b/runtests.py new file mode 100755 index 000000000..fa0e5d555 --- /dev/null +++ b/runtests.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python +import sys +from os.path import abspath, dirname + +from django.conf import settings + + +sys.path.insert(0, abspath(dirname(__file__))) + + +if not settings.configured: + settings.configure( + INSTALLED_APPS=( + 'django.contrib.contenttypes', + 'django.contrib.auth', + 'simple_history', + 'simple_history.tests' + ), + DATABASES={ + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + } + }, + ) + + +def main(): + from django.test.simple import DjangoTestSuiteRunner + failures = DjangoTestSuiteRunner( + verbosity=1, interactive=True, failfast=False).run_tests(['tests']) + sys.exit(failures) + + +if __name__ == "__main__": + main() diff --git a/runtests.sh b/runtests.sh new file mode 100755 index 000000000..4edc36033 --- /dev/null +++ b/runtests.sh @@ -0,0 +1,4 @@ +#!/bin/sh +coverage erase +tox +coverage html --include=simple_history/* --omit=simple_history/tests/* diff --git a/setup.py b/setup.py index 712d7d2d4..c64bfa2bb 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -from distutils.core import setup +from setuptools import setup import os # compile the list of packages available, because distutils doesn't have an easy way to do this @@ -38,4 +38,6 @@ "Development Status :: 5 - Production/Stable", "Framework :: Django", ], - ) + tests_require=["Django>=1.2"], + test_suite='runtests.main', +) diff --git a/simple_history/tests/__init__.py b/simple_history/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/simple_history/tests/models.py b/simple_history/tests/models.py new file mode 100644 index 000000000..59e61c142 --- /dev/null +++ b/simple_history/tests/models.py @@ -0,0 +1,17 @@ +from django.db import models +from simple_history.models import HistoricalRecords + + +class Poll(models.Model): + question = models.CharField(max_length=200) + pub_date = models.DateTimeField('date published') + + history = HistoricalRecords() + + +class Choice(models.Model): + poll = models.ForeignKey(Poll) + choice = models.CharField(max_length=200) + votes = models.IntegerField() + + history = HistoricalRecords() diff --git a/simple_history/tests/tests.py b/simple_history/tests/tests.py new file mode 100644 index 000000000..4ad7a8b44 --- /dev/null +++ b/simple_history/tests/tests.py @@ -0,0 +1,98 @@ +from datetime import datetime, timedelta +from django.test import TestCase + +from .models import Poll + + +today = datetime(2021, 1, 1, 10, 0) +tomorrow = today + timedelta(days=1) + + +class HistoricalRecordsTest(TestCase): + + def assertDatetimesEqual(self, time1, time2): + self.assertAlmostEqual(time1, time2, delta=timedelta(seconds=2)) + + def assertRecordValues(self, record, values_dict): + for key, value in values_dict.items(): + self.assertEqual(getattr(record, key), value) + + def test_create(self): + p = Poll(question="what's up?", pub_date=today) + p.save() + history = p.history.all() + record, = history + self.assertRecordValues(record, { + 'question': "what's up?", + 'pub_date': today, + 'id': p.id, + 'history_type': "+" + }) + self.assertDatetimesEqual(record.history_date, datetime.now()) + + def test_update(self): + Poll.objects.create(question="what's up?", pub_date=today) + p = Poll.objects.get() + p.pub_date = tomorrow + p.save() + history = p.history.all() + update_record, create_record = history + self.assertRecordValues(create_record, { + 'question': "what's up?", + 'pub_date': today, + 'id': p.id, + 'history_type': "+" + }) + self.assertRecordValues(update_record, { + 'question': "what's up?", + 'pub_date': tomorrow, + 'id': p.id, + 'history_type': "~" + }) + + def test_delete(self): + p = Poll.objects.create(question="what's up?", pub_date=today) + poll_id = p.id + p.delete() + history = Poll.history.all() + delete_record, create_record = history + self.assertRecordValues(create_record, { + 'question': "what's up?", + 'pub_date': today, + 'id': poll_id, + 'history_type': "+" + }) + self.assertRecordValues(delete_record, { + 'question': "what's up?", + 'pub_date': today, + 'id': poll_id, + 'history_type': "-" + }) + + +class HistoryManagerTest(TestCase): + def test_most_recent(self): + poll = Poll.objects.create(question="what's up?", pub_date=today) + poll.question = "how's it going?" + poll.save() + poll.question = "why?" + poll.save() + poll.question = "how?" + most_recent = poll.history.most_recent() + self.assertEqual(most_recent.__class__, Poll) + self.assertEqual(most_recent.question, "why?") + + def test_as_of(self): + poll = Poll.objects.create(question="what's up?", pub_date=today) + poll.question = "how's it going?" + poll.save() + poll.question = "why?" + poll.save() + poll.question = "how?" + most_recent = poll.history.most_recent() + self.assertEqual(most_recent.question, "why?") + times = [r.history_date for r in poll.history.all()] + question_as_of = lambda time: poll.history.as_of(time).question + self.assertEqual(question_as_of(times[0]), "why?") + self.assertEqual(question_as_of(times[1]), "how's it going?") + self.assertEqual(question_as_of(times[2]), "what's up?") diff --git a/tox.ini b/tox.ini new file mode 100644 index 000000000..82a5e9329 --- /dev/null +++ b/tox.ini @@ -0,0 +1,57 @@ +[tox] +envlist=py25-1.2,py25-1.3,py26-1.4,py26,py26-trunk,py27,py27-trunk,py27-1.5-nosouth + +[testenv] +deps= + django==1.5.1 + South==0.7.6 + coverage==3.6 +commands=coverage run -a --branch setup.py test + +[testenv:py25-1.2] +basepython=python2.5 +deps= + django==1.2.7 + South==0.7.6 + coverage==3.6 + +[testenv:py25-1.3] +basepython=python2.5 +deps= + django==1.3.7 + South==0.7.6 + coverage==3.6 + +[testenv:py26-1.4] +basepython=python2.6 +deps= + django==1.4.5 + South==0.7.6 + coverage==3.6 + +[testenv:py26] +basepython=python2.6 +deps= + django==1.5.1 + South==0.7.6 + coverage==3.6 + +[testenv:py26-trunk] +basepython=python2.6 +deps= + https://github.com/django/django/tarball/master + South==0.7.6 + coverage==3.6 + +[testenv:py27-trunk] +basepython=python2.7 +deps= + https://github.com/django/django/tarball/master + South==0.7.6 + coverage==3.6 + +[testenv:py27-1.5-nosouth] +basepython=python2.7 +deps= + django==1.5.0 + coverage==3.6