Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Made a bunch of django compatibility fixes and added test suite.

  • Loading branch information...
commit 3b770a6426a6d3fb9f46d1c9e3d92faa61adb5c9 1 parent 74ffaaf
@ionelmc authored
View
2  .gitignore
@@ -7,3 +7,5 @@ dist/
build/
.build/
pip-log.txt
+.tox
+.coverage
View
7 .travis.yml
@@ -0,0 +1,7 @@
+language: python
+python:
+ - "2.7"
+# command to install dependencies
+install: pip install tox
+# command to run tests
+script: tox
View
10 README.rst
@@ -25,7 +25,13 @@ TODO
Requirements
============
-* Django (versions tbd - maybe >= 1.3)
+* Django 1.2, 1.3, 1.4, trunk. Django 1.1 is NOT supported.
+* Python 2.6 or 2.7
+
+|status|_
+
+.. |status| image:: http://travis-ci.org/ionelmc/django-admin-customizer.png
+.. _status: http://travis-ci.org/ionelmc/django-admin-customizer
Installation guide
==================
@@ -44,7 +50,7 @@ Add ``admin_customizer`` to ``INSTALLED_APPS``::
Add the admin customizer's urls to your root url conf. This is the url where
your will access your custom admin instances. Eg: in your project's urls.py add::
-
+
(r'^admin/_/', include('admin_customizer.urls')),
After that you need to run::
View
174 src/admin_customizer/fields.py
@@ -1,7 +1,11 @@
+from django import VERSION
from django.forms.models import ModelMultipleChoiceField, ModelChoiceIterator
from django.forms.models import ChoiceField, ModelChoiceField
from django.db.models import ManyToManyField
-from django.db import router
+try:
+ from django.db import router
+except ImportError:
+ router = None
from django.db.models import signals
from django.db.models.fields import related
@@ -50,11 +54,19 @@ def clean(self, value):
return [mapping[pk] for pk in value]
class OrderPreservingManyToManyField(ManyToManyField):
+ """
+ This field will preserve the database order when fetching from db (via value_from_object)
+ and when saving (via the descriptor's _add_items).
+ """
+
def value_from_object(self, obj):
"Returns the value of this field in the given model instance."
- return getattr(obj, self.attname).extra(
- order_by = [self.rel.through._meta.db_table+'.id']
- )
+ if VERSION[:2] >= (1, 2):
+ return getattr(obj, self.attname).extra(
+ order_by = [self.rel.through._meta.db_table+'.id']
+ )
+ else:
+ raise RuntimeError("Unsupported django version %s." % (VERSION,))
def contribute_to_class(self, cls, name):
super(OrderPreservingManyToManyField, self).contribute_to_class(cls, name)
@@ -62,56 +74,110 @@ def contribute_to_class(self, cls, name):
__get__ = descriptor.__get__
def get(instance, instance_type=None):
manager = __get__(instance, instance_type)
- superclass = rel = "hax hax and more hax"
-
- def add_items(self, source_field_name, target_field_name, *objs):
- superclass # hax
-
- # join_table: name of the m2m link table
- # source_field_name: the PK fieldname in join_table for the source object
- # target_field_name: the PK fieldname in join_table for the target object
- # *objs - objects to add. Either object instances, or primary keys of object instances.
-
- # If there aren't any objects, there is nothing to do.
- from django.db.models import Model
- if objs:
- new_ids = OrderedSet()
- for obj in objs:
- if isinstance(obj, self.model):
- if not router.allow_relation(obj, self.instance):
- raise ValueError('Cannot add "%r": instance is on database "%s", value is on database "%s"' %
- (obj, self.instance._state.db, obj._state.db))
- new_ids.add(obj.pk)
- elif isinstance(obj, Model):
- raise TypeError("'%s' instance expected" % self.model._meta.object_name)
- else:
- new_ids.add(obj)
- db = router.db_for_write(self.through, instance=self.instance)
- vals = self.through._default_manager.using(db).values_list(target_field_name, flat=True)
- vals = vals.filter(**{
- source_field_name: self._pk_val,
- '%s__in' % target_field_name: new_ids,
- })
- new_ids = new_ids - set(vals)
-
- if self.reverse or source_field_name == self.source_field_name:
- # Don't send the signal when we are inserting the
- # duplicate data row for symmetrical reverse entries.
- signals.m2m_changed.send(sender=rel.through, action='pre_add',
- instance=self.instance, reverse=self.reverse,
- model=self.model, pk_set=new_ids, using=db)
- # Add the ones that aren't there already
- for obj_id in new_ids:
- self.through._default_manager.using(db).create(**{
- '%s_id' % source_field_name: self._pk_val,
- '%s_id' % target_field_name: obj_id,
+ # We want to override the _add_items method in the manager returned by the descriptor
+ # However, we cannot create a subclass so we'll patch the returned manager instance
+ # instead.
+
+ # The _add_items method from django has 1 free varaibles so we need
+ # to bind them outside the scope of the new function
+ rel = "hax"
+ if VERSION[:2] in ((1, 3), (1, 2)):
+ def add_items(self, source_field_name, target_field_name, *objs):
+ # join_table: name of the m2m link table
+ # source_field_name: the PK fieldname in join_table for the source object
+ # target_field_name: the PK fieldname in join_table for the target object
+ # *objs - objects to add. Either object instances, or primary keys of object instances.
+
+ # If there aren't any objects, there is nothing to do.
+ from django.db.models import Model
+ if objs:
+ new_ids = OrderedSet()
+ for obj in objs:
+ if isinstance(obj, self.model):
+ if not router.allow_relation(obj, self.instance):
+ raise ValueError('Cannot add "%r": instance is on database "%s", value is on database "%s"' %
+ (obj, self.instance._state.db, obj._state.db))
+ new_ids.add(obj.pk)
+ elif isinstance(obj, Model):
+ raise TypeError("'%s' instance expected" % self.model._meta.object_name)
+ else:
+ new_ids.add(obj)
+ db = router.db_for_write(self.through, instance=self.instance)
+ vals = self.through._default_manager.using(db).values_list(target_field_name, flat=True)
+ vals = vals.filter(**{
+ source_field_name: self._pk_val,
+ '%s__in' % target_field_name: new_ids,
+ })
+ new_ids = new_ids - set(vals)
+
+ if self.reverse or source_field_name == self.source_field_name:
+ # Don't send the signal when we are inserting the
+ # duplicate data row for symmetrical reverse entries.
+ signals.m2m_changed.send(sender=rel.through, action='pre_add',
+ instance=self.instance, reverse=self.reverse,
+ model=self.model, pk_set=new_ids, using=db)
+ # Add the ones that aren't there already
+ for obj_id in new_ids:
+ self.through._default_manager.using(db).create(**{
+ '%s_id' % source_field_name: self._pk_val,
+ '%s_id' % target_field_name: obj_id,
+ })
+ if self.reverse or source_field_name == self.source_field_name:
+ # Don't send the signal when we are inserting the
+ # duplicate data row for symmetrical reverse entries.
+ signals.m2m_changed.send(sender=rel.through, action='post_add',
+ instance=self.instance, reverse=self.reverse,
+ model=self.model, pk_set=new_ids, using=db)
+ elif VERSION[:2] >= (1, 4):
+ def add_items(self, source_field_name, target_field_name, *objs):
+ # source_field_name: the PK fieldname in join table for the source object
+ # target_field_name: the PK fieldname in join table for the target object
+ # *objs - objects to add. Either object instances, or primary keys of object instances.
+
+ # If there aren't any objects, there is nothing to do.
+ from django.db.models import Model
+ if objs:
+ new_ids = OrderedSet()
+ for obj in objs:
+ if isinstance(obj, self.model):
+ if not router.allow_relation(obj, self.instance):
+ raise ValueError('Cannot add "%r": instance is on database "%s", value is on database "%s"' %
+ (obj, self.instance._state.db, obj._state.db))
+ new_ids.add(obj.pk)
+ elif isinstance(obj, Model):
+ raise TypeError("'%s' instance expected, got %r" % (self.model._meta.object_name, obj))
+ else:
+ new_ids.add(obj)
+ db = router.db_for_write(self.through, instance=self.instance)
+ vals = self.through._default_manager.using(db).values_list(target_field_name, flat=True)
+ vals = vals.filter(**{
+ source_field_name: self._pk_val,
+ '%s__in' % target_field_name: new_ids,
})
- if self.reverse or source_field_name == self.source_field_name:
- # Don't send the signal when we are inserting the
- # duplicate data row for symmetrical reverse entries.
- signals.m2m_changed.send(sender=rel.through, action='post_add',
- instance=self.instance, reverse=self.reverse,
- model=self.model, pk_set=new_ids, using=db)
- manager._add_items.im_func.func_code = add_items.func_code # hax
+ new_ids = new_ids - set(vals)
+
+ if self.reverse or source_field_name == self.source_field_name:
+ # Don't send the signal when we are inserting the
+ # duplicate data row for symmetrical reverse entries.
+ signals.m2m_changed.send(sender=self.through, action='pre_add',
+ instance=self.instance, reverse=self.reverse,
+ model=self.model, pk_set=new_ids, using=db)
+ # Add the ones that aren't there already
+ for obj_id in new_ids:
+ self.through._default_manager.using(db).create(**{
+ '%s_id' % source_field_name: self._pk_val,
+ '%s_id' % target_field_name: obj_id,
+ })
+
+ if self.reverse or source_field_name == self.source_field_name:
+ # Don't send the signal when we are inserting the
+ # duplicate data row for symmetrical reverse entries.
+ signals.m2m_changed.send(sender=self.through, action='post_add',
+ instance=self.instance, reverse=self.reverse,
+ model=self.model, pk_set=new_ids, using=db)
+ else:
+ raise RuntimeError("Unsupported django version %s." % (VERSION,))
+ manager._add_items.im_func.func_code = add_items.func_code # now we can safely patch it
+ # (number of free variables match)
return manager
descriptor.__get__ = get
View
15 src/admin_customizer/models.py
@@ -7,7 +7,6 @@
from django.utils.translation import ugettext_lazy as _
from django.core.urlresolvers import reverse
from django.db.models.signals import post_save
-from django.dispatch import receiver
from django.core.cache import cache
from .managers import AvailableFieldManager
@@ -65,7 +64,7 @@ class Meta:
blank = True,
)
raw_id_fields = models.ManyToManyField("AvailableField",
- related_name = "+",
+ related_name = "egisteredmodels_with_raw_id_fields",
limit_choices_to = {'type__in': ('oto', 'fk', 'mtm')},
blank = True,
)
@@ -82,8 +81,12 @@ def __unicode__(self):
class AvailableField(models.Model):
class Meta:
unique_together = 'model', 'name', 'type', 'target', 'through'
+ ordering = 'id',
- model = models.ForeignKey("contenttypes.ContentType", related_name="+")
+ model = models.ForeignKey(
+ "contenttypes.ContentType",
+ related_name = "availablefields_with_model"
+ )
name = models.TextField()
LIST_DISPLAY_TYPES = ('fk', 'mtm', 'oto', 'meth', 'other')
LIST_FILTER_TYPES = ('fk', 'mtm', 'oto', 'other')
@@ -102,7 +105,7 @@ class Meta:
"contenttypes.ContentType",
null = True,
blank = True,
- related_name = "+"
+ related_name = "availablefields_with_target"
)
through = models.ForeignKey("self", null=True, blank=True)
@@ -158,7 +161,7 @@ def path_for(self, model):
else:
return label
-@receiver(post_save, sender=AdminSite)
-@receiver(post_save, sender=RegisteredModel)
def on_models_change(sender, **kwargs):
active_models, checksum = get_active_models()
+post_save.connect(on_models_change, sender=AdminSite)
+post_save.connect(on_models_change, sender=RegisteredModel)
View
7 src/admin_customizer/widgets.py
@@ -6,7 +6,11 @@
from django.template.defaulttags import mark_safe
from django.template.defaultfilters import force_escape
from django.utils.encoding import force_unicode
-from django.utils.html import escape, conditional_escape, escapejs
+from django.utils.html import escape, conditional_escape
+try:
+ from django.utils.html import escapejs
+except ImportError:
+ from django.template.defaultfilters import escapejs
from . import conf
from .orderedset import OrderedSet
@@ -68,6 +72,7 @@ def render_options(self, choices, selected_choices):
selected_choices, option_value, option_label, True))
for option_value, option_label in chain(self.choices, choices):
+ option_value = str(option_value)
if option_value not in selected_choices:
output.append(self.render_option(
selected_choices, option_value, option_label, False))
View
0  tests/test_app/__init__.py
No changes.
View
221 tests/test_app/fixtures/initial_data.json
@@ -0,0 +1,221 @@
+[{
+ "pk": 1,
+ "model": "admin_customizer.availablefield",
+ "fields": {
+ "through": null,
+ "model": ["test_app", "author"],
+ "type": "rev",
+ "name": "book_set",
+ "target": null
+ }
+}, {
+ "pk": 2,
+ "model": "admin_customizer.availablefield",
+ "fields": {
+ "through": null,
+ "model": ["test_app", "author"],
+ "type": "other",
+ "name": "id",
+ "target": null
+ }
+}, {
+ "pk": 3,
+ "model": "admin_customizer.availablefield",
+ "fields": {
+ "through": null,
+ "model": ["test_app", "author"],
+ "type": "other",
+ "name": "name",
+ "target": null
+ }
+}, {
+ "pk": 4,
+ "model": "admin_customizer.availablefield",
+ "fields": {
+ "through": null,
+ "model": ["test_app", "book"],
+ "type": "mtm",
+ "name": "authors",
+ "target": ["test_app", "author"]
+ }
+}, {
+ "pk": 5,
+ "model": "admin_customizer.availablefield",
+ "fields": {
+ "through": null,
+ "model": ["test_app", "book"],
+ "type": "rev",
+ "name": "booknote_set",
+ "target": null
+ }
+}, {
+ "pk": 6,
+ "model": "admin_customizer.availablefield",
+ "fields": {
+ "through": null,
+ "model": ["test_app", "book"],
+ "type": "other",
+ "name": "created",
+ "target": null
+ }
+}, {
+ "pk": 7,
+ "model": "admin_customizer.availablefield",
+ "fields": {
+ "through": null,
+ "model": ["test_app", "book"],
+ "type": "other",
+ "name": "id",
+ "target": null
+ }
+}, {
+ "pk": 8,
+ "model": "admin_customizer.availablefield",
+ "fields": {
+ "through": null,
+ "model": ["test_app", "book"],
+ "type": "other",
+ "name": "name",
+ "target": null
+ }
+}, {
+ "pk": 9,
+ "model": "admin_customizer.availablefield",
+ "fields": {
+ "through": null,
+ "model": ["test_app", "booknote"],
+ "type": "fk",
+ "name": "book",
+ "target": ["test_app", "book"]
+ }
+}, {
+ "pk": 10,
+ "model": "admin_customizer.availablefield",
+ "fields": {
+ "through": null,
+ "model": ["test_app", "booknote"],
+ "type": "other",
+ "name": "id",
+ "target": null
+ }
+}, {
+ "pk": 11,
+ "model": "admin_customizer.availablefield",
+ "fields": {
+ "through": null,
+ "model": ["test_app", "booknote"],
+ "type": "other",
+ "name": "notes",
+ "target": null
+ }
+}, {
+ "pk": 12,
+ "model": "admin_customizer.availablefield",
+ "fields": {
+ "through": 4,
+ "model": ["test_app", "author"],
+ "type": "rev",
+ "name": "book_set",
+ "target": null
+ }
+}, {
+ "pk": 13,
+ "model": "admin_customizer.availablefield",
+ "fields": {
+ "through": 4,
+ "model": ["test_app", "author"],
+ "type": "other",
+ "name": "id",
+ "target": null
+ }
+}, {
+ "pk": 14,
+ "model": "admin_customizer.availablefield",
+ "fields": {
+ "through": 4,
+ "model": ["test_app", "author"],
+ "type": "other",
+ "name": "name",
+ "target": null
+ }
+}, {
+ "pk": 15,
+ "model": "admin_customizer.availablefield",
+ "fields": {
+ "through": 9,
+ "model": ["test_app", "book"],
+ "type": "mtm",
+ "name": "authors",
+ "target": ["test_app", "author"]
+ }
+}, {
+ "pk": 16,
+ "model": "admin_customizer.availablefield",
+ "fields": {
+ "through": 9,
+ "model": ["test_app", "book"],
+ "type": "rev",
+ "name": "booknote_set",
+ "target": null
+ }
+}, {
+ "pk": 17,
+ "model": "admin_customizer.availablefield",
+ "fields": {
+ "through": 9,
+ "model": ["test_app", "book"],
+ "type": "other",
+ "name": "created",
+ "target": null
+ }
+}, {
+ "pk": 18,
+ "model": "admin_customizer.availablefield",
+ "fields": {
+ "through": 9,
+ "model": ["test_app", "book"],
+ "type": "other",
+ "name": "id",
+ "target": null
+ }
+}, {
+ "pk": 19,
+ "model": "admin_customizer.availablefield",
+ "fields": {
+ "through": 9,
+ "model": ["test_app", "book"],
+ "type": "other",
+ "name": "name",
+ "target": null
+ }
+}, {
+ "pk": 20,
+ "model": "admin_customizer.availablefield",
+ "fields": {
+ "through": 15,
+ "model": ["test_app", "author"],
+ "type": "rev",
+ "name": "book_set",
+ "target": null
+ }
+}, {
+ "pk": 21,
+ "model": "admin_customizer.availablefield",
+ "fields": {
+ "through": 15,
+ "model": ["test_app", "author"],
+ "type": "other",
+ "name": "id",
+ "target": null
+ }
+}, {
+ "pk": 22,
+ "model": "admin_customizer.availablefield",
+ "fields": {
+ "through": 15,
+ "model": ["test_app", "author"],
+ "type": "other",
+ "name": "name",
+ "target": null
+ }
+}]
View
15 tests/test_app/models.py
@@ -0,0 +1,15 @@
+from django import VERSION
+from admin_customizer.fields import OrderPreservingManyToManyField
+from django.db import models
+
+class Author(models.Model):
+ name = models.CharField(max_length=100)
+
+class Book(models.Model):
+ name = models.CharField(max_length=100)
+ created = models.DateTimeField(auto_now_add=True)
+ authors = OrderPreservingManyToManyField(Author)
+
+class BookNote(models.Model):
+ book = models.ForeignKey("Book", null=True)
+ notes = models.TextField()
View
159 tests/test_app/tests.py
@@ -0,0 +1,159 @@
+import logging
+import logging.handlers
+
+import random
+import time
+
+from django import VERSION
+from django.test import TestCase
+from django import forms
+
+import time
+import re
+
+from .models import Book, Author, BookNote
+from admin_customizer.models import RegisteredModel, AvailableField, AdminSite
+from admin_customizer.fields import FieldSelectField
+from django.contrib.contenttypes.models import ContentType
+
+class AssertingHandler(logging.handlers.BufferingHandler):
+
+ def __init__(self,capacity):
+ logging.handlers.BufferingHandler.__init__(self,capacity)
+
+ def assertLogged(self, test_case, msg):
+ for record in self.buffer:
+ s = self.format(record)
+ if s.startswith(msg):
+ return
+ test_case.assertTrue(False, "Failed to find log message: " + msg)
+
+class _AssertRaisesContext(object):
+ """A context manager used to implement TestCase.assertRaises* methods."""
+
+ def __init__(self, expected, test_case, expected_regexp=None):
+ self.expected = expected
+ self.failureException = test_case.failureException
+ self.expected_regexp = expected_regexp
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, exc_type, exc_value, tb):
+ if exc_type is None:
+ try:
+ exc_name = self.expected.__name__
+ except AttributeError:
+ exc_name = str(self.expected)
+ raise self.failureException(
+ "{0} not raised".format(exc_name))
+ if not issubclass(exc_type, self.expected):
+ # let unexpected exceptions pass through
+ return False
+ self.exception = exc_value # store for later retrieval
+ if self.expected_regexp is None:
+ return True
+
+ expected_regexp = self.expected_regexp
+ if isinstance(expected_regexp, basestring):
+ expected_regexp = re.compile(expected_regexp)
+ if not expected_regexp.search(str(exc_value)):
+ raise self.failureException('"%s" does not match "%s"' %
+ (expected_regexp.pattern, str(exc_value)))
+ return True
+
+class PrefetchTests(TestCase):
+ def assertRegexpMatches(self, text, expected_regexp, msg=None):
+ """Fail the test unless the text matches the regular expression."""
+ if isinstance(expected_regexp, basestring):
+ expected_regexp = re.compile(expected_regexp)
+ if not expected_regexp.search(text):
+ msg = msg or "Regexp didn't match"
+ msg = '%s: %r not found in %r' % (msg, expected_regexp.pattern, text)
+ raise self.failureException(msg)
+
+ def assertRaises(self, excClass, callableObj=None, *args, **kwargs):
+ """Fail unless an exception of class excClass is thrown
+ by callableObj when invoked with arguments args and keyword
+ arguments kwargs. If a different type of exception is
+ thrown, it will not be caught, and the test case will be
+ deemed to have suffered an error, exactly as for an
+ unexpected exception.
+
+ If called with callableObj omitted or None, will return a
+ context object used like this::
+
+ with self.assertRaises(SomeException):
+ do_something()
+
+ The context manager keeps a reference to the exception as
+ the 'exception' attribute. This allows you to inspect the
+ exception after the assertion::
+
+ with self.assertRaises(SomeException) as cm:
+ do_something()
+ the_exception = cm.exception
+ self.assertEqual(the_exception.error_code, 3)
+ """
+ context = _AssertRaisesContext(excClass, self)
+ if callableObj is None:
+ return context
+ with context:
+ callableObj(*args, **kwargs)
+
+ def test_ordered_mtm_widget(self):
+ book_ct = ContentType.objects.get_for_model(Book)
+ class RegModelForm(forms.ModelForm):
+ class Meta:
+ model = RegisteredModel
+ fields = 'list_display',
+
+ list_display = FieldSelectField(
+ "list_display",
+ Book,
+ AvailableField.objects.filter_for_model(model=book_ct).filter(
+ type__in = AvailableField.LIST_DISPLAY_TYPES
+ ),
+ enable_ordering = True,
+ )
+
+ reg_model = RegisteredModel.objects.create(
+ model = book_ct,
+ admin_site = AdminSite.objects.create(slug='test'),
+ active = True
+ )
+ afs = list(AvailableField.objects.filter_for_model(
+ model = book_ct
+ ).values_list(
+ 'id', 'name'
+ ).filter(
+ type__in = AvailableField.LIST_DISPLAY_TYPES
+ ))
+
+ empty_form = RegModelForm(instance=reg_model).as_p()
+ expected = '\n'.join(
+ '<option value="%s" data-label="%s" data-parent="">%s</option>'%
+ (id, name, name) for id, name in afs
+ )
+ self.assertTrue(
+ expected in empty_form, "%r not in %r" % (expected, empty_form)
+ )
+ new_afs = list(afs)
+ random.shuffle(new_afs, lambda: 0.345)
+
+ from django.http import MultiValueDict
+ bound_form = RegModelForm(MultiValueDict({
+ 'list_display': [str(id) for id, name in new_afs],
+ }), instance=reg_model)
+ self.assertTrue(bound_form.is_valid(), bound_form.errors)
+ bound_form.save()
+
+ empty_form = RegModelForm(instance=RegisteredModel.objects.get(id=reg_model.id)).as_p()
+ expected = '\n'.join(
+ '<option value="%s" data-label="%s" data-parent="" '
+ 'selected="selected">%s</option>' %
+ (id, name, name) for id, name in new_afs
+ )
+ self.assertTrue(
+ expected in empty_form, "%r not in %r" % (expected, empty_form)
+ )
View
6 tests/test_app/urls.py
@@ -0,0 +1,6 @@
+# -*- coding: utf-8 -*-
+from django.conf.urls.defaults import *
+
+urlpatterns = patterns('')
+
+
View
0  tests/test_project/__init__.py
No changes.
View
30 tests/test_project/settings.py
@@ -0,0 +1,30 @@
+# -*- coding: utf-8 -*-
+import os
+DEBUG = True
+
+DATABASE_ENGINE = 'sqlite3'
+DATABASE_NAME = os.path.join(os.path.dirname(__file__), 'database.sqlite')
+
+DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.' + DATABASE_ENGINE,
+ 'NAME': DATABASE_NAME
+ },
+}
+INSTALLED_APPS = (
+ 'django.contrib.contenttypes',
+ 'django.contrib.auth',
+ 'django.contrib.sessions',
+ 'django.contrib.sites',
+ 'admin_customizer',
+ 'test_app',
+)
+SITE_ID = 1
+ROOT_URLCONF = 'test_project.urls'
+
+MIDDLEWARE_CLASSES = (
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+)
+
+SECRET_KEY = ADMIN_MEDIA_PREFIX = "DON'T MATTER"
View
6 tests/test_project/urls.py
@@ -0,0 +1,6 @@
+from django.conf.urls.defaults import *
+
+
+urlpatterns = patterns('',
+ url(r'/', include('test_project.apps.testapp.urls'))
+)
View
79 tox.ini
@@ -0,0 +1,79 @@
+[tox]
+envlist =
+ py2.6-django-trunk,
+ py2.7-django-trunk,
+ py2.6-django1.4,
+ py2.7-django1.4,
+ py2.6-django1.3,
+ py2.7-django1.3,
+ py2.6-django1.2,
+ py2.7-django1.2,
+
+# unsupported
+ ;py2.6-django1.1,
+ ;py2.7-django1.1
+
+[testenv]
+setenv =
+ PYTHONPATH = {toxinidir}/src:{toxinidir}/tests
+ DJANGO_SETTINGS_MODULE = test_project.settings
+commands =
+ ;coverage erase
+ ;coverage run --branch {envbindir}/django-admin.py test test_app
+ ;{envbindir}/django-admin.py syncdb --noinput
+ ;{envbindir}/django-admin.py refresh_available_fields --erase --noinput
+ {envbindir}/django-admin.py test test_app
+
+ ;coverage report -m --include "{toxinidir}/src/*"
+ ;coverage html -d {envdir}/html_report
+
+[testenv:py2.6-django-trunk]
+basepython = python2.6
+deps =
+ coverage
+ https://github.com/django/django/zipball/master
+[testenv:py2.7-django-trunk]
+basepython = python2.7
+deps =
+ coverage
+ https://github.com/django/django/zipball/master
+[testenv:py2.6-django1.4]
+basepython = python2.6
+deps =
+ coverage
+ http://www.djangoproject.com/download/1.4/tarball/
+[testenv:py2.7-django1.4]
+basepython = python2.7
+deps =
+ coverage
+ http://www.djangoproject.com/download/1.4/tarball/
+[testenv:py2.6-django1.3]
+basepython = python2.6
+deps =
+ coverage
+ django>=1.3,<1.4
+[testenv:py2.7-django1.3]
+basepython = python2.7
+deps =
+ coverage
+ django>=1.3,<1.4
+[testenv:py2.6-django1.2]
+basepython = python2.6
+deps =
+ coverage
+ django>=1.2,<1.3
+[testenv:py2.7-django1.2]
+basepython = python2.7
+deps =
+ coverage
+ django>=1.2,<1.3
+[testenv:py2.6-django1.1]
+basepython = python2.6
+deps =
+ coverage
+ django>=1.1,<1.2
+[testenv:py2.7-django1.1]
+basepython = python2.7
+deps =
+ coverage
+ django>=1.1,<1.2
Please sign in to comment.
Something went wrong with that request. Please try again.