From 86ffe9707b8122982943b35e4dd23a80f1360d5f Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Thu, 11 Apr 2013 19:18:13 -0700 Subject: [PATCH 1/7] Add test files --- runtests.py | 35 ++++++++++++ setup.py | 6 +- simple_history/tests/__init__.py | 0 simple_history/tests/models.py | 17 ++++++ simple_history/tests/tests.py | 98 ++++++++++++++++++++++++++++++++ 5 files changed, 154 insertions(+), 2 deletions(-) create mode 100755 runtests.py create mode 100644 simple_history/tests/__init__.py create mode 100644 simple_history/tests/models.py create mode 100644 simple_history/tests/tests.py 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/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?") From c6d0f8e3c8f20d86addda2baeed4fef77c5559b9 Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Thu, 11 Apr 2013 19:18:49 -0700 Subject: [PATCH 2/7] Add travis and tox files --- .travis.yml | 31 +++++++++++++++++++++++++++++ runtests.sh | 4 ++++ tox.ini | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+) create mode 100644 .travis.yml create mode 100755 runtests.sh create mode 100644 tox.ini diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..67773c6f1 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,31 @@ +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: + include: + - python: 2.5 + env: DJANGO=Django==1.2.7 SOUTH=1 COVERALLS=0 + - 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/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/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 From 07a2f33e2f3edf92ea2bde8003a700213cfdfbf3 Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Thu, 11 Apr 2013 19:27:14 -0700 Subject: [PATCH 3/7] Rename README file to README.rst --- README => README.rst | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename README => README.rst (100%) diff --git a/README b/README.rst similarity index 100% rename from README rename to README.rst From 4b65b64c69a32f827864d72d2482769d29469ee9 Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Thu, 11 Apr 2013 19:34:30 -0700 Subject: [PATCH 4/7] Improve reStructuredText in README --- README.rst | 125 +++++++++++++++++++++++++++-------------------------- 1 file changed, 63 insertions(+), 62 deletions(-) diff --git a/README.rst b/README.rst index 3a12cfbf2..5950c91aa 100644 --- a/README.rst +++ b/README.rst @@ -1,77 +1,78 @@ 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 == +Install +------- Download the tar.gz, extract it and run the following inside the directory: - python setup.py install -== Basic usage == +.. 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: - 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. +.. code-block:: python -== Example == -class Poll(models.Model): - question = models.CharField(max_length = 200) - pub_date = models.DateTimeField('date published') + from simple_history.models import HistoricalRecords - history = HistoricalRecords() +Then in your model class, include the following line: -class Choice(models.Model): - poll = models.ForeignKey(Poll) - choice = models.CharField(max_length=200) - votes = models.IntegerField() +.. code-block:: python 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]: [, , ] +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() + [, , ] From 8fbd59537e54c2980d94330614cc5def874ceaf5 Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Thu, 11 Apr 2013 19:28:24 -0700 Subject: [PATCH 5/7] Add travis and coveralls badges to README --- README.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.rst b/README.rst index 5950c91aa..d45c679b3 100644 --- a/README.rst +++ b/README.rst @@ -1,3 +1,12 @@ +================== +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 From 3d5e5258c09190a960711e925c7f1851c94b2985 Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Thu, 11 Apr 2013 19:39:24 -0700 Subject: [PATCH 6/7] Add gitignore and remove hgignore --- .gitignore | 8 ++++++++ .hgignore | 7 ------- 2 files changed, 8 insertions(+), 7 deletions(-) create mode 100644 .gitignore delete mode 100644 .hgignore 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 From 4a52c0df94b914e9ef5b789d5e1a535e4b68f22b Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Fri, 12 Apr 2013 14:35:53 -0700 Subject: [PATCH 7/7] Exclude broken test scenarios --- .travis.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 67773c6f1..052ab6d5e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,9 +20,10 @@ install: 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.2.7 SOUTH=1 COVERALLS=0 - python: 2.5 env: DJANGO=Django==1.3.7 SOUTH=1 COVERALLS=0 - python: 2.5