diff options
Diffstat (limited to 'parts/django/tests/modeltests')
228 files changed, 16291 insertions, 0 deletions
diff --git a/parts/django/tests/modeltests/__init__.py b/parts/django/tests/modeltests/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/modeltests/__init__.py diff --git a/parts/django/tests/modeltests/aggregation/__init__.py b/parts/django/tests/modeltests/aggregation/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/modeltests/aggregation/__init__.py diff --git a/parts/django/tests/modeltests/aggregation/fixtures/initial_data.json b/parts/django/tests/modeltests/aggregation/fixtures/initial_data.json new file mode 100644 index 0000000..a002100 --- /dev/null +++ b/parts/django/tests/modeltests/aggregation/fixtures/initial_data.json @@ -0,0 +1,243 @@ +[ + { + "pk": 1, + "model": "aggregation.publisher", + "fields": { + "name": "Apress", + "num_awards": 3 + } + }, + { + "pk": 2, + "model": "aggregation.publisher", + "fields": { + "name": "Sams", + "num_awards": 1 + } + }, + { + "pk": 3, + "model": "aggregation.publisher", + "fields": { + "name": "Prentice Hall", + "num_awards": 7 + } + }, + { + "pk": 4, + "model": "aggregation.publisher", + "fields": { + "name": "Morgan Kaufmann", + "num_awards": 9 + } + }, + { + "pk": 5, + "model": "aggregation.publisher", + "fields": { + "name": "Jonno's House of Books", + "num_awards": 0 + } + }, + { + "pk": 1, + "model": "aggregation.book", + "fields": { + "publisher": 1, + "isbn": "159059725", + "name": "The Definitive Guide to Django: Web Development Done Right", + "price": "30.00", + "rating": 4.5, + "authors": [1, 2], + "contact": 1, + "pages": 447, + "pubdate": "2007-12-6" + } + }, + { + "pk": 2, + "model": "aggregation.book", + "fields": { + "publisher": 2, + "isbn": "067232959", + "name": "Sams Teach Yourself Django in 24 Hours", + "price": "23.09", + "rating": 3.0, + "authors": [3], + "contact": 3, + "pages": 528, + "pubdate": "2008-3-3" + } + }, + { + "pk": 3, + "model": "aggregation.book", + "fields": { + "publisher": 1, + "isbn": "159059996", + "name": "Practical Django Projects", + "price": "29.69", + "rating": 4.0, + "authors": [4], + "contact": 4, + "pages": 300, + "pubdate": "2008-6-23" + } + }, + { + "pk": 4, + "model": "aggregation.book", + "fields": { + "publisher": 3, + "isbn": "013235613", + "name": "Python Web Development with Django", + "price": "29.69", + "rating": 4.0, + "authors": [5, 6, 7], + "contact": 5, + "pages": 350, + "pubdate": "2008-11-3" + } + }, + { + "pk": 5, + "model": "aggregation.book", + "fields": { + "publisher": 3, + "isbn": "013790395", + "name": "Artificial Intelligence: A Modern Approach", + "price": "82.80", + "rating": 4.0, + "authors": [8, 9], + "contact": 8, + "pages": 1132, + "pubdate": "1995-1-15" + } + }, + { + "pk": 6, + "model": "aggregation.book", + "fields": { + "publisher": 4, + "isbn": "155860191", + "name": "Paradigms of Artificial Intelligence Programming: Case Studies in Common Lisp", + "price": "75.00", + "rating": 5.0, + "authors": [8], + "contact": 8, + "pages": 946, + "pubdate": "1991-10-15" + } + }, + { + "pk": 1, + "model": "aggregation.store", + "fields": { + "books": [1, 2, 3, 4, 5, 6], + "name": "Amazon.com", + "original_opening": "1994-4-23 9:17:42", + "friday_night_closing": "23:59:59" + } + }, + { + "pk": 2, + "model": "aggregation.store", + "fields": { + "books": [1, 3, 5, 6], + "name": "Books.com", + "original_opening": "2001-3-15 11:23:37", + "friday_night_closing": "23:59:59" + } + }, + { + "pk": 3, + "model": "aggregation.store", + "fields": { + "books": [3, 4, 6], + "name": "Mamma and Pappa's Books", + "original_opening": "1945-4-25 16:24:14", + "friday_night_closing": "21:30:00" + } + }, + { + "pk": 1, + "model": "aggregation.author", + "fields": { + "age": 34, + "friends": [2, 4], + "name": "Adrian Holovaty" + } + }, + { + "pk": 2, + "model": "aggregation.author", + "fields": { + "age": 35, + "friends": [1, 7], + "name": "Jacob Kaplan-Moss" + } + }, + { + "pk": 3, + "model": "aggregation.author", + "fields": { + "age": 45, + "friends": [], + "name": "Brad Dayley" + } + }, + { + "pk": 4, + "model": "aggregation.author", + "fields": { + "age": 29, + "friends": [1], + "name": "James Bennett" + } + }, + { + "pk": 5, + "model": "aggregation.author", + "fields": { + "age": 37, + "friends": [6, 7], + "name": "Jeffrey Forcier" + } + }, + { + "pk": 6, + "model": "aggregation.author", + "fields": { + "age": 29, + "friends": [5, 7], + "name": "Paul Bissex" + } + }, + { + "pk": 7, + "model": "aggregation.author", + "fields": { + "age": 25, + "friends": [2, 5, 6], + "name": "Wesley J. Chun" + } + }, + { + "pk": 8, + "model": "aggregation.author", + "fields": { + "age": 57, + "friends": [9], + "name": "Peter Norvig" + } + }, + { + "pk": 9, + "model": "aggregation.author", + "fields": { + "age": 46, + "friends": [8], + "name": "Stuart Russell" + } + } +] diff --git a/parts/django/tests/modeltests/aggregation/models.py b/parts/django/tests/modeltests/aggregation/models.py new file mode 100644 index 0000000..ccc1289 --- /dev/null +++ b/parts/django/tests/modeltests/aggregation/models.py @@ -0,0 +1,42 @@ +# coding: utf-8 +from django.db import models + + +class Author(models.Model): + name = models.CharField(max_length=100) + age = models.IntegerField() + friends = models.ManyToManyField('self', blank=True) + + def __unicode__(self): + return self.name + +class Publisher(models.Model): + name = models.CharField(max_length=255) + num_awards = models.IntegerField() + + def __unicode__(self): + return self.name + +class Book(models.Model): + isbn = models.CharField(max_length=9) + name = models.CharField(max_length=255) + pages = models.IntegerField() + rating = models.FloatField() + price = models.DecimalField(decimal_places=2, max_digits=6) + authors = models.ManyToManyField(Author) + contact = models.ForeignKey(Author, related_name='book_contact_set') + publisher = models.ForeignKey(Publisher) + pubdate = models.DateField() + + def __unicode__(self): + return self.name + +class Store(models.Model): + name = models.CharField(max_length=255) + books = models.ManyToManyField(Book) + original_opening = models.DateTimeField() + friday_night_closing = models.TimeField() + + def __unicode__(self): + return self.name + diff --git a/parts/django/tests/modeltests/aggregation/tests.py b/parts/django/tests/modeltests/aggregation/tests.py new file mode 100644 index 0000000..c830368 --- /dev/null +++ b/parts/django/tests/modeltests/aggregation/tests.py @@ -0,0 +1,565 @@ +import datetime +from decimal import Decimal + +from django.db.models import Avg, Sum, Count, Max, Min +from django.test import TestCase, Approximate + +from models import Author, Publisher, Book, Store + + +class BaseAggregateTestCase(TestCase): + fixtures = ["initial_data.json"] + + def test_empty_aggregate(self): + self.assertEqual(Author.objects.all().aggregate(), {}) + + def test_single_aggregate(self): + vals = Author.objects.aggregate(Avg("age")) + self.assertEqual(vals, {"age__avg": Approximate(37.4, places=1)}) + + def test_multiple_aggregates(self): + vals = Author.objects.aggregate(Sum("age"), Avg("age")) + self.assertEqual(vals, {"age__sum": 337, "age__avg": Approximate(37.4, places=1)}) + + def test_filter_aggregate(self): + vals = Author.objects.filter(age__gt=29).aggregate(Sum("age")) + self.assertEqual(len(vals), 1) + self.assertEqual(vals["age__sum"], 254) + + def test_related_aggregate(self): + vals = Author.objects.aggregate(Avg("friends__age")) + self.assertEqual(len(vals), 1) + self.assertAlmostEqual(vals["friends__age__avg"], 34.07, places=2) + + vals = Book.objects.filter(rating__lt=4.5).aggregate(Avg("authors__age")) + self.assertEqual(len(vals), 1) + self.assertAlmostEqual(vals["authors__age__avg"], 38.2857, places=2) + + vals = Author.objects.all().filter(name__contains="a").aggregate(Avg("book__rating")) + self.assertEqual(len(vals), 1) + self.assertEqual(vals["book__rating__avg"], 4.0) + + vals = Book.objects.aggregate(Sum("publisher__num_awards")) + self.assertEqual(len(vals), 1) + self.assertEquals(vals["publisher__num_awards__sum"], 30) + + vals = Publisher.objects.aggregate(Sum("book__price")) + self.assertEqual(len(vals), 1) + self.assertEqual(vals["book__price__sum"], Decimal("270.27")) + + def test_aggregate_multi_join(self): + vals = Store.objects.aggregate(Max("books__authors__age")) + self.assertEqual(len(vals), 1) + self.assertEqual(vals["books__authors__age__max"], 57) + + vals = Author.objects.aggregate(Min("book__publisher__num_awards")) + self.assertEqual(len(vals), 1) + self.assertEqual(vals["book__publisher__num_awards__min"], 1) + + def test_aggregate_alias(self): + vals = Store.objects.filter(name="Amazon.com").aggregate(amazon_mean=Avg("books__rating")) + self.assertEqual(len(vals), 1) + self.assertAlmostEqual(vals["amazon_mean"], 4.08, places=2) + + def test_annotate_basic(self): + self.assertQuerysetEqual( + Book.objects.annotate().order_by('pk'), [ + "The Definitive Guide to Django: Web Development Done Right", + "Sams Teach Yourself Django in 24 Hours", + "Practical Django Projects", + "Python Web Development with Django", + "Artificial Intelligence: A Modern Approach", + "Paradigms of Artificial Intelligence Programming: Case Studies in Common Lisp" + ], + lambda b: b.name + ) + + books = Book.objects.annotate(mean_age=Avg("authors__age")) + b = books.get(pk=1) + self.assertEqual( + b.name, + u'The Definitive Guide to Django: Web Development Done Right' + ) + self.assertEqual(b.mean_age, 34.5) + + def test_annotate_m2m(self): + books = Book.objects.filter(rating__lt=4.5).annotate(Avg("authors__age")).order_by("name") + self.assertQuerysetEqual( + books, [ + (u'Artificial Intelligence: A Modern Approach', 51.5), + (u'Practical Django Projects', 29.0), + (u'Python Web Development with Django', Approximate(30.3, places=1)), + (u'Sams Teach Yourself Django in 24 Hours', 45.0) + ], + lambda b: (b.name, b.authors__age__avg), + ) + + books = Book.objects.annotate(num_authors=Count("authors")).order_by("name") + self.assertQuerysetEqual( + books, [ + (u'Artificial Intelligence: A Modern Approach', 2), + (u'Paradigms of Artificial Intelligence Programming: Case Studies in Common Lisp', 1), + (u'Practical Django Projects', 1), + (u'Python Web Development with Django', 3), + (u'Sams Teach Yourself Django in 24 Hours', 1), + (u'The Definitive Guide to Django: Web Development Done Right', 2) + ], + lambda b: (b.name, b.num_authors) + ) + + def test_backwards_m2m_annotate(self): + authors = Author.objects.filter(name__contains="a").annotate(Avg("book__rating")).order_by("name") + self.assertQuerysetEqual( + authors, [ + (u'Adrian Holovaty', 4.5), + (u'Brad Dayley', 3.0), + (u'Jacob Kaplan-Moss', 4.5), + (u'James Bennett', 4.0), + (u'Paul Bissex', 4.0), + (u'Stuart Russell', 4.0) + ], + lambda a: (a.name, a.book__rating__avg) + ) + + authors = Author.objects.annotate(num_books=Count("book")).order_by("name") + self.assertQuerysetEqual( + authors, [ + (u'Adrian Holovaty', 1), + (u'Brad Dayley', 1), + (u'Jacob Kaplan-Moss', 1), + (u'James Bennett', 1), + (u'Jeffrey Forcier', 1), + (u'Paul Bissex', 1), + (u'Peter Norvig', 2), + (u'Stuart Russell', 1), + (u'Wesley J. Chun', 1) + ], + lambda a: (a.name, a.num_books) + ) + + def test_reverse_fkey_annotate(self): + books = Book.objects.annotate(Sum("publisher__num_awards")).order_by("name") + self.assertQuerysetEqual( + books, [ + (u'Artificial Intelligence: A Modern Approach', 7), + (u'Paradigms of Artificial Intelligence Programming: Case Studies in Common Lisp', 9), + (u'Practical Django Projects', 3), + (u'Python Web Development with Django', 7), + (u'Sams Teach Yourself Django in 24 Hours', 1), + (u'The Definitive Guide to Django: Web Development Done Right', 3) + ], + lambda b: (b.name, b.publisher__num_awards__sum) + ) + + publishers = Publisher.objects.annotate(Sum("book__price")).order_by("name") + self.assertQuerysetEqual( + publishers, [ + (u'Apress', Decimal("59.69")), + (u"Jonno's House of Books", None), + (u'Morgan Kaufmann', Decimal("75.00")), + (u'Prentice Hall', Decimal("112.49")), + (u'Sams', Decimal("23.09")) + ], + lambda p: (p.name, p.book__price__sum) + ) + + def test_annotate_values(self): + books = list(Book.objects.filter(pk=1).annotate(mean_age=Avg("authors__age")).values()) + self.assertEqual( + books, [ + { + "contact_id": 1, + "id": 1, + "isbn": "159059725", + "mean_age": 34.5, + "name": "The Definitive Guide to Django: Web Development Done Right", + "pages": 447, + "price": Approximate(Decimal("30")), + "pubdate": datetime.date(2007, 12, 6), + "publisher_id": 1, + "rating": 4.5, + } + ] + ) + + books = Book.objects.filter(pk=1).annotate(mean_age=Avg('authors__age')).values('pk', 'isbn', 'mean_age') + self.assertEqual( + list(books), [ + { + "pk": 1, + "isbn": "159059725", + "mean_age": 34.5, + } + ] + ) + + books = Book.objects.filter(pk=1).annotate(mean_age=Avg("authors__age")).values("name") + self.assertEqual( + list(books), [ + { + "name": "The Definitive Guide to Django: Web Development Done Right" + } + ] + ) + + books = Book.objects.filter(pk=1).values().annotate(mean_age=Avg('authors__age')) + self.assertEqual( + list(books), [ + { + "contact_id": 1, + "id": 1, + "isbn": "159059725", + "mean_age": 34.5, + "name": "The Definitive Guide to Django: Web Development Done Right", + "pages": 447, + "price": Approximate(Decimal("30")), + "pubdate": datetime.date(2007, 12, 6), + "publisher_id": 1, + "rating": 4.5, + } + ] + ) + + books = Book.objects.values("rating").annotate(n_authors=Count("authors__id"), mean_age=Avg("authors__age")).order_by("rating") + self.assertEqual( + list(books), [ + { + "rating": 3.0, + "n_authors": 1, + "mean_age": 45.0, + }, + { + "rating": 4.0, + "n_authors": 6, + "mean_age": Approximate(37.16, places=1) + }, + { + "rating": 4.5, + "n_authors": 2, + "mean_age": 34.5, + }, + { + "rating": 5.0, + "n_authors": 1, + "mean_age": 57.0, + } + ] + ) + + authors = Author.objects.annotate(Avg("friends__age")).order_by("name") + self.assertEqual(len(authors), 9) + self.assertQuerysetEqual( + authors, [ + (u'Adrian Holovaty', 32.0), + (u'Brad Dayley', None), + (u'Jacob Kaplan-Moss', 29.5), + (u'James Bennett', 34.0), + (u'Jeffrey Forcier', 27.0), + (u'Paul Bissex', 31.0), + (u'Peter Norvig', 46.0), + (u'Stuart Russell', 57.0), + (u'Wesley J. Chun', Approximate(33.66, places=1)) + ], + lambda a: (a.name, a.friends__age__avg) + ) + + def test_count(self): + vals = Book.objects.aggregate(Count("rating")) + self.assertEqual(vals, {"rating__count": 6}) + + vals = Book.objects.aggregate(Count("rating", distinct=True)) + self.assertEqual(vals, {"rating__count": 4}) + + def test_fkey_aggregate(self): + explicit = list(Author.objects.annotate(Count('book__id'))) + implicit = list(Author.objects.annotate(Count('book'))) + self.assertEqual(explicit, implicit) + + def test_annotate_ordering(self): + books = Book.objects.values('rating').annotate(oldest=Max('authors__age')).order_by('oldest', 'rating') + self.assertEqual( + list(books), [ + { + "rating": 4.5, + "oldest": 35, + }, + { + "rating": 3.0, + "oldest": 45 + }, + { + "rating": 4.0, + "oldest": 57, + }, + { + "rating": 5.0, + "oldest": 57, + } + ] + ) + + books = Book.objects.values("rating").annotate(oldest=Max("authors__age")).order_by("-oldest", "-rating") + self.assertEqual( + list(books), [ + { + "rating": 5.0, + "oldest": 57, + }, + { + "rating": 4.0, + "oldest": 57, + }, + { + "rating": 3.0, + "oldest": 45, + }, + { + "rating": 4.5, + "oldest": 35, + } + ] + ) + + def test_aggregate_annotation(self): + vals = Book.objects.annotate(num_authors=Count("authors__id")).aggregate(Avg("num_authors")) + self.assertEqual(vals, {"num_authors__avg": Approximate(1.66, places=1)}) + + def test_filtering(self): + p = Publisher.objects.create(name='Expensive Publisher', num_awards=0) + Book.objects.create( + name='ExpensiveBook1', + pages=1, + isbn='111', + rating=3.5, + price=Decimal("1000"), + publisher=p, + contact_id=1, + pubdate=datetime.date(2008,12,1) + ) + Book.objects.create( + name='ExpensiveBook2', + pages=1, + isbn='222', + rating=4.0, + price=Decimal("1000"), + publisher=p, + contact_id=1, + pubdate=datetime.date(2008,12,2) + ) + Book.objects.create( + name='ExpensiveBook3', + pages=1, + isbn='333', + rating=4.5, + price=Decimal("35"), + publisher=p, + contact_id=1, + pubdate=datetime.date(2008,12,3) + ) + + publishers = Publisher.objects.annotate(num_books=Count("book__id")).filter(num_books__gt=1).order_by("pk") + self.assertQuerysetEqual( + publishers, [ + "Apress", + "Prentice Hall", + "Expensive Publisher", + ], + lambda p: p.name, + ) + + publishers = Publisher.objects.filter(book__price__lt=Decimal("40.0")).order_by("pk") + self.assertQuerysetEqual( + publishers, [ + "Apress", + "Apress", + "Sams", + "Prentice Hall", + "Expensive Publisher", + ], + lambda p: p.name + ) + + publishers = Publisher.objects.annotate(num_books=Count("book__id")).filter(num_books__gt=1, book__price__lt=Decimal("40.0")).order_by("pk") + self.assertQuerysetEqual( + publishers, [ + "Apress", + "Prentice Hall", + "Expensive Publisher", + ], + lambda p: p.name, + ) + + publishers = Publisher.objects.filter(book__price__lt=Decimal("40.0")).annotate(num_books=Count("book__id")).filter(num_books__gt=1).order_by("pk") + self.assertQuerysetEqual( + publishers, [ + "Apress", + ], + lambda p: p.name + ) + + publishers = Publisher.objects.annotate(num_books=Count("book")).filter(num_books__range=[1, 3]).order_by("pk") + self.assertQuerysetEqual( + publishers, [ + "Apress", + "Sams", + "Prentice Hall", + "Morgan Kaufmann", + "Expensive Publisher", + ], + lambda p: p.name + ) + + publishers = Publisher.objects.annotate(num_books=Count("book")).filter(num_books__range=[1, 2]).order_by("pk") + self.assertQuerysetEqual( + publishers, [ + "Apress", + "Sams", + "Prentice Hall", + "Morgan Kaufmann", + ], + lambda p: p.name + ) + + publishers = Publisher.objects.annotate(num_books=Count("book")).filter(num_books__in=[1, 3]).order_by("pk") + self.assertQuerysetEqual( + publishers, [ + "Sams", + "Morgan Kaufmann", + "Expensive Publisher", + ], + lambda p: p.name, + ) + + publishers = Publisher.objects.annotate(num_books=Count("book")).filter(num_books__isnull=True) + self.assertEqual(len(publishers), 0) + + def test_annotation(self): + vals = Author.objects.filter(pk=1).aggregate(Count("friends__id")) + self.assertEqual(vals, {"friends__id__count": 2}) + + books = Book.objects.annotate(num_authors=Count("authors__name")).filter(num_authors__ge=2).order_by("pk") + self.assertQuerysetEqual( + books, [ + "The Definitive Guide to Django: Web Development Done Right", + "Artificial Intelligence: A Modern Approach", + ], + lambda b: b.name + ) + + authors = Author.objects.annotate(num_friends=Count("friends__id", distinct=True)).filter(num_friends=0).order_by("pk") + self.assertQuerysetEqual( + authors, [ + "Brad Dayley", + ], + lambda a: a.name + ) + + publishers = Publisher.objects.annotate(num_books=Count("book__id")).filter(num_books__gt=1).order_by("pk") + self.assertQuerysetEqual( + publishers, [ + "Apress", + "Prentice Hall", + ], + lambda p: p.name + ) + + publishers = Publisher.objects.filter(book__price__lt=Decimal("40.0")).annotate(num_books=Count("book__id")).filter(num_books__gt=1) + self.assertQuerysetEqual( + publishers, [ + "Apress", + ], + lambda p: p.name + ) + + books = Book.objects.annotate(num_authors=Count("authors__id")).filter(authors__name__contains="Norvig", num_authors__gt=1) + self.assertQuerysetEqual( + books, [ + "Artificial Intelligence: A Modern Approach", + ], + lambda b: b.name + ) + + def test_more_aggregation(self): + a = Author.objects.get(name__contains='Norvig') + b = Book.objects.get(name__contains='Done Right') + b.authors.add(a) + b.save() + + vals = Book.objects.annotate(num_authors=Count("authors__id")).filter(authors__name__contains="Norvig", num_authors__gt=1).aggregate(Avg("rating")) + self.assertEqual(vals, {"rating__avg": 4.25}) + + def test_even_more_aggregate(self): + publishers = Publisher.objects.annotate(earliest_book=Min("book__pubdate")).exclude(earliest_book=None).order_by("earliest_book").values() + self.assertEqual( + list(publishers), [ + { + 'earliest_book': datetime.date(1991, 10, 15), + 'num_awards': 9, + 'id': 4, + 'name': u'Morgan Kaufmann' + }, + { + 'earliest_book': datetime.date(1995, 1, 15), + 'num_awards': 7, + 'id': 3, + 'name': u'Prentice Hall' + }, + { + 'earliest_book': datetime.date(2007, 12, 6), + 'num_awards': 3, + 'id': 1, + 'name': u'Apress' + }, + { + 'earliest_book': datetime.date(2008, 3, 3), + 'num_awards': 1, + 'id': 2, + 'name': u'Sams' + } + ] + ) + + vals = Store.objects.aggregate(Max("friday_night_closing"), Min("original_opening")) + self.assertEqual( + vals, + { + "friday_night_closing__max": datetime.time(23, 59, 59), + "original_opening__min": datetime.datetime(1945, 4, 25, 16, 24, 14), + } + ) + + def test_annotate_values_list(self): + books = Book.objects.filter(pk=1).annotate(mean_age=Avg("authors__age")).values_list("pk", "isbn", "mean_age") + self.assertEqual( + list(books), [ + (1, "159059725", 34.5), + ] + ) + + books = Book.objects.filter(pk=1).annotate(mean_age=Avg("authors__age")).values_list("isbn") + self.assertEqual( + list(books), [ + ('159059725',) + ] + ) + + books = Book.objects.filter(pk=1).annotate(mean_age=Avg("authors__age")).values_list("mean_age") + self.assertEqual( + list(books), [ + (34.5,) + ] + ) + + books = Book.objects.filter(pk=1).annotate(mean_age=Avg("authors__age")).values_list("mean_age", flat=True) + self.assertEqual(list(books), [34.5]) + + books = Book.objects.values_list("price").annotate(count=Count("price")).order_by("-count", "price") + self.assertEqual( + list(books), [ + (Decimal("29.69"), 2), + (Decimal('23.09'), 1), + (Decimal('30'), 1), + (Decimal('75'), 1), + (Decimal('82.8'), 1), + ] + ) diff --git a/parts/django/tests/modeltests/basic/__init__.py b/parts/django/tests/modeltests/basic/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/modeltests/basic/__init__.py diff --git a/parts/django/tests/modeltests/basic/models.py b/parts/django/tests/modeltests/basic/models.py new file mode 100644 index 0000000..97552a9 --- /dev/null +++ b/parts/django/tests/modeltests/basic/models.py @@ -0,0 +1,17 @@ +# coding: utf-8 +""" +1. Bare-bones model + +This is a basic model with only two non-primary-key fields. +""" +from django.db import models, DEFAULT_DB_ALIAS + +class Article(models.Model): + headline = models.CharField(max_length=100, default='Default headline') + pub_date = models.DateTimeField() + + class Meta: + ordering = ('pub_date','headline') + + def __unicode__(self): + return self.headline diff --git a/parts/django/tests/modeltests/basic/tests.py b/parts/django/tests/modeltests/basic/tests.py new file mode 100644 index 0000000..bafe9a0 --- /dev/null +++ b/parts/django/tests/modeltests/basic/tests.py @@ -0,0 +1,562 @@ +from datetime import datetime +import re + +from django.conf import settings +from django.core.exceptions import ObjectDoesNotExist +from django.db import models, DEFAULT_DB_ALIAS, connection +from django.db.models.fields import FieldDoesNotExist +from django.test import TestCase + +from models import Article + + +class ModelTest(TestCase): + def assertRaisesErrorWithMessage(self, error, message, callable, *args, **kwargs): + self.assertRaises(error, callable, *args, **kwargs) + try: + callable(*args, **kwargs) + except error, e: + self.assertEqual(message, str(e)) + + def test_lookup(self): + # No articles are in the system yet. + self.assertQuerysetEqual(Article.objects.all(), []) + + # Create an Article. + a = Article( + id=None, + headline='Area man programs in Python', + pub_date=datetime(2005, 7, 28), + ) + + # Save it into the database. You have to call save() explicitly. + a.save() + + # Now it has an ID. + self.assertTrue(a.id != None) + + # Models have a pk property that is an alias for the primary key + # attribute (by default, the 'id' attribute). + self.assertEqual(a.pk, a.id) + + # Access database columns via Python attributes. + self.assertEqual(a.headline, 'Area man programs in Python') + self.assertEqual(a.pub_date, datetime(2005, 7, 28, 0, 0)) + + # Change values by changing the attributes, then calling save(). + a.headline = 'Area woman programs in Python' + a.save() + + # Article.objects.all() returns all the articles in the database. + self.assertQuerysetEqual(Article.objects.all(), + ['<Article: Area woman programs in Python>']) + + # Django provides a rich database lookup API. + self.assertEqual(Article.objects.get(id__exact=a.id), a) + self.assertEqual(Article.objects.get(headline__startswith='Area woman'), a) + self.assertEqual(Article.objects.get(pub_date__year=2005), a) + self.assertEqual(Article.objects.get(pub_date__year=2005, pub_date__month=7), a) + self.assertEqual(Article.objects.get(pub_date__year=2005, pub_date__month=7, pub_date__day=28), a) + self.assertEqual(Article.objects.get(pub_date__week_day=5), a) + + # The "__exact" lookup type can be omitted, as a shortcut. + self.assertEqual(Article.objects.get(id=a.id), a) + self.assertEqual(Article.objects.get(headline='Area woman programs in Python'), a) + + self.assertQuerysetEqual( + Article.objects.filter(pub_date__year=2005), + ['<Article: Area woman programs in Python>'], + ) + self.assertQuerysetEqual( + Article.objects.filter(pub_date__year=2004), + [], + ) + self.assertQuerysetEqual( + Article.objects.filter(pub_date__year=2005, pub_date__month=7), + ['<Article: Area woman programs in Python>'], + ) + + self.assertQuerysetEqual( + Article.objects.filter(pub_date__week_day=5), + ['<Article: Area woman programs in Python>'], + ) + self.assertQuerysetEqual( + Article.objects.filter(pub_date__week_day=6), + [], + ) + + # Django raises an Article.DoesNotExist exception for get() if the + # parameters don't match any object. + self.assertRaisesErrorWithMessage( + ObjectDoesNotExist, + "Article matching query does not exist.", + Article.objects.get, + id__exact=2000, + ) + + self.assertRaisesErrorWithMessage( + ObjectDoesNotExist, + "Article matching query does not exist.", + Article.objects.get, + pub_date__year=2005, + pub_date__month=8, + ) + + self.assertRaisesErrorWithMessage( + ObjectDoesNotExist, + "Article matching query does not exist.", + Article.objects.get, + pub_date__week_day=6, + ) + + # Lookup by a primary key is the most common case, so Django + # provides a shortcut for primary-key exact lookups. + # The following is identical to articles.get(id=a.id). + self.assertEqual(Article.objects.get(pk=a.id), a) + + # pk can be used as a shortcut for the primary key name in any query. + self.assertQuerysetEqual(Article.objects.filter(pk__in=[a.id]), + ["<Article: Area woman programs in Python>"]) + + # Model instances of the same type and same ID are considered equal. + a = Article.objects.get(pk=a.id) + b = Article.objects.get(pk=a.id) + self.assertEqual(a, b) + + def test_object_creation(self): + # Create an Article. + a = Article( + id=None, + headline='Area man programs in Python', + pub_date=datetime(2005, 7, 28), + ) + + # Save it into the database. You have to call save() explicitly. + a.save() + + # You can initialize a model instance using positional arguments, + # which should match the field order as defined in the model. + a2 = Article(None, 'Second article', datetime(2005, 7, 29)) + a2.save() + + self.assertNotEqual(a2.id, a.id) + self.assertEqual(a2.headline, 'Second article') + self.assertEqual(a2.pub_date, datetime(2005, 7, 29, 0, 0)) + + # ...or, you can use keyword arguments. + a3 = Article( + id=None, + headline='Third article', + pub_date=datetime(2005, 7, 30), + ) + a3.save() + + self.assertNotEqual(a3.id, a.id) + self.assertNotEqual(a3.id, a2.id) + self.assertEqual(a3.headline, 'Third article') + self.assertEqual(a3.pub_date, datetime(2005, 7, 30, 0, 0)) + + # You can also mix and match position and keyword arguments, but + # be sure not to duplicate field information. + a4 = Article(None, 'Fourth article', pub_date=datetime(2005, 7, 31)) + a4.save() + self.assertEqual(a4.headline, 'Fourth article') + + # Don't use invalid keyword arguments. + self.assertRaisesErrorWithMessage( + TypeError, + "'foo' is an invalid keyword argument for this function", + Article, + id=None, + headline='Invalid', + pub_date=datetime(2005, 7, 31), + foo='bar', + ) + + # You can leave off the value for an AutoField when creating an + # object, because it'll get filled in automatically when you save(). + a5 = Article(headline='Article 6', pub_date=datetime(2005, 7, 31)) + a5.save() + self.assertEqual(a5.headline, 'Article 6') + + # If you leave off a field with "default" set, Django will use + # the default. + a6 = Article(pub_date=datetime(2005, 7, 31)) + a6.save() + self.assertEqual(a6.headline, u'Default headline') + + # For DateTimeFields, Django saves as much precision (in seconds) + # as you give it. + a7 = Article( + headline='Article 7', + pub_date=datetime(2005, 7, 31, 12, 30), + ) + a7.save() + self.assertEqual(Article.objects.get(id__exact=a7.id).pub_date, + datetime(2005, 7, 31, 12, 30)) + + a8 = Article( + headline='Article 8', + pub_date=datetime(2005, 7, 31, 12, 30, 45), + ) + a8.save() + self.assertEqual(Article.objects.get(id__exact=a8.id).pub_date, + datetime(2005, 7, 31, 12, 30, 45)) + + # Saving an object again doesn't create a new object -- it just saves + # the old one. + current_id = a8.id + a8.save() + self.assertEqual(a8.id, current_id) + a8.headline = 'Updated article 8' + a8.save() + self.assertEqual(a8.id, current_id) + + # Check that != and == operators behave as expecte on instances + self.assertTrue(a7 != a8) + self.assertFalse(a7 == a8) + self.assertEqual(a8, Article.objects.get(id__exact=a8.id)) + + self.assertTrue(Article.objects.get(id__exact=a8.id) != Article.objects.get(id__exact=a7.id)) + self.assertFalse(Article.objects.get(id__exact=a8.id) == Article.objects.get(id__exact=a7.id)) + + # You can use 'in' to test for membership... + self.assertTrue(a8 in Article.objects.all()) + + # ... but there will often be more efficient ways if that is all you need: + self.assertTrue(Article.objects.filter(id=a8.id).exists()) + + # dates() returns a list of available dates of the given scope for + # the given field. + self.assertQuerysetEqual( + Article.objects.dates('pub_date', 'year'), + ["datetime.datetime(2005, 1, 1, 0, 0)"]) + self.assertQuerysetEqual( + Article.objects.dates('pub_date', 'month'), + ["datetime.datetime(2005, 7, 1, 0, 0)"]) + self.assertQuerysetEqual( + Article.objects.dates('pub_date', 'day'), + ["datetime.datetime(2005, 7, 28, 0, 0)", + "datetime.datetime(2005, 7, 29, 0, 0)", + "datetime.datetime(2005, 7, 30, 0, 0)", + "datetime.datetime(2005, 7, 31, 0, 0)"]) + self.assertQuerysetEqual( + Article.objects.dates('pub_date', 'day', order='ASC'), + ["datetime.datetime(2005, 7, 28, 0, 0)", + "datetime.datetime(2005, 7, 29, 0, 0)", + "datetime.datetime(2005, 7, 30, 0, 0)", + "datetime.datetime(2005, 7, 31, 0, 0)"]) + self.assertQuerysetEqual( + Article.objects.dates('pub_date', 'day', order='DESC'), + ["datetime.datetime(2005, 7, 31, 0, 0)", + "datetime.datetime(2005, 7, 30, 0, 0)", + "datetime.datetime(2005, 7, 29, 0, 0)", + "datetime.datetime(2005, 7, 28, 0, 0)"]) + + # dates() requires valid arguments. + self.assertRaisesErrorWithMessage( + TypeError, + "dates() takes at least 3 arguments (1 given)", + Article.objects.dates, + ) + + self.assertRaisesErrorWithMessage( + FieldDoesNotExist, + "Article has no field named 'invalid_field'", + Article.objects.dates, + "invalid_field", + "year", + ) + + self.assertRaisesErrorWithMessage( + AssertionError, + "'kind' must be one of 'year', 'month' or 'day'.", + Article.objects.dates, + "pub_date", + "bad_kind", + ) + + self.assertRaisesErrorWithMessage( + AssertionError, + "'order' must be either 'ASC' or 'DESC'.", + Article.objects.dates, + "pub_date", + "year", + order="bad order", + ) + + # Use iterator() with dates() to return a generator that lazily + # requests each result one at a time, to save memory. + dates = [] + for article in Article.objects.dates('pub_date', 'day', order='DESC').iterator(): + dates.append(article) + self.assertEqual(dates, [ + datetime(2005, 7, 31, 0, 0), + datetime(2005, 7, 30, 0, 0), + datetime(2005, 7, 29, 0, 0), + datetime(2005, 7, 28, 0, 0)]) + + # You can combine queries with & and |. + s1 = Article.objects.filter(id__exact=a.id) + s2 = Article.objects.filter(id__exact=a2.id) + self.assertQuerysetEqual(s1 | s2, + ["<Article: Area man programs in Python>", + "<Article: Second article>"]) + self.assertQuerysetEqual(s1 & s2, []) + + # You can get the number of objects like this: + self.assertEqual(len(Article.objects.filter(id__exact=a.id)), 1) + + # You can get items using index and slice notation. + self.assertEqual(Article.objects.all()[0], a) + self.assertQuerysetEqual(Article.objects.all()[1:3], + ["<Article: Second article>", "<Article: Third article>"]) + + s3 = Article.objects.filter(id__exact=a3.id) + self.assertQuerysetEqual((s1 | s2 | s3)[::2], + ["<Article: Area man programs in Python>", + "<Article: Third article>"]) + + # Slicing works with longs. + self.assertEqual(Article.objects.all()[0L], a) + self.assertQuerysetEqual(Article.objects.all()[1L:3L], + ["<Article: Second article>", "<Article: Third article>"]) + self.assertQuerysetEqual((s1 | s2 | s3)[::2L], + ["<Article: Area man programs in Python>", + "<Article: Third article>"]) + + # And can be mixed with ints. + self.assertQuerysetEqual(Article.objects.all()[1:3L], + ["<Article: Second article>", "<Article: Third article>"]) + + # Slices (without step) are lazy: + self.assertQuerysetEqual(Article.objects.all()[0:5].filter(), + ["<Article: Area man programs in Python>", + "<Article: Second article>", + "<Article: Third article>", + "<Article: Article 6>", + "<Article: Default headline>"]) + + # Slicing again works: + self.assertQuerysetEqual(Article.objects.all()[0:5][0:2], + ["<Article: Area man programs in Python>", + "<Article: Second article>"]) + self.assertQuerysetEqual(Article.objects.all()[0:5][:2], + ["<Article: Area man programs in Python>", + "<Article: Second article>"]) + self.assertQuerysetEqual(Article.objects.all()[0:5][4:], + ["<Article: Default headline>"]) + self.assertQuerysetEqual(Article.objects.all()[0:5][5:], []) + + # Some more tests! + self.assertQuerysetEqual(Article.objects.all()[2:][0:2], + ["<Article: Third article>", "<Article: Article 6>"]) + self.assertQuerysetEqual(Article.objects.all()[2:][:2], + ["<Article: Third article>", "<Article: Article 6>"]) + self.assertQuerysetEqual(Article.objects.all()[2:][2:3], + ["<Article: Default headline>"]) + + # Using an offset without a limit is also possible. + self.assertQuerysetEqual(Article.objects.all()[5:], + ["<Article: Fourth article>", + "<Article: Article 7>", + "<Article: Updated article 8>"]) + + # Also, once you have sliced you can't filter, re-order or combine + self.assertRaisesErrorWithMessage( + AssertionError, + "Cannot filter a query once a slice has been taken.", + Article.objects.all()[0:5].filter, + id=a.id, + ) + + self.assertRaisesErrorWithMessage( + AssertionError, + "Cannot reorder a query once a slice has been taken.", + Article.objects.all()[0:5].order_by, + 'id', + ) + + try: + Article.objects.all()[0:1] & Article.objects.all()[4:5] + self.fail('Should raise an AssertionError') + except AssertionError, e: + self.assertEqual(str(e), "Cannot combine queries once a slice has been taken.") + except Exception, e: + self.fail('Should raise an AssertionError, not %s' % e) + + # Negative slices are not supported, due to database constraints. + # (hint: inverting your ordering might do what you need). + try: + Article.objects.all()[-1] + self.fail('Should raise an AssertionError') + except AssertionError, e: + self.assertEqual(str(e), "Negative indexing is not supported.") + except Exception, e: + self.fail('Should raise an AssertionError, not %s' % e) + + error = None + try: + Article.objects.all()[0:-5] + except Exception, e: + error = e + self.assertTrue(isinstance(error, AssertionError)) + self.assertEqual(str(error), "Negative indexing is not supported.") + + # An Article instance doesn't have access to the "objects" attribute. + # That's only available on the class. + self.assertRaisesErrorWithMessage( + AttributeError, + "Manager isn't accessible via Article instances", + getattr, + a7, + "objects", + ) + + # Bulk delete test: How many objects before and after the delete? + self.assertQuerysetEqual(Article.objects.all(), + ["<Article: Area man programs in Python>", + "<Article: Second article>", + "<Article: Third article>", + "<Article: Article 6>", + "<Article: Default headline>", + "<Article: Fourth article>", + "<Article: Article 7>", + "<Article: Updated article 8>"]) + Article.objects.filter(id__lte=a4.id).delete() + self.assertQuerysetEqual(Article.objects.all(), + ["<Article: Article 6>", + "<Article: Default headline>", + "<Article: Article 7>", + "<Article: Updated article 8>"]) + + if settings.DATABASES[DEFAULT_DB_ALIAS]['ENGINE'].startswith('django.db.backends.postgresql'): + def test_microsecond_precision(self): + # In PostgreSQL, microsecond-level precision is available. + a9 = Article( + headline='Article 9', + pub_date=datetime(2005, 7, 31, 12, 30, 45, 180), + ) + a9.save() + self.assertEqual(Article.objects.get(pk=a9.pk).pub_date, + datetime(2005, 7, 31, 12, 30, 45, 180)) + + if settings.DATABASES[DEFAULT_DB_ALIAS]['ENGINE'] == 'django.db.backends.mysql': + def test_microsecond_precision_not_supported(self): + # In MySQL, microsecond-level precision isn't available. You'll lose + # microsecond-level precision once the data is saved. + a9 = Article( + headline='Article 9', + pub_date=datetime(2005, 7, 31, 12, 30, 45, 180), + ) + a9.save() + self.assertEqual(Article.objects.get(id__exact=a9.id).pub_date, + datetime(2005, 7, 31, 12, 30, 45)) + + def test_manually_specify_primary_key(self): + # You can manually specify the primary key when creating a new object. + a101 = Article( + id=101, + headline='Article 101', + pub_date=datetime(2005, 7, 31, 12, 30, 45), + ) + a101.save() + a101 = Article.objects.get(pk=101) + self.assertEqual(a101.headline, u'Article 101') + + def test_create_method(self): + # You can create saved objects in a single step + a10 = Article.objects.create( + headline="Article 10", + pub_date=datetime(2005, 7, 31, 12, 30, 45), + ) + self.assertEqual(Article.objects.get(headline="Article 10"), a10) + + def test_year_lookup_edge_case(self): + # Edge-case test: A year lookup should retrieve all objects in + # the given year, including Jan. 1 and Dec. 31. + a11 = Article.objects.create( + headline='Article 11', + pub_date=datetime(2008, 1, 1), + ) + a12 = Article.objects.create( + headline='Article 12', + pub_date=datetime(2008, 12, 31, 23, 59, 59, 999999), + ) + self.assertQuerysetEqual(Article.objects.filter(pub_date__year=2008), + ["<Article: Article 11>", "<Article: Article 12>"]) + + def test_unicode_data(self): + # Unicode data works, too. + a = Article( + headline=u'\u6797\u539f \u3081\u3050\u307f', + pub_date=datetime(2005, 7, 28), + ) + a.save() + self.assertEqual(Article.objects.get(pk=a.id).headline, + u'\u6797\u539f \u3081\u3050\u307f') + + def test_hash_function(self): + # Model instances have a hash function, so they can be used in sets + # or as dictionary keys. Two models compare as equal if their primary + # keys are equal. + a10 = Article.objects.create( + headline="Article 10", + pub_date=datetime(2005, 7, 31, 12, 30, 45), + ) + a11 = Article.objects.create( + headline='Article 11', + pub_date=datetime(2008, 1, 1), + ) + a12 = Article.objects.create( + headline='Article 12', + pub_date=datetime(2008, 12, 31, 23, 59, 59, 999999), + ) + + s = set([a10, a11, a12]) + self.assertTrue(Article.objects.get(headline='Article 11') in s) + + def test_extra_method_select_argument_with_dashes_and_values(self): + # The 'select' argument to extra() supports names with dashes in + # them, as long as you use values(). + a10 = Article.objects.create( + headline="Article 10", + pub_date=datetime(2005, 7, 31, 12, 30, 45), + ) + a11 = Article.objects.create( + headline='Article 11', + pub_date=datetime(2008, 1, 1), + ) + a12 = Article.objects.create( + headline='Article 12', + pub_date=datetime(2008, 12, 31, 23, 59, 59, 999999), + ) + + dicts = Article.objects.filter( + pub_date__year=2008).extra( + select={'dashed-value': '1'} + ).values('headline', 'dashed-value') + self.assertEqual([sorted(d.items()) for d in dicts], + [[('dashed-value', 1), ('headline', u'Article 11')], [('dashed-value', 1), ('headline', u'Article 12')]]) + + def test_extra_method_select_argument_with_dashes(self): + # If you use 'select' with extra() and names containing dashes on a + # query that's *not* a values() query, those extra 'select' values + # will silently be ignored. + a10 = Article.objects.create( + headline="Article 10", + pub_date=datetime(2005, 7, 31, 12, 30, 45), + ) + a11 = Article.objects.create( + headline='Article 11', + pub_date=datetime(2008, 1, 1), + ) + a12 = Article.objects.create( + headline='Article 12', + pub_date=datetime(2008, 12, 31, 23, 59, 59, 999999), + ) + + articles = Article.objects.filter( + pub_date__year=2008).extra( + select={'dashed-value': '1', 'undashedvalue': '2'}) + self.assertEqual(articles[0].undashedvalue, 2) diff --git a/parts/django/tests/modeltests/choices/__init__.py b/parts/django/tests/modeltests/choices/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/modeltests/choices/__init__.py diff --git a/parts/django/tests/modeltests/choices/models.py b/parts/django/tests/modeltests/choices/models.py new file mode 100644 index 0000000..27316f5 --- /dev/null +++ b/parts/django/tests/modeltests/choices/models.py @@ -0,0 +1,24 @@ +""" +21. Specifying 'choices' for a field + +Most fields take a ``choices`` parameter, which should be a tuple of tuples +specifying which are the valid values for that field. + +For each field that has ``choices``, a model instance gets a +``get_fieldname_display()`` method, where ``fieldname`` is the name of the +field. This method returns the "human-readable" value of the field. +""" + +from django.db import models + +GENDER_CHOICES = ( + ('M', 'Male'), + ('F', 'Female'), +) + +class Person(models.Model): + name = models.CharField(max_length=20) + gender = models.CharField(max_length=1, choices=GENDER_CHOICES) + + def __unicode__(self): + return self.name diff --git a/parts/django/tests/modeltests/choices/tests.py b/parts/django/tests/modeltests/choices/tests.py new file mode 100644 index 0000000..09023d8 --- /dev/null +++ b/parts/django/tests/modeltests/choices/tests.py @@ -0,0 +1,23 @@ +from django.test import TestCase + +from models import Person + + +class ChoicesTests(TestCase): + def test_display(self): + a = Person.objects.create(name='Adrian', gender='M') + s = Person.objects.create(name='Sara', gender='F') + self.assertEqual(a.gender, 'M') + self.assertEqual(s.gender, 'F') + + self.assertEqual(a.get_gender_display(), 'Male') + self.assertEqual(s.get_gender_display(), 'Female') + + # If the value for the field doesn't correspond to a valid choice, + # the value itself is provided as a display value. + a.gender = '' + self.assertEqual(a.get_gender_display(), '') + + a.gender = 'U' + self.assertEqual(a.get_gender_display(), 'U') + diff --git a/parts/django/tests/modeltests/custom_columns/__init__.py b/parts/django/tests/modeltests/custom_columns/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/modeltests/custom_columns/__init__.py diff --git a/parts/django/tests/modeltests/custom_columns/models.py b/parts/django/tests/modeltests/custom_columns/models.py new file mode 100644 index 0000000..651f8a6 --- /dev/null +++ b/parts/django/tests/modeltests/custom_columns/models.py @@ -0,0 +1,40 @@ +""" +17. Custom column/table names + +If your database column name is different than your model attribute, use the +``db_column`` parameter. Note that you'll use the field's name, not its column +name, in API usage. + +If your database table name is different than your model name, use the +``db_table`` Meta attribute. This has no effect on the API used to +query the database. + +If you need to use a table name for a many-to-many relationship that differs +from the default generated name, use the ``db_table`` parameter on the +``ManyToManyField``. This has no effect on the API for querying the database. + +""" + +from django.db import models + +class Author(models.Model): + first_name = models.CharField(max_length=30, db_column='firstname') + last_name = models.CharField(max_length=30, db_column='last') + + def __unicode__(self): + return u'%s %s' % (self.first_name, self.last_name) + + class Meta: + db_table = 'my_author_table' + ordering = ('last_name','first_name') + +class Article(models.Model): + headline = models.CharField(max_length=100) + authors = models.ManyToManyField(Author, db_table='my_m2m_table') + + def __unicode__(self): + return self.headline + + class Meta: + ordering = ('headline',) + diff --git a/parts/django/tests/modeltests/custom_columns/tests.py b/parts/django/tests/modeltests/custom_columns/tests.py new file mode 100644 index 0000000..f38f087 --- /dev/null +++ b/parts/django/tests/modeltests/custom_columns/tests.py @@ -0,0 +1,71 @@ +from django.core.exceptions import FieldError +from django.test import TestCase + +from models import Author, Article + + +class CustomColumnsTests(TestCase): + def test_db_column(self): + a1 = Author.objects.create(first_name="John", last_name="Smith") + a2 = Author.objects.create(first_name="Peter", last_name="Jones") + + art = Article.objects.create(headline="Django lets you build Web apps easily") + art.authors = [a1, a2] + + # Although the table and column names on Author have been set to custom + # values, nothing about using the Author model has changed... + + # Query the available authors + self.assertQuerysetEqual( + Author.objects.all(), [ + "Peter Jones", "John Smith", + ], + unicode + ) + self.assertQuerysetEqual( + Author.objects.filter(first_name__exact="John"), [ + "John Smith", + ], + unicode + ) + self.assertEqual( + Author.objects.get(first_name__exact="John"), + a1, + ) + + self.assertRaises(FieldError, + lambda: Author.objects.filter(firstname__exact="John") + ) + + a = Author.objects.get(last_name__exact="Smith") + a.first_name = "John" + a.last_name = "Smith" + + self.assertRaises(AttributeError, lambda: a.firstname) + self.assertRaises(AttributeError, lambda: a.last) + + # Although the Article table uses a custom m2m table, + # nothing about using the m2m relationship has changed... + + # Get all the authors for an article + self.assertQuerysetEqual( + art.authors.all(), [ + "Peter Jones", + "John Smith", + ], + unicode + ) + # Get the articles for an author + self.assertQuerysetEqual( + a.article_set.all(), [ + "Django lets you build Web apps easily", + ], + lambda a: a.headline + ) + # Query the authors across the m2m relation + self.assertQuerysetEqual( + art.authors.filter(last_name='Jones'), [ + "Peter Jones" + ], + unicode + ) diff --git a/parts/django/tests/modeltests/custom_managers/__init__.py b/parts/django/tests/modeltests/custom_managers/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/modeltests/custom_managers/__init__.py diff --git a/parts/django/tests/modeltests/custom_managers/models.py b/parts/django/tests/modeltests/custom_managers/models.py new file mode 100644 index 0000000..1052552 --- /dev/null +++ b/parts/django/tests/modeltests/custom_managers/models.py @@ -0,0 +1,59 @@ +""" +23. Giving models a custom manager + +You can use a custom ``Manager`` in a particular model by extending the base +``Manager`` class and instantiating your custom ``Manager`` in your model. + +There are two reasons you might want to customize a ``Manager``: to add extra +``Manager`` methods, and/or to modify the initial ``QuerySet`` the ``Manager`` +returns. +""" + +from django.db import models + +# An example of a custom manager called "objects". + +class PersonManager(models.Manager): + def get_fun_people(self): + return self.filter(fun=True) + +class Person(models.Model): + first_name = models.CharField(max_length=30) + last_name = models.CharField(max_length=30) + fun = models.BooleanField() + objects = PersonManager() + + def __unicode__(self): + return u"%s %s" % (self.first_name, self.last_name) + +# An example of a custom manager that sets get_query_set(). + +class PublishedBookManager(models.Manager): + def get_query_set(self): + return super(PublishedBookManager, self).get_query_set().filter(is_published=True) + +class Book(models.Model): + title = models.CharField(max_length=50) + author = models.CharField(max_length=30) + is_published = models.BooleanField() + published_objects = PublishedBookManager() + authors = models.ManyToManyField(Person, related_name='books') + + def __unicode__(self): + return self.title + +# An example of providing multiple custom managers. + +class FastCarManager(models.Manager): + def get_query_set(self): + return super(FastCarManager, self).get_query_set().filter(top_speed__gt=150) + +class Car(models.Model): + name = models.CharField(max_length=10) + mileage = models.IntegerField() + top_speed = models.IntegerField(help_text="In miles per hour.") + cars = models.Manager() + fast_cars = FastCarManager() + + def __unicode__(self): + return self.name diff --git a/parts/django/tests/modeltests/custom_managers/tests.py b/parts/django/tests/modeltests/custom_managers/tests.py new file mode 100644 index 0000000..8721e9a --- /dev/null +++ b/parts/django/tests/modeltests/custom_managers/tests.py @@ -0,0 +1,71 @@ +from django.test import TestCase + +from models import Person, Book, Car, PersonManager, PublishedBookManager + + +class CustomManagerTests(TestCase): + def test_manager(self): + p1 = Person.objects.create(first_name="Bugs", last_name="Bunny", fun=True) + p2 = Person.objects.create(first_name="Droopy", last_name="Dog", fun=False) + + self.assertQuerysetEqual( + Person.objects.get_fun_people(), [ + "Bugs Bunny" + ], + unicode + ) + # The RelatedManager used on the 'books' descriptor extends the default + # manager + self.assertTrue(isinstance(p2.books, PublishedBookManager)) + + b1 = Book.published_objects.create( + title="How to program", author="Rodney Dangerfield", is_published=True + ) + b2 = Book.published_objects.create( + title="How to be smart", author="Albert Einstein", is_published=False + ) + + # The default manager, "objects", doesn't exist, because a custom one + # was provided. + self.assertRaises(AttributeError, lambda: Book.objects) + + # The RelatedManager used on the 'authors' descriptor extends the + # default manager + self.assertTrue(isinstance(b2.authors, PersonManager)) + + self.assertQuerysetEqual( + Book.published_objects.all(), [ + "How to program", + ], + lambda b: b.title + ) + + c1 = Car.cars.create(name="Corvette", mileage=21, top_speed=180) + c2 = Car.cars.create(name="Neon", mileage=31, top_speed=100) + + self.assertQuerysetEqual( + Car.cars.order_by("name"), [ + "Corvette", + "Neon", + ], + lambda c: c.name + ) + + self.assertQuerysetEqual( + Car.fast_cars.all(), [ + "Corvette", + ], + lambda c: c.name + ) + + # Each model class gets a "_default_manager" attribute, which is a + # reference to the first manager defined in the class. In this case, + # it's "cars". + + self.assertQuerysetEqual( + Car._default_manager.order_by("name"), [ + "Corvette", + "Neon", + ], + lambda c: c.name + ) diff --git a/parts/django/tests/modeltests/custom_methods/__init__.py b/parts/django/tests/modeltests/custom_methods/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/modeltests/custom_methods/__init__.py diff --git a/parts/django/tests/modeltests/custom_methods/models.py b/parts/django/tests/modeltests/custom_methods/models.py new file mode 100644 index 0000000..15150a6 --- /dev/null +++ b/parts/django/tests/modeltests/custom_methods/models.py @@ -0,0 +1,36 @@ +""" +3. Giving models custom methods + +Any method you add to a model will be available to instances. +""" + +from django.db import models +import datetime + +class Article(models.Model): + headline = models.CharField(max_length=100) + pub_date = models.DateField() + + def __unicode__(self): + return self.headline + + def was_published_today(self): + return self.pub_date == datetime.date.today() + + def articles_from_same_day_1(self): + return Article.objects.filter(pub_date=self.pub_date).exclude(id=self.id) + + def articles_from_same_day_2(self): + """ + Verbose version of get_articles_from_same_day_1, which does a custom + database query for the sake of demonstration. + """ + from django.db import connection + cursor = connection.cursor() + cursor.execute(""" + SELECT id, headline, pub_date + FROM custom_methods_article + WHERE pub_date = %s + AND id != %s""", [connection.ops.value_to_db_date(self.pub_date), + self.id]) + return [self.__class__(*row) for row in cursor.fetchall()] diff --git a/parts/django/tests/modeltests/custom_methods/tests.py b/parts/django/tests/modeltests/custom_methods/tests.py new file mode 100644 index 0000000..90a7f0d --- /dev/null +++ b/parts/django/tests/modeltests/custom_methods/tests.py @@ -0,0 +1,42 @@ +from datetime import date + +from django.test import TestCase + +from models import Article + + +class MethodsTests(TestCase): + def test_custom_methods(self): + a = Article.objects.create( + headline="Area man programs in Python", pub_date=date(2005, 7, 27) + ) + b = Article.objects.create( + headline="Beatles reunite", pub_date=date(2005, 7, 27) + ) + + self.assertFalse(a.was_published_today()) + self.assertQuerysetEqual( + a.articles_from_same_day_1(), [ + "Beatles reunite", + ], + lambda a: a.headline, + ) + self.assertQuerysetEqual( + a.articles_from_same_day_2(), [ + "Beatles reunite", + ], + lambda a: a.headline + ) + + self.assertQuerysetEqual( + b.articles_from_same_day_1(), [ + "Area man programs in Python", + ], + lambda a: a.headline, + ) + self.assertQuerysetEqual( + b.articles_from_same_day_2(), [ + "Area man programs in Python", + ], + lambda a: a.headline + ) diff --git a/parts/django/tests/modeltests/custom_pk/__init__.py b/parts/django/tests/modeltests/custom_pk/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/modeltests/custom_pk/__init__.py diff --git a/parts/django/tests/modeltests/custom_pk/fields.py b/parts/django/tests/modeltests/custom_pk/fields.py new file mode 100644 index 0000000..2eeb80e --- /dev/null +++ b/parts/django/tests/modeltests/custom_pk/fields.py @@ -0,0 +1,55 @@ +import random +import string + +from django.db import models + + +class MyWrapper(object): + def __init__(self, value): + self.value = value + + def __repr__(self): + return "<%s: %s>" % (self.__class__.__name__, self.value) + + def __unicode__(self): + return self.value + + def __eq__(self, other): + if isinstance(other, self.__class__): + return self.value == other.value + return self.value == other + +class MyAutoField(models.CharField): + __metaclass__ = models.SubfieldBase + + def __init__(self, *args, **kwargs): + kwargs['max_length'] = 10 + super(MyAutoField, self).__init__(*args, **kwargs) + + def pre_save(self, instance, add): + value = getattr(instance, self.attname, None) + if not value: + value = MyWrapper(''.join(random.sample(string.lowercase, 10))) + setattr(instance, self.attname, value) + return value + + def to_python(self, value): + if not value: + return + if not isinstance(value, MyWrapper): + value = MyWrapper(value) + return value + + def get_db_prep_save(self, value): + if not value: + return + if isinstance(value, MyWrapper): + return unicode(value) + return value + + def get_db_prep_value(self, value): + if not value: + return + if isinstance(value, MyWrapper): + return unicode(value) + return value diff --git a/parts/django/tests/modeltests/custom_pk/models.py b/parts/django/tests/modeltests/custom_pk/models.py new file mode 100644 index 0000000..ff2f2ba --- /dev/null +++ b/parts/django/tests/modeltests/custom_pk/models.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +""" +14. Using a custom primary key + +By default, Django adds an ``"id"`` field to each model. But you can override +this behavior by explicitly adding ``primary_key=True`` to a field. +""" + +from django.conf import settings +from django.db import models, transaction, IntegrityError, DEFAULT_DB_ALIAS + +from fields import MyAutoField + +class Employee(models.Model): + employee_code = models.IntegerField(primary_key=True, db_column = 'code') + first_name = models.CharField(max_length=20) + last_name = models.CharField(max_length=20) + class Meta: + ordering = ('last_name', 'first_name') + + def __unicode__(self): + return u"%s %s" % (self.first_name, self.last_name) + +class Business(models.Model): + name = models.CharField(max_length=20, primary_key=True) + employees = models.ManyToManyField(Employee) + class Meta: + verbose_name_plural = 'businesses' + + def __unicode__(self): + return self.name + +class Bar(models.Model): + id = MyAutoField(primary_key=True, db_index=True) + + def __unicode__(self): + return repr(self.pk) + + +class Foo(models.Model): + bar = models.ForeignKey(Bar) + diff --git a/parts/django/tests/modeltests/custom_pk/tests.py b/parts/django/tests/modeltests/custom_pk/tests.py new file mode 100644 index 0000000..6ef4bdd --- /dev/null +++ b/parts/django/tests/modeltests/custom_pk/tests.py @@ -0,0 +1,183 @@ +# -*- coding: utf-8 -*- +from django.conf import settings +from django.db import DEFAULT_DB_ALIAS, transaction, IntegrityError +from django.test import TestCase + +from models import Employee, Business, Bar, Foo + + +class CustomPKTests(TestCase): + def test_custom_pk(self): + dan = Employee.objects.create( + employee_code=123, first_name="Dan", last_name="Jones" + ) + self.assertQuerysetEqual( + Employee.objects.all(), [ + "Dan Jones", + ], + unicode + ) + + fran = Employee.objects.create( + employee_code=456, first_name="Fran", last_name="Bones" + ) + self.assertQuerysetEqual( + Employee.objects.all(), [ + "Fran Bones", + "Dan Jones", + ], + unicode + ) + + self.assertEqual(Employee.objects.get(pk=123), dan) + self.assertEqual(Employee.objects.get(pk=456), fran) + + self.assertRaises(Employee.DoesNotExist, + lambda: Employee.objects.get(pk=42) + ) + + # Use the name of the primary key, rather than pk. + self.assertEqual(Employee.objects.get(employee_code=123), dan) + # pk can be used as a substitute for the primary key. + self.assertQuerysetEqual( + Employee.objects.filter(pk__in=[123, 456]), [ + "Fran Bones", + "Dan Jones", + ], + unicode + ) + # The primary key can be accessed via the pk property on the model. + e = Employee.objects.get(pk=123) + self.assertEqual(e.pk, 123) + # Or we can use the real attribute name for the primary key: + self.assertEqual(e.employee_code, 123) + + # Fran got married and changed her last name. + fran = Employee.objects.get(pk=456) + fran.last_name = "Jones" + fran.save() + + self.assertQuerysetEqual( + Employee.objects.filter(last_name="Jones"), [ + "Dan Jones", + "Fran Jones", + ], + unicode + ) + + emps = Employee.objects.in_bulk([123, 456]) + self.assertEqual(emps[123], dan) + + b = Business.objects.create(name="Sears") + b.employees.add(dan, fran) + self.assertQuerysetEqual( + b.employees.all(), [ + "Dan Jones", + "Fran Jones", + ], + unicode + ) + self.assertQuerysetEqual( + fran.business_set.all(), [ + "Sears", + ], + lambda b: b.name + ) + + self.assertEqual(Business.objects.in_bulk(["Sears"]), { + "Sears": b, + }) + + self.assertQuerysetEqual( + Business.objects.filter(name="Sears"), [ + "Sears" + ], + lambda b: b.name + ) + self.assertQuerysetEqual( + Business.objects.filter(pk="Sears"), [ + "Sears", + ], + lambda b: b.name + ) + + # Queries across tables, involving primary key + self.assertQuerysetEqual( + Employee.objects.filter(business__name="Sears"), [ + "Dan Jones", + "Fran Jones", + ], + unicode, + ) + self.assertQuerysetEqual( + Employee.objects.filter(business__pk="Sears"), [ + "Dan Jones", + "Fran Jones", + ], + unicode, + ) + + self.assertQuerysetEqual( + Business.objects.filter(employees__employee_code=123), [ + "Sears", + ], + lambda b: b.name + ) + self.assertQuerysetEqual( + Business.objects.filter(employees__pk=123), [ + "Sears", + ], + lambda b: b.name, + ) + + self.assertQuerysetEqual( + Business.objects.filter(employees__first_name__startswith="Fran"), [ + "Sears", + ], + lambda b: b.name + ) + + def test_unicode_pk(self): + # Primary key may be unicode string + bus = Business.objects.create(name=u'jaźń') + + def test_unique_pk(self): + # The primary key must also obviously be unique, so trying to create a + # new object with the same primary key will fail. + e = Employee.objects.create( + employee_code=123, first_name="Frank", last_name="Jones" + ) + sid = transaction.savepoint() + self.assertRaises(IntegrityError, + Employee.objects.create, employee_code=123, first_name="Fred", last_name="Jones" + ) + transaction.savepoint_rollback(sid) + + def test_custom_field_pk(self): + # Regression for #10785 -- Custom fields can be used for primary keys. + new_bar = Bar.objects.create() + new_foo = Foo.objects.create(bar=new_bar) + + # FIXME: This still doesn't work, but will require some changes in + # get_db_prep_lookup to fix it. + # f = Foo.objects.get(bar=new_bar.pk) + # self.assertEqual(f, new_foo) + # self.assertEqual(f.bar, new_bar) + + f = Foo.objects.get(bar=new_bar) + self.assertEqual(f, new_foo), + self.assertEqual(f.bar, new_bar) + + + # SQLite lets objects be saved with an empty primary key, even though an + # integer is expected. So we can't check for an error being raised in that + # case for SQLite. Remove it from the suite for this next bit. + if settings.DATABASES[DEFAULT_DB_ALIAS]['ENGINE'] != 'django.db.backends.sqlite3': + def test_required_pk(self): + # The primary key must be specified, so an error is raised if you + # try to create an object without it. + sid = transaction.savepoint() + self.assertRaises(IntegrityError, + Employee.objects.create, first_name="Tom", last_name="Smith" + ) + transaction.savepoint_rollback(sid) diff --git a/parts/django/tests/modeltests/defer/__init__.py b/parts/django/tests/modeltests/defer/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/modeltests/defer/__init__.py diff --git a/parts/django/tests/modeltests/defer/models.py b/parts/django/tests/modeltests/defer/models.py new file mode 100644 index 0000000..4fddd39 --- /dev/null +++ b/parts/django/tests/modeltests/defer/models.py @@ -0,0 +1,24 @@ +""" +Tests for defer() and only(). +""" + +from django.db import models + + +class Secondary(models.Model): + first = models.CharField(max_length=50) + second = models.CharField(max_length=50) + +class Primary(models.Model): + name = models.CharField(max_length=50) + value = models.CharField(max_length=50) + related = models.ForeignKey(Secondary) + + def __unicode__(self): + return self.name + +class Child(Primary): + pass + +class BigChild(Primary): + other = models.CharField(max_length=50) diff --git a/parts/django/tests/modeltests/defer/tests.py b/parts/django/tests/modeltests/defer/tests.py new file mode 100644 index 0000000..5f6c53d --- /dev/null +++ b/parts/django/tests/modeltests/defer/tests.py @@ -0,0 +1,137 @@ +from django.db.models.query_utils import DeferredAttribute +from django.test import TestCase + +from models import Secondary, Primary, Child, BigChild + + +class DeferTests(TestCase): + def assert_delayed(self, obj, num): + count = 0 + for field in obj._meta.fields: + if isinstance(obj.__class__.__dict__.get(field.attname), + DeferredAttribute): + count += 1 + self.assertEqual(count, num) + + def test_defer(self): + # To all outward appearances, instances with deferred fields look the + # same as normal instances when we examine attribute values. Therefore + # we test for the number of deferred fields on returned instances (by + # poking at the internals), as a way to observe what is going on. + + s1 = Secondary.objects.create(first="x1", second="y1") + p1 = Primary.objects.create(name="p1", value="xx", related=s1) + + qs = Primary.objects.all() + + self.assert_delayed(qs.defer("name")[0], 1) + self.assert_delayed(qs.only("name")[0], 2) + self.assert_delayed(qs.defer("related__first")[0], 0) + + obj = qs.select_related().only("related__first")[0] + self.assert_delayed(obj, 2) + + self.assertEqual(obj.related_id, s1.pk) + + self.assert_delayed(qs.defer("name").extra(select={"a": 1})[0], 1) + self.assert_delayed(qs.extra(select={"a": 1}).defer("name")[0], 1) + self.assert_delayed(qs.defer("name").defer("value")[0], 2) + self.assert_delayed(qs.only("name").only("value")[0], 2) + self.assert_delayed(qs.only("name").defer("value")[0], 2) + self.assert_delayed(qs.only("name", "value").defer("value")[0], 2) + self.assert_delayed(qs.defer("name").only("value")[0], 2) + + obj = qs.only()[0] + self.assert_delayed(qs.defer(None)[0], 0) + self.assert_delayed(qs.only("name").defer(None)[0], 0) + + # User values() won't defer anything (you get the full list of + # dictionaries back), but it still works. + self.assertEqual(qs.defer("name").values()[0], { + "id": p1.id, + "name": "p1", + "value": "xx", + "related_id": s1.id, + }) + self.assertEqual(qs.only("name").values()[0], { + "id": p1.id, + "name": "p1", + "value": "xx", + "related_id": s1.id, + }) + + # Using defer() and only() with get() is also valid. + self.assert_delayed(qs.defer("name").get(pk=p1.pk), 1) + self.assert_delayed(qs.only("name").get(pk=p1.pk), 2) + + # DOES THIS WORK? + self.assert_delayed(qs.only("name").select_related("related")[0], 1) + self.assert_delayed(qs.defer("related").select_related("related")[0], 0) + + # Saving models with deferred fields is possible (but inefficient, + # since every field has to be retrieved first). + obj = Primary.objects.defer("value").get(name="p1") + obj.name = "a new name" + obj.save() + self.assertQuerysetEqual( + Primary.objects.all(), [ + "a new name", + ], + lambda p: p.name + ) + + # Regression for #10572 - A subclass with no extra fields can defer + # fields from the base class + Child.objects.create(name="c1", value="foo", related=s1) + # You can defer a field on a baseclass when the subclass has no fields + obj = Child.objects.defer("value").get(name="c1") + self.assert_delayed(obj, 1) + self.assertEqual(obj.name, "c1") + self.assertEqual(obj.value, "foo") + obj.name = "c2" + obj.save() + + # You can retrive a single column on a base class with no fields + obj = Child.objects.only("name").get(name="c2") + self.assert_delayed(obj, 3) + self.assertEqual(obj.name, "c2") + self.assertEqual(obj.value, "foo") + obj.name = "cc" + obj.save() + + BigChild.objects.create(name="b1", value="foo", related=s1, other="bar") + # You can defer a field on a baseclass + obj = BigChild.objects.defer("value").get(name="b1") + self.assert_delayed(obj, 1) + self.assertEqual(obj.name, "b1") + self.assertEqual(obj.value, "foo") + self.assertEqual(obj.other, "bar") + obj.name = "b2" + obj.save() + + # You can defer a field on a subclass + obj = BigChild.objects.defer("other").get(name="b2") + self.assert_delayed(obj, 1) + self.assertEqual(obj.name, "b2") + self.assertEqual(obj.value, "foo") + self.assertEqual(obj.other, "bar") + obj.name = "b3" + obj.save() + + # You can retrieve a single field on a baseclass + obj = BigChild.objects.only("name").get(name="b3") + self.assert_delayed(obj, 4) + self.assertEqual(obj.name, "b3") + self.assertEqual(obj.value, "foo") + self.assertEqual(obj.other, "bar") + obj.name = "b4" + obj.save() + + # You can retrieve a single field on a baseclass + obj = BigChild.objects.only("other").get(name="b4") + self.assert_delayed(obj, 4) + self.assertEqual(obj.name, "b4") + self.assertEqual(obj.value, "foo") + self.assertEqual(obj.other, "bar") + obj.name = "bb" + obj.save() diff --git a/parts/django/tests/modeltests/delete/__init__.py b/parts/django/tests/modeltests/delete/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/parts/django/tests/modeltests/delete/__init__.py @@ -0,0 +1 @@ + diff --git a/parts/django/tests/modeltests/delete/models.py b/parts/django/tests/modeltests/delete/models.py new file mode 100644 index 0000000..9c81f6b --- /dev/null +++ b/parts/django/tests/modeltests/delete/models.py @@ -0,0 +1,42 @@ +# coding: utf-8 +""" +Tests for some corner cases with deleting. +""" + +from django.db import models + +class DefaultRepr(object): + def __repr__(self): + return u"<%s: %s>" % (self.__class__.__name__, self.__dict__) + +class A(DefaultRepr, models.Model): + pass + +class B(DefaultRepr, models.Model): + a = models.ForeignKey(A) + +class C(DefaultRepr, models.Model): + b = models.ForeignKey(B) + +class D(DefaultRepr, models.Model): + c = models.ForeignKey(C) + a = models.ForeignKey(A) + +# Simplified, we have: +# A +# B -> A +# C -> B +# D -> C +# D -> A + +# So, we must delete Ds first of all, then Cs then Bs then As. +# However, if we start at As, we might find Bs first (in which +# case things will be nice), or find Ds first. + +# Some mutually dependent models, but nullable +class E(DefaultRepr, models.Model): + f = models.ForeignKey('F', null=True, related_name='e_rel') + +class F(DefaultRepr, models.Model): + e = models.ForeignKey(E, related_name='f_rel') + diff --git a/parts/django/tests/modeltests/delete/tests.py b/parts/django/tests/modeltests/delete/tests.py new file mode 100644 index 0000000..7927cce --- /dev/null +++ b/parts/django/tests/modeltests/delete/tests.py @@ -0,0 +1,135 @@ +from django.db.models import sql +from django.db.models.loading import cache +from django.db.models.query import CollectedObjects +from django.db.models.query_utils import CyclicDependency +from django.test import TestCase + +from models import A, B, C, D, E, F + + +class DeleteTests(TestCase): + def clear_rel_obj_caches(self, *models): + for m in models: + if hasattr(m._meta, '_related_objects_cache'): + del m._meta._related_objects_cache + + def order_models(self, *models): + cache.app_models["delete"].keyOrder = models + + def setUp(self): + self.order_models("a", "b", "c", "d", "e", "f") + self.clear_rel_obj_caches(A, B, C, D, E, F) + + def tearDown(self): + self.order_models("a", "b", "c", "d", "e", "f") + self.clear_rel_obj_caches(A, B, C, D, E, F) + + def test_collected_objects(self): + g = CollectedObjects() + self.assertFalse(g.add("key1", 1, "item1", None)) + self.assertEqual(g["key1"], {1: "item1"}) + + self.assertFalse(g.add("key2", 1, "item1", "key1")) + self.assertFalse(g.add("key2", 2, "item2", "key1")) + + self.assertEqual(g["key2"], {1: "item1", 2: "item2"}) + + self.assertFalse(g.add("key3", 1, "item1", "key1")) + self.assertTrue(g.add("key3", 1, "item1", "key2")) + self.assertEqual(g.ordered_keys(), ["key3", "key2", "key1"]) + + self.assertTrue(g.add("key2", 1, "item1", "key3")) + self.assertRaises(CyclicDependency, g.ordered_keys) + + def test_delete(self): + ## Second, test the usage of CollectedObjects by Model.delete() + + # Due to the way that transactions work in the test harness, doing + # m.delete() here can work but fail in a real situation, since it may + # delete all objects, but not in the right order. So we manually check + # that the order of deletion is correct. + + # Also, it is possible that the order is correct 'accidentally', due + # solely to order of imports etc. To check this, we set the order that + # 'get_models()' will retrieve to a known 'nice' order, and then try + # again with a known 'tricky' order. Slightly naughty access to + # internals here :-) + + # If implementation changes, then the tests may need to be simplified: + # - remove the lines that set the .keyOrder and clear the related + # object caches + # - remove the second set of tests (with a2, b2 etc) + + a1 = A.objects.create() + b1 = B.objects.create(a=a1) + c1 = C.objects.create(b=b1) + d1 = D.objects.create(c=c1, a=a1) + + o = CollectedObjects() + a1._collect_sub_objects(o) + self.assertEqual(o.keys(), [D, C, B, A]) + a1.delete() + + # Same again with a known bad order + self.order_models("d", "c", "b", "a") + self.clear_rel_obj_caches(A, B, C, D) + + a2 = A.objects.create() + b2 = B.objects.create(a=a2) + c2 = C.objects.create(b=b2) + d2 = D.objects.create(c=c2, a=a2) + + o = CollectedObjects() + a2._collect_sub_objects(o) + self.assertEqual(o.keys(), [D, C, B, A]) + a2.delete() + + def test_collected_objects_null(self): + g = CollectedObjects() + self.assertFalse(g.add("key1", 1, "item1", None)) + self.assertFalse(g.add("key2", 1, "item1", "key1", nullable=True)) + self.assertTrue(g.add("key1", 1, "item1", "key2")) + self.assertEqual(g.ordered_keys(), ["key1", "key2"]) + + def test_delete_nullable(self): + e1 = E.objects.create() + f1 = F.objects.create(e=e1) + e1.f = f1 + e1.save() + + # Since E.f is nullable, we should delete F first (after nulling out + # the E.f field), then E. + + o = CollectedObjects() + e1._collect_sub_objects(o) + self.assertEqual(o.keys(), [F, E]) + + # temporarily replace the UpdateQuery class to verify that E.f is + # actually nulled out first + + logged = [] + class LoggingUpdateQuery(sql.UpdateQuery): + def clear_related(self, related_field, pk_list, using): + logged.append(related_field.name) + return super(LoggingUpdateQuery, self).clear_related(related_field, pk_list, using) + original = sql.UpdateQuery + sql.UpdateQuery = LoggingUpdateQuery + + e1.delete() + self.assertEqual(logged, ["f"]) + logged = [] + + e2 = E.objects.create() + f2 = F.objects.create(e=e2) + e2.f = f2 + e2.save() + + # Same deal as before, though we are starting from the other object. + o = CollectedObjects() + f2._collect_sub_objects(o) + self.assertEqual(o.keys(), [F, E]) + f2.delete() + self.assertEqual(logged, ["f"]) + logged = [] + + sql.UpdateQuery = original diff --git a/parts/django/tests/modeltests/empty/__init__.py b/parts/django/tests/modeltests/empty/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/modeltests/empty/__init__.py diff --git a/parts/django/tests/modeltests/empty/models.py b/parts/django/tests/modeltests/empty/models.py new file mode 100644 index 0000000..a6cdb0a --- /dev/null +++ b/parts/django/tests/modeltests/empty/models.py @@ -0,0 +1,12 @@ +""" +40. Empty model tests + +These test that things behave sensibly for the rare corner-case of a model with +no fields. +""" + +from django.db import models + + +class Empty(models.Model): + pass diff --git a/parts/django/tests/modeltests/empty/tests.py b/parts/django/tests/modeltests/empty/tests.py new file mode 100644 index 0000000..01fa1c5 --- /dev/null +++ b/parts/django/tests/modeltests/empty/tests.py @@ -0,0 +1,15 @@ +from django.test import TestCase + +from models import Empty + + +class EmptyModelTests(TestCase): + def test_empty(self): + m = Empty() + self.assertEqual(m.id, None) + m.save() + m2 = Empty.objects.create() + self.assertEqual(len(Empty.objects.all()), 2) + self.assertTrue(m.id is not None) + existing = Empty(m.id) + existing.save() diff --git a/parts/django/tests/modeltests/expressions/__init__.py b/parts/django/tests/modeltests/expressions/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/modeltests/expressions/__init__.py diff --git a/parts/django/tests/modeltests/expressions/models.py b/parts/django/tests/modeltests/expressions/models.py new file mode 100644 index 0000000..b004408 --- /dev/null +++ b/parts/django/tests/modeltests/expressions/models.py @@ -0,0 +1,27 @@ +""" +Tests for F() query expression syntax. +""" + +from django.db import models + +class Employee(models.Model): + firstname = models.CharField(max_length=50) + lastname = models.CharField(max_length=50) + + def __unicode__(self): + return u'%s %s' % (self.firstname, self.lastname) + +class Company(models.Model): + name = models.CharField(max_length=100) + num_employees = models.PositiveIntegerField() + num_chairs = models.PositiveIntegerField() + ceo = models.ForeignKey( + Employee, + related_name='company_ceo_set') + point_of_contact = models.ForeignKey( + Employee, + related_name='company_point_of_contact_set', + null=True) + + def __unicode__(self): + return self.name diff --git a/parts/django/tests/modeltests/expressions/tests.py b/parts/django/tests/modeltests/expressions/tests.py new file mode 100644 index 0000000..0a136ae --- /dev/null +++ b/parts/django/tests/modeltests/expressions/tests.py @@ -0,0 +1,218 @@ +from django.core.exceptions import FieldError +from django.db.models import F +from django.test import TestCase + +from models import Company, Employee + + +class ExpressionsTests(TestCase): + def test_filter(self): + Company.objects.create( + name="Example Inc.", num_employees=2300, num_chairs=5, + ceo=Employee.objects.create(firstname="Joe", lastname="Smith") + ) + Company.objects.create( + name="Foobar Ltd.", num_employees=3, num_chairs=4, + ceo=Employee.objects.create(firstname="Frank", lastname="Meyer") + ) + Company.objects.create( + name="Test GmbH", num_employees=32, num_chairs=1, + ceo=Employee.objects.create(firstname="Max", lastname="Mustermann") + ) + + company_query = Company.objects.values( + "name", "num_employees", "num_chairs" + ).order_by( + "name", "num_employees", "num_chairs" + ) + + # We can filter for companies where the number of employees is greater + # than the number of chairs. + self.assertQuerysetEqual( + company_query.filter(num_employees__gt=F("num_chairs")), [ + { + "num_chairs": 5, + "name": "Example Inc.", + "num_employees": 2300, + }, + { + "num_chairs": 1, + "name": "Test GmbH", + "num_employees": 32 + }, + ], + lambda o: o + ) + + # We can set one field to have the value of another field + # Make sure we have enough chairs + company_query.update(num_chairs=F("num_employees")) + self.assertQuerysetEqual( + company_query, [ + { + "num_chairs": 2300, + "name": "Example Inc.", + "num_employees": 2300 + }, + { + "num_chairs": 3, + "name": "Foobar Ltd.", + "num_employees": 3 + }, + { + "num_chairs": 32, + "name": "Test GmbH", + "num_employees": 32 + } + ], + lambda o: o + ) + + # We can perform arithmetic operations in expressions + # Make sure we have 2 spare chairs + company_query.update(num_chairs=F("num_employees")+2) + self.assertQuerysetEqual( + company_query, [ + { + 'num_chairs': 2302, + 'name': u'Example Inc.', + 'num_employees': 2300 + }, + { + 'num_chairs': 5, + 'name': u'Foobar Ltd.', + 'num_employees': 3 + }, + { + 'num_chairs': 34, + 'name': u'Test GmbH', + 'num_employees': 32 + } + ], + lambda o: o, + ) + + # Law of order of operations is followed + company_query.update( + num_chairs=F('num_employees') + 2 * F('num_employees') + ) + self.assertQuerysetEqual( + company_query, [ + { + 'num_chairs': 6900, + 'name': u'Example Inc.', + 'num_employees': 2300 + }, + { + 'num_chairs': 9, + 'name': u'Foobar Ltd.', + 'num_employees': 3 + }, + { + 'num_chairs': 96, + 'name': u'Test GmbH', + 'num_employees': 32 + } + ], + lambda o: o, + ) + + # Law of order of operations can be overridden by parentheses + company_query.update( + num_chairs=((F('num_employees') + 2) * F('num_employees')) + ) + self.assertQuerysetEqual( + company_query, [ + { + 'num_chairs': 5294600, + 'name': u'Example Inc.', + 'num_employees': 2300 + }, + { + 'num_chairs': 15, + 'name': u'Foobar Ltd.', + 'num_employees': 3 + }, + { + 'num_chairs': 1088, + 'name': u'Test GmbH', + 'num_employees': 32 + } + ], + lambda o: o, + ) + + # The relation of a foreign key can become copied over to an other + # foreign key. + self.assertEqual( + Company.objects.update(point_of_contact=F('ceo')), + 3 + ) + self.assertQuerysetEqual( + Company.objects.all(), [ + "Joe Smith", + "Frank Meyer", + "Max Mustermann", + ], + lambda c: unicode(c.point_of_contact), + ) + + c = Company.objects.all()[0] + c.point_of_contact = Employee.objects.create(firstname="Guido", lastname="van Rossum") + c.save() + + # F Expressions can also span joins + self.assertQuerysetEqual( + Company.objects.filter(ceo__firstname=F("point_of_contact__firstname")), [ + "Foobar Ltd.", + "Test GmbH", + ], + lambda c: c.name + ) + + Company.objects.exclude( + ceo__firstname=F("point_of_contact__firstname") + ).update(name="foo") + self.assertEqual( + Company.objects.exclude( + ceo__firstname=F('point_of_contact__firstname') + ).get().name, + "foo", + ) + + self.assertRaises(FieldError, + lambda: Company.objects.exclude( + ceo__firstname=F('point_of_contact__firstname') + ).update(name=F('point_of_contact__lastname')) + ) + + # F expressions can be used to update attributes on single objects + test_gmbh = Company.objects.get(name="Test GmbH") + self.assertEqual(test_gmbh.num_employees, 32) + test_gmbh.num_employees = F("num_employees") + 4 + test_gmbh.save() + test_gmbh = Company.objects.get(pk=test_gmbh.pk) + self.assertEqual(test_gmbh.num_employees, 36) + + # F expressions cannot be used to update attributes which are foreign + # keys, or attributes which involve joins. + test_gmbh.point_of_contact = None + test_gmbh.save() + self.assertTrue(test_gmbh.point_of_contact is None) + def test(): + test_gmbh.point_of_contact = F("ceo") + self.assertRaises(ValueError, test) + + test_gmbh.point_of_contact = test_gmbh.ceo + test_gmbh.save() + test_gmbh.name = F("ceo__last_name") + self.assertRaises(FieldError, test_gmbh.save) + + # F expressions cannot be used to update attributes on objects which do + # not yet exist in the database + acme = Company( + name="The Acme Widget Co.", num_employees=12, num_chairs=5, + ceo=test_gmbh.ceo + ) + acme.num_employees = F("num_employees") + 16 + self.assertRaises(TypeError, acme.save) diff --git a/parts/django/tests/modeltests/field_defaults/__init__.py b/parts/django/tests/modeltests/field_defaults/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/modeltests/field_defaults/__init__.py diff --git a/parts/django/tests/modeltests/field_defaults/models.py b/parts/django/tests/modeltests/field_defaults/models.py new file mode 100644 index 0000000..0dd1f72 --- /dev/null +++ b/parts/django/tests/modeltests/field_defaults/models.py @@ -0,0 +1,21 @@ +# coding: utf-8 +""" +32. Callable defaults + +You can pass callable objects as the ``default`` parameter to a field. When +the object is created without an explicit value passed in, Django will call +the method to determine the default value. + +This example uses ``datetime.datetime.now`` as the default for the ``pub_date`` +field. +""" + +from django.db import models +from datetime import datetime + +class Article(models.Model): + headline = models.CharField(max_length=100, default='Default headline') + pub_date = models.DateTimeField(default=datetime.now) + + def __unicode__(self): + return self.headline diff --git a/parts/django/tests/modeltests/field_defaults/tests.py b/parts/django/tests/modeltests/field_defaults/tests.py new file mode 100644 index 0000000..a23f644 --- /dev/null +++ b/parts/django/tests/modeltests/field_defaults/tests.py @@ -0,0 +1,16 @@ +from datetime import datetime + +from django.test import TestCase + +from models import Article + + +class DefaultTests(TestCase): + def test_field_defaults(self): + a = Article() + now = datetime.now() + a.save() + + self.assertTrue(isinstance(a.id, (int, long))) + self.assertEqual(a.headline, "Default headline") + self.assertTrue((now - a.pub_date).seconds < 5) diff --git a/parts/django/tests/modeltests/field_subclassing/__init__.py b/parts/django/tests/modeltests/field_subclassing/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/modeltests/field_subclassing/__init__.py diff --git a/parts/django/tests/modeltests/field_subclassing/fields.py b/parts/django/tests/modeltests/field_subclassing/fields.py new file mode 100644 index 0000000..8675b31 --- /dev/null +++ b/parts/django/tests/modeltests/field_subclassing/fields.py @@ -0,0 +1,74 @@ +from django.core.exceptions import FieldError +from django.db import models +from django.utils import simplejson as json +from django.utils.encoding import force_unicode + + +class Small(object): + """ + A simple class to show that non-trivial Python objects can be used as + attributes. + """ + def __init__(self, first, second): + self.first, self.second = first, second + + def __unicode__(self): + return u'%s%s' % (force_unicode(self.first), force_unicode(self.second)) + + def __str__(self): + return unicode(self).encode('utf-8') + +class SmallField(models.Field): + """ + Turns the "Small" class into a Django field. Because of the similarities + with normal character fields and the fact that Small.__unicode__ does + something sensible, we don't need to implement a lot here. + """ + __metaclass__ = models.SubfieldBase + + def __init__(self, *args, **kwargs): + kwargs['max_length'] = 2 + super(SmallField, self).__init__(*args, **kwargs) + + def get_internal_type(self): + return 'CharField' + + def to_python(self, value): + if isinstance(value, Small): + return value + return Small(value[0], value[1]) + + def get_db_prep_save(self, value): + return unicode(value) + + def get_prep_lookup(self, lookup_type, value): + if lookup_type == 'exact': + return force_unicode(value) + if lookup_type == 'in': + return [force_unicode(v) for v in value] + if lookup_type == 'isnull': + return [] + raise TypeError('Invalid lookup type: %r' % lookup_type) + +class SmallerField(SmallField): + pass + + +class JSONField(models.TextField): + __metaclass__ = models.SubfieldBase + + description = ("JSONField automatically serializes and desializes values to " + "and from JSON.") + + def to_python(self, value): + if not value: + return None + + if isinstance(value, basestring): + value = json.loads(value) + return value + + def get_db_prep_save(self, value): + if value is None: + return None + return json.dumps(value) diff --git a/parts/django/tests/modeltests/field_subclassing/models.py b/parts/django/tests/modeltests/field_subclassing/models.py new file mode 100644 index 0000000..b0d8336 --- /dev/null +++ b/parts/django/tests/modeltests/field_subclassing/models.py @@ -0,0 +1,22 @@ +""" +Tests for field subclassing. +""" + +from django.db import models +from django.utils.encoding import force_unicode + +from fields import Small, SmallField, SmallerField, JSONField + + +class MyModel(models.Model): + name = models.CharField(max_length=10) + data = SmallField('small field') + + def __unicode__(self): + return force_unicode(self.name) + +class OtherModel(models.Model): + data = SmallerField() + +class DataModel(models.Model): + data = JSONField() diff --git a/parts/django/tests/modeltests/field_subclassing/tests.py b/parts/django/tests/modeltests/field_subclassing/tests.py new file mode 100644 index 0000000..25f5160 --- /dev/null +++ b/parts/django/tests/modeltests/field_subclassing/tests.py @@ -0,0 +1,81 @@ +from django.core import serializers +from django.test import TestCase + +from fields import Small +from models import DataModel, MyModel, OtherModel + + +class CustomField(TestCase): + def test_defer(self): + d = DataModel.objects.create(data=[1, 2, 3]) + + self.assertTrue(isinstance(d.data, list)) + + d = DataModel.objects.get(pk=d.pk) + self.assertTrue(isinstance(d.data, list)) + self.assertEqual(d.data, [1, 2, 3]) + + d = DataModel.objects.defer("data").get(pk=d.pk) + d.save() + + d = DataModel.objects.get(pk=d.pk) + self.assertTrue(isinstance(d.data, list)) + self.assertEqual(d.data, [1, 2, 3]) + + def test_custom_field(self): + # Creating a model with custom fields is done as per normal. + s = Small(1, 2) + self.assertEqual(str(s), "12") + + m = MyModel.objects.create(name="m", data=s) + # Custom fields still have normal field's attributes. + self.assertEqual(m._meta.get_field("data").verbose_name, "small field") + + # The m.data attribute has been initialised correctly. It's a Small + # object. + self.assertEqual((m.data.first, m.data.second), (1, 2)) + + # The data loads back from the database correctly and 'data' has the + # right type. + m1 = MyModel.objects.get(pk=m.pk) + self.assertTrue(isinstance(m1.data, Small)) + self.assertEqual(str(m1.data), "12") + + # We can do normal filtering on the custom field (and will get an error + # when we use a lookup type that does not make sense). + s1 = Small(1, 3) + s2 = Small("a", "b") + self.assertQuerysetEqual( + MyModel.objects.filter(data__in=[s, s1, s2]), [ + "m", + ], + lambda m: m.name, + ) + self.assertRaises(TypeError, lambda: MyModel.objects.filter(data__lt=s)) + + # Serialization works, too. + stream = serializers.serialize("json", MyModel.objects.all()) + self.assertEqual(stream, '[{"pk": 1, "model": "field_subclassing.mymodel", "fields": {"data": "12", "name": "m"}}]') + + obj = list(serializers.deserialize("json", stream))[0] + self.assertEqual(obj.object, m) + + # Test retrieving custom field data + m.delete() + + m1 = MyModel.objects.create(name="1", data=Small(1, 2)) + m2 = MyModel.objects.create(name="2", data=Small(2, 3)) + + self.assertQuerysetEqual( + MyModel.objects.all(), [ + "12", + "23", + ], + lambda m: str(m.data) + ) + + def test_field_subclassing(self): + o = OtherModel.objects.create(data=Small("a", "b")) + o = OtherModel.objects.get() + self.assertEqual(o.data.first, "a") + self.assertEqual(o.data.second, "b") diff --git a/parts/django/tests/modeltests/files/__init__.py b/parts/django/tests/modeltests/files/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/parts/django/tests/modeltests/files/__init__.py @@ -0,0 +1 @@ + diff --git a/parts/django/tests/modeltests/files/models.py b/parts/django/tests/modeltests/files/models.py new file mode 100644 index 0000000..f798f74 --- /dev/null +++ b/parts/django/tests/modeltests/files/models.py @@ -0,0 +1,34 @@ +""" +42. Storing files according to a custom storage system + +``FileField`` and its variations can take a ``storage`` argument to specify how +and where files should be stored. +""" + +import random +import tempfile + +from django.db import models +from django.core.files.base import ContentFile +from django.core.files.storage import FileSystemStorage + + +temp_storage_location = tempfile.mkdtemp() +temp_storage = FileSystemStorage(location=temp_storage_location) + +# Write out a file to be used as default content +temp_storage.save('tests/default.txt', ContentFile('default content')) + +class Storage(models.Model): + def custom_upload_to(self, filename): + return 'foo' + + def random_upload_to(self, filename): + # This returns a different result each time, + # to make sure it only gets called once. + return '%s/%s' % (random.randint(100, 999), filename) + + normal = models.FileField(storage=temp_storage, upload_to='tests') + custom = models.FileField(storage=temp_storage, upload_to=custom_upload_to) + random = models.FileField(storage=temp_storage, upload_to=random_upload_to) + default = models.FileField(storage=temp_storage, upload_to='tests', default='tests/default.txt') diff --git a/parts/django/tests/modeltests/files/tests.py b/parts/django/tests/modeltests/files/tests.py new file mode 100644 index 0000000..025fcc5 --- /dev/null +++ b/parts/django/tests/modeltests/files/tests.py @@ -0,0 +1,100 @@ +import shutil + +from django.core.cache import cache +from django.core.files.base import ContentFile +from django.core.files.uploadedfile import SimpleUploadedFile +from django.test import TestCase + +from models import Storage, temp_storage, temp_storage_location + + +class FileTests(TestCase): + def tearDown(self): + shutil.rmtree(temp_storage_location) + + def test_files(self): + # Attempting to access a FileField from the class raises a descriptive + # error + self.assertRaises(AttributeError, lambda: Storage.normal) + + # An object without a file has limited functionality. + obj1 = Storage() + self.assertEqual(obj1.normal.name, "") + self.assertRaises(ValueError, lambda: obj1.normal.size) + + # Saving a file enables full functionality. + obj1.normal.save("django_test.txt", ContentFile("content")) + self.assertEqual(obj1.normal.name, "tests/django_test.txt") + self.assertEqual(obj1.normal.size, 7) + self.assertEqual(obj1.normal.read(), "content") + + # File objects can be assigned to FileField attributes, but shouldn't + # get committed until the model it's attached to is saved. + obj1.normal = SimpleUploadedFile("assignment.txt", "content") + dirs, files = temp_storage.listdir("tests") + self.assertEqual(dirs, []) + self.assertEqual(sorted(files), ["default.txt", "django_test.txt"]) + + obj1.save() + dirs, files = temp_storage.listdir("tests") + self.assertEqual( + sorted(files), ["assignment.txt", "default.txt", "django_test.txt"] + ) + + # Files can be read in a little at a time, if necessary. + obj1.normal.open() + self.assertEqual(obj1.normal.read(3), "con") + self.assertEqual(obj1.normal.read(), "tent") + self.assertEqual(list(obj1.normal.chunks(chunk_size=2)), ["co", "nt", "en", "t"]) + + # Save another file with the same name. + obj2 = Storage() + obj2.normal.save("django_test.txt", ContentFile("more content")) + self.assertEqual(obj2.normal.name, "tests/django_test_1.txt") + self.assertEqual(obj2.normal.size, 12) + + # Push the objects into the cache to make sure they pickle properly + cache.set("obj1", obj1) + cache.set("obj2", obj2) + self.assertEqual(cache.get("obj2").normal.name, "tests/django_test_1.txt") + + # Deleting an object deletes the file it uses, if there are no other + # objects still using that file. + obj2.delete() + obj2.normal.save("django_test.txt", ContentFile("more content")) + self.assertEqual(obj2.normal.name, "tests/django_test_1.txt") + + # Multiple files with the same name get _N appended to them. + objs = [Storage() for i in range(3)] + for o in objs: + o.normal.save("multiple_files.txt", ContentFile("Same Content")) + self.assertEqual( + [o.normal.name for o in objs], + ["tests/multiple_files.txt", "tests/multiple_files_1.txt", "tests/multiple_files_2.txt"] + ) + for o in objs: + o.delete() + + # Default values allow an object to access a single file. + obj3 = Storage.objects.create() + self.assertEqual(obj3.default.name, "tests/default.txt") + self.assertEqual(obj3.default.read(), "default content") + + # But it shouldn't be deleted, even if there are no more objects using + # it. + obj3.delete() + obj3 = Storage() + self.assertEqual(obj3.default.read(), "default content") + + # Verify the fix for #5655, making sure the directory is only + # determined once. + obj4 = Storage() + obj4.random.save("random_file", ContentFile("random content")) + self.assertTrue(obj4.random.name.endswith("/random_file")) + + # Clean up the temporary files and dir. + obj1.normal.delete() + obj2.normal.delete() + obj3.default.delete() + obj4.random.delete() + diff --git a/parts/django/tests/modeltests/fixtures/__init__.py b/parts/django/tests/modeltests/fixtures/__init__.py new file mode 100644 index 0000000..139597f --- /dev/null +++ b/parts/django/tests/modeltests/fixtures/__init__.py @@ -0,0 +1,2 @@ + + diff --git a/parts/django/tests/modeltests/fixtures/fixtures/db_fixture_1.default.json b/parts/django/tests/modeltests/fixtures/fixtures/db_fixture_1.default.json new file mode 100644 index 0000000..9bb39e4 --- /dev/null +++ b/parts/django/tests/modeltests/fixtures/fixtures/db_fixture_1.default.json @@ -0,0 +1,10 @@ +[ + { + "pk": "6", + "model": "fixtures.article", + "fields": { + "headline": "Who needs more than one database?", + "pub_date": "2006-06-16 14:00:00" + } + } +]
\ No newline at end of file diff --git a/parts/django/tests/modeltests/fixtures/fixtures/db_fixture_2.default.json.gz b/parts/django/tests/modeltests/fixtures/fixtures/db_fixture_2.default.json.gz Binary files differnew file mode 100644 index 0000000..80e4ba1 --- /dev/null +++ b/parts/django/tests/modeltests/fixtures/fixtures/db_fixture_2.default.json.gz diff --git a/parts/django/tests/modeltests/fixtures/fixtures/db_fixture_3.nosuchdb.json b/parts/django/tests/modeltests/fixtures/fixtures/db_fixture_3.nosuchdb.json new file mode 100644 index 0000000..3da326b --- /dev/null +++ b/parts/django/tests/modeltests/fixtures/fixtures/db_fixture_3.nosuchdb.json @@ -0,0 +1,10 @@ +[ + { + "pk": "8", + "model": "fixtures.article", + "fields": { + "headline": "There is no spoon.", + "pub_date": "2006-06-16 14:00:00" + } + } +]
\ No newline at end of file diff --git a/parts/django/tests/modeltests/fixtures/fixtures/fixture1.json b/parts/django/tests/modeltests/fixtures/fixtures/fixture1.json new file mode 100644 index 0000000..332feae --- /dev/null +++ b/parts/django/tests/modeltests/fixtures/fixtures/fixture1.json @@ -0,0 +1,34 @@ +[ + { + "pk": 1, + "model": "sites.site", + "fields": { + "domain": "example.com", + "name": "example.com" + } + }, + { + "pk": "2", + "model": "fixtures.article", + "fields": { + "headline": "Poker has no place on ESPN", + "pub_date": "2006-06-16 12:00:00" + } + }, + { + "pk": "3", + "model": "fixtures.article", + "fields": { + "headline": "Time to reform copyright", + "pub_date": "2006-06-16 13:00:00" + } + }, + { + "pk": 1, + "model": "fixtures.category", + "fields": { + "description": "Latest news stories", + "title": "News Stories" + } + } +]
\ No newline at end of file diff --git a/parts/django/tests/modeltests/fixtures/fixtures/fixture2.json b/parts/django/tests/modeltests/fixtures/fixtures/fixture2.json new file mode 100644 index 0000000..01b40d7 --- /dev/null +++ b/parts/django/tests/modeltests/fixtures/fixtures/fixture2.json @@ -0,0 +1,18 @@ +[ + { + "pk": "3", + "model": "fixtures.article", + "fields": { + "headline": "Copyright is fine the way it is", + "pub_date": "2006-06-16 14:00:00" + } + }, + { + "pk": "4", + "model": "fixtures.article", + "fields": { + "headline": "Django conquers world!", + "pub_date": "2006-06-16 15:00:00" + } + } +]
\ No newline at end of file diff --git a/parts/django/tests/modeltests/fixtures/fixtures/fixture2.xml b/parts/django/tests/modeltests/fixtures/fixtures/fixture2.xml new file mode 100644 index 0000000..9ced781 --- /dev/null +++ b/parts/django/tests/modeltests/fixtures/fixtures/fixture2.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<django-objects version="1.0"> + <object pk="2" model="fixtures.article"> + <field type="CharField" name="headline">Poker on TV is great!</field> + <field type="DateTimeField" name="pub_date">2006-06-16 11:00:00</field> + </object> + <object pk="5" model="fixtures.article"> + <field type="CharField" name="headline">XML identified as leading cause of cancer</field> + <field type="DateTimeField" name="pub_date">2006-06-16 16:00:00</field> + </object> +</django-objects>
\ No newline at end of file diff --git a/parts/django/tests/modeltests/fixtures/fixtures/fixture3.xml b/parts/django/tests/modeltests/fixtures/fixtures/fixture3.xml new file mode 100644 index 0000000..9ced781 --- /dev/null +++ b/parts/django/tests/modeltests/fixtures/fixtures/fixture3.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<django-objects version="1.0"> + <object pk="2" model="fixtures.article"> + <field type="CharField" name="headline">Poker on TV is great!</field> + <field type="DateTimeField" name="pub_date">2006-06-16 11:00:00</field> + </object> + <object pk="5" model="fixtures.article"> + <field type="CharField" name="headline">XML identified as leading cause of cancer</field> + <field type="DateTimeField" name="pub_date">2006-06-16 16:00:00</field> + </object> +</django-objects>
\ No newline at end of file diff --git a/parts/django/tests/modeltests/fixtures/fixtures/fixture4.json.zip b/parts/django/tests/modeltests/fixtures/fixtures/fixture4.json.zip Binary files differnew file mode 100644 index 0000000..270cccb --- /dev/null +++ b/parts/django/tests/modeltests/fixtures/fixtures/fixture4.json.zip diff --git a/parts/django/tests/modeltests/fixtures/fixtures/fixture5.json.gz b/parts/django/tests/modeltests/fixtures/fixtures/fixture5.json.gz Binary files differnew file mode 100644 index 0000000..bb6baca --- /dev/null +++ b/parts/django/tests/modeltests/fixtures/fixtures/fixture5.json.gz diff --git a/parts/django/tests/modeltests/fixtures/fixtures/fixture5.json.zip b/parts/django/tests/modeltests/fixtures/fixtures/fixture5.json.zip Binary files differnew file mode 100644 index 0000000..9380cef --- /dev/null +++ b/parts/django/tests/modeltests/fixtures/fixtures/fixture5.json.zip diff --git a/parts/django/tests/modeltests/fixtures/fixtures/fixture6.json b/parts/django/tests/modeltests/fixtures/fixtures/fixture6.json new file mode 100644 index 0000000..60e4733 --- /dev/null +++ b/parts/django/tests/modeltests/fixtures/fixtures/fixture6.json @@ -0,0 +1,41 @@ +[ + { + "pk": "1", + "model": "fixtures.tag", + "fields": { + "name": "copyright", + "tagged_type": ["fixtures", "article"], + "tagged_id": "3" + } + }, + { + "pk": "2", + "model": "fixtures.tag", + "fields": { + "name": "law", + "tagged_type": ["fixtures", "article"], + "tagged_id": "3" + } + }, + { + "pk": "1", + "model": "fixtures.person", + "fields": { + "name": "Django Reinhardt" + } + }, + { + "pk": "2", + "model": "fixtures.person", + "fields": { + "name": "Stephane Grappelli" + } + }, + { + "pk": "3", + "model": "fixtures.person", + "fields": { + "name": "Prince" + } + } +] diff --git a/parts/django/tests/modeltests/fixtures/fixtures/fixture7.xml b/parts/django/tests/modeltests/fixtures/fixtures/fixture7.xml new file mode 100644 index 0000000..547cba1 --- /dev/null +++ b/parts/django/tests/modeltests/fixtures/fixtures/fixture7.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<django-objects version="1.0"> + <object pk="2" model="fixtures.tag"> + <field type="CharField" name="name">legal</field> + <field to="contenttypes.contenttype" name="tagged_type" rel="ManyToOneRel"> + <natural>fixtures</natural> + <natural>article</natural> + </field> + <field type="PositiveIntegerField" name="tagged_id">3</field> + </object> + <object pk="3" model="fixtures.tag"> + <field type="CharField" name="name">django</field> + <field to="contenttypes.contenttype" name="tagged_type" rel="ManyToOneRel"> + <natural>fixtures</natural> + <natural>article</natural> + </field> + <field type="PositiveIntegerField" name="tagged_id">4</field> + </object> + <object pk="4" model="fixtures.tag"> + <field type="CharField" name="name">world domination</field> + <field to="contenttypes.contenttype" name="tagged_type" rel="ManyToOneRel"> + <natural>fixtures</natural> + <natural>article</natural> + </field> + <field type="PositiveIntegerField" name="tagged_id">4</field> + </object> +</django-objects> diff --git a/parts/django/tests/modeltests/fixtures/fixtures/fixture8.json b/parts/django/tests/modeltests/fixtures/fixtures/fixture8.json new file mode 100644 index 0000000..bc113aa --- /dev/null +++ b/parts/django/tests/modeltests/fixtures/fixtures/fixture8.json @@ -0,0 +1,32 @@ +[ + { + "pk": "1", + "model": "fixtures.visa", + "fields": { + "person": ["Django Reinhardt"], + "permissions": [ + ["add_user", "auth", "user"], + ["change_user", "auth", "user"], + ["delete_user", "auth", "user"] + ] + } + }, + { + "pk": "2", + "model": "fixtures.visa", + "fields": { + "person": ["Stephane Grappelli"], + "permissions": [ + ["add_user", "auth", "user"] + ] + } + }, + { + "pk": "3", + "model": "fixtures.visa", + "fields": { + "person": ["Prince"], + "permissions": [] + } + } +] diff --git a/parts/django/tests/modeltests/fixtures/fixtures/fixture9.xml b/parts/django/tests/modeltests/fixtures/fixtures/fixture9.xml new file mode 100644 index 0000000..100f63d --- /dev/null +++ b/parts/django/tests/modeltests/fixtures/fixtures/fixture9.xml @@ -0,0 +1,48 @@ +<?xml version="1.0" encoding="utf-8"?> +<django-objects version="1.0"> + <object pk="2" model="fixtures.visa"> + <field type="CharField" name="person"> + <natural>Stephane Grappelli</natural> + </field> + <field to="auth.permission" name="permissions" rel="ManyToManyRel"> + <object> + <natural>add_user</natural> + <natural>auth</natural> + <natural>user</natural> + </object> + <object> + <natural>delete_user</natural> + <natural>auth</natural> + <natural>user</natural> + </object> + </field> + </object> + <object pk="3" model="fixtures.person"> + <field type="CharField" name="name"> + <natural>Artist formerly known as "Prince"</natural> + </field> + </object> + <object pk="3" model="fixtures.visa"> + <field type="CharField" name="person"> + <natural>Artist formerly known as "Prince"</natural> + </field> + <field to="auth.permission" name="permissions" rel="ManyToManyRel"> + <object> + <natural>change_user</natural> + <natural>auth</natural> + <natural>user</natural> + </object> + </field> + </object> + <object pk="1" model="fixtures.book"> + <field type="CharField" name="name">Music for all ages</field> + <field to="fixtures.person" name="authors" rel="ManyToManyRel"> + <object> + <natural>Django Reinhardt</natural> + </object> + <object> + <natural>Artist formerly known as "Prince"</natural> + </object> + </field> + </object> +</django-objects> diff --git a/parts/django/tests/modeltests/fixtures/fixtures/initial_data.json b/parts/django/tests/modeltests/fixtures/fixtures/initial_data.json new file mode 100644 index 0000000..477d781 --- /dev/null +++ b/parts/django/tests/modeltests/fixtures/fixtures/initial_data.json @@ -0,0 +1,10 @@ +[ + { + "pk": "1", + "model": "fixtures.article", + "fields": { + "headline": "Python program becomes self aware", + "pub_date": "2006-06-16 11:00:00" + } + } +]
\ No newline at end of file diff --git a/parts/django/tests/modeltests/fixtures/models.py b/parts/django/tests/modeltests/fixtures/models.py new file mode 100644 index 0000000..216a8e2 --- /dev/null +++ b/parts/django/tests/modeltests/fixtures/models.py @@ -0,0 +1,92 @@ +""" +37. Fixtures. + +Fixtures are a way of loading data into the database in bulk. Fixure data +can be stored in any serializable format (including JSON and XML). Fixtures +are identified by name, and are stored in either a directory named 'fixtures' +in the application directory, on in one of the directories named in the +``FIXTURE_DIRS`` setting. +""" + +from django.contrib.auth.models import Permission +from django.contrib.contenttypes import generic +from django.contrib.contenttypes.models import ContentType +from django.db import models, DEFAULT_DB_ALIAS +from django.conf import settings + + +class Category(models.Model): + title = models.CharField(max_length=100) + description = models.TextField() + + def __unicode__(self): + return self.title + + class Meta: + ordering = ('title',) + +class Article(models.Model): + headline = models.CharField(max_length=100, default='Default headline') + pub_date = models.DateTimeField() + + def __unicode__(self): + return self.headline + + class Meta: + ordering = ('-pub_date', 'headline') + +class Blog(models.Model): + name = models.CharField(max_length=100) + featured = models.ForeignKey(Article, related_name='fixtures_featured_set') + articles = models.ManyToManyField(Article, blank=True, + related_name='fixtures_articles_set') + + def __unicode__(self): + return self.name + + +class Tag(models.Model): + name = models.CharField(max_length=100) + tagged_type = models.ForeignKey(ContentType, related_name="fixtures_tag_set") + tagged_id = models.PositiveIntegerField(default=0) + tagged = generic.GenericForeignKey(ct_field='tagged_type', + fk_field='tagged_id') + + def __unicode__(self): + return '<%s: %s> tagged "%s"' % (self.tagged.__class__.__name__, + self.tagged, self.name) + +class PersonManager(models.Manager): + def get_by_natural_key(self, name): + return self.get(name=name) + +class Person(models.Model): + objects = PersonManager() + name = models.CharField(max_length=100) + def __unicode__(self): + return self.name + + class Meta: + ordering = ('name',) + + def natural_key(self): + return (self.name,) + +class Visa(models.Model): + person = models.ForeignKey(Person) + permissions = models.ManyToManyField(Permission, blank=True) + + def __unicode__(self): + return '%s %s' % (self.person.name, + ', '.join(p.name for p in self.permissions.all())) + +class Book(models.Model): + name = models.CharField(max_length=100) + authors = models.ManyToManyField(Person) + + def __unicode__(self): + return '%s by %s' % (self.name, + ' and '.join(a.name for a in self.authors.all())) + + class Meta: + ordering = ('name',) diff --git a/parts/django/tests/modeltests/fixtures/tests.py b/parts/django/tests/modeltests/fixtures/tests.py new file mode 100644 index 0000000..4facc6d --- /dev/null +++ b/parts/django/tests/modeltests/fixtures/tests.py @@ -0,0 +1,277 @@ +import StringIO +import sys + +from django.test import TestCase, TransactionTestCase +from django.conf import settings +from django.core import management +from django.db import DEFAULT_DB_ALIAS + +from models import Article, Blog, Book, Category, Person, Tag, Visa + +class TestCaseFixtureLoadingTests(TestCase): + fixtures = ['fixture1.json', 'fixture2.json'] + + def testClassFixtures(self): + "Check that test case has installed 4 fixture objects" + self.assertEqual(Article.objects.count(), 4) + self.assertQuerysetEqual(Article.objects.all(), [ + '<Article: Django conquers world!>', + '<Article: Copyright is fine the way it is>', + '<Article: Poker has no place on ESPN>', + '<Article: Python program becomes self aware>' + ]) + +class FixtureLoadingTests(TestCase): + + def _dumpdata_assert(self, args, output, format='json', natural_keys=False): + new_io = StringIO.StringIO() + management.call_command('dumpdata', *args, **{'format':format, 'stdout':new_io, 'use_natural_keys':natural_keys}) + command_output = new_io.getvalue().strip() + self.assertEqual(command_output, output) + + def test_initial_data(self): + # Syncdb introduces 1 initial data object from initial_data.json. + self.assertQuerysetEqual(Article.objects.all(), [ + '<Article: Python program becomes self aware>' + ]) + + def test_loading_and_dumping(self): + new_io = StringIO.StringIO() + + # Load fixture 1. Single JSON file, with two objects. + management.call_command('loaddata', 'fixture1.json', verbosity=0, commit=False) + self.assertQuerysetEqual(Article.objects.all(), [ + '<Article: Time to reform copyright>', + '<Article: Poker has no place on ESPN>', + '<Article: Python program becomes self aware>' + ]) + + # Dump the current contents of the database as a JSON fixture + self._dumpdata_assert(['fixtures'], '[{"pk": 1, "model": "fixtures.category", "fields": {"description": "Latest news stories", "title": "News Stories"}}, {"pk": 3, "model": "fixtures.article", "fields": {"headline": "Time to reform copyright", "pub_date": "2006-06-16 13:00:00"}}, {"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker has no place on ESPN", "pub_date": "2006-06-16 12:00:00"}}, {"pk": 1, "model": "fixtures.article", "fields": {"headline": "Python program becomes self aware", "pub_date": "2006-06-16 11:00:00"}}]') + + # Try just dumping the contents of fixtures.Category + self._dumpdata_assert(['fixtures.Category'], '[{"pk": 1, "model": "fixtures.category", "fields": {"description": "Latest news stories", "title": "News Stories"}}]') + + # ...and just fixtures.Article + self._dumpdata_assert(['fixtures.Article'], '[{"pk": 3, "model": "fixtures.article", "fields": {"headline": "Time to reform copyright", "pub_date": "2006-06-16 13:00:00"}}, {"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker has no place on ESPN", "pub_date": "2006-06-16 12:00:00"}}, {"pk": 1, "model": "fixtures.article", "fields": {"headline": "Python program becomes self aware", "pub_date": "2006-06-16 11:00:00"}}]') + + # ...and both + self._dumpdata_assert(['fixtures.Category', 'fixtures.Article'], '[{"pk": 1, "model": "fixtures.category", "fields": {"description": "Latest news stories", "title": "News Stories"}}, {"pk": 3, "model": "fixtures.article", "fields": {"headline": "Time to reform copyright", "pub_date": "2006-06-16 13:00:00"}}, {"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker has no place on ESPN", "pub_date": "2006-06-16 12:00:00"}}, {"pk": 1, "model": "fixtures.article", "fields": {"headline": "Python program becomes self aware", "pub_date": "2006-06-16 11:00:00"}}]') + + # Specify a specific model twice + self._dumpdata_assert(['fixtures.Article', 'fixtures.Article'], '[{"pk": 3, "model": "fixtures.article", "fields": {"headline": "Time to reform copyright", "pub_date": "2006-06-16 13:00:00"}}, {"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker has no place on ESPN", "pub_date": "2006-06-16 12:00:00"}}, {"pk": 1, "model": "fixtures.article", "fields": {"headline": "Python program becomes self aware", "pub_date": "2006-06-16 11:00:00"}}]') + + # Specify a dump that specifies Article both explicitly and implicitly + self._dumpdata_assert(['fixtures.Article', 'fixtures'], '[{"pk": 1, "model": "fixtures.category", "fields": {"description": "Latest news stories", "title": "News Stories"}}, {"pk": 3, "model": "fixtures.article", "fields": {"headline": "Time to reform copyright", "pub_date": "2006-06-16 13:00:00"}}, {"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker has no place on ESPN", "pub_date": "2006-06-16 12:00:00"}}, {"pk": 1, "model": "fixtures.article", "fields": {"headline": "Python program becomes self aware", "pub_date": "2006-06-16 11:00:00"}}]') + + # Same again, but specify in the reverse order + self._dumpdata_assert(['fixtures'], '[{"pk": 1, "model": "fixtures.category", "fields": {"description": "Latest news stories", "title": "News Stories"}}, {"pk": 3, "model": "fixtures.article", "fields": {"headline": "Time to reform copyright", "pub_date": "2006-06-16 13:00:00"}}, {"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker has no place on ESPN", "pub_date": "2006-06-16 12:00:00"}}, {"pk": 1, "model": "fixtures.article", "fields": {"headline": "Python program becomes self aware", "pub_date": "2006-06-16 11:00:00"}}]') + + # Specify one model from one application, and an entire other application. + self._dumpdata_assert(['fixtures.Category', 'sites'], '[{"pk": 1, "model": "fixtures.category", "fields": {"description": "Latest news stories", "title": "News Stories"}}, {"pk": 1, "model": "sites.site", "fields": {"domain": "example.com", "name": "example.com"}}]') + + # Load fixture 2. JSON file imported by default. Overwrites some existing objects + management.call_command('loaddata', 'fixture2.json', verbosity=0, commit=False) + self.assertQuerysetEqual(Article.objects.all(), [ + '<Article: Django conquers world!>', + '<Article: Copyright is fine the way it is>', + '<Article: Poker has no place on ESPN>', + '<Article: Python program becomes self aware>' + ]) + + # Load fixture 3, XML format. + management.call_command('loaddata', 'fixture3.xml', verbosity=0, commit=False) + self.assertQuerysetEqual(Article.objects.all(), [ + '<Article: XML identified as leading cause of cancer>', + '<Article: Django conquers world!>', + '<Article: Copyright is fine the way it is>', + '<Article: Poker on TV is great!>', + '<Article: Python program becomes self aware>' + ]) + + # Load fixture 6, JSON file with dynamic ContentType fields. Testing ManyToOne. + management.call_command('loaddata', 'fixture6.json', verbosity=0, commit=False) + self.assertQuerysetEqual(Tag.objects.all(), [ + '<Tag: <Article: Copyright is fine the way it is> tagged "copyright">', + '<Tag: <Article: Copyright is fine the way it is> tagged "law">' + ]) + + # Load fixture 7, XML file with dynamic ContentType fields. Testing ManyToOne. + management.call_command('loaddata', 'fixture7.xml', verbosity=0, commit=False) + self.assertQuerysetEqual(Tag.objects.all(), [ + '<Tag: <Article: Copyright is fine the way it is> tagged "copyright">', + '<Tag: <Article: Copyright is fine the way it is> tagged "legal">', + '<Tag: <Article: Django conquers world!> tagged "django">', + '<Tag: <Article: Django conquers world!> tagged "world domination">' + ]) + + # Load fixture 8, JSON file with dynamic Permission fields. Testing ManyToMany. + management.call_command('loaddata', 'fixture8.json', verbosity=0, commit=False) + self.assertQuerysetEqual(Visa.objects.all(), [ + '<Visa: Django Reinhardt Can add user, Can change user, Can delete user>', + '<Visa: Stephane Grappelli Can add user>', + '<Visa: Prince >' + ]) + + # Load fixture 9, XML file with dynamic Permission fields. Testing ManyToMany. + management.call_command('loaddata', 'fixture9.xml', verbosity=0, commit=False) + self.assertQuerysetEqual(Visa.objects.all(), [ + '<Visa: Django Reinhardt Can add user, Can change user, Can delete user>', + '<Visa: Stephane Grappelli Can add user, Can delete user>', + '<Visa: Artist formerly known as "Prince" Can change user>' + ]) + + self.assertQuerysetEqual(Book.objects.all(), [ + '<Book: Music for all ages by Artist formerly known as "Prince" and Django Reinhardt>' + ]) + + # Load a fixture that doesn't exist + management.call_command('loaddata', 'unknown.json', verbosity=0, commit=False) + + # object list is unaffected + self.assertQuerysetEqual(Article.objects.all(), [ + '<Article: XML identified as leading cause of cancer>', + '<Article: Django conquers world!>', + '<Article: Copyright is fine the way it is>', + '<Article: Poker on TV is great!>', + '<Article: Python program becomes self aware>' + ]) + + # By default, you get raw keys on dumpdata + self._dumpdata_assert(['fixtures.book'], '[{"pk": 1, "model": "fixtures.book", "fields": {"name": "Music for all ages", "authors": [3, 1]}}]') + + # But you can get natural keys if you ask for them and they are available + self._dumpdata_assert(['fixtures.book'], '[{"pk": 1, "model": "fixtures.book", "fields": {"name": "Music for all ages", "authors": [["Artist formerly known as \\"Prince\\""], ["Django Reinhardt"]]}}]', natural_keys=True) + + # Dump the current contents of the database as a JSON fixture + self._dumpdata_assert(['fixtures'], '[{"pk": 1, "model": "fixtures.category", "fields": {"description": "Latest news stories", "title": "News Stories"}}, {"pk": 5, "model": "fixtures.article", "fields": {"headline": "XML identified as leading cause of cancer", "pub_date": "2006-06-16 16:00:00"}}, {"pk": 4, "model": "fixtures.article", "fields": {"headline": "Django conquers world!", "pub_date": "2006-06-16 15:00:00"}}, {"pk": 3, "model": "fixtures.article", "fields": {"headline": "Copyright is fine the way it is", "pub_date": "2006-06-16 14:00:00"}}, {"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker on TV is great!", "pub_date": "2006-06-16 11:00:00"}}, {"pk": 1, "model": "fixtures.article", "fields": {"headline": "Python program becomes self aware", "pub_date": "2006-06-16 11:00:00"}}, {"pk": 1, "model": "fixtures.tag", "fields": {"tagged_type": ["fixtures", "article"], "name": "copyright", "tagged_id": 3}}, {"pk": 2, "model": "fixtures.tag", "fields": {"tagged_type": ["fixtures", "article"], "name": "legal", "tagged_id": 3}}, {"pk": 3, "model": "fixtures.tag", "fields": {"tagged_type": ["fixtures", "article"], "name": "django", "tagged_id": 4}}, {"pk": 4, "model": "fixtures.tag", "fields": {"tagged_type": ["fixtures", "article"], "name": "world domination", "tagged_id": 4}}, {"pk": 3, "model": "fixtures.person", "fields": {"name": "Artist formerly known as \\"Prince\\""}}, {"pk": 1, "model": "fixtures.person", "fields": {"name": "Django Reinhardt"}}, {"pk": 2, "model": "fixtures.person", "fields": {"name": "Stephane Grappelli"}}, {"pk": 1, "model": "fixtures.visa", "fields": {"person": ["Django Reinhardt"], "permissions": [["add_user", "auth", "user"], ["change_user", "auth", "user"], ["delete_user", "auth", "user"]]}}, {"pk": 2, "model": "fixtures.visa", "fields": {"person": ["Stephane Grappelli"], "permissions": [["add_user", "auth", "user"], ["delete_user", "auth", "user"]]}}, {"pk": 3, "model": "fixtures.visa", "fields": {"person": ["Artist formerly known as \\"Prince\\""], "permissions": [["change_user", "auth", "user"]]}}, {"pk": 1, "model": "fixtures.book", "fields": {"name": "Music for all ages", "authors": [["Artist formerly known as \\"Prince\\""], ["Django Reinhardt"]]}}]', natural_keys=True) + + # Dump the current contents of the database as an XML fixture + self._dumpdata_assert(['fixtures'], """<?xml version="1.0" encoding="utf-8"?> +<django-objects version="1.0"><object pk="1" model="fixtures.category"><field type="CharField" name="title">News Stories</field><field type="TextField" name="description">Latest news stories</field></object><object pk="5" model="fixtures.article"><field type="CharField" name="headline">XML identified as leading cause of cancer</field><field type="DateTimeField" name="pub_date">2006-06-16 16:00:00</field></object><object pk="4" model="fixtures.article"><field type="CharField" name="headline">Django conquers world!</field><field type="DateTimeField" name="pub_date">2006-06-16 15:00:00</field></object><object pk="3" model="fixtures.article"><field type="CharField" name="headline">Copyright is fine the way it is</field><field type="DateTimeField" name="pub_date">2006-06-16 14:00:00</field></object><object pk="2" model="fixtures.article"><field type="CharField" name="headline">Poker on TV is great!</field><field type="DateTimeField" name="pub_date">2006-06-16 11:00:00</field></object><object pk="1" model="fixtures.article"><field type="CharField" name="headline">Python program becomes self aware</field><field type="DateTimeField" name="pub_date">2006-06-16 11:00:00</field></object><object pk="1" model="fixtures.tag"><field type="CharField" name="name">copyright</field><field to="contenttypes.contenttype" name="tagged_type" rel="ManyToOneRel"><natural>fixtures</natural><natural>article</natural></field><field type="PositiveIntegerField" name="tagged_id">3</field></object><object pk="2" model="fixtures.tag"><field type="CharField" name="name">legal</field><field to="contenttypes.contenttype" name="tagged_type" rel="ManyToOneRel"><natural>fixtures</natural><natural>article</natural></field><field type="PositiveIntegerField" name="tagged_id">3</field></object><object pk="3" model="fixtures.tag"><field type="CharField" name="name">django</field><field to="contenttypes.contenttype" name="tagged_type" rel="ManyToOneRel"><natural>fixtures</natural><natural>article</natural></field><field type="PositiveIntegerField" name="tagged_id">4</field></object><object pk="4" model="fixtures.tag"><field type="CharField" name="name">world domination</field><field to="contenttypes.contenttype" name="tagged_type" rel="ManyToOneRel"><natural>fixtures</natural><natural>article</natural></field><field type="PositiveIntegerField" name="tagged_id">4</field></object><object pk="3" model="fixtures.person"><field type="CharField" name="name">Artist formerly known as "Prince"</field></object><object pk="1" model="fixtures.person"><field type="CharField" name="name">Django Reinhardt</field></object><object pk="2" model="fixtures.person"><field type="CharField" name="name">Stephane Grappelli</field></object><object pk="1" model="fixtures.visa"><field to="fixtures.person" name="person" rel="ManyToOneRel"><natural>Django Reinhardt</natural></field><field to="auth.permission" name="permissions" rel="ManyToManyRel"><object><natural>add_user</natural><natural>auth</natural><natural>user</natural></object><object><natural>change_user</natural><natural>auth</natural><natural>user</natural></object><object><natural>delete_user</natural><natural>auth</natural><natural>user</natural></object></field></object><object pk="2" model="fixtures.visa"><field to="fixtures.person" name="person" rel="ManyToOneRel"><natural>Stephane Grappelli</natural></field><field to="auth.permission" name="permissions" rel="ManyToManyRel"><object><natural>add_user</natural><natural>auth</natural><natural>user</natural></object><object><natural>delete_user</natural><natural>auth</natural><natural>user</natural></object></field></object><object pk="3" model="fixtures.visa"><field to="fixtures.person" name="person" rel="ManyToOneRel"><natural>Artist formerly known as "Prince"</natural></field><field to="auth.permission" name="permissions" rel="ManyToManyRel"><object><natural>change_user</natural><natural>auth</natural><natural>user</natural></object></field></object><object pk="1" model="fixtures.book"><field type="CharField" name="name">Music for all ages</field><field to="fixtures.person" name="authors" rel="ManyToManyRel"><object><natural>Artist formerly known as "Prince"</natural></object><object><natural>Django Reinhardt</natural></object></field></object></django-objects>""", format='xml', natural_keys=True) + + def test_compress_format_loading(self): + # Load fixture 4 (compressed), using format specification + management.call_command('loaddata', 'fixture4.json', verbosity=0, commit=False) + self.assertQuerysetEqual(Article.objects.all(), [ + '<Article: Django pets kitten>', + '<Article: Python program becomes self aware>' + ]) + + def test_compressed_specified_loading(self): + # Load fixture 5 (compressed), using format *and* compression specification + management.call_command('loaddata', 'fixture5.json.zip', verbosity=0, commit=False) + self.assertQuerysetEqual(Article.objects.all(), [ + '<Article: WoW subscribers now outnumber readers>', + '<Article: Python program becomes self aware>' + ]) + + def test_compressed_loading(self): + # Load fixture 5 (compressed), only compression specification + management.call_command('loaddata', 'fixture5.zip', verbosity=0, commit=False) + self.assertQuerysetEqual(Article.objects.all(), [ + '<Article: WoW subscribers now outnumber readers>', + '<Article: Python program becomes self aware>' + ]) + + def test_ambiguous_compressed_fixture(self): + # The name "fixture5" is ambigous, so loading it will raise an error + new_io = StringIO.StringIO() + management.call_command('loaddata', 'fixture5', verbosity=0, stderr=new_io, commit=False) + output = new_io.getvalue().strip().split('\n') + self.assertEqual(len(output), 1) + self.assertTrue(output[0].startswith("Multiple fixtures named 'fixture5'")) + + def test_db_loading(self): + # Load db fixtures 1 and 2. These will load using the 'default' database identifier implicitly + management.call_command('loaddata', 'db_fixture_1', verbosity=0, commit=False) + management.call_command('loaddata', 'db_fixture_2', verbosity=0, commit=False) + self.assertQuerysetEqual(Article.objects.all(), [ + '<Article: Who needs more than one database?>', + '<Article: Who needs to use compressed data?>', + '<Article: Python program becomes self aware>' + ]) + + def test_loading_using(self): + # Load db fixtures 1 and 2. These will load using the 'default' database identifier explicitly + management.call_command('loaddata', 'db_fixture_1', verbosity=0, using='default', commit=False) + management.call_command('loaddata', 'db_fixture_2', verbosity=0, using='default', commit=False) + self.assertQuerysetEqual(Article.objects.all(), [ + '<Article: Who needs more than one database?>', + '<Article: Who needs to use compressed data?>', + '<Article: Python program becomes self aware>' + ]) + + def test_unmatched_identifier_loading(self): + # Try to load db fixture 3. This won't load because the database identifier doesn't match + management.call_command('loaddata', 'db_fixture_3', verbosity=0, commit=False) + self.assertQuerysetEqual(Article.objects.all(), [ + '<Article: Python program becomes self aware>' + ]) + + management.call_command('loaddata', 'db_fixture_3', verbosity=0, using='default', commit=False) + self.assertQuerysetEqual(Article.objects.all(), [ + '<Article: Python program becomes self aware>' + ]) + + def test_output_formats(self): + # Load back in fixture 1, we need the articles from it + management.call_command('loaddata', 'fixture1', verbosity=0, commit=False) + + # Try to load fixture 6 using format discovery + management.call_command('loaddata', 'fixture6', verbosity=0, commit=False) + self.assertQuerysetEqual(Tag.objects.all(), [ + '<Tag: <Article: Time to reform copyright> tagged "copyright">', + '<Tag: <Article: Time to reform copyright> tagged "law">' + ]) + + # Dump the current contents of the database as a JSON fixture + self._dumpdata_assert(['fixtures'], '[{"pk": 1, "model": "fixtures.category", "fields": {"description": "Latest news stories", "title": "News Stories"}}, {"pk": 3, "model": "fixtures.article", "fields": {"headline": "Time to reform copyright", "pub_date": "2006-06-16 13:00:00"}}, {"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker has no place on ESPN", "pub_date": "2006-06-16 12:00:00"}}, {"pk": 1, "model": "fixtures.article", "fields": {"headline": "Python program becomes self aware", "pub_date": "2006-06-16 11:00:00"}}, {"pk": 1, "model": "fixtures.tag", "fields": {"tagged_type": ["fixtures", "article"], "name": "copyright", "tagged_id": 3}}, {"pk": 2, "model": "fixtures.tag", "fields": {"tagged_type": ["fixtures", "article"], "name": "law", "tagged_id": 3}}, {"pk": 1, "model": "fixtures.person", "fields": {"name": "Django Reinhardt"}}, {"pk": 3, "model": "fixtures.person", "fields": {"name": "Prince"}}, {"pk": 2, "model": "fixtures.person", "fields": {"name": "Stephane Grappelli"}}]', natural_keys=True) + + # Dump the current contents of the database as an XML fixture + self._dumpdata_assert(['fixtures'], """<?xml version="1.0" encoding="utf-8"?> +<django-objects version="1.0"><object pk="1" model="fixtures.category"><field type="CharField" name="title">News Stories</field><field type="TextField" name="description">Latest news stories</field></object><object pk="3" model="fixtures.article"><field type="CharField" name="headline">Time to reform copyright</field><field type="DateTimeField" name="pub_date">2006-06-16 13:00:00</field></object><object pk="2" model="fixtures.article"><field type="CharField" name="headline">Poker has no place on ESPN</field><field type="DateTimeField" name="pub_date">2006-06-16 12:00:00</field></object><object pk="1" model="fixtures.article"><field type="CharField" name="headline">Python program becomes self aware</field><field type="DateTimeField" name="pub_date">2006-06-16 11:00:00</field></object><object pk="1" model="fixtures.tag"><field type="CharField" name="name">copyright</field><field to="contenttypes.contenttype" name="tagged_type" rel="ManyToOneRel"><natural>fixtures</natural><natural>article</natural></field><field type="PositiveIntegerField" name="tagged_id">3</field></object><object pk="2" model="fixtures.tag"><field type="CharField" name="name">law</field><field to="contenttypes.contenttype" name="tagged_type" rel="ManyToOneRel"><natural>fixtures</natural><natural>article</natural></field><field type="PositiveIntegerField" name="tagged_id">3</field></object><object pk="1" model="fixtures.person"><field type="CharField" name="name">Django Reinhardt</field></object><object pk="3" model="fixtures.person"><field type="CharField" name="name">Prince</field></object><object pk="2" model="fixtures.person"><field type="CharField" name="name">Stephane Grappelli</field></object></django-objects>""", format='xml', natural_keys=True) + +if settings.DATABASES[DEFAULT_DB_ALIAS]['ENGINE'] != 'django.db.backends.mysql': + class FixtureTransactionTests(TransactionTestCase): + def _dumpdata_assert(self, args, output, format='json'): + new_io = StringIO.StringIO() + management.call_command('dumpdata', *args, **{'format':format, 'stdout':new_io}) + command_output = new_io.getvalue().strip() + self.assertEqual(command_output, output) + + def test_format_discovery(self): + # Load fixture 1 again, using format discovery + management.call_command('loaddata', 'fixture1', verbosity=0, commit=False) + self.assertQuerysetEqual(Article.objects.all(), [ + '<Article: Time to reform copyright>', + '<Article: Poker has no place on ESPN>', + '<Article: Python program becomes self aware>' + ]) + + # Try to load fixture 2 using format discovery; this will fail + # because there are two fixture2's in the fixtures directory + new_io = StringIO.StringIO() + management.call_command('loaddata', 'fixture2', verbosity=0, stderr=new_io) + output = new_io.getvalue().strip().split('\n') + self.assertEqual(len(output), 1) + self.assertTrue(output[0].startswith("Multiple fixtures named 'fixture2'")) + + # object list is unaffected + self.assertQuerysetEqual(Article.objects.all(), [ + '<Article: Time to reform copyright>', + '<Article: Poker has no place on ESPN>', + '<Article: Python program becomes self aware>' + ]) + + # Dump the current contents of the database as a JSON fixture + self._dumpdata_assert(['fixtures'], '[{"pk": 1, "model": "fixtures.category", "fields": {"description": "Latest news stories", "title": "News Stories"}}, {"pk": 3, "model": "fixtures.article", "fields": {"headline": "Time to reform copyright", "pub_date": "2006-06-16 13:00:00"}}, {"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker has no place on ESPN", "pub_date": "2006-06-16 12:00:00"}}, {"pk": 1, "model": "fixtures.article", "fields": {"headline": "Python program becomes self aware", "pub_date": "2006-06-16 11:00:00"}}]') + + # Load fixture 4 (compressed), using format discovery + management.call_command('loaddata', 'fixture4', verbosity=0, commit=False) + self.assertQuerysetEqual(Article.objects.all(), [ + '<Article: Django pets kitten>', + '<Article: Time to reform copyright>', + '<Article: Poker has no place on ESPN>', + '<Article: Python program becomes self aware>' + ]) diff --git a/parts/django/tests/modeltests/fixtures_model_package/__init__.py b/parts/django/tests/modeltests/fixtures_model_package/__init__.py new file mode 100644 index 0000000..139597f --- /dev/null +++ b/parts/django/tests/modeltests/fixtures_model_package/__init__.py @@ -0,0 +1,2 @@ + + diff --git a/parts/django/tests/modeltests/fixtures_model_package/fixtures/fixture1.json b/parts/django/tests/modeltests/fixtures_model_package/fixtures/fixture1.json new file mode 100644 index 0000000..7684d84 --- /dev/null +++ b/parts/django/tests/modeltests/fixtures_model_package/fixtures/fixture1.json @@ -0,0 +1,18 @@ +[ + { + "pk": "2", + "model": "fixtures_model_package.article", + "fields": { + "headline": "Poker has no place on ESPN", + "pub_date": "2006-06-16 12:00:00" + } + }, + { + "pk": "3", + "model": "fixtures_model_package.article", + "fields": { + "headline": "Time to reform copyright", + "pub_date": "2006-06-16 13:00:00" + } + } +] diff --git a/parts/django/tests/modeltests/fixtures_model_package/fixtures/fixture2.json b/parts/django/tests/modeltests/fixtures_model_package/fixtures/fixture2.json new file mode 100644 index 0000000..4997627 --- /dev/null +++ b/parts/django/tests/modeltests/fixtures_model_package/fixtures/fixture2.json @@ -0,0 +1,18 @@ +[ + { + "pk": "3", + "model": "fixtures_model_package.article", + "fields": { + "headline": "Copyright is fine the way it is", + "pub_date": "2006-06-16 14:00:00" + } + }, + { + "pk": "4", + "model": "fixtures_model_package.article", + "fields": { + "headline": "Django conquers world!", + "pub_date": "2006-06-16 15:00:00" + } + } +] diff --git a/parts/django/tests/modeltests/fixtures_model_package/fixtures/fixture2.xml b/parts/django/tests/modeltests/fixtures_model_package/fixtures/fixture2.xml new file mode 100644 index 0000000..55337cf --- /dev/null +++ b/parts/django/tests/modeltests/fixtures_model_package/fixtures/fixture2.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<django-objects version="1.0"> + <object pk="2" model="fixtures_model_package.article"> + <field type="CharField" name="headline">Poker on TV is great!</field> + <field type="DateTimeField" name="pub_date">2006-06-16 11:00:00</field> + </object> + <object pk="5" model="fixtures_model_package.article"> + <field type="CharField" name="headline">XML identified as leading cause of cancer</field> + <field type="DateTimeField" name="pub_date">2006-06-16 16:00:00</field> + </object> +</django-objects> diff --git a/parts/django/tests/modeltests/fixtures_model_package/fixtures/initial_data.json b/parts/django/tests/modeltests/fixtures_model_package/fixtures/initial_data.json new file mode 100644 index 0000000..66cb5d7 --- /dev/null +++ b/parts/django/tests/modeltests/fixtures_model_package/fixtures/initial_data.json @@ -0,0 +1,10 @@ +[ + { + "pk": "1", + "model": "fixtures_model_package.article", + "fields": { + "headline": "Python program becomes self aware", + "pub_date": "2006-06-16 11:00:00" + } + } +] diff --git a/parts/django/tests/modeltests/fixtures_model_package/models/__init__.py b/parts/django/tests/modeltests/fixtures_model_package/models/__init__.py new file mode 100644 index 0000000..c0450b2 --- /dev/null +++ b/parts/django/tests/modeltests/fixtures_model_package/models/__init__.py @@ -0,0 +1,14 @@ +from django.db import models +from django.conf import settings + +class Article(models.Model): + headline = models.CharField(max_length=100, default='Default headline') + pub_date = models.DateTimeField() + + def __unicode__(self): + return self.headline + + class Meta: + app_label = 'fixtures_model_package' + ordering = ('-pub_date', 'headline') + diff --git a/parts/django/tests/modeltests/fixtures_model_package/tests.py b/parts/django/tests/modeltests/fixtures_model_package/tests.py new file mode 100644 index 0000000..1fae5ee --- /dev/null +++ b/parts/django/tests/modeltests/fixtures_model_package/tests.py @@ -0,0 +1,71 @@ +from django.core import management +from django.test import TestCase + +from models import Article + + +class SampleTestCase(TestCase): + fixtures = ['fixture1.json', 'fixture2.json'] + + def testClassFixtures(self): + "Test cases can load fixture objects into models defined in packages" + self.assertEqual(Article.objects.count(), 4) + self.assertQuerysetEqual( + Article.objects.all(),[ + "Django conquers world!", + "Copyright is fine the way it is", + "Poker has no place on ESPN", + "Python program becomes self aware" + ], + lambda a: a.headline + ) + + +class FixtureTestCase(TestCase): + def test_initial_data(self): + "Fixtures can load initial data into models defined in packages" + #Syncdb introduces 1 initial data object from initial_data.json + self.assertQuerysetEqual( + Article.objects.all(), [ + "Python program becomes self aware" + ], + lambda a: a.headline + ) + + def test_loaddata(self): + "Fixtures can load data into models defined in packages" + # Load fixture 1. Single JSON file, with two objects + management.call_command("loaddata", "fixture1.json", verbosity=0, commit=False) + self.assertQuerysetEqual( + Article.objects.all(), [ + "Time to reform copyright", + "Poker has no place on ESPN", + "Python program becomes self aware", + ], + lambda a: a.headline, + ) + + # Load fixture 2. JSON file imported by default. Overwrites some + # existing objects + management.call_command("loaddata", "fixture2.json", verbosity=0, commit=False) + self.assertQuerysetEqual( + Article.objects.all(), [ + "Django conquers world!", + "Copyright is fine the way it is", + "Poker has no place on ESPN", + "Python program becomes self aware", + ], + lambda a: a.headline, + ) + + # Load a fixture that doesn't exist + management.call_command("loaddata", "unknown.json", verbosity=0, commit=False) + self.assertQuerysetEqual( + Article.objects.all(), [ + "Django conquers world!", + "Copyright is fine the way it is", + "Poker has no place on ESPN", + "Python program becomes self aware", + ], + lambda a: a.headline, + ) diff --git a/parts/django/tests/modeltests/force_insert_update/__init__.py b/parts/django/tests/modeltests/force_insert_update/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/modeltests/force_insert_update/__init__.py diff --git a/parts/django/tests/modeltests/force_insert_update/models.py b/parts/django/tests/modeltests/force_insert_update/models.py new file mode 100644 index 0000000..9516be7 --- /dev/null +++ b/parts/django/tests/modeltests/force_insert_update/models.py @@ -0,0 +1,13 @@ +""" +Tests for forcing insert and update queries (instead of Django's normal +automatic behaviour). +""" +from django.db import models, transaction, IntegrityError + +class Counter(models.Model): + name = models.CharField(max_length = 10) + value = models.IntegerField() + +class WithCustomPK(models.Model): + name = models.IntegerField(primary_key=True) + value = models.IntegerField() diff --git a/parts/django/tests/modeltests/force_insert_update/tests.py b/parts/django/tests/modeltests/force_insert_update/tests.py new file mode 100644 index 0000000..bd3eb7d --- /dev/null +++ b/parts/django/tests/modeltests/force_insert_update/tests.py @@ -0,0 +1,38 @@ +from django.db import transaction, IntegrityError, DatabaseError +from django.test import TestCase + +from models import Counter, WithCustomPK + + +class ForceTests(TestCase): + def test_force_update(self): + c = Counter.objects.create(name="one", value=1) + # The normal case + + c.value = 2 + c.save() + # Same thing, via an update + c.value = 3 + c.save(force_update=True) + + # Won't work because force_update and force_insert are mutually + # exclusive + c.value = 4 + self.assertRaises(ValueError, c.save, force_insert=True, force_update=True) + + # Try to update something that doesn't have a primary key in the first + # place. + c1 = Counter(name="two", value=2) + self.assertRaises(ValueError, c1.save, force_update=True) + c1.save(force_insert=True) + + # Won't work because we can't insert a pk of the same value. + sid = transaction.savepoint() + c.value = 5 + self.assertRaises(IntegrityError, c.save, force_insert=True) + transaction.savepoint_rollback(sid) + + # Trying to update should still fail, even with manual primary keys, if + # the data isn't in the database already. + obj = WithCustomPK(name=1, value=1) + self.assertRaises(DatabaseError, obj.save, force_update=True) diff --git a/parts/django/tests/modeltests/generic_relations/__init__.py b/parts/django/tests/modeltests/generic_relations/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/modeltests/generic_relations/__init__.py diff --git a/parts/django/tests/modeltests/generic_relations/models.py b/parts/django/tests/modeltests/generic_relations/models.py new file mode 100644 index 0000000..18b77a3 --- /dev/null +++ b/parts/django/tests/modeltests/generic_relations/models.py @@ -0,0 +1,80 @@ +""" +34. Generic relations + +Generic relations let an object have a foreign key to any object through a +content-type/object-id field. A ``GenericForeignKey`` field can point to any +object, be it animal, vegetable, or mineral. + +The canonical example is tags (although this example implementation is *far* +from complete). +""" + +from django.contrib.contenttypes import generic +from django.contrib.contenttypes.models import ContentType +from django.db import models + + +class TaggedItem(models.Model): + """A tag on an item.""" + tag = models.SlugField() + content_type = models.ForeignKey(ContentType) + object_id = models.PositiveIntegerField() + + content_object = generic.GenericForeignKey() + + class Meta: + ordering = ["tag", "content_type__name"] + + def __unicode__(self): + return self.tag + +class ValuableTaggedItem(TaggedItem): + value = models.PositiveIntegerField() + +class Comparison(models.Model): + """ + A model that tests having multiple GenericForeignKeys + """ + comparative = models.CharField(max_length=50) + + content_type1 = models.ForeignKey(ContentType, related_name="comparative1_set") + object_id1 = models.PositiveIntegerField() + + content_type2 = models.ForeignKey(ContentType, related_name="comparative2_set") + object_id2 = models.PositiveIntegerField() + + first_obj = generic.GenericForeignKey(ct_field="content_type1", fk_field="object_id1") + other_obj = generic.GenericForeignKey(ct_field="content_type2", fk_field="object_id2") + + def __unicode__(self): + return u"%s is %s than %s" % (self.first_obj, self.comparative, self.other_obj) + +class Animal(models.Model): + common_name = models.CharField(max_length=150) + latin_name = models.CharField(max_length=150) + + tags = generic.GenericRelation(TaggedItem) + comparisons = generic.GenericRelation(Comparison, + object_id_field="object_id1", + content_type_field="content_type1") + + def __unicode__(self): + return self.common_name + +class Vegetable(models.Model): + name = models.CharField(max_length=150) + is_yucky = models.BooleanField(default=True) + + tags = generic.GenericRelation(TaggedItem) + + def __unicode__(self): + return self.name + +class Mineral(models.Model): + name = models.CharField(max_length=150) + hardness = models.PositiveSmallIntegerField() + + # note the lack of an explicit GenericRelation here... + + def __unicode__(self): + return self.name diff --git a/parts/django/tests/modeltests/generic_relations/tests.py b/parts/django/tests/modeltests/generic_relations/tests.py new file mode 100644 index 0000000..3d25301 --- /dev/null +++ b/parts/django/tests/modeltests/generic_relations/tests.py @@ -0,0 +1,223 @@ +from django.contrib.contenttypes.generic import generic_inlineformset_factory +from django.contrib.contenttypes.models import ContentType +from django.test import TestCase + +from models import (TaggedItem, ValuableTaggedItem, Comparison, Animal, + Vegetable, Mineral) + + +class GenericRelationsTests(TestCase): + def test_generic_relations(self): + # Create the world in 7 lines of code... + lion = Animal.objects.create(common_name="Lion", latin_name="Panthera leo") + platypus = Animal.objects.create( + common_name="Platypus", latin_name="Ornithorhynchus anatinus" + ) + eggplant = Vegetable.objects.create(name="Eggplant", is_yucky=True) + bacon = Vegetable.objects.create(name="Bacon", is_yucky=False) + quartz = Mineral.objects.create(name="Quartz", hardness=7) + + # Objects with declared GenericRelations can be tagged directly -- the + # API mimics the many-to-many API. + bacon.tags.create(tag="fatty") + bacon.tags.create(tag="salty") + lion.tags.create(tag="yellow") + lion.tags.create(tag="hairy") + platypus.tags.create(tag="fatty") + self.assertQuerysetEqual(lion.tags.all(), [ + "<TaggedItem: hairy>", + "<TaggedItem: yellow>" + ]) + self.assertQuerysetEqual(bacon.tags.all(), [ + "<TaggedItem: fatty>", + "<TaggedItem: salty>" + ]) + + # You can easily access the content object like a foreign key. + t = TaggedItem.objects.get(tag="salty") + self.assertEqual(t.content_object, bacon) + + # Recall that the Mineral class doesn't have an explicit GenericRelation + # defined. That's OK, because you can create TaggedItems explicitly. + tag1 = TaggedItem.objects.create(content_object=quartz, tag="shiny") + tag2 = TaggedItem.objects.create(content_object=quartz, tag="clearish") + + # However, excluding GenericRelations means your lookups have to be a + # bit more explicit. + ctype = ContentType.objects.get_for_model(quartz) + q = TaggedItem.objects.filter( + content_type__pk=ctype.id, object_id=quartz.id + ) + self.assertQuerysetEqual(q, [ + "<TaggedItem: clearish>", + "<TaggedItem: shiny>" + ]) + + # You can set a generic foreign key in the way you'd expect. + tag1.content_object = platypus + tag1.save() + self.assertQuerysetEqual(platypus.tags.all(), [ + "<TaggedItem: fatty>", + "<TaggedItem: shiny>" + ]) + q = TaggedItem.objects.filter( + content_type__pk=ctype.id, object_id=quartz.id + ) + self.assertQuerysetEqual(q, ["<TaggedItem: clearish>"]) + + # Queries across generic relations respect the content types. Even + # though there are two TaggedItems with a tag of "fatty", this query + # only pulls out the one with the content type related to Animals. + self.assertQuerysetEqual(Animal.objects.order_by('common_name'), [ + "<Animal: Lion>", + "<Animal: Platypus>" + ]) + self.assertQuerysetEqual(Animal.objects.filter(tags__tag='fatty'), [ + "<Animal: Platypus>" + ]) + self.assertQuerysetEqual(Animal.objects.exclude(tags__tag='fatty'), [ + "<Animal: Lion>" + ]) + + # If you delete an object with an explicit Generic relation, the related + # objects are deleted when the source object is deleted. + # Original list of tags: + comp_func = lambda obj: ( + obj.tag, obj.content_type.model_class(), obj.object_id + ) + + self.assertQuerysetEqual(TaggedItem.objects.all(), [ + (u'clearish', Mineral, quartz.pk), + (u'fatty', Animal, platypus.pk), + (u'fatty', Vegetable, bacon.pk), + (u'hairy', Animal, lion.pk), + (u'salty', Vegetable, bacon.pk), + (u'shiny', Animal, platypus.pk), + (u'yellow', Animal, lion.pk) + ], + comp_func + ) + lion.delete() + self.assertQuerysetEqual(TaggedItem.objects.all(), [ + (u'clearish', Mineral, quartz.pk), + (u'fatty', Animal, platypus.pk), + (u'fatty', Vegetable, bacon.pk), + (u'salty', Vegetable, bacon.pk), + (u'shiny', Animal, platypus.pk) + ], + comp_func + ) + + # If Generic Relation is not explicitly defined, any related objects + # remain after deletion of the source object. + quartz_pk = quartz.pk + quartz.delete() + self.assertQuerysetEqual(TaggedItem.objects.all(), [ + (u'clearish', Mineral, quartz_pk), + (u'fatty', Animal, platypus.pk), + (u'fatty', Vegetable, bacon.pk), + (u'salty', Vegetable, bacon.pk), + (u'shiny', Animal, platypus.pk) + ], + comp_func + ) + # If you delete a tag, the objects using the tag are unaffected + # (other than losing a tag) + tag = TaggedItem.objects.order_by("id")[0] + tag.delete() + self.assertQuerysetEqual(bacon.tags.all(), ["<TaggedItem: salty>"]) + self.assertQuerysetEqual(TaggedItem.objects.all(), [ + (u'clearish', Mineral, quartz_pk), + (u'fatty', Animal, platypus.pk), + (u'salty', Vegetable, bacon.pk), + (u'shiny', Animal, platypus.pk) + ], + comp_func + ) + TaggedItem.objects.filter(tag='fatty').delete() + ctype = ContentType.objects.get_for_model(lion) + self.assertQuerysetEqual(Animal.objects.filter(tags__content_type=ctype), [ + "<Animal: Platypus>" + ]) + + + def test_multiple_gfk(self): + # Simple tests for multiple GenericForeignKeys + # only uses one model, since the above tests should be sufficient. + tiger = Animal.objects.create(common_name="tiger") + cheetah = Animal.objects.create(common_name="cheetah") + bear = Animal.objects.create(common_name="bear") + + # Create directly + Comparison.objects.create( + first_obj=cheetah, other_obj=tiger, comparative="faster" + ) + Comparison.objects.create( + first_obj=tiger, other_obj=cheetah, comparative="cooler" + ) + + # Create using GenericRelation + tiger.comparisons.create(other_obj=bear, comparative="cooler") + tiger.comparisons.create(other_obj=cheetah, comparative="stronger") + self.assertQuerysetEqual(cheetah.comparisons.all(), [ + "<Comparison: cheetah is faster than tiger>" + ]) + + # Filtering works + self.assertQuerysetEqual(tiger.comparisons.filter(comparative="cooler"), [ + "<Comparison: tiger is cooler than cheetah>", + "<Comparison: tiger is cooler than bear>" + ]) + + # Filtering and deleting works + subjective = ["cooler"] + tiger.comparisons.filter(comparative__in=subjective).delete() + self.assertQuerysetEqual(Comparison.objects.all(), [ + "<Comparison: cheetah is faster than tiger>", + "<Comparison: tiger is stronger than cheetah>" + ]) + + # If we delete cheetah, Comparisons with cheetah as 'first_obj' will be + # deleted since Animal has an explicit GenericRelation to Comparison + # through first_obj. Comparisons with cheetah as 'other_obj' will not + # be deleted. + cheetah.delete() + self.assertQuerysetEqual(Comparison.objects.all(), [ + "<Comparison: tiger is stronger than None>" + ]) + + def test_gfk_subclasses(self): + # GenericForeignKey should work with subclasses (see #8309) + quartz = Mineral.objects.create(name="Quartz", hardness=7) + valuedtag = ValuableTaggedItem.objects.create( + content_object=quartz, tag="shiny", value=10 + ) + self.assertEqual(valuedtag.content_object, quartz) + + def test_generic_inline_formsets(self): + GenericFormSet = generic_inlineformset_factory(TaggedItem, extra=1) + formset = GenericFormSet() + self.assertEqual(u''.join(form.as_p() for form in formset.forms), u"""<p><label for="id_generic_relations-taggeditem-content_type-object_id-0-tag">Tag:</label> <input id="id_generic_relations-taggeditem-content_type-object_id-0-tag" type="text" name="generic_relations-taggeditem-content_type-object_id-0-tag" maxlength="50" /></p> +<p><label for="id_generic_relations-taggeditem-content_type-object_id-0-DELETE">Delete:</label> <input type="checkbox" name="generic_relations-taggeditem-content_type-object_id-0-DELETE" id="id_generic_relations-taggeditem-content_type-object_id-0-DELETE" /><input type="hidden" name="generic_relations-taggeditem-content_type-object_id-0-id" id="id_generic_relations-taggeditem-content_type-object_id-0-id" /></p>""") + + formset = GenericFormSet(instance=Animal()) + self.assertEqual(u''.join(form.as_p() for form in formset.forms), u"""<p><label for="id_generic_relations-taggeditem-content_type-object_id-0-tag">Tag:</label> <input id="id_generic_relations-taggeditem-content_type-object_id-0-tag" type="text" name="generic_relations-taggeditem-content_type-object_id-0-tag" maxlength="50" /></p> +<p><label for="id_generic_relations-taggeditem-content_type-object_id-0-DELETE">Delete:</label> <input type="checkbox" name="generic_relations-taggeditem-content_type-object_id-0-DELETE" id="id_generic_relations-taggeditem-content_type-object_id-0-DELETE" /><input type="hidden" name="generic_relations-taggeditem-content_type-object_id-0-id" id="id_generic_relations-taggeditem-content_type-object_id-0-id" /></p>""") + + platypus = Animal.objects.create( + common_name="Platypus", latin_name="Ornithorhynchus anatinus" + ) + platypus.tags.create(tag="shiny") + GenericFormSet = generic_inlineformset_factory(TaggedItem, extra=1) + formset = GenericFormSet(instance=platypus) + tagged_item_id = TaggedItem.objects.get( + tag='shiny', object_id=platypus.id + ).id + self.assertEqual(u''.join(form.as_p() for form in formset.forms), u"""<p><label for="id_generic_relations-taggeditem-content_type-object_id-0-tag">Tag:</label> <input id="id_generic_relations-taggeditem-content_type-object_id-0-tag" type="text" name="generic_relations-taggeditem-content_type-object_id-0-tag" value="shiny" maxlength="50" /></p> +<p><label for="id_generic_relations-taggeditem-content_type-object_id-0-DELETE">Delete:</label> <input type="checkbox" name="generic_relations-taggeditem-content_type-object_id-0-DELETE" id="id_generic_relations-taggeditem-content_type-object_id-0-DELETE" /><input type="hidden" name="generic_relations-taggeditem-content_type-object_id-0-id" value="%s" id="id_generic_relations-taggeditem-content_type-object_id-0-id" /></p><p><label for="id_generic_relations-taggeditem-content_type-object_id-1-tag">Tag:</label> <input id="id_generic_relations-taggeditem-content_type-object_id-1-tag" type="text" name="generic_relations-taggeditem-content_type-object_id-1-tag" maxlength="50" /></p> +<p><label for="id_generic_relations-taggeditem-content_type-object_id-1-DELETE">Delete:</label> <input type="checkbox" name="generic_relations-taggeditem-content_type-object_id-1-DELETE" id="id_generic_relations-taggeditem-content_type-object_id-1-DELETE" /><input type="hidden" name="generic_relations-taggeditem-content_type-object_id-1-id" id="id_generic_relations-taggeditem-content_type-object_id-1-id" /></p>""" % tagged_item_id) + + lion = Animal.objects.create(common_name="Lion", latin_name="Panthera leo") + formset = GenericFormSet(instance=lion, prefix='x') + self.assertEqual(u''.join(form.as_p() for form in formset.forms), u"""<p><label for="id_x-0-tag">Tag:</label> <input id="id_x-0-tag" type="text" name="x-0-tag" maxlength="50" /></p> +<p><label for="id_x-0-DELETE">Delete:</label> <input type="checkbox" name="x-0-DELETE" id="id_x-0-DELETE" /><input type="hidden" name="x-0-id" id="id_x-0-id" /></p>""") diff --git a/parts/django/tests/modeltests/get_latest/__init__.py b/parts/django/tests/modeltests/get_latest/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/modeltests/get_latest/__init__.py diff --git a/parts/django/tests/modeltests/get_latest/models.py b/parts/django/tests/modeltests/get_latest/models.py new file mode 100644 index 0000000..1eeb299 --- /dev/null +++ b/parts/django/tests/modeltests/get_latest/models.py @@ -0,0 +1,30 @@ +""" +8. get_latest_by + +Models can have a ``get_latest_by`` attribute, which should be set to the name +of a ``DateField`` or ``DateTimeField``. If ``get_latest_by`` exists, the +model's manager will get a ``latest()`` method, which will return the latest +object in the database according to that field. "Latest" means "having the date +farthest into the future." +""" + +from django.db import models + +class Article(models.Model): + headline = models.CharField(max_length=100) + pub_date = models.DateField() + expire_date = models.DateField() + class Meta: + get_latest_by = 'pub_date' + + def __unicode__(self): + return self.headline + +class Person(models.Model): + name = models.CharField(max_length=30) + birthday = models.DateField() + + # Note that this model doesn't have "get_latest_by" set. + + def __unicode__(self): + return self.name diff --git a/parts/django/tests/modeltests/get_latest/tests.py b/parts/django/tests/modeltests/get_latest/tests.py new file mode 100644 index 0000000..3c3588b --- /dev/null +++ b/parts/django/tests/modeltests/get_latest/tests.py @@ -0,0 +1,53 @@ +from datetime import datetime + +from django.test import TestCase + +from models import Article, Person + + +class LatestTests(TestCase): + def test_latest(self): + # Because no Articles exist yet, latest() raises ArticleDoesNotExist. + self.assertRaises(Article.DoesNotExist, Article.objects.latest) + + a1 = Article.objects.create( + headline="Article 1", pub_date=datetime(2005, 7, 26), + expire_date=datetime(2005, 9, 1) + ) + a2 = Article.objects.create( + headline="Article 2", pub_date=datetime(2005, 7, 27), + expire_date=datetime(2005, 7, 28) + ) + a3 = Article.objects.create( + headline="Article 3", pub_date=datetime(2005, 7, 27), + expire_date=datetime(2005, 8, 27) + ) + a4 = Article.objects.create( + headline="Article 4", pub_date=datetime(2005, 7, 28), + expire_date=datetime(2005, 7, 30) + ) + + # Get the latest Article. + self.assertEqual(Article.objects.latest(), a4) + # Get the latest Article that matches certain filters. + self.assertEqual( + Article.objects.filter(pub_date__lt=datetime(2005, 7, 27)).latest(), + a1 + ) + + # Pass a custom field name to latest() to change the field that's used + # to determine the latest object. + self.assertEqual(Article.objects.latest('expire_date'), a1) + self.assertEqual( + Article.objects.filter(pub_date__gt=datetime(2005, 7, 26)).latest('expire_date'), + a3, + ) + + def test_latest_manual(self): + # You can still use latest() with a model that doesn't have + # "get_latest_by" set -- just pass in the field name manually. + p1 = Person.objects.create(name="Ralph", birthday=datetime(1950, 1, 1)) + p2 = Person.objects.create(name="Stephanie", birthday=datetime(1960, 2, 3)) + self.assertRaises(AssertionError, Person.objects.latest) + + self.assertEqual(Person.objects.latest("birthday"), p2) diff --git a/parts/django/tests/modeltests/get_object_or_404/__init__.py b/parts/django/tests/modeltests/get_object_or_404/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/modeltests/get_object_or_404/__init__.py diff --git a/parts/django/tests/modeltests/get_object_or_404/models.py b/parts/django/tests/modeltests/get_object_or_404/models.py new file mode 100644 index 0000000..eb3cd82 --- /dev/null +++ b/parts/django/tests/modeltests/get_object_or_404/models.py @@ -0,0 +1,34 @@ +""" +35. DB-API Shortcuts + +``get_object_or_404()`` is a shortcut function to be used in view functions for +performing a ``get()`` lookup and raising a ``Http404`` exception if a +``DoesNotExist`` exception was raised during the ``get()`` call. + +``get_list_or_404()`` is a shortcut function to be used in view functions for +performing a ``filter()`` lookup and raising a ``Http404`` exception if a +``DoesNotExist`` exception was raised during the ``filter()`` call. +""" + +from django.db import models +from django.http import Http404 +from django.shortcuts import get_object_or_404, get_list_or_404 + +class Author(models.Model): + name = models.CharField(max_length=50) + + def __unicode__(self): + return self.name + +class ArticleManager(models.Manager): + def get_query_set(self): + return super(ArticleManager, self).get_query_set().filter(authors__name__icontains='sir') + +class Article(models.Model): + authors = models.ManyToManyField(Author) + title = models.CharField(max_length=50) + objects = models.Manager() + by_a_sir = ArticleManager() + + def __unicode__(self): + return self.title diff --git a/parts/django/tests/modeltests/get_object_or_404/tests.py b/parts/django/tests/modeltests/get_object_or_404/tests.py new file mode 100644 index 0000000..b8c4f75 --- /dev/null +++ b/parts/django/tests/modeltests/get_object_or_404/tests.py @@ -0,0 +1,80 @@ +from django.http import Http404 +from django.shortcuts import get_object_or_404, get_list_or_404 +from django.test import TestCase + +from models import Author, Article + + +class GetObjectOr404Tests(TestCase): + def test_get_object_or_404(self): + a1 = Author.objects.create(name="Brave Sir Robin") + a2 = Author.objects.create(name="Patsy") + + # No Articles yet, so we should get a Http404 error. + self.assertRaises(Http404, get_object_or_404, Article, title="Foo") + + article = Article.objects.create(title="Run away!") + article.authors = [a1, a2] + # get_object_or_404 can be passed a Model to query. + self.assertEqual( + get_object_or_404(Article, title__contains="Run"), + article + ) + + # We can also use the Article manager through an Author object. + self.assertEqual( + get_object_or_404(a1.article_set, title__contains="Run"), + article + ) + + # No articles containing "Camelot". This should raise a Http404 error. + self.assertRaises(Http404, + get_object_or_404, a1.article_set, title__contains="Camelot" + ) + + # Custom managers can be used too. + self.assertEqual( + get_object_or_404(Article.by_a_sir, title="Run away!"), + article + ) + + # QuerySets can be used too. + self.assertEqual( + get_object_or_404(Article.objects.all(), title__contains="Run"), + article + ) + + # Just as when using a get() lookup, you will get an error if more than + # one object is returned. + + self.assertRaises(Author.MultipleObjectsReturned, + get_object_or_404, Author.objects.all() + ) + + # Using an EmptyQuerySet raises a Http404 error. + self.assertRaises(Http404, + get_object_or_404, Article.objects.none(), title__contains="Run" + ) + + # get_list_or_404 can be used to get lists of objects + self.assertEqual( + get_list_or_404(a1.article_set, title__icontains="Run"), + [article] + ) + + # Http404 is returned if the list is empty. + self.assertRaises(Http404, + get_list_or_404, a1.article_set, title__icontains="Shrubbery" + ) + + # Custom managers can be used too. + self.assertEqual( + get_list_or_404(Article.by_a_sir, title__icontains="Run"), + [article] + ) + + # QuerySets can be used too. + self.assertEqual( + get_list_or_404(Article.objects.all(), title__icontains="Run"), + [article] + ) diff --git a/parts/django/tests/modeltests/get_or_create/__init__.py b/parts/django/tests/modeltests/get_or_create/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/modeltests/get_or_create/__init__.py diff --git a/parts/django/tests/modeltests/get_or_create/models.py b/parts/django/tests/modeltests/get_or_create/models.py new file mode 100644 index 0000000..db5719b --- /dev/null +++ b/parts/django/tests/modeltests/get_or_create/models.py @@ -0,0 +1,21 @@ +""" +33. get_or_create() + +``get_or_create()`` does what it says: it tries to look up an object with the +given parameters. If an object isn't found, it creates one with the given +parameters. +""" + +from django.db import models, IntegrityError + +class Person(models.Model): + first_name = models.CharField(max_length=100) + last_name = models.CharField(max_length=100) + birthday = models.DateField() + + def __unicode__(self): + return u'%s %s' % (self.first_name, self.last_name) + +class ManualPrimaryKeyTest(models.Model): + id = models.IntegerField(primary_key=True) + data = models.CharField(max_length=100) diff --git a/parts/django/tests/modeltests/get_or_create/tests.py b/parts/django/tests/modeltests/get_or_create/tests.py new file mode 100644 index 0000000..1999b20 --- /dev/null +++ b/parts/django/tests/modeltests/get_or_create/tests.py @@ -0,0 +1,52 @@ +from datetime import date + +from django.db import IntegrityError +from django.test import TransactionTestCase + +from models import Person, ManualPrimaryKeyTest + + +class GetOrCreateTests(TransactionTestCase): + def test_get_or_create(self): + p = Person.objects.create( + first_name='John', last_name='Lennon', birthday=date(1940, 10, 9) + ) + + p, created = Person.objects.get_or_create( + first_name="John", last_name="Lennon", defaults={ + "birthday": date(1940, 10, 9) + } + ) + self.assertFalse(created) + self.assertEqual(Person.objects.count(), 1) + + p, created = Person.objects.get_or_create( + first_name='George', last_name='Harrison', defaults={ + 'birthday': date(1943, 2, 25) + } + ) + self.assertTrue(created) + self.assertEqual(Person.objects.count(), 2) + + # If we execute the exact same statement, it won't create a Person. + p, created = Person.objects.get_or_create( + first_name='George', last_name='Harrison', defaults={ + 'birthday': date(1943, 2, 25) + } + ) + self.assertFalse(created) + self.assertEqual(Person.objects.count(), 2) + + # If you don't specify a value or default value for all required + # fields, you will get an error. + self.assertRaises(IntegrityError, + Person.objects.get_or_create, first_name="Tom", last_name="Smith" + ) + + # If you specify an existing primary key, but different other fields, + # then you will get an error and data will not be updated. + m = ManualPrimaryKeyTest.objects.create(id=1, data="Original") + self.assertRaises(IntegrityError, + ManualPrimaryKeyTest.objects.get_or_create, id=1, data="Different" + ) + self.assertEqual(ManualPrimaryKeyTest.objects.get(id=1).data, "Original") diff --git a/parts/django/tests/modeltests/invalid_models/__init__.py b/parts/django/tests/modeltests/invalid_models/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/parts/django/tests/modeltests/invalid_models/__init__.py @@ -0,0 +1 @@ + diff --git a/parts/django/tests/modeltests/invalid_models/models.py b/parts/django/tests/modeltests/invalid_models/models.py new file mode 100644 index 0000000..09301ed --- /dev/null +++ b/parts/django/tests/modeltests/invalid_models/models.py @@ -0,0 +1,335 @@ +""" +26. Invalid models + +This example exists purely to point out errors in models. +""" + +from django.contrib.contenttypes import generic +from django.db import models + +class FieldErrors(models.Model): + charfield = models.CharField() + charfield2 = models.CharField(max_length=-1) + charfield3 = models.CharField(max_length="bad") + decimalfield = models.DecimalField() + decimalfield2 = models.DecimalField(max_digits=-1, decimal_places=-1) + decimalfield3 = models.DecimalField(max_digits="bad", decimal_places="bad") + filefield = models.FileField() + choices = models.CharField(max_length=10, choices='bad') + choices2 = models.CharField(max_length=10, choices=[(1,2,3),(1,2,3)]) + index = models.CharField(max_length=10, db_index='bad') + field_ = models.CharField(max_length=10) + nullbool = models.BooleanField(null=True) + +class Target(models.Model): + tgt_safe = models.CharField(max_length=10) + clash1 = models.CharField(max_length=10) + clash2 = models.CharField(max_length=10) + + clash1_set = models.CharField(max_length=10) + +class Clash1(models.Model): + src_safe = models.CharField(max_length=10) + + foreign = models.ForeignKey(Target) + m2m = models.ManyToManyField(Target) + +class Clash2(models.Model): + src_safe = models.CharField(max_length=10) + + foreign_1 = models.ForeignKey(Target, related_name='id') + foreign_2 = models.ForeignKey(Target, related_name='src_safe') + + m2m_1 = models.ManyToManyField(Target, related_name='id') + m2m_2 = models.ManyToManyField(Target, related_name='src_safe') + +class Target2(models.Model): + clash3 = models.CharField(max_length=10) + foreign_tgt = models.ForeignKey(Target) + clashforeign_set = models.ForeignKey(Target) + + m2m_tgt = models.ManyToManyField(Target) + clashm2m_set = models.ManyToManyField(Target) + +class Clash3(models.Model): + src_safe = models.CharField(max_length=10) + + foreign_1 = models.ForeignKey(Target2, related_name='foreign_tgt') + foreign_2 = models.ForeignKey(Target2, related_name='m2m_tgt') + + m2m_1 = models.ManyToManyField(Target2, related_name='foreign_tgt') + m2m_2 = models.ManyToManyField(Target2, related_name='m2m_tgt') + +class ClashForeign(models.Model): + foreign = models.ForeignKey(Target2) + +class ClashM2M(models.Model): + m2m = models.ManyToManyField(Target2) + +class SelfClashForeign(models.Model): + src_safe = models.CharField(max_length=10) + selfclashforeign = models.CharField(max_length=10) + + selfclashforeign_set = models.ForeignKey("SelfClashForeign") + foreign_1 = models.ForeignKey("SelfClashForeign", related_name='id') + foreign_2 = models.ForeignKey("SelfClashForeign", related_name='src_safe') + +class ValidM2M(models.Model): + src_safe = models.CharField(max_length=10) + validm2m = models.CharField(max_length=10) + + # M2M fields are symmetrical by default. Symmetrical M2M fields + # on self don't require a related accessor, so many potential + # clashes are avoided. + validm2m_set = models.ManyToManyField("self") + + m2m_1 = models.ManyToManyField("self", related_name='id') + m2m_2 = models.ManyToManyField("self", related_name='src_safe') + + m2m_3 = models.ManyToManyField('self') + m2m_4 = models.ManyToManyField('self') + +class SelfClashM2M(models.Model): + src_safe = models.CharField(max_length=10) + selfclashm2m = models.CharField(max_length=10) + + # Non-symmetrical M2M fields _do_ have related accessors, so + # there is potential for clashes. + selfclashm2m_set = models.ManyToManyField("self", symmetrical=False) + + m2m_1 = models.ManyToManyField("self", related_name='id', symmetrical=False) + m2m_2 = models.ManyToManyField("self", related_name='src_safe', symmetrical=False) + + m2m_3 = models.ManyToManyField('self', symmetrical=False) + m2m_4 = models.ManyToManyField('self', symmetrical=False) + +class Model(models.Model): + "But it's valid to call a model Model." + year = models.PositiveIntegerField() #1960 + make = models.CharField(max_length=10) #Aston Martin + name = models.CharField(max_length=10) #DB 4 GT + +class Car(models.Model): + colour = models.CharField(max_length=5) + model = models.ForeignKey(Model) + +class MissingRelations(models.Model): + rel1 = models.ForeignKey("Rel1") + rel2 = models.ManyToManyField("Rel2") + +class MissingManualM2MModel(models.Model): + name = models.CharField(max_length=5) + missing_m2m = models.ManyToManyField(Model, through="MissingM2MModel") + +class Person(models.Model): + name = models.CharField(max_length=5) + +class Group(models.Model): + name = models.CharField(max_length=5) + primary = models.ManyToManyField(Person, through="Membership", related_name="primary") + secondary = models.ManyToManyField(Person, through="Membership", related_name="secondary") + tertiary = models.ManyToManyField(Person, through="RelationshipDoubleFK", related_name="tertiary") + +class GroupTwo(models.Model): + name = models.CharField(max_length=5) + primary = models.ManyToManyField(Person, through="Membership") + secondary = models.ManyToManyField(Group, through="MembershipMissingFK") + +class Membership(models.Model): + person = models.ForeignKey(Person) + group = models.ForeignKey(Group) + not_default_or_null = models.CharField(max_length=5) + +class MembershipMissingFK(models.Model): + person = models.ForeignKey(Person) + +class PersonSelfRefM2M(models.Model): + name = models.CharField(max_length=5) + friends = models.ManyToManyField('self', through="Relationship") + too_many_friends = models.ManyToManyField('self', through="RelationshipTripleFK") + +class PersonSelfRefM2MExplicit(models.Model): + name = models.CharField(max_length=5) + friends = models.ManyToManyField('self', through="ExplicitRelationship", symmetrical=True) + +class Relationship(models.Model): + first = models.ForeignKey(PersonSelfRefM2M, related_name="rel_from_set") + second = models.ForeignKey(PersonSelfRefM2M, related_name="rel_to_set") + date_added = models.DateTimeField() + +class ExplicitRelationship(models.Model): + first = models.ForeignKey(PersonSelfRefM2MExplicit, related_name="rel_from_set") + second = models.ForeignKey(PersonSelfRefM2MExplicit, related_name="rel_to_set") + date_added = models.DateTimeField() + +class RelationshipTripleFK(models.Model): + first = models.ForeignKey(PersonSelfRefM2M, related_name="rel_from_set_2") + second = models.ForeignKey(PersonSelfRefM2M, related_name="rel_to_set_2") + third = models.ForeignKey(PersonSelfRefM2M, related_name="too_many_by_far") + date_added = models.DateTimeField() + +class RelationshipDoubleFK(models.Model): + first = models.ForeignKey(Person, related_name="first_related_name") + second = models.ForeignKey(Person, related_name="second_related_name") + third = models.ForeignKey(Group, related_name="rel_to_set") + date_added = models.DateTimeField() + +class AbstractModel(models.Model): + name = models.CharField(max_length=10) + class Meta: + abstract = True + +class AbstractRelationModel(models.Model): + fk1 = models.ForeignKey('AbstractModel') + fk2 = models.ManyToManyField('AbstractModel') + +class UniqueM2M(models.Model): + """ Model to test for unique ManyToManyFields, which are invalid. """ + unique_people = models.ManyToManyField(Person, unique=True) + +class NonUniqueFKTarget1(models.Model): + """ Model to test for non-unique FK target in yet-to-be-defined model: expect an error """ + tgt = models.ForeignKey('FKTarget', to_field='bad') + +class UniqueFKTarget1(models.Model): + """ Model to test for unique FK target in yet-to-be-defined model: expect no error """ + tgt = models.ForeignKey('FKTarget', to_field='good') + +class FKTarget(models.Model): + bad = models.IntegerField() + good = models.IntegerField(unique=True) + +class NonUniqueFKTarget2(models.Model): + """ Model to test for non-unique FK target in previously seen model: expect an error """ + tgt = models.ForeignKey(FKTarget, to_field='bad') + +class UniqueFKTarget2(models.Model): + """ Model to test for unique FK target in previously seen model: expect no error """ + tgt = models.ForeignKey(FKTarget, to_field='good') + +class NonExistingOrderingWithSingleUnderscore(models.Model): + class Meta: + ordering = ("does_not_exist",) + +class Tag(models.Model): + name = models.CharField("name", max_length=20) + +class TaggedObject(models.Model): + object_id = models.PositiveIntegerField("Object ID") + tag = models.ForeignKey(Tag) + content_object = generic.GenericForeignKey() + +class UserTaggedObject(models.Model): + object_tag = models.ForeignKey(TaggedObject) + +class ArticleAttachment(models.Model): + tags = generic.GenericRelation(TaggedObject) + user_tags = generic.GenericRelation(UserTaggedObject) + +model_errors = """invalid_models.fielderrors: "charfield": CharFields require a "max_length" attribute that is a positive integer. +invalid_models.fielderrors: "charfield2": CharFields require a "max_length" attribute that is a positive integer. +invalid_models.fielderrors: "charfield3": CharFields require a "max_length" attribute that is a positive integer. +invalid_models.fielderrors: "decimalfield": DecimalFields require a "decimal_places" attribute that is a non-negative integer. +invalid_models.fielderrors: "decimalfield": DecimalFields require a "max_digits" attribute that is a positive integer. +invalid_models.fielderrors: "decimalfield2": DecimalFields require a "decimal_places" attribute that is a non-negative integer. +invalid_models.fielderrors: "decimalfield2": DecimalFields require a "max_digits" attribute that is a positive integer. +invalid_models.fielderrors: "decimalfield3": DecimalFields require a "decimal_places" attribute that is a non-negative integer. +invalid_models.fielderrors: "decimalfield3": DecimalFields require a "max_digits" attribute that is a positive integer. +invalid_models.fielderrors: "filefield": FileFields require an "upload_to" attribute. +invalid_models.fielderrors: "choices": "choices" should be iterable (e.g., a tuple or list). +invalid_models.fielderrors: "choices2": "choices" should be a sequence of two-tuples. +invalid_models.fielderrors: "choices2": "choices" should be a sequence of two-tuples. +invalid_models.fielderrors: "index": "db_index" should be either None, True or False. +invalid_models.fielderrors: "field_": Field names cannot end with underscores, because this would lead to ambiguous queryset filters. +invalid_models.fielderrors: "nullbool": BooleanFields do not accept null values. Use a NullBooleanField instead. +invalid_models.clash1: Accessor for field 'foreign' clashes with field 'Target.clash1_set'. Add a related_name argument to the definition for 'foreign'. +invalid_models.clash1: Accessor for field 'foreign' clashes with related m2m field 'Target.clash1_set'. Add a related_name argument to the definition for 'foreign'. +invalid_models.clash1: Reverse query name for field 'foreign' clashes with field 'Target.clash1'. Add a related_name argument to the definition for 'foreign'. +invalid_models.clash1: Accessor for m2m field 'm2m' clashes with field 'Target.clash1_set'. Add a related_name argument to the definition for 'm2m'. +invalid_models.clash1: Accessor for m2m field 'm2m' clashes with related field 'Target.clash1_set'. Add a related_name argument to the definition for 'm2m'. +invalid_models.clash1: Reverse query name for m2m field 'm2m' clashes with field 'Target.clash1'. Add a related_name argument to the definition for 'm2m'. +invalid_models.clash2: Accessor for field 'foreign_1' clashes with field 'Target.id'. Add a related_name argument to the definition for 'foreign_1'. +invalid_models.clash2: Accessor for field 'foreign_1' clashes with related m2m field 'Target.id'. Add a related_name argument to the definition for 'foreign_1'. +invalid_models.clash2: Reverse query name for field 'foreign_1' clashes with field 'Target.id'. Add a related_name argument to the definition for 'foreign_1'. +invalid_models.clash2: Reverse query name for field 'foreign_1' clashes with related m2m field 'Target.id'. Add a related_name argument to the definition for 'foreign_1'. +invalid_models.clash2: Accessor for field 'foreign_2' clashes with related m2m field 'Target.src_safe'. Add a related_name argument to the definition for 'foreign_2'. +invalid_models.clash2: Reverse query name for field 'foreign_2' clashes with related m2m field 'Target.src_safe'. Add a related_name argument to the definition for 'foreign_2'. +invalid_models.clash2: Accessor for m2m field 'm2m_1' clashes with field 'Target.id'. Add a related_name argument to the definition for 'm2m_1'. +invalid_models.clash2: Accessor for m2m field 'm2m_1' clashes with related field 'Target.id'. Add a related_name argument to the definition for 'm2m_1'. +invalid_models.clash2: Reverse query name for m2m field 'm2m_1' clashes with field 'Target.id'. Add a related_name argument to the definition for 'm2m_1'. +invalid_models.clash2: Reverse query name for m2m field 'm2m_1' clashes with related field 'Target.id'. Add a related_name argument to the definition for 'm2m_1'. +invalid_models.clash2: Accessor for m2m field 'm2m_2' clashes with related field 'Target.src_safe'. Add a related_name argument to the definition for 'm2m_2'. +invalid_models.clash2: Reverse query name for m2m field 'm2m_2' clashes with related field 'Target.src_safe'. Add a related_name argument to the definition for 'm2m_2'. +invalid_models.clash3: Accessor for field 'foreign_1' clashes with field 'Target2.foreign_tgt'. Add a related_name argument to the definition for 'foreign_1'. +invalid_models.clash3: Accessor for field 'foreign_1' clashes with related m2m field 'Target2.foreign_tgt'. Add a related_name argument to the definition for 'foreign_1'. +invalid_models.clash3: Reverse query name for field 'foreign_1' clashes with field 'Target2.foreign_tgt'. Add a related_name argument to the definition for 'foreign_1'. +invalid_models.clash3: Reverse query name for field 'foreign_1' clashes with related m2m field 'Target2.foreign_tgt'. Add a related_name argument to the definition for 'foreign_1'. +invalid_models.clash3: Accessor for field 'foreign_2' clashes with m2m field 'Target2.m2m_tgt'. Add a related_name argument to the definition for 'foreign_2'. +invalid_models.clash3: Accessor for field 'foreign_2' clashes with related m2m field 'Target2.m2m_tgt'. Add a related_name argument to the definition for 'foreign_2'. +invalid_models.clash3: Reverse query name for field 'foreign_2' clashes with m2m field 'Target2.m2m_tgt'. Add a related_name argument to the definition for 'foreign_2'. +invalid_models.clash3: Reverse query name for field 'foreign_2' clashes with related m2m field 'Target2.m2m_tgt'. Add a related_name argument to the definition for 'foreign_2'. +invalid_models.clash3: Accessor for m2m field 'm2m_1' clashes with field 'Target2.foreign_tgt'. Add a related_name argument to the definition for 'm2m_1'. +invalid_models.clash3: Accessor for m2m field 'm2m_1' clashes with related field 'Target2.foreign_tgt'. Add a related_name argument to the definition for 'm2m_1'. +invalid_models.clash3: Reverse query name for m2m field 'm2m_1' clashes with field 'Target2.foreign_tgt'. Add a related_name argument to the definition for 'm2m_1'. +invalid_models.clash3: Reverse query name for m2m field 'm2m_1' clashes with related field 'Target2.foreign_tgt'. Add a related_name argument to the definition for 'm2m_1'. +invalid_models.clash3: Accessor for m2m field 'm2m_2' clashes with m2m field 'Target2.m2m_tgt'. Add a related_name argument to the definition for 'm2m_2'. +invalid_models.clash3: Accessor for m2m field 'm2m_2' clashes with related field 'Target2.m2m_tgt'. Add a related_name argument to the definition for 'm2m_2'. +invalid_models.clash3: Reverse query name for m2m field 'm2m_2' clashes with m2m field 'Target2.m2m_tgt'. Add a related_name argument to the definition for 'm2m_2'. +invalid_models.clash3: Reverse query name for m2m field 'm2m_2' clashes with related field 'Target2.m2m_tgt'. Add a related_name argument to the definition for 'm2m_2'. +invalid_models.clashforeign: Accessor for field 'foreign' clashes with field 'Target2.clashforeign_set'. Add a related_name argument to the definition for 'foreign'. +invalid_models.clashm2m: Accessor for m2m field 'm2m' clashes with m2m field 'Target2.clashm2m_set'. Add a related_name argument to the definition for 'm2m'. +invalid_models.target2: Accessor for field 'foreign_tgt' clashes with related m2m field 'Target.target2_set'. Add a related_name argument to the definition for 'foreign_tgt'. +invalid_models.target2: Accessor for field 'foreign_tgt' clashes with related m2m field 'Target.target2_set'. Add a related_name argument to the definition for 'foreign_tgt'. +invalid_models.target2: Accessor for field 'foreign_tgt' clashes with related field 'Target.target2_set'. Add a related_name argument to the definition for 'foreign_tgt'. +invalid_models.target2: Accessor for field 'clashforeign_set' clashes with related m2m field 'Target.target2_set'. Add a related_name argument to the definition for 'clashforeign_set'. +invalid_models.target2: Accessor for field 'clashforeign_set' clashes with related m2m field 'Target.target2_set'. Add a related_name argument to the definition for 'clashforeign_set'. +invalid_models.target2: Accessor for field 'clashforeign_set' clashes with related field 'Target.target2_set'. Add a related_name argument to the definition for 'clashforeign_set'. +invalid_models.target2: Accessor for m2m field 'm2m_tgt' clashes with related field 'Target.target2_set'. Add a related_name argument to the definition for 'm2m_tgt'. +invalid_models.target2: Accessor for m2m field 'm2m_tgt' clashes with related field 'Target.target2_set'. Add a related_name argument to the definition for 'm2m_tgt'. +invalid_models.target2: Accessor for m2m field 'm2m_tgt' clashes with related m2m field 'Target.target2_set'. Add a related_name argument to the definition for 'm2m_tgt'. +invalid_models.target2: Accessor for m2m field 'm2m_tgt' clashes with related m2m field 'Target.target2_set'. Add a related_name argument to the definition for 'm2m_tgt'. +invalid_models.target2: Accessor for m2m field 'm2m_tgt' clashes with related m2m field 'Target.target2_set'. Add a related_name argument to the definition for 'm2m_tgt'. +invalid_models.target2: Accessor for m2m field 'clashm2m_set' clashes with related field 'Target.target2_set'. Add a related_name argument to the definition for 'clashm2m_set'. +invalid_models.target2: Accessor for m2m field 'clashm2m_set' clashes with related field 'Target.target2_set'. Add a related_name argument to the definition for 'clashm2m_set'. +invalid_models.target2: Accessor for m2m field 'clashm2m_set' clashes with related m2m field 'Target.target2_set'. Add a related_name argument to the definition for 'clashm2m_set'. +invalid_models.target2: Accessor for m2m field 'clashm2m_set' clashes with related m2m field 'Target.target2_set'. Add a related_name argument to the definition for 'clashm2m_set'. +invalid_models.target2: Accessor for m2m field 'clashm2m_set' clashes with related m2m field 'Target.target2_set'. Add a related_name argument to the definition for 'clashm2m_set'. +invalid_models.selfclashforeign: Accessor for field 'selfclashforeign_set' clashes with field 'SelfClashForeign.selfclashforeign_set'. Add a related_name argument to the definition for 'selfclashforeign_set'. +invalid_models.selfclashforeign: Reverse query name for field 'selfclashforeign_set' clashes with field 'SelfClashForeign.selfclashforeign'. Add a related_name argument to the definition for 'selfclashforeign_set'. +invalid_models.selfclashforeign: Accessor for field 'foreign_1' clashes with field 'SelfClashForeign.id'. Add a related_name argument to the definition for 'foreign_1'. +invalid_models.selfclashforeign: Reverse query name for field 'foreign_1' clashes with field 'SelfClashForeign.id'. Add a related_name argument to the definition for 'foreign_1'. +invalid_models.selfclashforeign: Accessor for field 'foreign_2' clashes with field 'SelfClashForeign.src_safe'. Add a related_name argument to the definition for 'foreign_2'. +invalid_models.selfclashforeign: Reverse query name for field 'foreign_2' clashes with field 'SelfClashForeign.src_safe'. Add a related_name argument to the definition for 'foreign_2'. +invalid_models.selfclashm2m: Accessor for m2m field 'selfclashm2m_set' clashes with m2m field 'SelfClashM2M.selfclashm2m_set'. Add a related_name argument to the definition for 'selfclashm2m_set'. +invalid_models.selfclashm2m: Reverse query name for m2m field 'selfclashm2m_set' clashes with field 'SelfClashM2M.selfclashm2m'. Add a related_name argument to the definition for 'selfclashm2m_set'. +invalid_models.selfclashm2m: Accessor for m2m field 'selfclashm2m_set' clashes with related m2m field 'SelfClashM2M.selfclashm2m_set'. Add a related_name argument to the definition for 'selfclashm2m_set'. +invalid_models.selfclashm2m: Accessor for m2m field 'm2m_1' clashes with field 'SelfClashM2M.id'. Add a related_name argument to the definition for 'm2m_1'. +invalid_models.selfclashm2m: Accessor for m2m field 'm2m_2' clashes with field 'SelfClashM2M.src_safe'. Add a related_name argument to the definition for 'm2m_2'. +invalid_models.selfclashm2m: Reverse query name for m2m field 'm2m_1' clashes with field 'SelfClashM2M.id'. Add a related_name argument to the definition for 'm2m_1'. +invalid_models.selfclashm2m: Reverse query name for m2m field 'm2m_2' clashes with field 'SelfClashM2M.src_safe'. Add a related_name argument to the definition for 'm2m_2'. +invalid_models.selfclashm2m: Accessor for m2m field 'm2m_3' clashes with m2m field 'SelfClashM2M.selfclashm2m_set'. Add a related_name argument to the definition for 'm2m_3'. +invalid_models.selfclashm2m: Accessor for m2m field 'm2m_3' clashes with related m2m field 'SelfClashM2M.selfclashm2m_set'. Add a related_name argument to the definition for 'm2m_3'. +invalid_models.selfclashm2m: Accessor for m2m field 'm2m_3' clashes with related m2m field 'SelfClashM2M.selfclashm2m_set'. Add a related_name argument to the definition for 'm2m_3'. +invalid_models.selfclashm2m: Accessor for m2m field 'm2m_4' clashes with m2m field 'SelfClashM2M.selfclashm2m_set'. Add a related_name argument to the definition for 'm2m_4'. +invalid_models.selfclashm2m: Accessor for m2m field 'm2m_4' clashes with related m2m field 'SelfClashM2M.selfclashm2m_set'. Add a related_name argument to the definition for 'm2m_4'. +invalid_models.selfclashm2m: Accessor for m2m field 'm2m_4' clashes with related m2m field 'SelfClashM2M.selfclashm2m_set'. Add a related_name argument to the definition for 'm2m_4'. +invalid_models.selfclashm2m: Reverse query name for m2m field 'm2m_3' clashes with field 'SelfClashM2M.selfclashm2m'. Add a related_name argument to the definition for 'm2m_3'. +invalid_models.selfclashm2m: Reverse query name for m2m field 'm2m_4' clashes with field 'SelfClashM2M.selfclashm2m'. Add a related_name argument to the definition for 'm2m_4'. +invalid_models.missingrelations: 'rel1' has a relation with model Rel1, which has either not been installed or is abstract. +invalid_models.missingrelations: 'rel2' has an m2m relation with model Rel2, which has either not been installed or is abstract. +invalid_models.grouptwo: 'primary' is a manually-defined m2m relation through model Membership, which does not have foreign keys to Person and GroupTwo +invalid_models.grouptwo: 'secondary' is a manually-defined m2m relation through model MembershipMissingFK, which does not have foreign keys to Group and GroupTwo +invalid_models.missingmanualm2mmodel: 'missing_m2m' specifies an m2m relation through model MissingM2MModel, which has not been installed +invalid_models.group: The model Group has two manually-defined m2m relations through the model Membership, which is not permitted. Please consider using an extra field on your intermediary model instead. +invalid_models.group: Intermediary model RelationshipDoubleFK has more than one foreign key to Person, which is ambiguous and is not permitted. +invalid_models.personselfrefm2m: Many-to-many fields with intermediate tables cannot be symmetrical. +invalid_models.personselfrefm2m: Intermediary model RelationshipTripleFK has more than two foreign keys to PersonSelfRefM2M, which is ambiguous and is not permitted. +invalid_models.personselfrefm2mexplicit: Many-to-many fields with intermediate tables cannot be symmetrical. +invalid_models.abstractrelationmodel: 'fk1' has a relation with model AbstractModel, which has either not been installed or is abstract. +invalid_models.abstractrelationmodel: 'fk2' has an m2m relation with model AbstractModel, which has either not been installed or is abstract. +invalid_models.uniquem2m: ManyToManyFields cannot be unique. Remove the unique argument on 'unique_people'. +invalid_models.nonuniquefktarget1: Field 'bad' under model 'FKTarget' must have a unique=True constraint. +invalid_models.nonuniquefktarget2: Field 'bad' under model 'FKTarget' must have a unique=True constraint. +invalid_models.nonexistingorderingwithsingleunderscore: "ordering" refers to "does_not_exist", a field that doesn't exist. +invalid_models.articleattachment: Model 'UserTaggedObject' must have a GenericForeignKey in order to create a GenericRelation that points to it. +""" diff --git a/parts/django/tests/modeltests/lookup/__init__.py b/parts/django/tests/modeltests/lookup/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/modeltests/lookup/__init__.py diff --git a/parts/django/tests/modeltests/lookup/models.py b/parts/django/tests/modeltests/lookup/models.py new file mode 100644 index 0000000..99eec51 --- /dev/null +++ b/parts/django/tests/modeltests/lookup/models.py @@ -0,0 +1,16 @@ +""" +7. The lookup API + +This demonstrates features of the database API. +""" + +from django.db import models + +class Article(models.Model): + headline = models.CharField(max_length=100) + pub_date = models.DateTimeField() + class Meta: + ordering = ('-pub_date', 'headline') + + def __unicode__(self): + return self.headline diff --git a/parts/django/tests/modeltests/lookup/tests.py b/parts/django/tests/modeltests/lookup/tests.py new file mode 100644 index 0000000..9e0b68e --- /dev/null +++ b/parts/django/tests/modeltests/lookup/tests.py @@ -0,0 +1,547 @@ +from datetime import datetime +from operator import attrgetter + +from django.conf import settings +from django.core.exceptions import FieldError +from django.db import connection, DEFAULT_DB_ALIAS +from django.test import TestCase + +from models import Article + +class LookupTests(TestCase): + + #def setUp(self): + def setUp(self): + # Create a couple of Articles. + self.a1 = Article(headline='Article 1', pub_date=datetime(2005, 7, 26)) + self.a1.save() + self.a2 = Article(headline='Article 2', pub_date=datetime(2005, 7, 27)) + self.a2.save() + self.a3 = Article(headline='Article 3', pub_date=datetime(2005, 7, 27)) + self.a3.save() + self.a4 = Article(headline='Article 4', pub_date=datetime(2005, 7, 28)) + self.a4.save() + self.a5 = Article(headline='Article 5', pub_date=datetime(2005, 8, 1, 9, 0)) + self.a5.save() + self.a6 = Article(headline='Article 6', pub_date=datetime(2005, 8, 1, 8, 0)) + self.a6.save() + self.a7 = Article(headline='Article 7', pub_date=datetime(2005, 7, 27)) + self.a7.save() + + def test_exists(self): + # We can use .exists() to check that there are some + self.assertTrue(Article.objects.exists()) + for a in Article.objects.all(): + a.delete() + # There should be none now! + self.assertFalse(Article.objects.exists()) + + def test_lookup_int_as_str(self): + # Integer value can be queried using string + self.assertQuerysetEqual(Article.objects.filter(id__iexact=str(self.a1.id)), + ['<Article: Article 1>']) + + if settings.DATABASES[DEFAULT_DB_ALIAS]['ENGINE'] in ( + 'django.db.backends.postgresql', + 'django.db.backends.postgresql_psycopg2'): + def test_lookup_date_as_str(self): + # A date lookup can be performed using a string search + self.assertQuerysetEqual(Article.objects.filter(pub_date__startswith='2005'), + [ + '<Article: Article 5>', + '<Article: Article 6>', + '<Article: Article 4>', + '<Article: Article 2>', + '<Article: Article 3>', + '<Article: Article 7>', + '<Article: Article 1>', + ]) + + def test_iterator(self): + # Each QuerySet gets iterator(), which is a generator that "lazily" + # returns results using database-level iteration. + self.assertQuerysetEqual(Article.objects.iterator(), + [ + 'Article 5', + 'Article 6', + 'Article 4', + 'Article 2', + 'Article 3', + 'Article 7', + 'Article 1', + ], + transform=attrgetter('headline')) + # iterator() can be used on any QuerySet. + self.assertQuerysetEqual( + Article.objects.filter(headline__endswith='4').iterator(), + ['Article 4'], + transform=attrgetter('headline')) + + def test_count(self): + # count() returns the number of objects matching search criteria. + self.assertEqual(Article.objects.count(), 7) + self.assertEqual(Article.objects.filter(pub_date__exact=datetime(2005, 7, 27)).count(), 3) + self.assertEqual(Article.objects.filter(headline__startswith='Blah blah').count(), 0) + + # count() should respect sliced query sets. + articles = Article.objects.all() + self.assertEqual(articles.count(), 7) + self.assertEqual(articles[:4].count(), 4) + self.assertEqual(articles[1:100].count(), 6) + self.assertEqual(articles[10:100].count(), 0) + + # Date and date/time lookups can also be done with strings. + self.assertEqual(Article.objects.filter(pub_date__exact='2005-07-27 00:00:00').count(), 3) + + def test_in_bulk(self): + # in_bulk() takes a list of IDs and returns a dictionary mapping IDs to objects. + arts = Article.objects.in_bulk([self.a1.id, self.a2.id]) + self.assertEqual(arts[self.a1.id], self.a1) + self.assertEqual(arts[self.a2.id], self.a2) + self.assertEqual(Article.objects.in_bulk([self.a3.id]), {self.a3.id: self.a3}) + self.assertEqual(Article.objects.in_bulk(set([self.a3.id])), {self.a3.id: self.a3}) + self.assertEqual(Article.objects.in_bulk(frozenset([self.a3.id])), {self.a3.id: self.a3}) + self.assertEqual(Article.objects.in_bulk((self.a3.id,)), {self.a3.id: self.a3}) + self.assertEqual(Article.objects.in_bulk([1000]), {}) + self.assertEqual(Article.objects.in_bulk([]), {}) + self.assertRaises(AssertionError, Article.objects.in_bulk, 'foo') + self.assertRaises(TypeError, Article.objects.in_bulk) + self.assertRaises(TypeError, Article.objects.in_bulk, headline__startswith='Blah') + + def test_values(self): + # values() returns a list of dictionaries instead of object instances -- + # and you can specify which fields you want to retrieve. + identity = lambda x:x + self.assertQuerysetEqual(Article.objects.values('headline'), + [ + {'headline': u'Article 5'}, + {'headline': u'Article 6'}, + {'headline': u'Article 4'}, + {'headline': u'Article 2'}, + {'headline': u'Article 3'}, + {'headline': u'Article 7'}, + {'headline': u'Article 1'}, + ], + transform=identity) + self.assertQuerysetEqual( + Article.objects.filter(pub_date__exact=datetime(2005, 7, 27)).values('id'), + [{'id': self.a2.id}, {'id': self.a3.id}, {'id': self.a7.id}], + transform=identity) + self.assertQuerysetEqual(Article.objects.values('id', 'headline'), + [ + {'id': self.a5.id, 'headline': 'Article 5'}, + {'id': self.a6.id, 'headline': 'Article 6'}, + {'id': self.a4.id, 'headline': 'Article 4'}, + {'id': self.a2.id, 'headline': 'Article 2'}, + {'id': self.a3.id, 'headline': 'Article 3'}, + {'id': self.a7.id, 'headline': 'Article 7'}, + {'id': self.a1.id, 'headline': 'Article 1'}, + ], + transform=identity) + # You can use values() with iterator() for memory savings, + # because iterator() uses database-level iteration. + self.assertQuerysetEqual(Article.objects.values('id', 'headline').iterator(), + [ + {'headline': u'Article 5', 'id': self.a5.id}, + {'headline': u'Article 6', 'id': self.a6.id}, + {'headline': u'Article 4', 'id': self.a4.id}, + {'headline': u'Article 2', 'id': self.a2.id}, + {'headline': u'Article 3', 'id': self.a3.id}, + {'headline': u'Article 7', 'id': self.a7.id}, + {'headline': u'Article 1', 'id': self.a1.id}, + ], + transform=identity) + # The values() method works with "extra" fields specified in extra(select). + self.assertQuerysetEqual( + Article.objects.extra(select={'id_plus_one': 'id + 1'}).values('id', 'id_plus_one'), + [ + {'id': self.a5.id, 'id_plus_one': self.a5.id + 1}, + {'id': self.a6.id, 'id_plus_one': self.a6.id + 1}, + {'id': self.a4.id, 'id_plus_one': self.a4.id + 1}, + {'id': self.a2.id, 'id_plus_one': self.a2.id + 1}, + {'id': self.a3.id, 'id_plus_one': self.a3.id + 1}, + {'id': self.a7.id, 'id_plus_one': self.a7.id + 1}, + {'id': self.a1.id, 'id_plus_one': self.a1.id + 1}, + ], + transform=identity) + data = { + 'id_plus_one': 'id+1', + 'id_plus_two': 'id+2', + 'id_plus_three': 'id+3', + 'id_plus_four': 'id+4', + 'id_plus_five': 'id+5', + 'id_plus_six': 'id+6', + 'id_plus_seven': 'id+7', + 'id_plus_eight': 'id+8', + } + self.assertQuerysetEqual( + Article.objects.filter(id=self.a1.id).extra(select=data).values(*data.keys()), + [{ + 'id_plus_one': self.a1.id + 1, + 'id_plus_two': self.a1.id + 2, + 'id_plus_three': self.a1.id + 3, + 'id_plus_four': self.a1.id + 4, + 'id_plus_five': self.a1.id + 5, + 'id_plus_six': self.a1.id + 6, + 'id_plus_seven': self.a1.id + 7, + 'id_plus_eight': self.a1.id + 8, + }], transform=identity) + # However, an exception FieldDoesNotExist will be thrown if you specify + # a non-existent field name in values() (a field that is neither in the + # model nor in extra(select)). + self.assertRaises(FieldError, + Article.objects.extra(select={'id_plus_one': 'id + 1'}).values, + 'id', 'id_plus_two') + # If you don't specify field names to values(), all are returned. + self.assertQuerysetEqual(Article.objects.filter(id=self.a5.id).values(), + [{ + 'id': self.a5.id, + 'headline': 'Article 5', + 'pub_date': datetime(2005, 8, 1, 9, 0) + }], transform=identity) + + def test_values_list(self): + # values_list() is similar to values(), except that the results are + # returned as a list of tuples, rather than a list of dictionaries. + # Within each tuple, the order of the elemnts is the same as the order + # of fields in the values_list() call. + identity = lambda x:x + self.assertQuerysetEqual(Article.objects.values_list('headline'), + [ + (u'Article 5',), + (u'Article 6',), + (u'Article 4',), + (u'Article 2',), + (u'Article 3',), + (u'Article 7',), + (u'Article 1',), + ], transform=identity) + self.assertQuerysetEqual(Article.objects.values_list('id').order_by('id'), + [(self.a1.id,), (self.a2.id,), (self.a3.id,), (self.a4.id,), (self.a5.id,), (self.a6.id,), (self.a7.id,)], + transform=identity) + self.assertQuerysetEqual( + Article.objects.values_list('id', flat=True).order_by('id'), + [self.a1.id, self.a2.id, self.a3.id, self.a4.id, self.a5.id, self.a6.id, self.a7.id], + transform=identity) + self.assertQuerysetEqual( + Article.objects.extra(select={'id_plus_one': 'id+1'}) + .order_by('id').values_list('id'), + [(self.a1.id,), (self.a2.id,), (self.a3.id,), (self.a4.id,), (self.a5.id,), (self.a6.id,), (self.a7.id,)], + transform=identity) + self.assertQuerysetEqual( + Article.objects.extra(select={'id_plus_one': 'id+1'}) + .order_by('id').values_list('id_plus_one', 'id'), + [ + (self.a1.id+1, self.a1.id), + (self.a2.id+1, self.a2.id), + (self.a3.id+1, self.a3.id), + (self.a4.id+1, self.a4.id), + (self.a5.id+1, self.a5.id), + (self.a6.id+1, self.a6.id), + (self.a7.id+1, self.a7.id) + ], + transform=identity) + self.assertQuerysetEqual( + Article.objects.extra(select={'id_plus_one': 'id+1'}) + .order_by('id').values_list('id', 'id_plus_one'), + [ + (self.a1.id, self.a1.id+1), + (self.a2.id, self.a2.id+1), + (self.a3.id, self.a3.id+1), + (self.a4.id, self.a4.id+1), + (self.a5.id, self.a5.id+1), + (self.a6.id, self.a6.id+1), + (self.a7.id, self.a7.id+1) + ], + transform=identity) + self.assertRaises(TypeError, Article.objects.values_list, 'id', 'headline', flat=True) + + def test_get_next_previous_by(self): + # Every DateField and DateTimeField creates get_next_by_FOO() and + # get_previous_by_FOO() methods. In the case of identical date values, + # these methods will use the ID as a fallback check. This guarantees + # that no records are skipped or duplicated. + self.assertEqual(repr(self.a1.get_next_by_pub_date()), + '<Article: Article 2>') + self.assertEqual(repr(self.a2.get_next_by_pub_date()), + '<Article: Article 3>') + self.assertEqual(repr(self.a2.get_next_by_pub_date(headline__endswith='6')), + '<Article: Article 6>') + self.assertEqual(repr(self.a3.get_next_by_pub_date()), + '<Article: Article 7>') + self.assertEqual(repr(self.a4.get_next_by_pub_date()), + '<Article: Article 6>') + self.assertRaises(Article.DoesNotExist, self.a5.get_next_by_pub_date) + self.assertEqual(repr(self.a6.get_next_by_pub_date()), + '<Article: Article 5>') + self.assertEqual(repr(self.a7.get_next_by_pub_date()), + '<Article: Article 4>') + + self.assertEqual(repr(self.a7.get_previous_by_pub_date()), + '<Article: Article 3>') + self.assertEqual(repr(self.a6.get_previous_by_pub_date()), + '<Article: Article 4>') + self.assertEqual(repr(self.a5.get_previous_by_pub_date()), + '<Article: Article 6>') + self.assertEqual(repr(self.a4.get_previous_by_pub_date()), + '<Article: Article 7>') + self.assertEqual(repr(self.a3.get_previous_by_pub_date()), + '<Article: Article 2>') + self.assertEqual(repr(self.a2.get_previous_by_pub_date()), + '<Article: Article 1>') + + def test_escaping(self): + # Underscores, percent signs and backslashes have special meaning in the + # underlying SQL code, but Django handles the quoting of them automatically. + a8 = Article(headline='Article_ with underscore', pub_date=datetime(2005, 11, 20)) + a8.save() + self.assertQuerysetEqual(Article.objects.filter(headline__startswith='Article'), + [ + '<Article: Article_ with underscore>', + '<Article: Article 5>', + '<Article: Article 6>', + '<Article: Article 4>', + '<Article: Article 2>', + '<Article: Article 3>', + '<Article: Article 7>', + '<Article: Article 1>', + ]) + self.assertQuerysetEqual(Article.objects.filter(headline__startswith='Article_'), + ['<Article: Article_ with underscore>']) + a9 = Article(headline='Article% with percent sign', pub_date=datetime(2005, 11, 21)) + a9.save() + self.assertQuerysetEqual(Article.objects.filter(headline__startswith='Article'), + [ + '<Article: Article% with percent sign>', + '<Article: Article_ with underscore>', + '<Article: Article 5>', + '<Article: Article 6>', + '<Article: Article 4>', + '<Article: Article 2>', + '<Article: Article 3>', + '<Article: Article 7>', + '<Article: Article 1>', + ]) + self.assertQuerysetEqual(Article.objects.filter(headline__startswith='Article%'), + ['<Article: Article% with percent sign>']) + a10 = Article(headline='Article with \\ backslash', pub_date=datetime(2005, 11, 22)) + a10.save() + self.assertQuerysetEqual(Article.objects.filter(headline__contains='\\'), + ['<Article: Article with \ backslash>']) + + def test_exclude(self): + a8 = Article.objects.create(headline='Article_ with underscore', pub_date=datetime(2005, 11, 20)) + a9 = Article.objects.create(headline='Article% with percent sign', pub_date=datetime(2005, 11, 21)) + a10 = Article.objects.create(headline='Article with \\ backslash', pub_date=datetime(2005, 11, 22)) + + # exclude() is the opposite of filter() when doing lookups: + self.assertQuerysetEqual( + Article.objects.filter(headline__contains='Article').exclude(headline__contains='with'), + [ + '<Article: Article 5>', + '<Article: Article 6>', + '<Article: Article 4>', + '<Article: Article 2>', + '<Article: Article 3>', + '<Article: Article 7>', + '<Article: Article 1>', + ]) + self.assertQuerysetEqual(Article.objects.exclude(headline__startswith="Article_"), + [ + '<Article: Article with \\ backslash>', + '<Article: Article% with percent sign>', + '<Article: Article 5>', + '<Article: Article 6>', + '<Article: Article 4>', + '<Article: Article 2>', + '<Article: Article 3>', + '<Article: Article 7>', + '<Article: Article 1>', + ]) + self.assertQuerysetEqual(Article.objects.exclude(headline="Article 7"), + [ + '<Article: Article with \\ backslash>', + '<Article: Article% with percent sign>', + '<Article: Article_ with underscore>', + '<Article: Article 5>', + '<Article: Article 6>', + '<Article: Article 4>', + '<Article: Article 2>', + '<Article: Article 3>', + '<Article: Article 1>', + ]) + + def test_none(self): + # none() returns an EmptyQuerySet that behaves like any other QuerySet object + self.assertQuerysetEqual(Article.objects.none(), []) + self.assertQuerysetEqual( + Article.objects.none().filter(headline__startswith='Article'), []) + self.assertQuerysetEqual( + Article.objects.filter(headline__startswith='Article').none(), []) + self.assertEqual(Article.objects.none().count(), 0) + self.assertEqual( + Article.objects.none().update(headline="This should not take effect"), 0) + self.assertQuerysetEqual( + [article for article in Article.objects.none().iterator()], + []) + + def test_in(self): + # using __in with an empty list should return an empty query set + self.assertQuerysetEqual(Article.objects.filter(id__in=[]), []) + self.assertQuerysetEqual(Article.objects.exclude(id__in=[]), + [ + '<Article: Article 5>', + '<Article: Article 6>', + '<Article: Article 4>', + '<Article: Article 2>', + '<Article: Article 3>', + '<Article: Article 7>', + '<Article: Article 1>', + ]) + + def test_error_messages(self): + # Programming errors are pointed out with nice error messages + try: + Article.objects.filter(pub_date_year='2005').count() + self.fail('FieldError not raised') + except FieldError, ex: + self.assertEqual(str(ex), "Cannot resolve keyword 'pub_date_year' " + "into field. Choices are: headline, id, pub_date") + try: + Article.objects.filter(headline__starts='Article') + self.fail('FieldError not raised') + except FieldError, ex: + self.assertEqual(str(ex), "Join on field 'headline' not permitted. " + "Did you misspell 'starts' for the lookup type?") + + def test_regex(self): + # Create some articles with a bit more interesting headlines for testing field lookups: + for a in Article.objects.all(): + a.delete() + now = datetime.now() + a1 = Article(pub_date=now, headline='f') + a1.save() + a2 = Article(pub_date=now, headline='fo') + a2.save() + a3 = Article(pub_date=now, headline='foo') + a3.save() + a4 = Article(pub_date=now, headline='fooo') + a4.save() + a5 = Article(pub_date=now, headline='hey-Foo') + a5.save() + a6 = Article(pub_date=now, headline='bar') + a6.save() + a7 = Article(pub_date=now, headline='AbBa') + a7.save() + a8 = Article(pub_date=now, headline='baz') + a8.save() + a9 = Article(pub_date=now, headline='baxZ') + a9.save() + # zero-or-more + self.assertQuerysetEqual(Article.objects.filter(headline__regex=r'fo*'), + ['<Article: f>', '<Article: fo>', '<Article: foo>', '<Article: fooo>']) + self.assertQuerysetEqual(Article.objects.filter(headline__iregex=r'fo*'), + [ + '<Article: f>', + '<Article: fo>', + '<Article: foo>', + '<Article: fooo>', + '<Article: hey-Foo>', + ]) + # one-or-more + self.assertQuerysetEqual(Article.objects.filter(headline__regex=r'fo+'), + ['<Article: fo>', '<Article: foo>', '<Article: fooo>']) + # wildcard + self.assertQuerysetEqual(Article.objects.filter(headline__regex=r'fooo?'), + ['<Article: foo>', '<Article: fooo>']) + # leading anchor + self.assertQuerysetEqual(Article.objects.filter(headline__regex=r'^b'), + ['<Article: bar>', '<Article: baxZ>', '<Article: baz>']) + self.assertQuerysetEqual(Article.objects.filter(headline__iregex=r'^a'), + ['<Article: AbBa>']) + # trailing anchor + self.assertQuerysetEqual(Article.objects.filter(headline__regex=r'z$'), + ['<Article: baz>']) + self.assertQuerysetEqual(Article.objects.filter(headline__iregex=r'z$'), + ['<Article: baxZ>', '<Article: baz>']) + # character sets + self.assertQuerysetEqual(Article.objects.filter(headline__regex=r'ba[rz]'), + ['<Article: bar>', '<Article: baz>']) + self.assertQuerysetEqual(Article.objects.filter(headline__regex=r'ba.[RxZ]'), + ['<Article: baxZ>']) + self.assertQuerysetEqual(Article.objects.filter(headline__iregex=r'ba[RxZ]'), + ['<Article: bar>', '<Article: baxZ>', '<Article: baz>']) + + # and more articles: + a10 = Article(pub_date=now, headline='foobar') + a10.save() + a11 = Article(pub_date=now, headline='foobaz') + a11.save() + a12 = Article(pub_date=now, headline='ooF') + a12.save() + a13 = Article(pub_date=now, headline='foobarbaz') + a13.save() + a14 = Article(pub_date=now, headline='zoocarfaz') + a14.save() + a15 = Article(pub_date=now, headline='barfoobaz') + a15.save() + a16 = Article(pub_date=now, headline='bazbaRFOO') + a16.save() + + # alternation + self.assertQuerysetEqual(Article.objects.filter(headline__regex=r'oo(f|b)'), + [ + '<Article: barfoobaz>', + '<Article: foobar>', + '<Article: foobarbaz>', + '<Article: foobaz>', + ]) + self.assertQuerysetEqual(Article.objects.filter(headline__iregex=r'oo(f|b)'), + [ + '<Article: barfoobaz>', + '<Article: foobar>', + '<Article: foobarbaz>', + '<Article: foobaz>', + '<Article: ooF>', + ]) + self.assertQuerysetEqual(Article.objects.filter(headline__regex=r'^foo(f|b)'), + ['<Article: foobar>', '<Article: foobarbaz>', '<Article: foobaz>']) + + # greedy matching + self.assertQuerysetEqual(Article.objects.filter(headline__regex=r'b.*az'), + [ + '<Article: barfoobaz>', + '<Article: baz>', + '<Article: bazbaRFOO>', + '<Article: foobarbaz>', + '<Article: foobaz>', + ]) + self.assertQuerysetEqual(Article.objects.filter(headline__iregex=r'b.*ar'), + [ + '<Article: bar>', + '<Article: barfoobaz>', + '<Article: bazbaRFOO>', + '<Article: foobar>', + '<Article: foobarbaz>', + ]) + + if settings.DATABASES[DEFAULT_DB_ALIAS]['ENGINE'] != 'django.db.backends.mysql': + def test_regex_backreferencing(self): + # grouping and backreferences + now = datetime.now() + a10 = Article(pub_date=now, headline='foobar') + a10.save() + a11 = Article(pub_date=now, headline='foobaz') + a11.save() + a12 = Article(pub_date=now, headline='ooF') + a12.save() + a13 = Article(pub_date=now, headline='foobarbaz') + a13.save() + a14 = Article(pub_date=now, headline='zoocarfaz') + a14.save() + a15 = Article(pub_date=now, headline='barfoobaz') + a15.save() + a16 = Article(pub_date=now, headline='bazbaRFOO') + a16.save() + self.assertQuerysetEqual(Article.objects.filter(headline__regex=r'b(.).*b\1'), + ['<Article: barfoobaz>', '<Article: bazbaRFOO>', '<Article: foobarbaz>']) diff --git a/parts/django/tests/modeltests/m2m_and_m2o/__init__.py b/parts/django/tests/modeltests/m2m_and_m2o/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/modeltests/m2m_and_m2o/__init__.py diff --git a/parts/django/tests/modeltests/m2m_and_m2o/models.py b/parts/django/tests/modeltests/m2m_and_m2o/models.py new file mode 100644 index 0000000..0fea1a2 --- /dev/null +++ b/parts/django/tests/modeltests/m2m_and_m2o/models.py @@ -0,0 +1,21 @@ +""" +29. Many-to-many and many-to-one relationships to the same table + +Make sure to set ``related_name`` if you use relationships to the same table. +""" + +from django.db import models + +class User(models.Model): + username = models.CharField(max_length=20) + +class Issue(models.Model): + num = models.IntegerField() + cc = models.ManyToManyField(User, blank=True, related_name='test_issue_cc') + client = models.ForeignKey(User, related_name='test_issue_client') + + def __unicode__(self): + return unicode(self.num) + + class Meta: + ordering = ('num',) diff --git a/parts/django/tests/modeltests/m2m_and_m2o/tests.py b/parts/django/tests/modeltests/m2m_and_m2o/tests.py new file mode 100644 index 0000000..dedf9cd --- /dev/null +++ b/parts/django/tests/modeltests/m2m_and_m2o/tests.py @@ -0,0 +1,75 @@ +from django.db.models import Q +from django.test import TestCase + +from models import Issue, User + + +class RelatedObjectTests(TestCase): + def test_m2m_and_m2o(self): + r = User.objects.create(username="russell") + g = User.objects.create(username="gustav") + + i1 = Issue(num=1) + i1.client = r + i1.save() + + i2 = Issue(num=2) + i2.client = r + i2.save() + i2.cc.add(r) + + i3 = Issue(num=3) + i3.client = g + i3.save() + i3.cc.add(r) + + self.assertQuerysetEqual( + Issue.objects.filter(client=r.id), [ + 1, + 2, + ], + lambda i: i.num + ) + self.assertQuerysetEqual( + Issue.objects.filter(client=g.id), [ + 3, + ], + lambda i: i.num + ) + self.assertQuerysetEqual( + Issue.objects.filter(cc__id__exact=g.id), [] + ) + self.assertQuerysetEqual( + Issue.objects.filter(cc__id__exact=r.id), [ + 2, + 3, + ], + lambda i: i.num + ) + + # These queries combine results from the m2m and the m2o relationships. + # They're three ways of saying the same thing. + self.assertQuerysetEqual( + Issue.objects.filter(Q(cc__id__exact = r.id) | Q(client=r.id)), [ + 1, + 2, + 3, + ], + lambda i: i.num + ) + self.assertQuerysetEqual( + Issue.objects.filter(cc__id__exact=r.id) | Issue.objects.filter(client=r.id), [ + 1, + 2, + 3, + ], + lambda i: i.num + ) + self.assertQuerysetEqual( + Issue.objects.filter(Q(client=r.id) | Q(cc__id__exact=r.id)), [ + 1, + 2, + 3, + ], + lambda i: i.num + ) diff --git a/parts/django/tests/modeltests/m2m_intermediary/__init__.py b/parts/django/tests/modeltests/m2m_intermediary/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/modeltests/m2m_intermediary/__init__.py diff --git a/parts/django/tests/modeltests/m2m_intermediary/models.py b/parts/django/tests/modeltests/m2m_intermediary/models.py new file mode 100644 index 0000000..8042a52 --- /dev/null +++ b/parts/django/tests/modeltests/m2m_intermediary/models.py @@ -0,0 +1,36 @@ +""" +9. Many-to-many relationships via an intermediary table + +For many-to-many relationships that need extra fields on the intermediary +table, use an intermediary model. + +In this example, an ``Article`` can have multiple ``Reporter`` objects, and +each ``Article``-``Reporter`` combination (a ``Writer``) has a ``position`` +field, which specifies the ``Reporter``'s position for the given article +(e.g. "Staff writer"). +""" + +from django.db import models + +class Reporter(models.Model): + first_name = models.CharField(max_length=30) + last_name = models.CharField(max_length=30) + + def __unicode__(self): + return u"%s %s" % (self.first_name, self.last_name) + +class Article(models.Model): + headline = models.CharField(max_length=100) + pub_date = models.DateField() + + def __unicode__(self): + return self.headline + +class Writer(models.Model): + reporter = models.ForeignKey(Reporter) + article = models.ForeignKey(Article) + position = models.CharField(max_length=100) + + def __unicode__(self): + return u'%s (%s)' % (self.reporter, self.position) + diff --git a/parts/django/tests/modeltests/m2m_intermediary/tests.py b/parts/django/tests/modeltests/m2m_intermediary/tests.py new file mode 100644 index 0000000..5f35741 --- /dev/null +++ b/parts/django/tests/modeltests/m2m_intermediary/tests.py @@ -0,0 +1,38 @@ +from datetime import datetime + +from django.test import TestCase + +from models import Reporter, Article, Writer + + +class M2MIntermediaryTests(TestCase): + def test_intermeiary(self): + r1 = Reporter.objects.create(first_name="John", last_name="Smith") + r2 = Reporter.objects.create(first_name="Jane", last_name="Doe") + + a = Article.objects.create( + headline="This is a test", pub_date=datetime(2005, 7, 27) + ) + + w1 = Writer.objects.create(reporter=r1, article=a, position="Main writer") + w2 = Writer.objects.create(reporter=r2, article=a, position="Contributor") + + self.assertQuerysetEqual( + a.writer_set.select_related().order_by("-position"), [ + ("John Smith", "Main writer"), + ("Jane Doe", "Contributor"), + ], + lambda w: (unicode(w.reporter), w.position) + ) + self.assertEqual(w1.reporter, r1) + self.assertEqual(w2.reporter, r2) + + self.assertEqual(w1.article, a) + self.assertEqual(w2.article, a) + + self.assertQuerysetEqual( + r1.writer_set.all(), [ + ("John Smith", "Main writer") + ], + lambda w: (unicode(w.reporter), w.position) + ) diff --git a/parts/django/tests/modeltests/m2m_multiple/__init__.py b/parts/django/tests/modeltests/m2m_multiple/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/modeltests/m2m_multiple/__init__.py diff --git a/parts/django/tests/modeltests/m2m_multiple/models.py b/parts/django/tests/modeltests/m2m_multiple/models.py new file mode 100644 index 0000000..e53f840 --- /dev/null +++ b/parts/django/tests/modeltests/m2m_multiple/models.py @@ -0,0 +1,30 @@ +""" +20. Multiple many-to-many relationships between the same two tables + +In this example, an ``Article`` can have many "primary" ``Category`` objects +and many "secondary" ``Category`` objects. + +Set ``related_name`` to designate what the reverse relationship is called. +""" + +from django.db import models + +class Category(models.Model): + name = models.CharField(max_length=20) + class Meta: + ordering = ('name',) + + def __unicode__(self): + return self.name + +class Article(models.Model): + headline = models.CharField(max_length=50) + pub_date = models.DateTimeField() + primary_categories = models.ManyToManyField(Category, related_name='primary_article_set') + secondary_categories = models.ManyToManyField(Category, related_name='secondary_article_set') + class Meta: + ordering = ('pub_date',) + + def __unicode__(self): + return self.headline + diff --git a/parts/django/tests/modeltests/m2m_multiple/tests.py b/parts/django/tests/modeltests/m2m_multiple/tests.py new file mode 100644 index 0000000..1f4503a --- /dev/null +++ b/parts/django/tests/modeltests/m2m_multiple/tests.py @@ -0,0 +1,84 @@ +from datetime import datetime + +from django.test import TestCase + +from models import Article, Category + + +class M2MMultipleTests(TestCase): + def test_multiple(self): + c1, c2, c3, c4 = [ + Category.objects.create(name=name) + for name in ["Sports", "News", "Crime", "Life"] + ] + + a1 = Article.objects.create( + headline="Area man steals", pub_date=datetime(2005, 11, 27) + ) + a1.primary_categories.add(c2, c3) + a1.secondary_categories.add(c4) + + a2 = Article.objects.create( + headline="Area man runs", pub_date=datetime(2005, 11, 28) + ) + a2.primary_categories.add(c1, c2) + a2.secondary_categories.add(c4) + + self.assertQuerysetEqual( + a1.primary_categories.all(), [ + "Crime", + "News", + ], + lambda c: c.name + ) + self.assertQuerysetEqual( + a2.primary_categories.all(), [ + "News", + "Sports", + ], + lambda c: c.name + ) + self.assertQuerysetEqual( + a1.secondary_categories.all(), [ + "Life", + ], + lambda c: c.name + ) + self.assertQuerysetEqual( + c1.primary_article_set.all(), [ + "Area man runs", + ], + lambda a: a.headline + ) + self.assertQuerysetEqual( + c1.secondary_article_set.all(), [] + ) + self.assertQuerysetEqual( + c2.primary_article_set.all(), [ + "Area man steals", + "Area man runs", + ], + lambda a: a.headline + ) + self.assertQuerysetEqual( + c2.secondary_article_set.all(), [] + ) + self.assertQuerysetEqual( + c3.primary_article_set.all(), [ + "Area man steals", + ], + lambda a: a.headline + ) + self.assertQuerysetEqual( + c3.secondary_article_set.all(), [] + ) + self.assertQuerysetEqual( + c4.primary_article_set.all(), [] + ) + self.assertQuerysetEqual( + c4.secondary_article_set.all(), [ + "Area man steals", + "Area man runs", + ], + lambda a: a.headline + ) diff --git a/parts/django/tests/modeltests/m2m_recursive/__init__.py b/parts/django/tests/modeltests/m2m_recursive/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/modeltests/m2m_recursive/__init__.py diff --git a/parts/django/tests/modeltests/m2m_recursive/models.py b/parts/django/tests/modeltests/m2m_recursive/models.py new file mode 100644 index 0000000..83c943a --- /dev/null +++ b/parts/django/tests/modeltests/m2m_recursive/models.py @@ -0,0 +1,28 @@ +""" +28. Many-to-many relationships between the same two tables + +In this example, a ``Person`` can have many friends, who are also ``Person`` +objects. Friendship is a symmetrical relationship - if I am your friend, you +are my friend. Here, ``friends`` is an example of a symmetrical +``ManyToManyField``. + +A ``Person`` can also have many idols - but while I may idolize you, you may +not think the same of me. Here, ``idols`` is an example of a non-symmetrical +``ManyToManyField``. Only recursive ``ManyToManyField`` fields may be +non-symmetrical, and they are symmetrical by default. + +This test validates that the many-to-many table is created using a mangled name +if there is a name clash, and tests that symmetry is preserved where +appropriate. +""" + +from django.db import models + + +class Person(models.Model): + name = models.CharField(max_length=20) + friends = models.ManyToManyField('self') + idols = models.ManyToManyField('self', symmetrical=False, related_name='stalkers') + + def __unicode__(self): + return self.name diff --git a/parts/django/tests/modeltests/m2m_recursive/tests.py b/parts/django/tests/modeltests/m2m_recursive/tests.py new file mode 100644 index 0000000..4251028 --- /dev/null +++ b/parts/django/tests/modeltests/m2m_recursive/tests.py @@ -0,0 +1,253 @@ +from operator import attrgetter + +from django.test import TestCase + +from models import Person + + +class RecursiveM2MTests(TestCase): + def test_recursive_m2m(self): + a, b, c, d = [ + Person.objects.create(name=name) + for name in ["Anne", "Bill", "Chuck", "David"] + ] + + # Add some friends in the direction of field definition + # Anne is friends with Bill and Chuck + a.friends.add(b, c) + + # David is friends with Anne and Chuck - add in reverse direction + d.friends.add(a,c) + + # Who is friends with Anne? + self.assertQuerysetEqual( + a.friends.all(), [ + "Bill", + "Chuck", + "David" + ], + attrgetter("name") + ) + # Who is friends with Bill? + self.assertQuerysetEqual( + b.friends.all(), [ + "Anne", + ], + attrgetter("name") + ) + # Who is friends with Chuck? + self.assertQuerysetEqual( + c.friends.all(), [ + "Anne", + "David" + ], + attrgetter("name") + ) + # Who is friends with David? + self.assertQuerysetEqual( + d.friends.all(), [ + "Anne", + "Chuck", + ], + attrgetter("name") + ) + # Bill is already friends with Anne - add Anne again, but in the + # reverse direction + b.friends.add(a) + + # Who is friends with Anne? + self.assertQuerysetEqual( + a.friends.all(), [ + "Bill", + "Chuck", + "David", + ], + attrgetter("name") + ) + # Who is friends with Bill? + self.assertQuerysetEqual( + b.friends.all(), [ + "Anne", + ], + attrgetter("name") + ) + # Remove Anne from Bill's friends + b.friends.remove(a) + # Who is friends with Anne? + self.assertQuerysetEqual( + a.friends.all(), [ + "Chuck", + "David", + ], + attrgetter("name") + ) + # Who is friends with Bill? + self.assertQuerysetEqual( + b.friends.all(), [] + ) + + # Clear Anne's group of friends + a.friends.clear() + # Who is friends with Anne? + self.assertQuerysetEqual( + a.friends.all(), [] + ) + # Reverse relationships should also be gone + # Who is friends with Chuck? + self.assertQuerysetEqual( + c.friends.all(), [ + "David", + ], + attrgetter("name") + ) + # Who is friends with David? + self.assertQuerysetEqual( + d.friends.all(), [ + "Chuck", + ], + attrgetter("name") + ) + + # Add some idols in the direction of field definition + # Anne idolizes Bill and Chuck + a.idols.add(b, c) + # Bill idolizes Anne right back + b.idols.add(a) + # David is idolized by Anne and Chuck - add in reverse direction + d.stalkers.add(a, c) + + # Who are Anne's idols? + self.assertQuerysetEqual( + a.idols.all(), [ + "Bill", + "Chuck", + "David", + ], + attrgetter("name") + ) + # Who is stalking Anne? + self.assertQuerysetEqual( + a.stalkers.all(), [ + "Bill", + ], + attrgetter("name") + ) + # Who are Bill's idols? + self.assertQuerysetEqual( + b.idols.all(), [ + "Anne", + ], + attrgetter("name") + ) + # Who is stalking Bill? + self.assertQuerysetEqual( + b.stalkers.all(), [ + "Anne", + ], + attrgetter("name") + ) + # Who are Chuck's idols? + self.assertQuerysetEqual( + c.idols.all(), [ + "David", + ], + attrgetter("name"), + ) + # Who is stalking Chuck? + self.assertQuerysetEqual( + c.stalkers.all(), [ + "Anne", + ], + attrgetter("name") + ) + # Who are David's idols? + self.assertQuerysetEqual( + d.idols.all(), [] + ) + # Who is stalking David + self.assertQuerysetEqual( + d.stalkers.all(), [ + "Anne", + "Chuck", + ], + attrgetter("name") + ) + # Bill is already being stalked by Anne - add Anne again, but in the + # reverse direction + b.stalkers.add(a) + # Who are Anne's idols? + self.assertQuerysetEqual( + a.idols.all(), [ + "Bill", + "Chuck", + "David", + ], + attrgetter("name") + ) + # Who is stalking Anne? + self.assertQuerysetEqual( + a.stalkers.all(), [ + "Bill", + ], + attrgetter("name") + ) + # Who are Bill's idols + self.assertQuerysetEqual( + b.idols.all(), [ + "Anne", + ], + attrgetter("name") + ) + # Who is stalking Bill? + self.assertQuerysetEqual( + b.stalkers.all(), [ + "Anne", + ], + attrgetter("name"), + ) + # Remove Anne from Bill's list of stalkers + b.stalkers.remove(a) + # Who are Anne's idols? + self.assertQuerysetEqual( + a.idols.all(), [ + "Chuck", + "David", + ], + attrgetter("name") + ) + # Who is stalking Anne? + self.assertQuerysetEqual( + a.stalkers.all(), [ + "Bill", + ], + attrgetter("name") + ) + # Who are Bill's idols? + self.assertQuerysetEqual( + b.idols.all(), [ + "Anne", + ], + attrgetter("name") + ) + # Who is stalking Bill? + self.assertQuerysetEqual( + b.stalkers.all(), [] + ) + # Clear Anne's group of idols + a.idols.clear() + # Who are Anne's idols + self.assertQuerysetEqual( + a.idols.all(), [] + ) + # Reverse relationships should also be gone + # Who is stalking Chuck? + self.assertQuerysetEqual( + c.stalkers.all(), [] + ) + # Who is friends with David? + self.assertQuerysetEqual( + d.stalkers.all(), [ + "Chuck", + ], + attrgetter("name") + ) diff --git a/parts/django/tests/modeltests/m2m_signals/__init__.py b/parts/django/tests/modeltests/m2m_signals/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/parts/django/tests/modeltests/m2m_signals/__init__.py @@ -0,0 +1 @@ + diff --git a/parts/django/tests/modeltests/m2m_signals/models.py b/parts/django/tests/modeltests/m2m_signals/models.py new file mode 100644 index 0000000..526c4a7 --- /dev/null +++ b/parts/django/tests/modeltests/m2m_signals/models.py @@ -0,0 +1,36 @@ +from django.db import models + + +class Part(models.Model): + name = models.CharField(max_length=20) + + class Meta: + ordering = ('name',) + + def __unicode__(self): + return self.name + +class Car(models.Model): + name = models.CharField(max_length=20) + default_parts = models.ManyToManyField(Part) + optional_parts = models.ManyToManyField(Part, related_name='cars_optional') + + class Meta: + ordering = ('name',) + + def __unicode__(self): + return self.name + +class SportsCar(Car): + price = models.IntegerField() + +class Person(models.Model): + name = models.CharField(max_length=20) + fans = models.ManyToManyField('self', related_name='idols', symmetrical=False) + friends = models.ManyToManyField('self') + + class Meta: + ordering = ('name',) + + def __unicode__(self): + return self.name diff --git a/parts/django/tests/modeltests/m2m_signals/tests.py b/parts/django/tests/modeltests/m2m_signals/tests.py new file mode 100644 index 0000000..9e9158f --- /dev/null +++ b/parts/django/tests/modeltests/m2m_signals/tests.py @@ -0,0 +1,427 @@ +""" +Testing signals emitted on changing m2m relations. +""" + +from django.db import models +from django.test import TestCase + +from models import Part, Car, SportsCar, Person + + +class ManyToManySignalsTest(TestCase): + def m2m_changed_signal_receiver(self, signal, sender, **kwargs): + message = { + 'instance': kwargs['instance'], + 'action': kwargs['action'], + 'reverse': kwargs['reverse'], + 'model': kwargs['model'], + } + if kwargs['pk_set']: + message['objects'] = list( + kwargs['model'].objects.filter(pk__in=kwargs['pk_set']) + ) + self.m2m_changed_messages.append(message) + + def setUp(self): + self.m2m_changed_messages = [] + + self.vw = Car.objects.create(name='VW') + self.bmw = Car.objects.create(name='BMW') + self.toyota = Car.objects.create(name='Toyota') + self.wheelset = Part.objects.create(name='Wheelset') + self.doors = Part.objects.create(name='Doors') + self.engine = Part.objects.create(name='Engine') + self.airbag = Part.objects.create(name='Airbag') + self.sunroof = Part.objects.create(name='Sunroof') + + self.alice = Person.objects.create(name='Alice') + self.bob = Person.objects.create(name='Bob') + self.chuck = Person.objects.create(name='Chuck') + self.daisy = Person.objects.create(name='Daisy') + + def tearDown(self): + # disconnect all signal handlers + models.signals.m2m_changed.disconnect( + self.m2m_changed_signal_receiver, Car.default_parts.through + ) + models.signals.m2m_changed.disconnect( + self.m2m_changed_signal_receiver, Car.optional_parts.through + ) + models.signals.m2m_changed.disconnect( + self.m2m_changed_signal_receiver, Person.fans.through + ) + models.signals.m2m_changed.disconnect( + self.m2m_changed_signal_receiver, Person.friends.through + ) + + def test_m2m_relations_add_remove_clear(self): + expected_messages = [] + + # Install a listener on one of the two m2m relations. + models.signals.m2m_changed.connect( + self.m2m_changed_signal_receiver, Car.optional_parts.through + ) + + # Test the add, remove and clear methods on both sides of the + # many-to-many relation + + # adding a default part to our car - no signal listener installed + self.vw.default_parts.add(self.sunroof) + + # Now install a listener + models.signals.m2m_changed.connect( + self.m2m_changed_signal_receiver, Car.default_parts.through + ) + + self.vw.default_parts.add(self.wheelset, self.doors, self.engine) + expected_messages.append({ + 'instance': self.vw, + 'action': 'pre_add', + 'reverse': False, + 'model': Part, + 'objects': [self.doors, self.engine, self.wheelset], + }) + expected_messages.append({ + 'instance': self.vw, + 'action': 'post_add', + 'reverse': False, + 'model': Part, + 'objects': [self.doors, self.engine, self.wheelset], + }) + self.assertEqual(self.m2m_changed_messages, expected_messages) + + # give the BMW and Toyata some doors as well + self.doors.car_set.add(self.bmw, self.toyota) + expected_messages.append({ + 'instance': self.doors, + 'action': 'pre_add', + 'reverse': True, + 'model': Car, + 'objects': [self.bmw, self.toyota], + }) + expected_messages.append({ + 'instance': self.doors, + 'action': 'post_add', + 'reverse': True, + 'model': Car, + 'objects': [self.bmw, self.toyota], + }) + self.assertEqual(self.m2m_changed_messages, expected_messages) + + # remove the engine from the self.vw and the airbag (which is not set + # but is returned) + self.vw.default_parts.remove(self.engine, self.airbag) + expected_messages.append({ + 'instance': self.vw, + 'action': 'pre_remove', + 'reverse': False, + 'model': Part, + 'objects': [self.airbag, self.engine], + }) + expected_messages.append({ + 'instance': self.vw, + 'action': 'post_remove', + 'reverse': False, + 'model': Part, + 'objects': [self.airbag, self.engine], + }) + self.assertEqual(self.m2m_changed_messages, expected_messages) + + # give the self.vw some optional parts (second relation to same model) + self.vw.optional_parts.add(self.airbag, self.sunroof) + expected_messages.append({ + 'instance': self.vw, + 'action': 'pre_add', + 'reverse': False, + 'model': Part, + 'objects': [self.airbag, self.sunroof], + }) + expected_messages.append({ + 'instance': self.vw, + 'action': 'post_add', + 'reverse': False, + 'model': Part, + 'objects': [self.airbag, self.sunroof], + }) + self.assertEqual(self.m2m_changed_messages, expected_messages) + + # add airbag to all the cars (even though the self.vw already has one) + self.airbag.cars_optional.add(self.vw, self.bmw, self.toyota) + expected_messages.append({ + 'instance': self.airbag, + 'action': 'pre_add', + 'reverse': True, + 'model': Car, + 'objects': [self.bmw, self.toyota], + }) + expected_messages.append({ + 'instance': self.airbag, + 'action': 'post_add', + 'reverse': True, + 'model': Car, + 'objects': [self.bmw, self.toyota], + }) + self.assertEqual(self.m2m_changed_messages, expected_messages) + + # remove airbag from the self.vw (reverse relation with custom + # related_name) + self.airbag.cars_optional.remove(self.vw) + expected_messages.append({ + 'instance': self.airbag, + 'action': 'pre_remove', + 'reverse': True, + 'model': Car, + 'objects': [self.vw], + }) + expected_messages.append({ + 'instance': self.airbag, + 'action': 'post_remove', + 'reverse': True, + 'model': Car, + 'objects': [self.vw], + }) + self.assertEqual(self.m2m_changed_messages, expected_messages) + + # clear all parts of the self.vw + self.vw.default_parts.clear() + expected_messages.append({ + 'instance': self.vw, + 'action': 'pre_clear', + 'reverse': False, + 'model': Part, + }) + expected_messages.append({ + 'instance': self.vw, + 'action': 'post_clear', + 'reverse': False, + 'model': Part, + }) + self.assertEqual(self.m2m_changed_messages, expected_messages) + + # take all the doors off of cars + self.doors.car_set.clear() + expected_messages.append({ + 'instance': self.doors, + 'action': 'pre_clear', + 'reverse': True, + 'model': Car, + }) + expected_messages.append({ + 'instance': self.doors, + 'action': 'post_clear', + 'reverse': True, + 'model': Car, + }) + self.assertEqual(self.m2m_changed_messages, expected_messages) + + # take all the airbags off of cars (clear reverse relation with custom + # related_name) + self.airbag.cars_optional.clear() + expected_messages.append({ + 'instance': self.airbag, + 'action': 'pre_clear', + 'reverse': True, + 'model': Car, + }) + expected_messages.append({ + 'instance': self.airbag, + 'action': 'post_clear', + 'reverse': True, + 'model': Car, + }) + self.assertEqual(self.m2m_changed_messages, expected_messages) + + # alternative ways of setting relation: + self.vw.default_parts.create(name='Windows') + p6 = Part.objects.get(name='Windows') + expected_messages.append({ + 'instance': self.vw, + 'action': 'pre_add', + 'reverse': False, + 'model': Part, + 'objects': [p6], + }) + expected_messages.append({ + 'instance': self.vw, + 'action': 'post_add', + 'reverse': False, + 'model': Part, + 'objects': [p6], + }) + self.assertEqual(self.m2m_changed_messages, expected_messages) + + # direct assignment clears the set first, then adds + self.vw.default_parts = [self.wheelset,self.doors,self.engine] + expected_messages.append({ + 'instance': self.vw, + 'action': 'pre_clear', + 'reverse': False, + 'model': Part, + }) + expected_messages.append({ + 'instance': self.vw, + 'action': 'post_clear', + 'reverse': False, + 'model': Part, + }) + expected_messages.append({ + 'instance': self.vw, + 'action': 'pre_add', + 'reverse': False, + 'model': Part, + 'objects': [self.doors, self.engine, self.wheelset], + }) + expected_messages.append({ + 'instance': self.vw, + 'action': 'post_add', + 'reverse': False, + 'model': Part, + 'objects': [self.doors, self.engine, self.wheelset], + }) + self.assertEqual(self.m2m_changed_messages, expected_messages) + + # Check that signals still work when model inheritance is involved + c4 = SportsCar.objects.create(name='Bugatti', price='1000000') + c4b = Car.objects.get(name='Bugatti') + c4.default_parts = [self.doors] + expected_messages.append({ + 'instance': c4, + 'action': 'pre_clear', + 'reverse': False, + 'model': Part, + }) + expected_messages.append({ + 'instance': c4, + 'action': 'post_clear', + 'reverse': False, + 'model': Part, + }) + expected_messages.append({ + 'instance': c4, + 'action': 'pre_add', + 'reverse': False, + 'model': Part, + 'objects': [self.doors], + }) + expected_messages.append({ + 'instance': c4, + 'action': 'post_add', + 'reverse': False, + 'model': Part, + 'objects': [self.doors], + }) + self.assertEqual(self.m2m_changed_messages, expected_messages) + + self.engine.car_set.add(c4) + expected_messages.append({ + 'instance': self.engine, + 'action': 'pre_add', + 'reverse': True, + 'model': Car, + 'objects': [c4b], + }) + expected_messages.append({ + 'instance': self.engine, + 'action': 'post_add', + 'reverse': True, + 'model': Car, + 'objects': [c4b], + }) + self.assertEqual(self.m2m_changed_messages, expected_messages) + + def test_m2m_relations_with_self(self): + expected_messages = [] + + models.signals.m2m_changed.connect( + self.m2m_changed_signal_receiver, Person.fans.through + ) + models.signals.m2m_changed.connect( + self.m2m_changed_signal_receiver, Person.friends.through + ) + + self.alice.friends = [self.bob, self.chuck] + expected_messages.append({ + 'instance': self.alice, + 'action': 'pre_clear', + 'reverse': False, + 'model': Person, + }) + expected_messages.append({ + 'instance': self.alice, + 'action': 'post_clear', + 'reverse': False, + 'model': Person, + }) + expected_messages.append({ + 'instance': self.alice, + 'action': 'pre_add', + 'reverse': False, + 'model': Person, + 'objects': [self.bob, self.chuck], + }) + expected_messages.append({ + 'instance': self.alice, + 'action': 'post_add', + 'reverse': False, + 'model': Person, + 'objects': [self.bob, self.chuck], + }) + self.assertEqual(self.m2m_changed_messages, expected_messages) + + self.alice.fans = [self.daisy] + expected_messages.append({ + 'instance': self.alice, + 'action': 'pre_clear', + 'reverse': False, + 'model': Person, + }) + expected_messages.append({ + 'instance': self.alice, + 'action': 'post_clear', + 'reverse': False, + 'model': Person, + }) + expected_messages.append({ + 'instance': self.alice, + 'action': 'pre_add', + 'reverse': False, + 'model': Person, + 'objects': [self.daisy], + }) + expected_messages.append({ + 'instance': self.alice, + 'action': 'post_add', + 'reverse': False, + 'model': Person, + 'objects': [self.daisy], + }) + self.assertEqual(self.m2m_changed_messages, expected_messages) + + self.chuck.idols = [self.alice,self.bob] + expected_messages.append({ + 'instance': self.chuck, + 'action': 'pre_clear', + 'reverse': True, + 'model': Person, + }) + expected_messages.append({ + 'instance': self.chuck, + 'action': 'post_clear', + 'reverse': True, + 'model': Person, + }) + expected_messages.append({ + 'instance': self.chuck, + 'action': 'pre_add', + 'reverse': True, + 'model': Person, + 'objects': [self.alice, self.bob], + }) + expected_messages.append({ + 'instance': self.chuck, + 'action': 'post_add', + 'reverse': True, + 'model': Person, + 'objects': [self.alice, self.bob], + }) + self.assertEqual(self.m2m_changed_messages, expected_messages) diff --git a/parts/django/tests/modeltests/m2m_through/__init__.py b/parts/django/tests/modeltests/m2m_through/__init__.py new file mode 100644 index 0000000..139597f --- /dev/null +++ b/parts/django/tests/modeltests/m2m_through/__init__.py @@ -0,0 +1,2 @@ + + diff --git a/parts/django/tests/modeltests/m2m_through/models.py b/parts/django/tests/modeltests/m2m_through/models.py new file mode 100644 index 0000000..d41fe8d --- /dev/null +++ b/parts/django/tests/modeltests/m2m_through/models.py @@ -0,0 +1,65 @@ +from django.db import models +from datetime import datetime + +# M2M described on one of the models +class Person(models.Model): + name = models.CharField(max_length=128) + + class Meta: + ordering = ('name',) + + def __unicode__(self): + return self.name + +class Group(models.Model): + name = models.CharField(max_length=128) + members = models.ManyToManyField(Person, through='Membership') + custom_members = models.ManyToManyField(Person, through='CustomMembership', related_name="custom") + nodefaultsnonulls = models.ManyToManyField(Person, through='TestNoDefaultsOrNulls', related_name="testnodefaultsnonulls") + + class Meta: + ordering = ('name',) + + def __unicode__(self): + return self.name + +class Membership(models.Model): + person = models.ForeignKey(Person) + group = models.ForeignKey(Group) + date_joined = models.DateTimeField(default=datetime.now) + invite_reason = models.CharField(max_length=64, null=True) + + class Meta: + ordering = ('date_joined', 'invite_reason', 'group') + + def __unicode__(self): + return "%s is a member of %s" % (self.person.name, self.group.name) + +class CustomMembership(models.Model): + person = models.ForeignKey(Person, db_column="custom_person_column", related_name="custom_person_related_name") + group = models.ForeignKey(Group) + weird_fk = models.ForeignKey(Membership, null=True) + date_joined = models.DateTimeField(default=datetime.now) + + def __unicode__(self): + return "%s is a member of %s" % (self.person.name, self.group.name) + + class Meta: + db_table = "test_table" + +class TestNoDefaultsOrNulls(models.Model): + person = models.ForeignKey(Person) + group = models.ForeignKey(Group) + nodefaultnonull = models.CharField(max_length=5) + +class PersonSelfRefM2M(models.Model): + name = models.CharField(max_length=5) + friends = models.ManyToManyField('self', through="Friendship", symmetrical=False) + + def __unicode__(self): + return self.name + +class Friendship(models.Model): + first = models.ForeignKey(PersonSelfRefM2M, related_name="rel_from_set") + second = models.ForeignKey(PersonSelfRefM2M, related_name="rel_to_set") + date_friended = models.DateTimeField() diff --git a/parts/django/tests/modeltests/m2m_through/tests.py b/parts/django/tests/modeltests/m2m_through/tests.py new file mode 100644 index 0000000..807e952 --- /dev/null +++ b/parts/django/tests/modeltests/m2m_through/tests.py @@ -0,0 +1,343 @@ +from datetime import datetime +from operator import attrgetter + +from django.test import TestCase + +from models import Person, Group, Membership, CustomMembership, \ + TestNoDefaultsOrNulls, PersonSelfRefM2M, Friendship + + +class M2mThroughTests(TestCase): + def setUp(self): + self.bob = Person.objects.create(name='Bob') + self.jim = Person.objects.create(name='Jim') + self.jane = Person.objects.create(name='Jane') + self.rock = Group.objects.create(name='Rock') + self.roll = Group.objects.create(name='Roll') + + def test_m2m_through(self): + # We start out by making sure that the Group 'rock' has no members. + self.assertQuerysetEqual( + self.rock.members.all(), + [] + ) + # To make Jim a member of Group Rock, simply create a Membership object. + m1 = Membership.objects.create(person=self.jim, group=self.rock) + # We can do the same for Jane and Rock. + m2 = Membership.objects.create(person=self.jane, group=self.rock) + # Let's check to make sure that it worked. Jane and Jim should be members of Rock. + self.assertQuerysetEqual( + self.rock.members.all(), [ + 'Jane', + 'Jim' + ], + attrgetter("name") + ) + # Now we can add a bunch more Membership objects to test with. + m3 = Membership.objects.create(person=self.bob, group=self.roll) + m4 = Membership.objects.create(person=self.jim, group=self.roll) + m5 = Membership.objects.create(person=self.jane, group=self.roll) + # We can get Jim's Group membership as with any ForeignKey. + self.assertQuerysetEqual( + self.jim.group_set.all(), [ + 'Rock', + 'Roll' + ], + attrgetter("name") + ) + # Querying the intermediary model works like normal. + self.assertEqual( + repr(Membership.objects.get(person=self.jane, group=self.rock)), + '<Membership: Jane is a member of Rock>' + ) + # It's not only get that works. Filter works like normal as well. + self.assertQuerysetEqual( + Membership.objects.filter(person=self.jim), [ + '<Membership: Jim is a member of Rock>', + '<Membership: Jim is a member of Roll>' + ] + ) + self.rock.members.clear() + # Now there will be no members of Rock. + self.assertQuerysetEqual( + self.rock.members.all(), + [] + ) + + + + def test_forward_descriptors(self): + # Due to complications with adding via an intermediary model, + # the add method is not provided. + self.assertRaises(AttributeError, lambda: self.rock.members.add(self.bob)) + # Create is also disabled as it suffers from the same problems as add. + self.assertRaises(AttributeError, lambda: self.rock.members.create(name='Anne')) + # Remove has similar complications, and is not provided either. + self.assertRaises(AttributeError, lambda: self.rock.members.remove(self.jim)) + + m1 = Membership.objects.create(person=self.jim, group=self.rock) + m2 = Membership.objects.create(person=self.jane, group=self.rock) + + # Here we back up the list of all members of Rock. + backup = list(self.rock.members.all()) + # ...and we verify that it has worked. + self.assertEqual( + [p.name for p in backup], + ['Jane', 'Jim'] + ) + # The clear function should still work. + self.rock.members.clear() + # Now there will be no members of Rock. + self.assertQuerysetEqual( + self.rock.members.all(), + [] + ) + + # Assignment should not work with models specifying a through model for many of + # the same reasons as adding. + self.assertRaises(AttributeError, setattr, self.rock, "members", backup) + # Let's re-save those instances that we've cleared. + m1.save() + m2.save() + # Verifying that those instances were re-saved successfully. + self.assertQuerysetEqual( + self.rock.members.all(),[ + 'Jane', + 'Jim' + ], + attrgetter("name") + ) + + def test_reverse_descriptors(self): + # Due to complications with adding via an intermediary model, + # the add method is not provided. + self.assertRaises(AttributeError, lambda: self.bob.group_set.add(self.rock)) + # Create is also disabled as it suffers from the same problems as add. + self.assertRaises(AttributeError, lambda: self.bob.group_set.create(name="funk")) + # Remove has similar complications, and is not provided either. + self.assertRaises(AttributeError, lambda: self.jim.group_set.remove(self.rock)) + + m1 = Membership.objects.create(person=self.jim, group=self.rock) + m2 = Membership.objects.create(person=self.jim, group=self.roll) + + # Here we back up the list of all of Jim's groups. + backup = list(self.jim.group_set.all()) + self.assertEqual( + [g.name for g in backup], + ['Rock', 'Roll'] + ) + # The clear function should still work. + self.jim.group_set.clear() + # Now Jim will be in no groups. + self.assertQuerysetEqual( + self.jim.group_set.all(), + [] + ) + # Assignment should not work with models specifying a through model for many of + # the same reasons as adding. + self.assertRaises(AttributeError, setattr, self.jim, "group_set", backup) + # Let's re-save those instances that we've cleared. + + m1.save() + m2.save() + # Verifying that those instances were re-saved successfully. + self.assertQuerysetEqual( + self.jim.group_set.all(),[ + 'Rock', + 'Roll' + ], + attrgetter("name") + ) + + def test_custom_tests(self): + # Let's see if we can query through our second relationship. + self.assertQuerysetEqual( + self.rock.custom_members.all(), + [] + ) + # We can query in the opposite direction as well. + self.assertQuerysetEqual( + self.bob.custom.all(), + [] + ) + + cm1 = CustomMembership.objects.create(person=self.bob, group=self.rock) + cm2 = CustomMembership.objects.create(person=self.jim, group=self.rock) + + # If we get the number of people in Rock, it should be both Bob and Jim. + self.assertQuerysetEqual( + self.rock.custom_members.all(),[ + 'Bob', + 'Jim' + ], + attrgetter("name") + ) + # Bob should only be in one custom group. + self.assertQuerysetEqual( + self.bob.custom.all(),[ + 'Rock' + ], + attrgetter("name") + ) + # Let's make sure our new descriptors don't conflict with the FK related_name. + self.assertQuerysetEqual( + self.bob.custom_person_related_name.all(),[ + '<CustomMembership: Bob is a member of Rock>' + ] + ) + + def test_self_referential_tests(self): + # Let's first create a person who has no friends. + tony = PersonSelfRefM2M.objects.create(name="Tony") + self.assertQuerysetEqual( + tony.friends.all(), + [] + ) + + chris = PersonSelfRefM2M.objects.create(name="Chris") + f = Friendship.objects.create(first=tony, second=chris, date_friended=datetime.now()) + + # Tony should now show that Chris is his friend. + self.assertQuerysetEqual( + tony.friends.all(),[ + 'Chris' + ], + attrgetter("name") + ) + # But we haven't established that Chris is Tony's Friend. + self.assertQuerysetEqual( + chris.friends.all(), + [] + ) + f2 = Friendship.objects.create(first=chris, second=tony, date_friended=datetime.now()) + + # Having added Chris as a friend, let's make sure that his friend set reflects + # that addition. + self.assertQuerysetEqual( + chris.friends.all(),[ + 'Tony' + ], + attrgetter("name") + ) + + # Chris gets mad and wants to get rid of all of his friends. + chris.friends.clear() + # Now he should not have any more friends. + self.assertQuerysetEqual( + chris.friends.all(), + [] + ) + # Since this isn't a symmetrical relation, Tony's friend link still exists. + self.assertQuerysetEqual( + tony.friends.all(),[ + 'Chris' + ], + attrgetter("name") + ) + + def test_query_tests(self): + m1 = Membership.objects.create(person=self.jim, group=self.rock) + m2 = Membership.objects.create(person=self.jane, group=self.rock) + m3 = Membership.objects.create(person=self.bob, group=self.roll) + m4 = Membership.objects.create(person=self.jim, group=self.roll) + m5 = Membership.objects.create(person=self.jane, group=self.roll) + + m2.invite_reason = "She was just awesome." + m2.date_joined = datetime(2006, 1, 1) + m2.save() + m3.date_joined = datetime(2004, 1, 1) + m3.save() + m5.date_joined = datetime(2004, 1, 1) + m5.save() + + # We can query for the related model by using its attribute name (members, in + # this case). + self.assertQuerysetEqual( + Group.objects.filter(members__name='Bob'),[ + 'Roll' + ], + attrgetter("name") + ) + + # To query through the intermediary model, we specify its model name. + # In this case, membership. + self.assertQuerysetEqual( + Group.objects.filter(membership__invite_reason="She was just awesome."),[ + 'Rock' + ], + attrgetter("name") + ) + + # If we want to query in the reverse direction by the related model, use its + # model name (group, in this case). + self.assertQuerysetEqual( + Person.objects.filter(group__name="Rock"),[ + 'Jane', + 'Jim' + ], + attrgetter("name") + ) + + cm1 = CustomMembership.objects.create(person=self.bob, group=self.rock) + cm2 = CustomMembership.objects.create(person=self.jim, group=self.rock) + # If the m2m field has specified a related_name, using that will work. + self.assertQuerysetEqual( + Person.objects.filter(custom__name="Rock"),[ + 'Bob', + 'Jim' + ], + attrgetter("name") + ) + + # To query through the intermediary model in the reverse direction, we again + # specify its model name (membership, in this case). + self.assertQuerysetEqual( + Person.objects.filter(membership__invite_reason="She was just awesome."),[ + 'Jane' + ], + attrgetter("name") + ) + + # Let's see all of the groups that Jane joined after 1 Jan 2005: + self.assertQuerysetEqual( + Group.objects.filter(membership__date_joined__gt=datetime(2005, 1, 1), membership__person=self.jane),[ + 'Rock' + ], + attrgetter("name") + ) + + # Queries also work in the reverse direction: Now let's see all of the people + # that have joined Rock since 1 Jan 2005: + self.assertQuerysetEqual( + Person.objects.filter(membership__date_joined__gt=datetime(2005, 1, 1), membership__group=self.rock),[ + 'Jane', + 'Jim' + ], + attrgetter("name") + ) + + # Conceivably, queries through membership could return correct, but non-unique + # querysets. To demonstrate this, we query for all people who have joined a + # group after 2004: + self.assertQuerysetEqual( + Person.objects.filter(membership__date_joined__gt=datetime(2004, 1, 1)),[ + 'Jane', + 'Jim', + 'Jim' + ], + attrgetter("name") + ) + + # Jim showed up twice, because he joined two groups ('Rock', and 'Roll'): + self.assertEqual( + [(m.person.name, m.group.name) for m in Membership.objects.filter(date_joined__gt=datetime(2004, 1, 1))], + [(u'Jane', u'Rock'), (u'Jim', u'Rock'), (u'Jim', u'Roll')] + ) + # QuerySet's distinct() method can correct this problem. + self.assertQuerysetEqual( + Person.objects.filter(membership__date_joined__gt=datetime(2004, 1, 1)).distinct(),[ + 'Jane', + 'Jim' + ], + attrgetter("name") + ) diff --git a/parts/django/tests/modeltests/m2o_recursive/__init__.py b/parts/django/tests/modeltests/m2o_recursive/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/modeltests/m2o_recursive/__init__.py diff --git a/parts/django/tests/modeltests/m2o_recursive/models.py b/parts/django/tests/modeltests/m2o_recursive/models.py new file mode 100644 index 0000000..ed9945a --- /dev/null +++ b/parts/django/tests/modeltests/m2o_recursive/models.py @@ -0,0 +1,28 @@ +""" +11. Relating an object to itself, many-to-one + +To define a many-to-one relationship between a model and itself, use +``ForeignKey('self')``. + +In this example, a ``Category`` is related to itself. That is, each +``Category`` has a parent ``Category``. + +Set ``related_name`` to designate what the reverse relationship is called. +""" + +from django.db import models + +class Category(models.Model): + name = models.CharField(max_length=20) + parent = models.ForeignKey('self', blank=True, null=True, related_name='child_set') + + def __unicode__(self): + return self.name + +class Person(models.Model): + full_name = models.CharField(max_length=20) + mother = models.ForeignKey('self', null=True, related_name='mothers_child_set') + father = models.ForeignKey('self', null=True, related_name='fathers_child_set') + + def __unicode__(self): + return self.full_name diff --git a/parts/django/tests/modeltests/m2o_recursive/tests.py b/parts/django/tests/modeltests/m2o_recursive/tests.py new file mode 100644 index 0000000..79dde8b --- /dev/null +++ b/parts/django/tests/modeltests/m2o_recursive/tests.py @@ -0,0 +1,38 @@ +from django.test import TestCase +from models import Category, Person + +class ManyToOneRecursiveTests(TestCase): + + def setUp(self): + self.r = Category(id=None, name='Root category', parent=None) + self.r.save() + self.c = Category(id=None, name='Child category', parent=self.r) + self.c.save() + + def test_m2o_recursive(self): + self.assertQuerysetEqual(self.r.child_set.all(), + ['<Category: Child category>']) + self.assertEqual(self.r.child_set.get(name__startswith='Child').id, self.c.id) + self.assertEqual(self.r.parent, None) + self.assertQuerysetEqual(self.c.child_set.all(), []) + self.assertEqual(self.c.parent.id, self.r.id) + +class MultipleManyToOneRecursiveTests(TestCase): + + def setUp(self): + self.dad = Person(full_name='John Smith Senior', mother=None, father=None) + self.dad.save() + self.mom = Person(full_name='Jane Smith', mother=None, father=None) + self.mom.save() + self.kid = Person(full_name='John Smith Junior', mother=self.mom, father=self.dad) + self.kid.save() + + def test_m2o_recursive2(self): + self.assertEqual(self.kid.mother.id, self.mom.id) + self.assertEqual(self.kid.father.id, self.dad.id) + self.assertQuerysetEqual(self.dad.fathers_child_set.all(), + ['<Person: John Smith Junior>']) + self.assertQuerysetEqual(self.mom.mothers_child_set.all(), + ['<Person: John Smith Junior>']) + self.assertQuerysetEqual(self.kid.mothers_child_set.all(), []) + self.assertQuerysetEqual(self.kid.fathers_child_set.all(), []) diff --git a/parts/django/tests/modeltests/many_to_many/__init__.py b/parts/django/tests/modeltests/many_to_many/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/modeltests/many_to_many/__init__.py diff --git a/parts/django/tests/modeltests/many_to_many/models.py b/parts/django/tests/modeltests/many_to_many/models.py new file mode 100644 index 0000000..96636da --- /dev/null +++ b/parts/django/tests/modeltests/many_to_many/models.py @@ -0,0 +1,29 @@ +""" +5. Many-to-many relationships + +To define a many-to-many relationship, use ``ManyToManyField()``. + +In this example, an ``Article`` can be published in multiple ``Publication`` +objects, and a ``Publication`` has multiple ``Article`` objects. +""" + +from django.db import models + +class Publication(models.Model): + title = models.CharField(max_length=30) + + def __unicode__(self): + return self.title + + class Meta: + ordering = ('title',) + +class Article(models.Model): + headline = models.CharField(max_length=100) + publications = models.ManyToManyField(Publication) + + def __unicode__(self): + return self.headline + + class Meta: + ordering = ('headline',) diff --git a/parts/django/tests/modeltests/many_to_many/tests.py b/parts/django/tests/modeltests/many_to_many/tests.py new file mode 100644 index 0000000..39fe581 --- /dev/null +++ b/parts/django/tests/modeltests/many_to_many/tests.py @@ -0,0 +1,384 @@ +from django.test import TestCase +from models import Article, Publication + +class ManyToManyTests(TestCase): + + def setUp(self): + # Create a couple of Publications. + self.p1 = Publication.objects.create(id=None, title='The Python Journal') + self.p2 = Publication.objects.create(id=None, title='Science News') + self.p3 = Publication.objects.create(id=None, title='Science Weekly') + self.p4 = Publication.objects.create(title='Highlights for Children') + + self.a1 = Article.objects.create(id=None, headline='Django lets you build Web apps easily') + self.a1.publications.add(self.p1) + + self.a2 = Article.objects.create(id=None, headline='NASA uses Python') + self.a2.publications.add(self.p1, self.p2, self.p3, self.p4) + + self.a3 = Article.objects.create(headline='NASA finds intelligent life on Earth') + self.a3.publications.add(self.p2) + + self.a4 = Article.objects.create(headline='Oxygen-free diet works wonders') + self.a4.publications.add(self.p2) + + def test_add(self): + # Create an Article. + a5 = Article(id=None, headline='Django lets you reate Web apps easily') + # You can't associate it with a Publication until it's been saved. + self.assertRaises(ValueError, getattr, a5, 'publications') + # Save it! + a5.save() + # Associate the Article with a Publication. + a5.publications.add(self.p1) + self.assertQuerysetEqual(a5.publications.all(), + ['<Publication: The Python Journal>']) + # Create another Article, and set it to appear in both Publications. + a6 = Article(id=None, headline='ESA uses Python') + a6.save() + a6.publications.add(self.p1, self.p2) + a6.publications.add(self.p3) + # Adding a second time is OK + a6.publications.add(self.p3) + self.assertQuerysetEqual(a6.publications.all(), + [ + '<Publication: Science News>', + '<Publication: Science Weekly>', + '<Publication: The Python Journal>', + ]) + + # Adding an object of the wrong type raises TypeError + self.assertRaises(TypeError, a6.publications.add, a5) + # Add a Publication directly via publications.add by using keyword arguments. + p4 = a6.publications.create(title='Highlights for Adults') + self.assertQuerysetEqual(a6.publications.all(), + [ + '<Publication: Highlights for Adults>', + '<Publication: Science News>', + '<Publication: Science Weekly>', + '<Publication: The Python Journal>', + ]) + + def test_reverse_add(self): + # Adding via the 'other' end of an m2m + a5 = Article(headline='NASA finds intelligent life on Mars') + a5.save() + self.p2.article_set.add(a5) + self.assertQuerysetEqual(self.p2.article_set.all(), + [ + '<Article: NASA finds intelligent life on Earth>', + '<Article: NASA finds intelligent life on Mars>', + '<Article: NASA uses Python>', + '<Article: Oxygen-free diet works wonders>', + ]) + self.assertQuerysetEqual(a5.publications.all(), + ['<Publication: Science News>']) + + # Adding via the other end using keywords + new_article = self.p2.article_set.create(headline='Carbon-free diet works wonders') + self.assertQuerysetEqual( + self.p2.article_set.all(), + [ + '<Article: Carbon-free diet works wonders>', + '<Article: NASA finds intelligent life on Earth>', + '<Article: NASA finds intelligent life on Mars>', + '<Article: NASA uses Python>', + '<Article: Oxygen-free diet works wonders>', + ]) + a6 = self.p2.article_set.all()[3] + self.assertQuerysetEqual(a6.publications.all(), + [ + '<Publication: Highlights for Children>', + '<Publication: Science News>', + '<Publication: Science Weekly>', + '<Publication: The Python Journal>', + ]) + + def test_related_sets(self): + # Article objects have access to their related Publication objects. + self.assertQuerysetEqual(self.a1.publications.all(), + ['<Publication: The Python Journal>']) + self.assertQuerysetEqual(self.a2.publications.all(), + [ + '<Publication: Highlights for Children>', + '<Publication: Science News>', + '<Publication: Science Weekly>', + '<Publication: The Python Journal>', + ]) + # Publication objects have access to their related Article objects. + self.assertQuerysetEqual(self.p2.article_set.all(), + [ + '<Article: NASA finds intelligent life on Earth>', + '<Article: NASA uses Python>', + '<Article: Oxygen-free diet works wonders>', + ]) + self.assertQuerysetEqual(self.p1.article_set.all(), + [ + '<Article: Django lets you build Web apps easily>', + '<Article: NASA uses Python>', + ]) + self.assertQuerysetEqual(Publication.objects.get(id=self.p4.id).article_set.all(), + ['<Article: NASA uses Python>']) + + def test_selects(self): + # We can perform kwarg queries across m2m relationships + self.assertQuerysetEqual( + Article.objects.filter(publications__id__exact=self.p1.id), + [ + '<Article: Django lets you build Web apps easily>', + '<Article: NASA uses Python>', + ]) + self.assertQuerysetEqual( + Article.objects.filter(publications__pk=self.p1.id), + [ + '<Article: Django lets you build Web apps easily>', + '<Article: NASA uses Python>', + ]) + self.assertQuerysetEqual( + Article.objects.filter(publications=self.p1.id), + [ + '<Article: Django lets you build Web apps easily>', + '<Article: NASA uses Python>', + ]) + self.assertQuerysetEqual( + Article.objects.filter(publications=self.p1), + [ + '<Article: Django lets you build Web apps easily>', + '<Article: NASA uses Python>', + ]) + self.assertQuerysetEqual( + Article.objects.filter(publications__title__startswith="Science"), + [ + '<Article: NASA finds intelligent life on Earth>', + '<Article: NASA uses Python>', + '<Article: NASA uses Python>', + '<Article: Oxygen-free diet works wonders>', + ]) + self.assertQuerysetEqual( + Article.objects.filter(publications__title__startswith="Science").distinct(), + [ + '<Article: NASA finds intelligent life on Earth>', + '<Article: NASA uses Python>', + '<Article: Oxygen-free diet works wonders>', + ]) + + # The count() function respects distinct() as well. + self.assertEqual(Article.objects.filter(publications__title__startswith="Science").count(), 4) + self.assertEqual(Article.objects.filter(publications__title__startswith="Science").distinct().count(), 3) + self.assertQuerysetEqual( + Article.objects.filter(publications__in=[self.p1.id,self.p2.id]).distinct(), + [ + '<Article: Django lets you build Web apps easily>', + '<Article: NASA finds intelligent life on Earth>', + '<Article: NASA uses Python>', + '<Article: Oxygen-free diet works wonders>', + ]) + self.assertQuerysetEqual( + Article.objects.filter(publications__in=[self.p1.id,self.p2]).distinct(), + [ + '<Article: Django lets you build Web apps easily>', + '<Article: NASA finds intelligent life on Earth>', + '<Article: NASA uses Python>', + '<Article: Oxygen-free diet works wonders>', + ]) + self.assertQuerysetEqual( + Article.objects.filter(publications__in=[self.p1,self.p2]).distinct(), + [ + '<Article: Django lets you build Web apps easily>', + '<Article: NASA finds intelligent life on Earth>', + '<Article: NASA uses Python>', + '<Article: Oxygen-free diet works wonders>', + ]) + + # Excluding a related item works as you would expect, too (although the SQL + # involved is a little complex). + self.assertQuerysetEqual(Article.objects.exclude(publications=self.p2), + ['<Article: Django lets you build Web apps easily>']) + + def test_reverse_selects(self): + # Reverse m2m queries are supported (i.e., starting at the table that + # doesn't have a ManyToManyField). + self.assertQuerysetEqual(Publication.objects.filter(id__exact=self.p1.id), + ['<Publication: The Python Journal>']) + self.assertQuerysetEqual(Publication.objects.filter(pk=self.p1.id), + ['<Publication: The Python Journal>']) + self.assertQuerysetEqual( + Publication.objects.filter(article__headline__startswith="NASA"), + [ + '<Publication: Highlights for Children>', + '<Publication: Science News>', + '<Publication: Science News>', + '<Publication: Science Weekly>', + '<Publication: The Python Journal>', + ]) + self.assertQuerysetEqual(Publication.objects.filter(article__id__exact=self.a1.id), + ['<Publication: The Python Journal>']) + self.assertQuerysetEqual(Publication.objects.filter(article__pk=self.a1.id), + ['<Publication: The Python Journal>']) + self.assertQuerysetEqual(Publication.objects.filter(article=self.a1.id), + ['<Publication: The Python Journal>']) + self.assertQuerysetEqual(Publication.objects.filter(article=self.a1), + ['<Publication: The Python Journal>']) + + self.assertQuerysetEqual( + Publication.objects.filter(article__in=[self.a1.id,self.a2.id]).distinct(), + [ + '<Publication: Highlights for Children>', + '<Publication: Science News>', + '<Publication: Science Weekly>', + '<Publication: The Python Journal>', + ]) + self.assertQuerysetEqual( + Publication.objects.filter(article__in=[self.a1.id,self.a2]).distinct(), + [ + '<Publication: Highlights for Children>', + '<Publication: Science News>', + '<Publication: Science Weekly>', + '<Publication: The Python Journal>', + ]) + self.assertQuerysetEqual( + Publication.objects.filter(article__in=[self.a1,self.a2]).distinct(), + [ + '<Publication: Highlights for Children>', + '<Publication: Science News>', + '<Publication: Science Weekly>', + '<Publication: The Python Journal>', + ]) + + def test_delete(self): + # If we delete a Publication, its Articles won't be able to access it. + self.p1.delete() + self.assertQuerysetEqual(Publication.objects.all(), + [ + '<Publication: Highlights for Children>', + '<Publication: Science News>', + '<Publication: Science Weekly>', + ]) + self.assertQuerysetEqual(self.a1.publications.all(), []) + # If we delete an Article, its Publications won't be able to access it. + self.a2.delete() + self.assertQuerysetEqual(Article.objects.all(), + [ + '<Article: Django lets you build Web apps easily>', + '<Article: NASA finds intelligent life on Earth>', + '<Article: Oxygen-free diet works wonders>', + ]) + self.assertQuerysetEqual(self.p2.article_set.all(), + [ + '<Article: NASA finds intelligent life on Earth>', + '<Article: Oxygen-free diet works wonders>', + ]) + + def test_bulk_delete(self): + # Bulk delete some Publications - references to deleted publications should go + Publication.objects.filter(title__startswith='Science').delete() + self.assertQuerysetEqual(Publication.objects.all(), + [ + '<Publication: Highlights for Children>', + '<Publication: The Python Journal>', + ]) + self.assertQuerysetEqual(Article.objects.all(), + [ + '<Article: Django lets you build Web apps easily>', + '<Article: NASA finds intelligent life on Earth>', + '<Article: NASA uses Python>', + '<Article: Oxygen-free diet works wonders>', + ]) + self.assertQuerysetEqual(self.a2.publications.all(), + [ + '<Publication: Highlights for Children>', + '<Publication: The Python Journal>', + ]) + + # Bulk delete some articles - references to deleted objects should go + q = Article.objects.filter(headline__startswith='Django') + self.assertQuerysetEqual(q, ['<Article: Django lets you build Web apps easily>']) + q.delete() + # After the delete, the QuerySet cache needs to be cleared, + # and the referenced objects should be gone + self.assertQuerysetEqual(q, []) + self.assertQuerysetEqual(self.p1.article_set.all(), + ['<Article: NASA uses Python>']) + + def test_remove(self): + # Removing publication from an article: + self.assertQuerysetEqual(self.p2.article_set.all(), + [ + '<Article: NASA finds intelligent life on Earth>', + '<Article: NASA uses Python>', + '<Article: Oxygen-free diet works wonders>', + ]) + self.a4.publications.remove(self.p2) + self.assertQuerysetEqual(self.p2.article_set.all(), + [ + '<Article: NASA finds intelligent life on Earth>', + '<Article: NASA uses Python>', + ]) + self.assertQuerysetEqual(self.a4.publications.all(), []) + # And from the other end + self.p2.article_set.remove(self.a3) + self.assertQuerysetEqual(self.p2.article_set.all(), + [ + '<Article: NASA uses Python>', + ]) + self.assertQuerysetEqual(self.a3.publications.all(), []) + + def test_assign(self): + # Relation sets can be assigned. Assignment clears any existing set members + self.p2.article_set = [self.a4, self.a3] + self.assertQuerysetEqual(self.p2.article_set.all(), + [ + '<Article: NASA finds intelligent life on Earth>', + '<Article: Oxygen-free diet works wonders>', + ]) + self.assertQuerysetEqual(self.a4.publications.all(), + ['<Publication: Science News>']) + self.a4.publications = [self.p3.id] + self.assertQuerysetEqual(self.p2.article_set.all(), + ['<Article: NASA finds intelligent life on Earth>']) + self.assertQuerysetEqual(self.a4.publications.all(), + ['<Publication: Science Weekly>']) + + # An alternate to calling clear() is to assign the empty set + self.p2.article_set = [] + self.assertQuerysetEqual(self.p2.article_set.all(), []) + self.a4.publications = [] + self.assertQuerysetEqual(self.a4.publications.all(), []) + + def test_assign_ids(self): + # Relation sets can also be set using primary key values + self.p2.article_set = [self.a4.id, self.a3.id] + self.assertQuerysetEqual(self.p2.article_set.all(), + [ + '<Article: NASA finds intelligent life on Earth>', + '<Article: Oxygen-free diet works wonders>', + ]) + self.assertQuerysetEqual(self.a4.publications.all(), + ['<Publication: Science News>']) + self.a4.publications = [self.p3.id] + self.assertQuerysetEqual(self.p2.article_set.all(), + ['<Article: NASA finds intelligent life on Earth>']) + self.assertQuerysetEqual(self.a4.publications.all(), + ['<Publication: Science Weekly>']) + + def test_clear(self): + # Relation sets can be cleared: + self.p2.article_set.clear() + self.assertQuerysetEqual(self.p2.article_set.all(), []) + self.assertQuerysetEqual(self.a4.publications.all(), []) + + # And you can clear from the other end + self.p2.article_set.add(self.a3, self.a4) + self.assertQuerysetEqual(self.p2.article_set.all(), + [ + '<Article: NASA finds intelligent life on Earth>', + '<Article: Oxygen-free diet works wonders>', + ]) + self.assertQuerysetEqual(self.a4.publications.all(), + [ + '<Publication: Science News>', + ]) + self.a4.publications.clear() + self.assertQuerysetEqual(self.a4.publications.all(), []) + self.assertQuerysetEqual(self.p2.article_set.all(), + ['<Article: NASA finds intelligent life on Earth>']) diff --git a/parts/django/tests/modeltests/many_to_one/__init__.py b/parts/django/tests/modeltests/many_to_one/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/modeltests/many_to_one/__init__.py diff --git a/parts/django/tests/modeltests/many_to_one/models.py b/parts/django/tests/modeltests/many_to_one/models.py new file mode 100644 index 0000000..b4a0f37 --- /dev/null +++ b/parts/django/tests/modeltests/many_to_one/models.py @@ -0,0 +1,26 @@ +""" +4. Many-to-one relationships + +To define a many-to-one relationship, use ``ForeignKey()``. +""" + +from django.db import models + +class Reporter(models.Model): + first_name = models.CharField(max_length=30) + last_name = models.CharField(max_length=30) + email = models.EmailField() + + def __unicode__(self): + return u"%s %s" % (self.first_name, self.last_name) + +class Article(models.Model): + headline = models.CharField(max_length=100) + pub_date = models.DateField() + reporter = models.ForeignKey(Reporter) + + def __unicode__(self): + return self.headline + + class Meta: + ordering = ('headline',) diff --git a/parts/django/tests/modeltests/many_to_one/tests.py b/parts/django/tests/modeltests/many_to_one/tests.py new file mode 100644 index 0000000..53306b7 --- /dev/null +++ b/parts/django/tests/modeltests/many_to_one/tests.py @@ -0,0 +1,371 @@ +from datetime import datetime +from django.test import TestCase +from django.core.exceptions import FieldError +from models import Article, Reporter + +class ManyToOneTests(TestCase): + + def setUp(self): + # Create a few Reporters. + self.r = Reporter(first_name='John', last_name='Smith', email='john@example.com') + self.r.save() + self.r2 = Reporter(first_name='Paul', last_name='Jones', email='paul@example.com') + self.r2.save() + # Create an Article. + self.a = Article(id=None, headline="This is a test", + pub_date=datetime(2005, 7, 27), reporter=self.r) + self.a.save() + + def test_get(self): + # Article objects have access to their related Reporter objects. + r = self.a.reporter + self.assertEqual(r.id, self.r.id) + # These are strings instead of unicode strings because that's what was used in + # the creation of this reporter (and we haven't refreshed the data from the + # database, which always returns unicode strings). + self.assertEqual((r.first_name, self.r.last_name), ('John', 'Smith')) + + def test_create(self): + # You can also instantiate an Article by passing the Reporter's ID + # instead of a Reporter object. + a3 = Article(id=None, headline="Third article", + pub_date=datetime(2005, 7, 27), reporter_id=self.r.id) + a3.save() + self.assertEqual(a3.reporter.id, self.r.id) + + # Similarly, the reporter ID can be a string. + a4 = Article(id=None, headline="Fourth article", + pub_date=datetime(2005, 7, 27), reporter_id=str(self.r.id)) + a4.save() + self.assertEqual(repr(a4.reporter), "<Reporter: John Smith>") + + def test_add(self): + # Create an Article via the Reporter object. + new_article = self.r.article_set.create(headline="John's second story", + pub_date=datetime(2005, 7, 29)) + self.assertEqual(repr(new_article), "<Article: John's second story>") + self.assertEqual(new_article.reporter.id, self.r.id) + + # Create a new article, and add it to the article set. + new_article2 = Article(headline="Paul's story", pub_date=datetime(2006, 1, 17)) + self.r.article_set.add(new_article2) + self.assertEqual(new_article2.reporter.id, self.r.id) + self.assertQuerysetEqual(self.r.article_set.all(), + [ + "<Article: John's second story>", + "<Article: Paul's story>", + "<Article: This is a test>", + ]) + + # Add the same article to a different article set - check that it moves. + self.r2.article_set.add(new_article2) + self.assertEqual(new_article2.reporter.id, self.r2.id) + self.assertQuerysetEqual(self.r2.article_set.all(), ["<Article: Paul's story>"]) + + # Adding an object of the wrong type raises TypeError. + self.assertRaises(TypeError, self.r.article_set.add, self.r2) + self.assertQuerysetEqual(self.r.article_set.all(), + [ + "<Article: John's second story>", + "<Article: This is a test>", + ]) + + def test_assign(self): + new_article = self.r.article_set.create(headline="John's second story", + pub_date=datetime(2005, 7, 29)) + new_article2 = self.r2.article_set.create(headline="Paul's story", + pub_date=datetime(2006, 1, 17)) + # Assign the article to the reporter directly using the descriptor. + new_article2.reporter = self.r + new_article2.save() + self.assertEqual(repr(new_article2.reporter), "<Reporter: John Smith>") + self.assertEqual(new_article2.reporter.id, self.r.id) + self.assertQuerysetEqual(self.r.article_set.all(), [ + "<Article: John's second story>", + "<Article: Paul's story>", + "<Article: This is a test>", + ]) + self.assertQuerysetEqual(self.r2.article_set.all(), []) + # Set the article back again using set descriptor. + self.r2.article_set = [new_article, new_article2] + self.assertQuerysetEqual(self.r.article_set.all(), ["<Article: This is a test>"]) + self.assertQuerysetEqual(self.r2.article_set.all(), + [ + "<Article: John's second story>", + "<Article: Paul's story>", + ]) + + # Funny case - assignment notation can only go so far; because the + # ForeignKey cannot be null, existing members of the set must remain. + self.r.article_set = [new_article] + self.assertQuerysetEqual(self.r.article_set.all(), + [ + "<Article: John's second story>", + "<Article: This is a test>", + ]) + self.assertQuerysetEqual(self.r2.article_set.all(), ["<Article: Paul's story>"]) + # Reporter cannot be null - there should not be a clear or remove method + self.assertFalse(hasattr(self.r2.article_set, 'remove')) + self.assertFalse(hasattr(self.r2.article_set, 'clear')) + + def test_selects(self): + new_article = self.r.article_set.create(headline="John's second story", + pub_date=datetime(2005, 7, 29)) + new_article2 = self.r2.article_set.create(headline="Paul's story", + pub_date=datetime(2006, 1, 17)) + # Reporter objects have access to their related Article objects. + self.assertQuerysetEqual(self.r.article_set.all(), [ + "<Article: John's second story>", + "<Article: This is a test>", + ]) + self.assertQuerysetEqual(self.r.article_set.filter(headline__startswith='This'), + ["<Article: This is a test>"]) + self.assertEqual(self.r.article_set.count(), 2) + self.assertEqual(self.r2.article_set.count(), 1) + # Get articles by id + self.assertQuerysetEqual(Article.objects.filter(id__exact=self.a.id), + ["<Article: This is a test>"]) + self.assertQuerysetEqual(Article.objects.filter(pk=self.a.id), + ["<Article: This is a test>"]) + # Query on an article property + self.assertQuerysetEqual(Article.objects.filter(headline__startswith='This'), + ["<Article: This is a test>"]) + # The API automatically follows relationships as far as you need. + # Use double underscores to separate relationships. + # This works as many levels deep as you want. There's no limit. + # Find all Articles for any Reporter whose first name is "John". + self.assertQuerysetEqual(Article.objects.filter(reporter__first_name__exact='John'), + [ + "<Article: John's second story>", + "<Article: This is a test>", + ]) + # Check that implied __exact also works + self.assertQuerysetEqual(Article.objects.filter(reporter__first_name='John'), + [ + "<Article: John's second story>", + "<Article: This is a test>", + ]) + # Query twice over the related field. + self.assertQuerysetEqual( + Article.objects.filter(reporter__first_name__exact='John', + reporter__last_name__exact='Smith'), + [ + "<Article: John's second story>", + "<Article: This is a test>", + ]) + # The underlying query only makes one join when a related table is referenced twice. + queryset = Article.objects.filter(reporter__first_name__exact='John', + reporter__last_name__exact='Smith') + self.assertEqual(queryset.query.get_compiler(queryset.db).as_sql()[0].count('INNER JOIN'), 1) + + # The automatically joined table has a predictable name. + self.assertQuerysetEqual( + Article.objects.filter(reporter__first_name__exact='John').extra( + where=["many_to_one_reporter.last_name='Smith'"]), + [ + "<Article: John's second story>", + "<Article: This is a test>", + ]) + # ... and should work fine with the unicode that comes out of forms.Form.cleaned_data + self.assertQuerysetEqual( + Article.objects.filter(reporter__first_name__exact='John' + ).extra(where=["many_to_one_reporter.last_name='%s'" % u'Smith']), + [ + "<Article: John's second story>", + "<Article: This is a test>", + ]) + # Find all Articles for a Reporter. + # Use direct ID check, pk check, and object comparison + self.assertQuerysetEqual( + Article.objects.filter(reporter__id__exact=self.r.id), + [ + "<Article: John's second story>", + "<Article: This is a test>", + ]) + self.assertQuerysetEqual( + Article.objects.filter(reporter__pk=self.r.id), + [ + "<Article: John's second story>", + "<Article: This is a test>", + ]) + self.assertQuerysetEqual( + Article.objects.filter(reporter=self.r.id), + [ + "<Article: John's second story>", + "<Article: This is a test>", + ]) + self.assertQuerysetEqual( + Article.objects.filter(reporter=self.r), + [ + "<Article: John's second story>", + "<Article: This is a test>", + ]) + self.assertQuerysetEqual( + Article.objects.filter(reporter__in=[self.r.id,self.r2.id]).distinct(), + [ + "<Article: John's second story>", + "<Article: Paul's story>", + "<Article: This is a test>", + ]) + self.assertQuerysetEqual( + Article.objects.filter(reporter__in=[self.r,self.r2]).distinct(), + [ + "<Article: John's second story>", + "<Article: Paul's story>", + "<Article: This is a test>", + ]) + # You can also use a queryset instead of a literal list of instances. + # The queryset must be reduced to a list of values using values(), + # then converted into a query + self.assertQuerysetEqual( + Article.objects.filter( + reporter__in=Reporter.objects.filter(first_name='John').values('pk').query + ).distinct(), + [ + "<Article: John's second story>", + "<Article: This is a test>", + ]) + # You need two underscores between "reporter" and "id" -- not one. + self.assertRaises(FieldError, Article.objects.filter, reporter_id__exact=self.r.id) + # You need to specify a comparison clause + self.assertRaises(FieldError, Article.objects.filter, reporter_id=self.r.id) + + def test_reverse_selects(self): + a3 = Article.objects.create(id=None, headline="Third article", + pub_date=datetime(2005, 7, 27), reporter_id=self.r.id) + a4 = Article.objects.create(id=None, headline="Fourth article", + pub_date=datetime(2005, 7, 27), reporter_id=str(self.r.id)) + # Reporters can be queried + self.assertQuerysetEqual(Reporter.objects.filter(id__exact=self.r.id), + ["<Reporter: John Smith>"]) + self.assertQuerysetEqual(Reporter.objects.filter(pk=self.r.id), + ["<Reporter: John Smith>"]) + self.assertQuerysetEqual(Reporter.objects.filter(first_name__startswith='John'), + ["<Reporter: John Smith>"]) + # Reporters can query in opposite direction of ForeignKey definition + self.assertQuerysetEqual(Reporter.objects.filter(article__id__exact=self.a.id), + ["<Reporter: John Smith>"]) + self.assertQuerysetEqual(Reporter.objects.filter(article__pk=self.a.id), + ["<Reporter: John Smith>"]) + self.assertQuerysetEqual(Reporter.objects.filter(article=self.a.id), + ["<Reporter: John Smith>"]) + self.assertQuerysetEqual(Reporter.objects.filter(article=self.a), + ["<Reporter: John Smith>"]) + self.assertQuerysetEqual( + Reporter.objects.filter(article__in=[self.a.id,a3.id]).distinct(), + ["<Reporter: John Smith>"]) + self.assertQuerysetEqual( + Reporter.objects.filter(article__in=[self.a.id,a3]).distinct(), + ["<Reporter: John Smith>"]) + self.assertQuerysetEqual( + Reporter.objects.filter(article__in=[self.a,a3]).distinct(), + ["<Reporter: John Smith>"]) + self.assertQuerysetEqual( + Reporter.objects.filter(article__headline__startswith='T'), + ["<Reporter: John Smith>", "<Reporter: John Smith>"]) + self.assertQuerysetEqual( + Reporter.objects.filter(article__headline__startswith='T').distinct(), + ["<Reporter: John Smith>"]) + + # Counting in the opposite direction works in conjunction with distinct() + self.assertEqual( + Reporter.objects.filter(article__headline__startswith='T').count(), 2) + self.assertEqual( + Reporter.objects.filter(article__headline__startswith='T').distinct().count(), 1) + + # Queries can go round in circles. + self.assertQuerysetEqual( + Reporter.objects.filter(article__reporter__first_name__startswith='John'), + [ + "<Reporter: John Smith>", + "<Reporter: John Smith>", + "<Reporter: John Smith>", + ]) + self.assertQuerysetEqual( + Reporter.objects.filter(article__reporter__first_name__startswith='John').distinct(), + ["<Reporter: John Smith>"]) + self.assertQuerysetEqual( + Reporter.objects.filter(article__reporter__exact=self.r).distinct(), + ["<Reporter: John Smith>"]) + + # Check that implied __exact also works. + self.assertQuerysetEqual( + Reporter.objects.filter(article__reporter=self.r).distinct(), + ["<Reporter: John Smith>"]) + + # It's possible to use values() calls across many-to-one relations. + # (Note, too, that we clear the ordering here so as not to drag the + # 'headline' field into the columns being used to determine uniqueness) + d = {'reporter__first_name': u'John', 'reporter__last_name': u'Smith'} + self.assertEqual([d], + list(Article.objects.filter(reporter=self.r).distinct().order_by() + .values('reporter__first_name', 'reporter__last_name'))) + + def test_select_related(self): + # Check that Article.objects.select_related().dates() works properly when + # there are multiple Articles with the same date but different foreign-key + # objects (Reporters). + r1 = Reporter.objects.create(first_name='Mike', last_name='Royko', email='royko@suntimes.com') + r2 = Reporter.objects.create(first_name='John', last_name='Kass', email='jkass@tribune.com') + a1 = Article.objects.create(headline='First', pub_date=datetime(1980, 4, 23), reporter=r1) + a2 = Article.objects.create(headline='Second', pub_date=datetime(1980, 4, 23), reporter=r2) + self.assertEqual(list(Article.objects.select_related().dates('pub_date', 'day')), + [ + datetime(1980, 4, 23, 0, 0), + datetime(2005, 7, 27, 0, 0), + ]) + self.assertEqual(list(Article.objects.select_related().dates('pub_date', 'month')), + [ + datetime(1980, 4, 1, 0, 0), + datetime(2005, 7, 1, 0, 0), + ]) + self.assertEqual(list(Article.objects.select_related().dates('pub_date', 'year')), + [ + datetime(1980, 1, 1, 0, 0), + datetime(2005, 1, 1, 0, 0), + ]) + + def test_delete(self): + new_article = self.r.article_set.create(headline="John's second story", + pub_date=datetime(2005, 7, 29)) + new_article2 = self.r2.article_set.create(headline="Paul's story", + pub_date=datetime(2006, 1, 17)) + a3 = Article.objects.create(id=None, headline="Third article", + pub_date=datetime(2005, 7, 27), reporter_id=self.r.id) + a4 = Article.objects.create(id=None, headline="Fourth article", + pub_date=datetime(2005, 7, 27), reporter_id=str(self.r.id)) + # If you delete a reporter, his articles will be deleted. + self.assertQuerysetEqual(Article.objects.all(), + [ + "<Article: Fourth article>", + "<Article: John's second story>", + "<Article: Paul's story>", + "<Article: Third article>", + "<Article: This is a test>", + ]) + self.assertQuerysetEqual(Reporter.objects.order_by('first_name'), + [ + "<Reporter: John Smith>", + "<Reporter: Paul Jones>", + ]) + self.r2.delete() + self.assertQuerysetEqual(Article.objects.all(), + [ + "<Article: Fourth article>", + "<Article: John's second story>", + "<Article: Third article>", + "<Article: This is a test>", + ]) + self.assertQuerysetEqual(Reporter.objects.order_by('first_name'), + ["<Reporter: John Smith>"]) + # You can delete using a JOIN in the query. + Reporter.objects.filter(article__headline__startswith='This').delete() + self.assertQuerysetEqual(Reporter.objects.all(), []) + self.assertQuerysetEqual(Article.objects.all(), []) + + def test_regression_12876(self): + # Regression for #12876 -- Model methods that include queries that + # recursive don't cause recursion depth problems under deepcopy. + self.r.cached_query = Article.objects.filter(reporter=self.r) + from copy import deepcopy + self.assertEqual(repr(deepcopy(self.r)), "<Reporter: John Smith>") diff --git a/parts/django/tests/modeltests/many_to_one_null/__init__.py b/parts/django/tests/modeltests/many_to_one_null/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/modeltests/many_to_one_null/__init__.py diff --git a/parts/django/tests/modeltests/many_to_one_null/models.py b/parts/django/tests/modeltests/many_to_one_null/models.py new file mode 100644 index 0000000..5f824b4 --- /dev/null +++ b/parts/django/tests/modeltests/many_to_one_null/models.py @@ -0,0 +1,24 @@ +""" +16. Many-to-one relationships that can be null + +To define a many-to-one relationship that can have a null foreign key, use +``ForeignKey()`` with ``null=True`` . +""" + +from django.db import models + +class Reporter(models.Model): + name = models.CharField(max_length=30) + + def __unicode__(self): + return self.name + +class Article(models.Model): + headline = models.CharField(max_length=100) + reporter = models.ForeignKey(Reporter, null=True) + + class Meta: + ordering = ('headline',) + + def __unicode__(self): + return self.headline diff --git a/parts/django/tests/modeltests/many_to_one_null/tests.py b/parts/django/tests/modeltests/many_to_one_null/tests.py new file mode 100644 index 0000000..c78f980 --- /dev/null +++ b/parts/django/tests/modeltests/many_to_one_null/tests.py @@ -0,0 +1,84 @@ +from django.test import TestCase +from models import Reporter, Article + +class ManyToOneNullTests(TestCase): + + def setUp(self): + # Create a Reporter. + self.r = Reporter(name='John Smith') + self.r.save() + # Create an Article. + self.a = Article(headline="First", reporter=self.r) + self.a.save() + # Create an Article via the Reporter object. + self.a2 = self.r.article_set.create(headline="Second") + # Create an Article with no Reporter by passing "reporter=None". + self.a3 = Article(headline="Third", reporter=None) + self.a3.save() + # Create another article and reporter + self.r2 = Reporter(name='Paul Jones') + self.r2.save() + self.a4 = self.r2.article_set.create(headline='Fourth') + + def test_get_related(self): + self.assertEqual(self.a.reporter.id, self.r.id) + # Article objects have access to their related Reporter objects. + r = self.a.reporter + self.assertEqual(r.id, self.r.id) + + def test_created_via_related_set(self): + self.assertEqual(self.a2.reporter.id, self.r.id) + + def test_related_set(self): + # Reporter objects have access to their related Article objects. + self.assertQuerysetEqual(self.r.article_set.all(), + ['<Article: First>', '<Article: Second>']) + self.assertQuerysetEqual(self.r.article_set.filter(headline__startswith='Fir'), + ['<Article: First>']) + self.assertEqual(self.r.article_set.count(), 2) + + def test_created_without_related(self): + self.assertEqual(self.a3.reporter, None) + # Need to reget a3 to refresh the cache + a3 = Article.objects.get(pk=self.a3.pk) + self.assertRaises(AttributeError, getattr, a3.reporter, 'id') + # Accessing an article's 'reporter' attribute returns None + # if the reporter is set to None. + self.assertEqual(a3.reporter, None) + # To retrieve the articles with no reporters set, use "reporter__isnull=True". + self.assertQuerysetEqual(Article.objects.filter(reporter__isnull=True), + ['<Article: Third>']) + # We can achieve the same thing by filtering for the case where the + # reporter is None. + self.assertQuerysetEqual(Article.objects.filter(reporter=None), + ['<Article: Third>']) + # Set the reporter for the Third article + self.assertQuerysetEqual(self.r.article_set.all(), + ['<Article: First>', '<Article: Second>']) + self.r.article_set.add(a3) + self.assertQuerysetEqual(self.r.article_set.all(), + ['<Article: First>', '<Article: Second>', '<Article: Third>']) + # Remove an article from the set, and check that it was removed. + self.r.article_set.remove(a3) + self.assertQuerysetEqual(self.r.article_set.all(), + ['<Article: First>', '<Article: Second>']) + self.assertQuerysetEqual(Article.objects.filter(reporter__isnull=True), + ['<Article: Third>']) + + def test_remove_from_wrong_set(self): + self.assertQuerysetEqual(self.r2.article_set.all(), ['<Article: Fourth>']) + # Try to remove a4 from a set it does not belong to + self.assertRaises(Reporter.DoesNotExist, self.r.article_set.remove, self.a4) + self.assertQuerysetEqual(self.r2.article_set.all(), ['<Article: Fourth>']) + + def test_assign_clear_related_set(self): + # Use descriptor assignment to allocate ForeignKey. Null is legal, so + # existing members of set that are not in the assignment set are set null + self.r2.article_set = [self.a2, self.a3] + self.assertQuerysetEqual(self.r2.article_set.all(), + ['<Article: Second>', '<Article: Third>']) + # Clear the rest of the set + self.r.article_set.clear() + self.assertQuerysetEqual(self.r.article_set.all(), []) + self.assertQuerysetEqual(Article.objects.filter(reporter__isnull=True), + ['<Article: First>', '<Article: Fourth>']) diff --git a/parts/django/tests/modeltests/model_forms/__init__.py b/parts/django/tests/modeltests/model_forms/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/modeltests/model_forms/__init__.py diff --git a/parts/django/tests/modeltests/model_forms/mforms.py b/parts/django/tests/modeltests/model_forms/mforms.py new file mode 100644 index 0000000..aef763e --- /dev/null +++ b/parts/django/tests/modeltests/model_forms/mforms.py @@ -0,0 +1,39 @@ +from django import forms +from django.forms import ModelForm + +from models import Product, Price, Book, DerivedBook, ExplicitPK, Post, DerivedPost, Writer + +class ProductForm(ModelForm): + class Meta: + model = Product + +class PriceForm(ModelForm): + class Meta: + model = Price + +class BookForm(ModelForm): + class Meta: + model = Book + +class DerivedBookForm(ModelForm): + class Meta: + model = DerivedBook + +class ExplicitPKForm(ModelForm): + class Meta: + model = ExplicitPK + fields = ('key', 'desc',) + +class PostForm(ModelForm): + class Meta: + model = Post + +class DerivedPostForm(ModelForm): + class Meta: + model = DerivedPost + +class CustomWriterForm(ModelForm): + name = forms.CharField(required=False) + + class Meta: + model = Writer diff --git a/parts/django/tests/modeltests/model_forms/models.py b/parts/django/tests/modeltests/model_forms/models.py new file mode 100644 index 0000000..1087cf8 --- /dev/null +++ b/parts/django/tests/modeltests/model_forms/models.py @@ -0,0 +1,1575 @@ +""" +XX. Generating HTML forms from models + +This is mostly just a reworking of the ``form_for_model``/``form_for_instance`` +tests to use ``ModelForm``. As such, the text may not make sense in all cases, +and the examples are probably a poor fit for the ``ModelForm`` syntax. In other +words, most of these tests should be rewritten. +""" + +import os +import tempfile + +from django.db import models +from django.core.files.storage import FileSystemStorage + +temp_storage_dir = tempfile.mkdtemp() +temp_storage = FileSystemStorage(temp_storage_dir) + +ARTICLE_STATUS = ( + (1, 'Draft'), + (2, 'Pending'), + (3, 'Live'), +) + +ARTICLE_STATUS_CHAR = ( + ('d', 'Draft'), + ('p', 'Pending'), + ('l', 'Live'), +) + +class Category(models.Model): + name = models.CharField(max_length=20) + slug = models.SlugField(max_length=20) + url = models.CharField('The URL', max_length=40) + + def __unicode__(self): + return self.name + +class Writer(models.Model): + name = models.CharField(max_length=50, help_text='Use both first and last names.') + + def __unicode__(self): + return self.name + +class Article(models.Model): + headline = models.CharField(max_length=50) + slug = models.SlugField() + pub_date = models.DateField() + created = models.DateField(editable=False) + writer = models.ForeignKey(Writer) + article = models.TextField() + categories = models.ManyToManyField(Category, blank=True) + status = models.PositiveIntegerField(choices=ARTICLE_STATUS, blank=True, null=True) + + def save(self): + import datetime + if not self.id: + self.created = datetime.date.today() + return super(Article, self).save() + + def __unicode__(self): + return self.headline + +class ImprovedArticle(models.Model): + article = models.OneToOneField(Article) + +class ImprovedArticleWithParentLink(models.Model): + article = models.OneToOneField(Article, parent_link=True) + +class BetterWriter(Writer): + score = models.IntegerField() + +class WriterProfile(models.Model): + writer = models.OneToOneField(Writer, primary_key=True) + age = models.PositiveIntegerField() + + def __unicode__(self): + return "%s is %s" % (self.writer, self.age) + +from django.contrib.localflavor.us.models import PhoneNumberField +class PhoneNumber(models.Model): + phone = PhoneNumberField() + description = models.CharField(max_length=20) + + def __unicode__(self): + return self.phone + +class TextFile(models.Model): + description = models.CharField(max_length=20) + file = models.FileField(storage=temp_storage, upload_to='tests', max_length=15) + + def __unicode__(self): + return self.description + +try: + # If PIL is available, try testing ImageFields. Checking for the existence + # of Image is enough for CPython, but for PyPy, you need to check for the + # underlying modules If PIL is not available, ImageField tests are omitted. + # Try to import PIL in either of the two ways it can end up installed. + try: + from PIL import Image, _imaging + except ImportError: + import Image, _imaging + + test_images = True + + class ImageFile(models.Model): + def custom_upload_path(self, filename): + path = self.path or 'tests' + return '%s/%s' % (path, filename) + + description = models.CharField(max_length=20) + + # Deliberately put the image field *after* the width/height fields to + # trigger the bug in #10404 with width/height not getting assigned. + width = models.IntegerField(editable=False) + height = models.IntegerField(editable=False) + image = models.ImageField(storage=temp_storage, upload_to=custom_upload_path, + width_field='width', height_field='height') + path = models.CharField(max_length=16, blank=True, default='') + + def __unicode__(self): + return self.description + + class OptionalImageFile(models.Model): + def custom_upload_path(self, filename): + path = self.path or 'tests' + return '%s/%s' % (path, filename) + + description = models.CharField(max_length=20) + image = models.ImageField(storage=temp_storage, upload_to=custom_upload_path, + width_field='width', height_field='height', + blank=True, null=True) + width = models.IntegerField(editable=False, null=True) + height = models.IntegerField(editable=False, null=True) + path = models.CharField(max_length=16, blank=True, default='') + + def __unicode__(self): + return self.description +except ImportError: + test_images = False + +class CommaSeparatedInteger(models.Model): + field = models.CommaSeparatedIntegerField(max_length=20) + + def __unicode__(self): + return self.field + +class Product(models.Model): + slug = models.SlugField(unique=True) + + def __unicode__(self): + return self.slug + +class Price(models.Model): + price = models.DecimalField(max_digits=10, decimal_places=2) + quantity = models.PositiveIntegerField() + + def __unicode__(self): + return u"%s for %s" % (self.quantity, self.price) + + class Meta: + unique_together = (('price', 'quantity'),) + +class ArticleStatus(models.Model): + status = models.CharField(max_length=2, choices=ARTICLE_STATUS_CHAR, blank=True, null=True) + +class Inventory(models.Model): + barcode = models.PositiveIntegerField(unique=True) + parent = models.ForeignKey('self', to_field='barcode', blank=True, null=True) + name = models.CharField(blank=False, max_length=20) + + def __unicode__(self): + return self.name + +class Book(models.Model): + title = models.CharField(max_length=40) + author = models.ForeignKey(Writer, blank=True, null=True) + special_id = models.IntegerField(blank=True, null=True, unique=True) + + class Meta: + unique_together = ('title', 'author') + +class BookXtra(models.Model): + isbn = models.CharField(max_length=16, unique=True) + suffix1 = models.IntegerField(blank=True, default=0) + suffix2 = models.IntegerField(blank=True, default=0) + + class Meta: + unique_together = (('suffix1', 'suffix2')) + abstract = True + +class DerivedBook(Book, BookXtra): + pass + +class ExplicitPK(models.Model): + key = models.CharField(max_length=20, primary_key=True) + desc = models.CharField(max_length=20, blank=True, unique=True) + class Meta: + unique_together = ('key', 'desc') + + def __unicode__(self): + return self.key + +class Post(models.Model): + title = models.CharField(max_length=50, unique_for_date='posted', blank=True) + slug = models.CharField(max_length=50, unique_for_year='posted', blank=True) + subtitle = models.CharField(max_length=50, unique_for_month='posted', blank=True) + posted = models.DateField() + + def __unicode__(self): + return self.name + +class DerivedPost(Post): + pass + +class BigInt(models.Model): + biggie = models.BigIntegerField() + + def __unicode__(self): + return unicode(self.biggie) + +class MarkupField(models.CharField): + def __init__(self, *args, **kwargs): + kwargs["max_length"] = 20 + super(MarkupField, self).__init__(*args, **kwargs) + + def formfield(self, **kwargs): + # don't allow this field to be used in form (real use-case might be + # that you know the markup will always be X, but it is among an app + # that allows the user to say it could be something else) + # regressed at r10062 + return None + +class CustomFieldForExclusionModel(models.Model): + name = models.CharField(max_length=10) + markup = MarkupField() + +__test__ = {'API_TESTS': """ +>>> from django import forms +>>> from django.forms.models import ModelForm, model_to_dict +>>> from django.core.files.uploadedfile import SimpleUploadedFile + +The bare bones, absolutely nothing custom, basic case. + +>>> class CategoryForm(ModelForm): +... class Meta: +... model = Category +>>> CategoryForm.base_fields.keys() +['name', 'slug', 'url'] + + +Extra fields. + +>>> class CategoryForm(ModelForm): +... some_extra_field = forms.BooleanField() +... +... class Meta: +... model = Category + +>>> CategoryForm.base_fields.keys() +['name', 'slug', 'url', 'some_extra_field'] + +Extra field that has a name collision with a related object accessor. + +>>> class WriterForm(ModelForm): +... book = forms.CharField(required=False) +... +... class Meta: +... model = Writer + +>>> wf = WriterForm({'name': 'Richard Lockridge'}) +>>> wf.is_valid() +True + +Replacing a field. + +>>> class CategoryForm(ModelForm): +... url = forms.BooleanField() +... +... class Meta: +... model = Category + +>>> CategoryForm.base_fields['url'].__class__ +<class 'django.forms.fields.BooleanField'> + + +Using 'fields'. + +>>> class CategoryForm(ModelForm): +... +... class Meta: +... model = Category +... fields = ['url'] + +>>> CategoryForm.base_fields.keys() +['url'] + + +Using 'exclude' + +>>> class CategoryForm(ModelForm): +... +... class Meta: +... model = Category +... exclude = ['url'] + +>>> CategoryForm.base_fields.keys() +['name', 'slug'] + + +Using 'fields' *and* 'exclude'. Not sure why you'd want to do this, but uh, +"be liberal in what you accept" and all. + +>>> class CategoryForm(ModelForm): +... +... class Meta: +... model = Category +... fields = ['name', 'url'] +... exclude = ['url'] + +>>> CategoryForm.base_fields.keys() +['name'] + +Using 'widgets' + +>>> class CategoryForm(ModelForm): +... +... class Meta: +... model = Category +... fields = ['name', 'url', 'slug'] +... widgets = { +... 'name': forms.Textarea, +... 'url': forms.TextInput(attrs={'class': 'url'}) +... } + +>>> str(CategoryForm()['name']) +'<textarea id="id_name" rows="10" cols="40" name="name"></textarea>' + +>>> str(CategoryForm()['url']) +'<input id="id_url" type="text" class="url" name="url" maxlength="40" />' + +>>> str(CategoryForm()['slug']) +'<input id="id_slug" type="text" name="slug" maxlength="20" />' + +Don't allow more than one 'model' definition in the inheritance hierarchy. +Technically, it would generate a valid form, but the fact that the resulting +save method won't deal with multiple objects is likely to trip up people not +familiar with the mechanics. + +>>> class CategoryForm(ModelForm): +... class Meta: +... model = Category + +>>> class OddForm(CategoryForm): +... class Meta: +... model = Article + +OddForm is now an Article-related thing, because BadForm.Meta overrides +CategoryForm.Meta. +>>> OddForm.base_fields.keys() +['headline', 'slug', 'pub_date', 'writer', 'article', 'status', 'categories'] + +>>> class ArticleForm(ModelForm): +... class Meta: +... model = Article + +First class with a Meta class wins. + +>>> class BadForm(ArticleForm, CategoryForm): +... pass +>>> OddForm.base_fields.keys() +['headline', 'slug', 'pub_date', 'writer', 'article', 'status', 'categories'] + +Subclassing without specifying a Meta on the class will use the parent's Meta +(or the first parent in the MRO if there are multiple parent classes). + +>>> class CategoryForm(ModelForm): +... class Meta: +... model = Category +>>> class SubCategoryForm(CategoryForm): +... pass +>>> SubCategoryForm.base_fields.keys() +['name', 'slug', 'url'] + +We can also subclass the Meta inner class to change the fields list. + +>>> class CategoryForm(ModelForm): +... checkbox = forms.BooleanField() +... +... class Meta: +... model = Category +>>> class SubCategoryForm(CategoryForm): +... class Meta(CategoryForm.Meta): +... exclude = ['url'] + +>>> print SubCategoryForm() +<tr><th><label for="id_name">Name:</label></th><td><input id="id_name" type="text" name="name" maxlength="20" /></td></tr> +<tr><th><label for="id_slug">Slug:</label></th><td><input id="id_slug" type="text" name="slug" maxlength="20" /></td></tr> +<tr><th><label for="id_checkbox">Checkbox:</label></th><td><input type="checkbox" name="checkbox" id="id_checkbox" /></td></tr> + +# test using fields to provide ordering to the fields +>>> class CategoryForm(ModelForm): +... class Meta: +... model = Category +... fields = ['url', 'name'] + +>>> CategoryForm.base_fields.keys() +['url', 'name'] + + +>>> print CategoryForm() +<tr><th><label for="id_url">The URL:</label></th><td><input id="id_url" type="text" name="url" maxlength="40" /></td></tr> +<tr><th><label for="id_name">Name:</label></th><td><input id="id_name" type="text" name="name" maxlength="20" /></td></tr> + +>>> class CategoryForm(ModelForm): +... class Meta: +... model = Category +... fields = ['slug', 'url', 'name'] +... exclude = ['url'] + +>>> CategoryForm.base_fields.keys() +['slug', 'name'] + +# Old form_for_x tests ####################################################### + +>>> from django.forms import ModelForm, CharField +>>> import datetime + +>>> Category.objects.all() +[] + +>>> class CategoryForm(ModelForm): +... class Meta: +... model = Category +>>> f = CategoryForm() +>>> print f +<tr><th><label for="id_name">Name:</label></th><td><input id="id_name" type="text" name="name" maxlength="20" /></td></tr> +<tr><th><label for="id_slug">Slug:</label></th><td><input id="id_slug" type="text" name="slug" maxlength="20" /></td></tr> +<tr><th><label for="id_url">The URL:</label></th><td><input id="id_url" type="text" name="url" maxlength="40" /></td></tr> +>>> print f.as_ul() +<li><label for="id_name">Name:</label> <input id="id_name" type="text" name="name" maxlength="20" /></li> +<li><label for="id_slug">Slug:</label> <input id="id_slug" type="text" name="slug" maxlength="20" /></li> +<li><label for="id_url">The URL:</label> <input id="id_url" type="text" name="url" maxlength="40" /></li> +>>> print f['name'] +<input id="id_name" type="text" name="name" maxlength="20" /> + +>>> f = CategoryForm(auto_id=False) +>>> print f.as_ul() +<li>Name: <input type="text" name="name" maxlength="20" /></li> +<li>Slug: <input type="text" name="slug" maxlength="20" /></li> +<li>The URL: <input type="text" name="url" maxlength="40" /></li> + +>>> f = CategoryForm({'name': 'Entertainment', 'slug': 'entertainment', 'url': 'entertainment'}) +>>> f.is_valid() +True +>>> f.cleaned_data['url'] +u'entertainment' +>>> f.cleaned_data['name'] +u'Entertainment' +>>> f.cleaned_data['slug'] +u'entertainment' +>>> obj = f.save() +>>> obj +<Category: Entertainment> +>>> Category.objects.all() +[<Category: Entertainment>] + +>>> f = CategoryForm({'name': "It's a test", 'slug': 'its-test', 'url': 'test'}) +>>> f.is_valid() +True +>>> f.cleaned_data['url'] +u'test' +>>> f.cleaned_data['name'] +u"It's a test" +>>> f.cleaned_data['slug'] +u'its-test' +>>> obj = f.save() +>>> obj +<Category: It's a test> +>>> Category.objects.order_by('name') +[<Category: Entertainment>, <Category: It's a test>] + +If you call save() with commit=False, then it will return an object that +hasn't yet been saved to the database. In this case, it's up to you to call +save() on the resulting model instance. +>>> f = CategoryForm({'name': 'Third test', 'slug': 'third-test', 'url': 'third'}) +>>> f.is_valid() +True +>>> f.cleaned_data['url'] +u'third' +>>> f.cleaned_data['name'] +u'Third test' +>>> f.cleaned_data['slug'] +u'third-test' +>>> obj = f.save(commit=False) +>>> obj +<Category: Third test> +>>> Category.objects.order_by('name') +[<Category: Entertainment>, <Category: It's a test>] +>>> obj.save() +>>> Category.objects.order_by('name') +[<Category: Entertainment>, <Category: It's a test>, <Category: Third test>] + +If you call save() with invalid data, you'll get a ValueError. +>>> f = CategoryForm({'name': '', 'slug': 'not a slug!', 'url': 'foo'}) +>>> f.errors['name'] +[u'This field is required.'] +>>> f.errors['slug'] +[u"Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."] +>>> f.cleaned_data +Traceback (most recent call last): +... +AttributeError: 'CategoryForm' object has no attribute 'cleaned_data' +>>> f.save() +Traceback (most recent call last): +... +ValueError: The Category could not be created because the data didn't validate. +>>> f = CategoryForm({'name': '', 'slug': '', 'url': 'foo'}) +>>> f.save() +Traceback (most recent call last): +... +ValueError: The Category could not be created because the data didn't validate. + +Create a couple of Writers. +>>> w_royko = Writer(name='Mike Royko') +>>> w_royko.save() +>>> w_woodward = Writer(name='Bob Woodward') +>>> w_woodward.save() + +ManyToManyFields are represented by a MultipleChoiceField, ForeignKeys and any +fields with the 'choices' attribute are represented by a ChoiceField. +>>> class ArticleForm(ModelForm): +... class Meta: +... model = Article +>>> f = ArticleForm(auto_id=False) +>>> print f +<tr><th>Headline:</th><td><input type="text" name="headline" maxlength="50" /></td></tr> +<tr><th>Slug:</th><td><input type="text" name="slug" maxlength="50" /></td></tr> +<tr><th>Pub date:</th><td><input type="text" name="pub_date" /></td></tr> +<tr><th>Writer:</th><td><select name="writer"> +<option value="" selected="selected">---------</option> +<option value="...">Mike Royko</option> +<option value="...">Bob Woodward</option> +</select></td></tr> +<tr><th>Article:</th><td><textarea rows="10" cols="40" name="article"></textarea></td></tr> +<tr><th>Status:</th><td><select name="status"> +<option value="" selected="selected">---------</option> +<option value="1">Draft</option> +<option value="2">Pending</option> +<option value="3">Live</option> +</select></td></tr> +<tr><th>Categories:</th><td><select multiple="multiple" name="categories"> +<option value="1">Entertainment</option> +<option value="2">It's a test</option> +<option value="3">Third test</option> +</select><br /> Hold down "Control", or "Command" on a Mac, to select more than one.</td></tr> + +You can restrict a form to a subset of the complete list of fields +by providing a 'fields' argument. If you try to save a +model created with such a form, you need to ensure that the fields +that are _not_ on the form have default values, or are allowed to have +a value of None. If a field isn't specified on a form, the object created +from the form can't provide a value for that field! +>>> class PartialArticleForm(ModelForm): +... class Meta: +... model = Article +... fields = ('headline','pub_date') +>>> f = PartialArticleForm(auto_id=False) +>>> print f +<tr><th>Headline:</th><td><input type="text" name="headline" maxlength="50" /></td></tr> +<tr><th>Pub date:</th><td><input type="text" name="pub_date" /></td></tr> + +When the ModelForm is passed an instance, that instance's current values are +inserted as 'initial' data in each Field. +>>> w = Writer.objects.get(name='Mike Royko') +>>> class RoykoForm(ModelForm): +... class Meta: +... model = Writer +>>> f = RoykoForm(auto_id=False, instance=w) +>>> print f +<tr><th>Name:</th><td><input type="text" name="name" value="Mike Royko" maxlength="50" /><br />Use both first and last names.</td></tr> + +>>> art = Article(headline='Test article', slug='test-article', pub_date=datetime.date(1988, 1, 4), writer=w, article='Hello.') +>>> art.save() +>>> art.id +1 +>>> class TestArticleForm(ModelForm): +... class Meta: +... model = Article +>>> f = TestArticleForm(auto_id=False, instance=art) +>>> print f.as_ul() +<li>Headline: <input type="text" name="headline" value="Test article" maxlength="50" /></li> +<li>Slug: <input type="text" name="slug" value="test-article" maxlength="50" /></li> +<li>Pub date: <input type="text" name="pub_date" value="1988-01-04" /></li> +<li>Writer: <select name="writer"> +<option value="">---------</option> +<option value="..." selected="selected">Mike Royko</option> +<option value="...">Bob Woodward</option> +</select></li> +<li>Article: <textarea rows="10" cols="40" name="article">Hello.</textarea></li> +<li>Status: <select name="status"> +<option value="" selected="selected">---------</option> +<option value="1">Draft</option> +<option value="2">Pending</option> +<option value="3">Live</option> +</select></li> +<li>Categories: <select multiple="multiple" name="categories"> +<option value="1">Entertainment</option> +<option value="2">It's a test</option> +<option value="3">Third test</option> +</select> Hold down "Control", or "Command" on a Mac, to select more than one.</li> +>>> f = TestArticleForm({'headline': u'Test headline', 'slug': 'test-headline', 'pub_date': u'1984-02-06', 'writer': unicode(w_royko.pk), 'article': 'Hello.'}, instance=art) +>>> f.errors +{} +>>> f.is_valid() +True +>>> test_art = f.save() +>>> test_art.id +1 +>>> test_art = Article.objects.get(id=1) +>>> test_art.headline +u'Test headline' + +You can create a form over a subset of the available fields +by specifying a 'fields' argument to form_for_instance. +>>> class PartialArticleForm(ModelForm): +... class Meta: +... model = Article +... fields=('headline', 'slug', 'pub_date') +>>> f = PartialArticleForm({'headline': u'New headline', 'slug': 'new-headline', 'pub_date': u'1988-01-04'}, auto_id=False, instance=art) +>>> print f.as_ul() +<li>Headline: <input type="text" name="headline" value="New headline" maxlength="50" /></li> +<li>Slug: <input type="text" name="slug" value="new-headline" maxlength="50" /></li> +<li>Pub date: <input type="text" name="pub_date" value="1988-01-04" /></li> +>>> f.is_valid() +True +>>> new_art = f.save() +>>> new_art.id +1 +>>> new_art = Article.objects.get(id=1) +>>> new_art.headline +u'New headline' + +Add some categories and test the many-to-many form output. +>>> new_art.categories.all() +[] +>>> new_art.categories.add(Category.objects.get(name='Entertainment')) +>>> new_art.categories.all() +[<Category: Entertainment>] +>>> class TestArticleForm(ModelForm): +... class Meta: +... model = Article +>>> f = TestArticleForm(auto_id=False, instance=new_art) +>>> print f.as_ul() +<li>Headline: <input type="text" name="headline" value="New headline" maxlength="50" /></li> +<li>Slug: <input type="text" name="slug" value="new-headline" maxlength="50" /></li> +<li>Pub date: <input type="text" name="pub_date" value="1988-01-04" /></li> +<li>Writer: <select name="writer"> +<option value="">---------</option> +<option value="..." selected="selected">Mike Royko</option> +<option value="...">Bob Woodward</option> +</select></li> +<li>Article: <textarea rows="10" cols="40" name="article">Hello.</textarea></li> +<li>Status: <select name="status"> +<option value="" selected="selected">---------</option> +<option value="1">Draft</option> +<option value="2">Pending</option> +<option value="3">Live</option> +</select></li> +<li>Categories: <select multiple="multiple" name="categories"> +<option value="1" selected="selected">Entertainment</option> +<option value="2">It's a test</option> +<option value="3">Third test</option> +</select> Hold down "Control", or "Command" on a Mac, to select more than one.</li> + +Initial values can be provided for model forms +>>> f = TestArticleForm(auto_id=False, initial={'headline': 'Your headline here', 'categories': ['1','2']}) +>>> print f.as_ul() +<li>Headline: <input type="text" name="headline" value="Your headline here" maxlength="50" /></li> +<li>Slug: <input type="text" name="slug" maxlength="50" /></li> +<li>Pub date: <input type="text" name="pub_date" /></li> +<li>Writer: <select name="writer"> +<option value="" selected="selected">---------</option> +<option value="...">Mike Royko</option> +<option value="...">Bob Woodward</option> +</select></li> +<li>Article: <textarea rows="10" cols="40" name="article"></textarea></li> +<li>Status: <select name="status"> +<option value="" selected="selected">---------</option> +<option value="1">Draft</option> +<option value="2">Pending</option> +<option value="3">Live</option> +</select></li> +<li>Categories: <select multiple="multiple" name="categories"> +<option value="1" selected="selected">Entertainment</option> +<option value="2" selected="selected">It's a test</option> +<option value="3">Third test</option> +</select> Hold down "Control", or "Command" on a Mac, to select more than one.</li> + +>>> f = TestArticleForm({'headline': u'New headline', 'slug': u'new-headline', 'pub_date': u'1988-01-04', +... 'writer': unicode(w_royko.pk), 'article': u'Hello.', 'categories': [u'1', u'2']}, instance=new_art) +>>> new_art = f.save() +>>> new_art.id +1 +>>> new_art = Article.objects.get(id=1) +>>> new_art.categories.order_by('name') +[<Category: Entertainment>, <Category: It's a test>] + +Now, submit form data with no categories. This deletes the existing categories. +>>> f = TestArticleForm({'headline': u'New headline', 'slug': u'new-headline', 'pub_date': u'1988-01-04', +... 'writer': unicode(w_royko.pk), 'article': u'Hello.'}, instance=new_art) +>>> new_art = f.save() +>>> new_art.id +1 +>>> new_art = Article.objects.get(id=1) +>>> new_art.categories.all() +[] + +Create a new article, with categories, via the form. +>>> class ArticleForm(ModelForm): +... class Meta: +... model = Article +>>> f = ArticleForm({'headline': u'The walrus was Paul', 'slug': u'walrus-was-paul', 'pub_date': u'1967-11-01', +... 'writer': unicode(w_royko.pk), 'article': u'Test.', 'categories': [u'1', u'2']}) +>>> new_art = f.save() +>>> new_art.id +2 +>>> new_art = Article.objects.get(id=2) +>>> new_art.categories.order_by('name') +[<Category: Entertainment>, <Category: It's a test>] + +Create a new article, with no categories, via the form. +>>> class ArticleForm(ModelForm): +... class Meta: +... model = Article +>>> f = ArticleForm({'headline': u'The walrus was Paul', 'slug': u'walrus-was-paul', 'pub_date': u'1967-11-01', +... 'writer': unicode(w_royko.pk), 'article': u'Test.'}) +>>> new_art = f.save() +>>> new_art.id +3 +>>> new_art = Article.objects.get(id=3) +>>> new_art.categories.all() +[] + +Create a new article, with categories, via the form, but use commit=False. +The m2m data won't be saved until save_m2m() is invoked on the form. +>>> class ArticleForm(ModelForm): +... class Meta: +... model = Article +>>> f = ArticleForm({'headline': u'The walrus was Paul', 'slug': 'walrus-was-paul', 'pub_date': u'1967-11-01', +... 'writer': unicode(w_royko.pk), 'article': u'Test.', 'categories': [u'1', u'2']}) +>>> new_art = f.save(commit=False) + +# Manually save the instance +>>> new_art.save() +>>> new_art.id +4 + +# The instance doesn't have m2m data yet +>>> new_art = Article.objects.get(id=4) +>>> new_art.categories.all() +[] + +# Save the m2m data on the form +>>> f.save_m2m() +>>> new_art.categories.order_by('name') +[<Category: Entertainment>, <Category: It's a test>] + +Here, we define a custom ModelForm. Because it happens to have the same fields as +the Category model, we can just call the form's save() to apply its changes to an +existing Category instance. +>>> class ShortCategory(ModelForm): +... name = CharField(max_length=5) +... slug = CharField(max_length=5) +... url = CharField(max_length=3) +>>> cat = Category.objects.get(name='Third test') +>>> cat +<Category: Third test> +>>> cat.id +3 +>>> form = ShortCategory({'name': 'Third', 'slug': 'third', 'url': '3rd'}, instance=cat) +>>> form.save() +<Category: Third> +>>> Category.objects.get(id=3) +<Category: Third> + +Here, we demonstrate that choices for a ForeignKey ChoiceField are determined +at runtime, based on the data in the database when the form is displayed, not +the data in the database when the form is instantiated. +>>> class ArticleForm(ModelForm): +... class Meta: +... model = Article +>>> f = ArticleForm(auto_id=False) +>>> print f.as_ul() +<li>Headline: <input type="text" name="headline" maxlength="50" /></li> +<li>Slug: <input type="text" name="slug" maxlength="50" /></li> +<li>Pub date: <input type="text" name="pub_date" /></li> +<li>Writer: <select name="writer"> +<option value="" selected="selected">---------</option> +<option value="...">Mike Royko</option> +<option value="...">Bob Woodward</option> +</select></li> +<li>Article: <textarea rows="10" cols="40" name="article"></textarea></li> +<li>Status: <select name="status"> +<option value="" selected="selected">---------</option> +<option value="1">Draft</option> +<option value="2">Pending</option> +<option value="3">Live</option> +</select></li> +<li>Categories: <select multiple="multiple" name="categories"> +<option value="1">Entertainment</option> +<option value="2">It's a test</option> +<option value="3">Third</option> +</select> Hold down "Control", or "Command" on a Mac, to select more than one.</li> +>>> Category.objects.create(name='Fourth', url='4th') +<Category: Fourth> +>>> Writer.objects.create(name='Carl Bernstein') +<Writer: Carl Bernstein> +>>> print f.as_ul() +<li>Headline: <input type="text" name="headline" maxlength="50" /></li> +<li>Slug: <input type="text" name="slug" maxlength="50" /></li> +<li>Pub date: <input type="text" name="pub_date" /></li> +<li>Writer: <select name="writer"> +<option value="" selected="selected">---------</option> +<option value="...">Mike Royko</option> +<option value="...">Bob Woodward</option> +<option value="...">Carl Bernstein</option> +</select></li> +<li>Article: <textarea rows="10" cols="40" name="article"></textarea></li> +<li>Status: <select name="status"> +<option value="" selected="selected">---------</option> +<option value="1">Draft</option> +<option value="2">Pending</option> +<option value="3">Live</option> +</select></li> +<li>Categories: <select multiple="multiple" name="categories"> +<option value="1">Entertainment</option> +<option value="2">It's a test</option> +<option value="3">Third</option> +<option value="4">Fourth</option> +</select> Hold down "Control", or "Command" on a Mac, to select more than one.</li> + +# ModelChoiceField ############################################################ + +>>> from django.forms import ModelChoiceField, ModelMultipleChoiceField + +>>> f = ModelChoiceField(Category.objects.all()) +>>> list(f.choices) +[(u'', u'---------'), (1, u'Entertainment'), (2, u"It's a test"), (3, u'Third'), (4, u'Fourth')] +>>> f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] +>>> f.clean(None) +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] +>>> f.clean(0) +Traceback (most recent call last): +... +ValidationError: [u'Select a valid choice. That choice is not one of the available choices.'] +>>> f.clean(3) +<Category: Third> +>>> f.clean(2) +<Category: It's a test> + +# Add a Category object *after* the ModelChoiceField has already been +# instantiated. This proves clean() checks the database during clean() rather +# than caching it at time of instantiation. +>>> Category.objects.create(name='Fifth', url='5th') +<Category: Fifth> +>>> f.clean(5) +<Category: Fifth> + +# Delete a Category object *after* the ModelChoiceField has already been +# instantiated. This proves clean() checks the database during clean() rather +# than caching it at time of instantiation. +>>> Category.objects.get(url='5th').delete() +>>> f.clean(5) +Traceback (most recent call last): +... +ValidationError: [u'Select a valid choice. That choice is not one of the available choices.'] + +>>> f = ModelChoiceField(Category.objects.filter(pk=1), required=False) +>>> print f.clean('') +None +>>> f.clean('') +>>> f.clean('1') +<Category: Entertainment> +>>> f.clean('100') +Traceback (most recent call last): +... +ValidationError: [u'Select a valid choice. That choice is not one of the available choices.'] + +# queryset can be changed after the field is created. +>>> f.queryset = Category.objects.exclude(name='Fourth') +>>> list(f.choices) +[(u'', u'---------'), (1, u'Entertainment'), (2, u"It's a test"), (3, u'Third')] +>>> f.clean(3) +<Category: Third> +>>> f.clean(4) +Traceback (most recent call last): +... +ValidationError: [u'Select a valid choice. That choice is not one of the available choices.'] + +# check that we can safely iterate choices repeatedly +>>> gen_one = list(f.choices) +>>> gen_two = f.choices +>>> gen_one[2] +(2L, u"It's a test") +>>> list(gen_two) +[(u'', u'---------'), (1L, u'Entertainment'), (2L, u"It's a test"), (3L, u'Third')] + +# check that we can override the label_from_instance method to print custom labels (#4620) +>>> f.queryset = Category.objects.all() +>>> f.label_from_instance = lambda obj: "category " + str(obj) +>>> list(f.choices) +[(u'', u'---------'), (1L, 'category Entertainment'), (2L, "category It's a test"), (3L, 'category Third'), (4L, 'category Fourth')] + +# ModelMultipleChoiceField #################################################### + +>>> f = ModelMultipleChoiceField(Category.objects.all()) +>>> list(f.choices) +[(1, u'Entertainment'), (2, u"It's a test"), (3, u'Third'), (4, u'Fourth')] +>>> f.clean(None) +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] +>>> f.clean([]) +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] +>>> f.clean([1]) +[<Category: Entertainment>] +>>> f.clean([2]) +[<Category: It's a test>] +>>> f.clean(['1']) +[<Category: Entertainment>] +>>> f.clean(['1', '2']) +[<Category: Entertainment>, <Category: It's a test>] +>>> f.clean([1, '2']) +[<Category: Entertainment>, <Category: It's a test>] +>>> f.clean((1, '2')) +[<Category: Entertainment>, <Category: It's a test>] +>>> f.clean(['100']) +Traceback (most recent call last): +... +ValidationError: [u'Select a valid choice. 100 is not one of the available choices.'] +>>> f.clean('hello') +Traceback (most recent call last): +... +ValidationError: [u'Enter a list of values.'] +>>> f.clean(['fail']) +Traceback (most recent call last): +... +ValidationError: [u'"fail" is not a valid value for a primary key.'] + +# Add a Category object *after* the ModelMultipleChoiceField has already been +# instantiated. This proves clean() checks the database during clean() rather +# than caching it at time of instantiation. +>>> Category.objects.create(id=6, name='Sixth', url='6th') +<Category: Sixth> +>>> f.clean([6]) +[<Category: Sixth>] + +# Delete a Category object *after* the ModelMultipleChoiceField has already been +# instantiated. This proves clean() checks the database during clean() rather +# than caching it at time of instantiation. +>>> Category.objects.get(url='6th').delete() +>>> f.clean([6]) +Traceback (most recent call last): +... +ValidationError: [u'Select a valid choice. 6 is not one of the available choices.'] + +>>> f = ModelMultipleChoiceField(Category.objects.all(), required=False) +>>> f.clean([]) +[] +>>> f.clean(()) +[] +>>> f.clean(['10']) +Traceback (most recent call last): +... +ValidationError: [u'Select a valid choice. 10 is not one of the available choices.'] +>>> f.clean(['3', '10']) +Traceback (most recent call last): +... +ValidationError: [u'Select a valid choice. 10 is not one of the available choices.'] +>>> f.clean(['1', '10']) +Traceback (most recent call last): +... +ValidationError: [u'Select a valid choice. 10 is not one of the available choices.'] + +# queryset can be changed after the field is created. +>>> f.queryset = Category.objects.exclude(name='Fourth') +>>> list(f.choices) +[(1, u'Entertainment'), (2, u"It's a test"), (3, u'Third')] +>>> f.clean([3]) +[<Category: Third>] +>>> f.clean([4]) +Traceback (most recent call last): +... +ValidationError: [u'Select a valid choice. 4 is not one of the available choices.'] +>>> f.clean(['3', '4']) +Traceback (most recent call last): +... +ValidationError: [u'Select a valid choice. 4 is not one of the available choices.'] + +>>> f.queryset = Category.objects.all() +>>> f.label_from_instance = lambda obj: "multicategory " + str(obj) +>>> list(f.choices) +[(1L, 'multicategory Entertainment'), (2L, "multicategory It's a test"), (3L, 'multicategory Third'), (4L, 'multicategory Fourth')] + +# OneToOneField ############################################################### + +>>> class ImprovedArticleForm(ModelForm): +... class Meta: +... model = ImprovedArticle +>>> ImprovedArticleForm.base_fields.keys() +['article'] + +>>> class ImprovedArticleWithParentLinkForm(ModelForm): +... class Meta: +... model = ImprovedArticleWithParentLink +>>> ImprovedArticleWithParentLinkForm.base_fields.keys() +[] + +>>> bw = BetterWriter(name=u'Joe Better', score=10) +>>> bw.save() +>>> sorted(model_to_dict(bw).keys()) +['id', 'name', 'score', 'writer_ptr'] + +>>> class BetterWriterForm(ModelForm): +... class Meta: +... model = BetterWriter +>>> form = BetterWriterForm({'name': 'Some Name', 'score': 12}) +>>> form.is_valid() +True +>>> bw2 = form.save() +>>> bw2.delete() + + +>>> class WriterProfileForm(ModelForm): +... class Meta: +... model = WriterProfile +>>> form = WriterProfileForm() +>>> print form.as_p() +<p><label for="id_writer">Writer:</label> <select name="writer" id="id_writer"> +<option value="" selected="selected">---------</option> +<option value="...">Mike Royko</option> +<option value="...">Bob Woodward</option> +<option value="...">Carl Bernstein</option> +<option value="...">Joe Better</option> +</select></p> +<p><label for="id_age">Age:</label> <input type="text" name="age" id="id_age" /></p> + +>>> data = { +... 'writer': unicode(w_woodward.pk), +... 'age': u'65', +... } +>>> form = WriterProfileForm(data) +>>> instance = form.save() +>>> instance +<WriterProfile: Bob Woodward is 65> + +>>> form = WriterProfileForm(instance=instance) +>>> print form.as_p() +<p><label for="id_writer">Writer:</label> <select name="writer" id="id_writer"> +<option value="">---------</option> +<option value="...">Mike Royko</option> +<option value="..." selected="selected">Bob Woodward</option> +<option value="...">Carl Bernstein</option> +<option value="...">Joe Better</option> +</select></p> +<p><label for="id_age">Age:</label> <input type="text" name="age" value="65" id="id_age" /></p> + +# PhoneNumberField ############################################################ + +>>> class PhoneNumberForm(ModelForm): +... class Meta: +... model = PhoneNumber +>>> f = PhoneNumberForm({'phone': '(312) 555-1212', 'description': 'Assistance'}) +>>> f.is_valid() +True +>>> f.cleaned_data['phone'] +u'312-555-1212' +>>> f.cleaned_data['description'] +u'Assistance' + +# FileField ################################################################### + +# File forms. + +>>> class TextFileForm(ModelForm): +... class Meta: +... model = TextFile + +# Test conditions when files is either not given or empty. + +>>> f = TextFileForm(data={'description': u'Assistance'}) +>>> f.is_valid() +False +>>> f = TextFileForm(data={'description': u'Assistance'}, files={}) +>>> f.is_valid() +False + +# Upload a file and ensure it all works as expected. + +>>> f = TextFileForm(data={'description': u'Assistance'}, files={'file': SimpleUploadedFile('test1.txt', 'hello world')}) +>>> f.is_valid() +True +>>> type(f.cleaned_data['file']) +<class 'django.core.files.uploadedfile.SimpleUploadedFile'> +>>> instance = f.save() +>>> instance.file +<FieldFile: tests/test1.txt> + +>>> instance.file.delete() + +>>> f = TextFileForm(data={'description': u'Assistance'}, files={'file': SimpleUploadedFile('test1.txt', 'hello world')}) +>>> f.is_valid() +True +>>> type(f.cleaned_data['file']) +<class 'django.core.files.uploadedfile.SimpleUploadedFile'> +>>> instance = f.save() +>>> instance.file +<FieldFile: tests/test1.txt> + +# Check if the max_length attribute has been inherited from the model. +>>> f = TextFileForm(data={'description': u'Assistance'}, files={'file': SimpleUploadedFile('test-maxlength.txt', 'hello world')}) +>>> f.is_valid() +False + +# Edit an instance that already has the file defined in the model. This will not +# save the file again, but leave it exactly as it is. + +>>> f = TextFileForm(data={'description': u'Assistance'}, instance=instance) +>>> f.is_valid() +True +>>> f.cleaned_data['file'] +<FieldFile: tests/test1.txt> +>>> instance = f.save() +>>> instance.file +<FieldFile: tests/test1.txt> + +# Delete the current file since this is not done by Django. +>>> instance.file.delete() + +# Override the file by uploading a new one. + +>>> f = TextFileForm(data={'description': u'Assistance'}, files={'file': SimpleUploadedFile('test2.txt', 'hello world')}, instance=instance) +>>> f.is_valid() +True +>>> instance = f.save() +>>> instance.file +<FieldFile: tests/test2.txt> + +# Delete the current file since this is not done by Django. +>>> instance.file.delete() + +>>> f = TextFileForm(data={'description': u'Assistance'}, files={'file': SimpleUploadedFile('test2.txt', 'hello world')}) +>>> f.is_valid() +True +>>> instance = f.save() +>>> instance.file +<FieldFile: tests/test2.txt> + +# Delete the current file since this is not done by Django. +>>> instance.file.delete() + +>>> instance.delete() + +# Test the non-required FileField +>>> f = TextFileForm(data={'description': u'Assistance'}) +>>> f.fields['file'].required = False +>>> f.is_valid() +True +>>> instance = f.save() +>>> instance.file +<FieldFile: None> + +>>> f = TextFileForm(data={'description': u'Assistance'}, files={'file': SimpleUploadedFile('test3.txt', 'hello world')}, instance=instance) +>>> f.is_valid() +True +>>> instance = f.save() +>>> instance.file +<FieldFile: tests/test3.txt> + +# Instance can be edited w/out re-uploading the file and existing file should be preserved. + +>>> f = TextFileForm(data={'description': u'New Description'}, instance=instance) +>>> f.fields['file'].required = False +>>> f.is_valid() +True +>>> instance = f.save() +>>> instance.description +u'New Description' +>>> instance.file +<FieldFile: tests/test3.txt> + +# Delete the current file since this is not done by Django. +>>> instance.file.delete() +>>> instance.delete() + +>>> f = TextFileForm(data={'description': u'Assistance'}, files={'file': SimpleUploadedFile('test3.txt', 'hello world')}) +>>> f.is_valid() +True +>>> instance = f.save() +>>> instance.file +<FieldFile: tests/test3.txt> + +# Delete the current file since this is not done by Django. +>>> instance.file.delete() +>>> instance.delete() + +# BigIntegerField ################################################################ +>>> class BigIntForm(forms.ModelForm): +... class Meta: +... model = BigInt +... +>>> bif = BigIntForm({'biggie': '-9223372036854775808'}) +>>> bif.is_valid() +True +>>> bif = BigIntForm({'biggie': '-9223372036854775809'}) +>>> bif.is_valid() +False +>>> bif.errors +{'biggie': [u'Ensure this value is greater than or equal to -9223372036854775808.']} +>>> bif = BigIntForm({'biggie': '9223372036854775807'}) +>>> bif.is_valid() +True +>>> bif = BigIntForm({'biggie': '9223372036854775808'}) +>>> bif.is_valid() +False +>>> bif.errors +{'biggie': [u'Ensure this value is less than or equal to 9223372036854775807.']} +"""} + +if test_images: + __test__['API_TESTS'] += """ +# ImageField ################################################################### + +# ImageField and FileField are nearly identical, but they differ slighty when +# it comes to validation. This specifically tests that #6302 is fixed for +# both file fields and image fields. + +>>> class ImageFileForm(ModelForm): +... class Meta: +... model = ImageFile + +>>> image_data = open(os.path.join(os.path.dirname(__file__), "test.png"), 'rb').read() +>>> image_data2 = open(os.path.join(os.path.dirname(__file__), "test2.png"), 'rb').read() + +>>> f = ImageFileForm(data={'description': u'An image'}, files={'image': SimpleUploadedFile('test.png', image_data)}) +>>> f.is_valid() +True +>>> type(f.cleaned_data['image']) +<class 'django.core.files.uploadedfile.SimpleUploadedFile'> +>>> instance = f.save() +>>> instance.image +<...FieldFile: tests/test.png> +>>> instance.width +16 +>>> instance.height +16 + +# Delete the current file since this is not done by Django, but don't save +# because the dimension fields are not null=True. +>>> instance.image.delete(save=False) + +>>> f = ImageFileForm(data={'description': u'An image'}, files={'image': SimpleUploadedFile('test.png', image_data)}) +>>> f.is_valid() +True +>>> type(f.cleaned_data['image']) +<class 'django.core.files.uploadedfile.SimpleUploadedFile'> +>>> instance = f.save() +>>> instance.image +<...FieldFile: tests/test.png> +>>> instance.width +16 +>>> instance.height +16 + +# Edit an instance that already has the (required) image defined in the model. This will not +# save the image again, but leave it exactly as it is. + +>>> f = ImageFileForm(data={'description': u'Look, it changed'}, instance=instance) +>>> f.is_valid() +True +>>> f.cleaned_data['image'] +<...FieldFile: tests/test.png> +>>> instance = f.save() +>>> instance.image +<...FieldFile: tests/test.png> +>>> instance.height +16 +>>> instance.width +16 + +# Delete the current file since this is not done by Django, but don't save +# because the dimension fields are not null=True. +>>> instance.image.delete(save=False) + +# Override the file by uploading a new one. + +>>> f = ImageFileForm(data={'description': u'Changed it'}, files={'image': SimpleUploadedFile('test2.png', image_data2)}, instance=instance) +>>> f.is_valid() +True +>>> instance = f.save() +>>> instance.image +<...FieldFile: tests/test2.png> +>>> instance.height +32 +>>> instance.width +48 + +# Delete the current file since this is not done by Django, but don't save +# because the dimension fields are not null=True. +>>> instance.image.delete(save=False) +>>> instance.delete() + +>>> f = ImageFileForm(data={'description': u'Changed it'}, files={'image': SimpleUploadedFile('test2.png', image_data2)}) +>>> f.is_valid() +True +>>> instance = f.save() +>>> instance.image +<...FieldFile: tests/test2.png> +>>> instance.height +32 +>>> instance.width +48 + +# Delete the current file since this is not done by Django, but don't save +# because the dimension fields are not null=True. +>>> instance.image.delete(save=False) +>>> instance.delete() + +# Test the non-required ImageField + +>>> class OptionalImageFileForm(ModelForm): +... class Meta: +... model = OptionalImageFile + +>>> f = OptionalImageFileForm(data={'description': u'Test'}) +>>> f.is_valid() +True +>>> instance = f.save() +>>> instance.image +<...FieldFile: None> +>>> instance.width +>>> instance.height + +>>> f = OptionalImageFileForm(data={'description': u'And a final one'}, files={'image': SimpleUploadedFile('test3.png', image_data)}, instance=instance) +>>> f.is_valid() +True +>>> instance = f.save() +>>> instance.image +<...FieldFile: tests/test3.png> +>>> instance.width +16 +>>> instance.height +16 + +# Editing the instance without re-uploading the image should not affect the image or its width/height properties +>>> f = OptionalImageFileForm(data={'description': u'New Description'}, instance=instance) +>>> f.is_valid() +True +>>> instance = f.save() +>>> instance.description +u'New Description' +>>> instance.image +<...FieldFile: tests/test3.png> +>>> instance.width +16 +>>> instance.height +16 + +# Delete the current file since this is not done by Django. +>>> instance.image.delete() +>>> instance.delete() + +>>> f = OptionalImageFileForm(data={'description': u'And a final one'}, files={'image': SimpleUploadedFile('test4.png', image_data2)}) +>>> f.is_valid() +True +>>> instance = f.save() +>>> instance.image +<...FieldFile: tests/test4.png> +>>> instance.width +48 +>>> instance.height +32 +>>> instance.delete() + +# Test callable upload_to behavior that's dependent on the value of another field in the model +>>> f = ImageFileForm(data={'description': u'And a final one', 'path': 'foo'}, files={'image': SimpleUploadedFile('test4.png', image_data)}) +>>> f.is_valid() +True +>>> instance = f.save() +>>> instance.image +<...FieldFile: foo/test4.png> +>>> instance.delete() +""" + +__test__['API_TESTS'] += """ + +# Media on a ModelForm ######################################################## + +# Similar to a regular Form class you can define custom media to be used on +# the ModelForm. + +>>> class ModelFormWithMedia(ModelForm): +... class Media: +... js = ('/some/form/javascript',) +... css = { +... 'all': ('/some/form/css',) +... } +... class Meta: +... model = PhoneNumber +>>> f = ModelFormWithMedia() +>>> print f.media +<link href="/some/form/css" type="text/css" media="all" rel="stylesheet" /> +<script type="text/javascript" src="/some/form/javascript"></script> + +>>> class CommaSeparatedIntegerForm(ModelForm): +... class Meta: +... model = CommaSeparatedInteger + +>>> f = CommaSeparatedIntegerForm({'field': '1,2,3'}) +>>> f.is_valid() +True +>>> f.cleaned_data +{'field': u'1,2,3'} +>>> f = CommaSeparatedIntegerForm({'field': '1a,2'}) +>>> f.errors +{'field': [u'Enter only digits separated by commas.']} +>>> f = CommaSeparatedIntegerForm({'field': ',,,,'}) +>>> f.is_valid() +True +>>> f.cleaned_data +{'field': u',,,,'} +>>> f = CommaSeparatedIntegerForm({'field': '1.2'}) +>>> f.errors +{'field': [u'Enter only digits separated by commas.']} +>>> f = CommaSeparatedIntegerForm({'field': '1,a,2'}) +>>> f.errors +{'field': [u'Enter only digits separated by commas.']} +>>> f = CommaSeparatedIntegerForm({'field': '1,,2'}) +>>> f.is_valid() +True +>>> f.cleaned_data +{'field': u'1,,2'} +>>> f = CommaSeparatedIntegerForm({'field': '1'}) +>>> f.is_valid() +True +>>> f.cleaned_data +{'field': u'1'} + +This Price instance generated by this form is not valid because the quantity +field is required, but the form is valid because the field is excluded from +the form. This is for backwards compatibility. + +>>> class PriceForm(ModelForm): +... class Meta: +... model = Price +... exclude = ('quantity',) +>>> form = PriceForm({'price': '6.00'}) +>>> form.is_valid() +True +>>> price = form.save(commit=False) +>>> price.full_clean() +Traceback (most recent call last): + ... +ValidationError: {'quantity': [u'This field cannot be null.']} + +The form should not validate fields that it doesn't contain even if they are +specified using 'fields', not 'exclude'. +... class Meta: +... model = Price +... fields = ('price',) +>>> form = PriceForm({'price': '6.00'}) +>>> form.is_valid() +True + +The form should still have an instance of a model that is not complete and +not saved into a DB yet. + +>>> form.instance.price +Decimal('6.00') +>>> form.instance.quantity is None +True +>>> form.instance.pk is None +True + +# Choices on CharField and IntegerField +>>> class ArticleForm(ModelForm): +... class Meta: +... model = Article +>>> f = ArticleForm() +>>> f.fields['status'].clean('42') +Traceback (most recent call last): +... +ValidationError: [u'Select a valid choice. 42 is not one of the available choices.'] + +>>> class ArticleStatusForm(ModelForm): +... class Meta: +... model = ArticleStatus +>>> f = ArticleStatusForm() +>>> f.fields['status'].clean('z') +Traceback (most recent call last): +... +ValidationError: [u'Select a valid choice. z is not one of the available choices.'] + +# Foreign keys which use to_field ############################################# + +>>> apple = Inventory.objects.create(barcode=86, name='Apple') +>>> pear = Inventory.objects.create(barcode=22, name='Pear') +>>> core = Inventory.objects.create(barcode=87, name='Core', parent=apple) + +>>> field = ModelChoiceField(Inventory.objects.all(), to_field_name='barcode') +>>> for choice in field.choices: +... print choice +(u'', u'---------') +(86, u'Apple') +(22, u'Pear') +(87, u'Core') + +>>> class InventoryForm(ModelForm): +... class Meta: +... model = Inventory +>>> form = InventoryForm(instance=core) +>>> print form['parent'] +<select name="parent" id="id_parent"> +<option value="">---------</option> +<option value="86" selected="selected">Apple</option> +<option value="22">Pear</option> +<option value="87">Core</option> +</select> + +>>> data = model_to_dict(core) +>>> data['parent'] = '22' +>>> form = InventoryForm(data=data, instance=core) +>>> core = form.save() +>>> core.parent +<Inventory: Pear> + +>>> class CategoryForm(ModelForm): +... description = forms.CharField() +... class Meta: +... model = Category +... fields = ['description', 'url'] + +>>> CategoryForm.base_fields.keys() +['description', 'url'] + +>>> print CategoryForm() +<tr><th><label for="id_description">Description:</label></th><td><input type="text" name="description" id="id_description" /></td></tr> +<tr><th><label for="id_url">The URL:</label></th><td><input id="id_url" type="text" name="url" maxlength="40" /></td></tr> + +# Model field that returns None to exclude itself with explicit fields ######## + +>>> class CustomFieldForExclusionForm(ModelForm): +... class Meta: +... model = CustomFieldForExclusionModel +... fields = ['name', 'markup'] + +>>> CustomFieldForExclusionForm.base_fields.keys() +['name'] + +>>> print CustomFieldForExclusionForm() +<tr><th><label for="id_name">Name:</label></th><td><input id="id_name" type="text" name="name" maxlength="10" /></td></tr> + +# Clean up +>>> import shutil +>>> shutil.rmtree(temp_storage_dir) +""" diff --git a/parts/django/tests/modeltests/model_forms/test.png b/parts/django/tests/modeltests/model_forms/test.png Binary files differnew file mode 100644 index 0000000..4f17cd0 --- /dev/null +++ b/parts/django/tests/modeltests/model_forms/test.png diff --git a/parts/django/tests/modeltests/model_forms/test2.png b/parts/django/tests/modeltests/model_forms/test2.png Binary files differnew file mode 100644 index 0000000..10702f7 --- /dev/null +++ b/parts/django/tests/modeltests/model_forms/test2.png diff --git a/parts/django/tests/modeltests/model_forms/tests.py b/parts/django/tests/modeltests/model_forms/tests.py new file mode 100644 index 0000000..c5647c7 --- /dev/null +++ b/parts/django/tests/modeltests/model_forms/tests.py @@ -0,0 +1,185 @@ +import datetime +from django.test import TestCase +from django import forms +from models import Category, Writer, Book, DerivedBook, Post +from mforms import (ProductForm, PriceForm, BookForm, DerivedBookForm, + ExplicitPKForm, PostForm, DerivedPostForm, CustomWriterForm) + + +class IncompleteCategoryFormWithFields(forms.ModelForm): + """ + A form that replaces the model's url field with a custom one. This should + prevent the model field's validation from being called. + """ + url = forms.CharField(required=False) + + class Meta: + fields = ('name', 'slug') + model = Category + +class IncompleteCategoryFormWithExclude(forms.ModelForm): + """ + A form that replaces the model's url field with a custom one. This should + prevent the model field's validation from being called. + """ + url = forms.CharField(required=False) + + class Meta: + exclude = ['url'] + model = Category + + +class ValidationTest(TestCase): + def test_validates_with_replaced_field_not_specified(self): + form = IncompleteCategoryFormWithFields(data={'name': 'some name', 'slug': 'some-slug'}) + assert form.is_valid() + + def test_validates_with_replaced_field_excluded(self): + form = IncompleteCategoryFormWithExclude(data={'name': 'some name', 'slug': 'some-slug'}) + assert form.is_valid() + + def test_notrequired_overrides_notblank(self): + form = CustomWriterForm({}) + assert form.is_valid() + +# unique/unique_together validation +class UniqueTest(TestCase): + def setUp(self): + self.writer = Writer.objects.create(name='Mike Royko') + + def test_simple_unique(self): + form = ProductForm({'slug': 'teddy-bear-blue'}) + self.assertTrue(form.is_valid()) + obj = form.save() + form = ProductForm({'slug': 'teddy-bear-blue'}) + self.assertEqual(len(form.errors), 1) + self.assertEqual(form.errors['slug'], [u'Product with this Slug already exists.']) + form = ProductForm({'slug': 'teddy-bear-blue'}, instance=obj) + self.assertTrue(form.is_valid()) + + def test_unique_together(self): + """ModelForm test of unique_together constraint""" + form = PriceForm({'price': '6.00', 'quantity': '1'}) + self.assertTrue(form.is_valid()) + form.save() + form = PriceForm({'price': '6.00', 'quantity': '1'}) + self.assertFalse(form.is_valid()) + self.assertEqual(len(form.errors), 1) + self.assertEqual(form.errors['__all__'], [u'Price with this Price and Quantity already exists.']) + + def test_unique_null(self): + title = 'I May Be Wrong But I Doubt It' + form = BookForm({'title': title, 'author': self.writer.pk}) + self.assertTrue(form.is_valid()) + form.save() + form = BookForm({'title': title, 'author': self.writer.pk}) + self.assertFalse(form.is_valid()) + self.assertEqual(len(form.errors), 1) + self.assertEqual(form.errors['__all__'], [u'Book with this Title and Author already exists.']) + form = BookForm({'title': title}) + self.assertTrue(form.is_valid()) + form.save() + form = BookForm({'title': title}) + self.assertTrue(form.is_valid()) + + def test_inherited_unique(self): + title = 'Boss' + Book.objects.create(title=title, author=self.writer, special_id=1) + form = DerivedBookForm({'title': 'Other', 'author': self.writer.pk, 'special_id': u'1', 'isbn': '12345'}) + self.assertFalse(form.is_valid()) + self.assertEqual(len(form.errors), 1) + self.assertEqual(form.errors['special_id'], [u'Book with this Special id already exists.']) + + def test_inherited_unique_together(self): + title = 'Boss' + form = BookForm({'title': title, 'author': self.writer.pk}) + self.assertTrue(form.is_valid()) + form.save() + form = DerivedBookForm({'title': title, 'author': self.writer.pk, 'isbn': '12345'}) + self.assertFalse(form.is_valid()) + self.assertEqual(len(form.errors), 1) + self.assertEqual(form.errors['__all__'], [u'Book with this Title and Author already exists.']) + + def test_abstract_inherited_unique(self): + title = 'Boss' + isbn = '12345' + dbook = DerivedBook.objects.create(title=title, author=self.writer, isbn=isbn) + form = DerivedBookForm({'title': 'Other', 'author': self.writer.pk, 'isbn': isbn}) + self.assertFalse(form.is_valid()) + self.assertEqual(len(form.errors), 1) + self.assertEqual(form.errors['isbn'], [u'Derived book with this Isbn already exists.']) + + def test_abstract_inherited_unique_together(self): + title = 'Boss' + isbn = '12345' + dbook = DerivedBook.objects.create(title=title, author=self.writer, isbn=isbn) + form = DerivedBookForm({'title': 'Other', 'author': self.writer.pk, 'isbn': '9876', 'suffix1': u'0', 'suffix2': u'0'}) + self.assertFalse(form.is_valid()) + self.assertEqual(len(form.errors), 1) + self.assertEqual(form.errors['__all__'], [u'Derived book with this Suffix1 and Suffix2 already exists.']) + + def test_explicitpk_unspecified(self): + """Test for primary_key being in the form and failing validation.""" + form = ExplicitPKForm({'key': u'', 'desc': u'' }) + self.assertFalse(form.is_valid()) + + def test_explicitpk_unique(self): + """Ensure keys and blank character strings are tested for uniqueness.""" + form = ExplicitPKForm({'key': u'key1', 'desc': u''}) + self.assertTrue(form.is_valid()) + form.save() + form = ExplicitPKForm({'key': u'key1', 'desc': u''}) + self.assertFalse(form.is_valid()) + self.assertEqual(len(form.errors), 3) + self.assertEqual(form.errors['__all__'], [u'Explicit pk with this Key and Desc already exists.']) + self.assertEqual(form.errors['desc'], [u'Explicit pk with this Desc already exists.']) + self.assertEqual(form.errors['key'], [u'Explicit pk with this Key already exists.']) + + def test_unique_for_date(self): + p = Post.objects.create(title="Django 1.0 is released", + slug="Django 1.0", subtitle="Finally", posted=datetime.date(2008, 9, 3)) + form = PostForm({'title': "Django 1.0 is released", 'posted': '2008-09-03'}) + self.assertFalse(form.is_valid()) + self.assertEqual(len(form.errors), 1) + self.assertEqual(form.errors['title'], [u'Title must be unique for Posted date.']) + form = PostForm({'title': "Work on Django 1.1 begins", 'posted': '2008-09-03'}) + self.assertTrue(form.is_valid()) + form = PostForm({'title': "Django 1.0 is released", 'posted': '2008-09-04'}) + self.assertTrue(form.is_valid()) + form = PostForm({'slug': "Django 1.0", 'posted': '2008-01-01'}) + self.assertFalse(form.is_valid()) + self.assertEqual(len(form.errors), 1) + self.assertEqual(form.errors['slug'], [u'Slug must be unique for Posted year.']) + form = PostForm({'subtitle': "Finally", 'posted': '2008-09-30'}) + self.assertFalse(form.is_valid()) + self.assertEqual(form.errors['subtitle'], [u'Subtitle must be unique for Posted month.']) + form = PostForm({'subtitle': "Finally", "title": "Django 1.0 is released", + "slug": "Django 1.0", 'posted': '2008-09-03'}, instance=p) + self.assertTrue(form.is_valid()) + form = PostForm({'title': "Django 1.0 is released"}) + self.assertFalse(form.is_valid()) + self.assertEqual(len(form.errors), 1) + self.assertEqual(form.errors['posted'], [u'This field is required.']) + + def test_inherited_unique_for_date(self): + p = Post.objects.create(title="Django 1.0 is released", + slug="Django 1.0", subtitle="Finally", posted=datetime.date(2008, 9, 3)) + form = DerivedPostForm({'title': "Django 1.0 is released", 'posted': '2008-09-03'}) + self.assertFalse(form.is_valid()) + self.assertEqual(len(form.errors), 1) + self.assertEqual(form.errors['title'], [u'Title must be unique for Posted date.']) + form = DerivedPostForm({'title': "Work on Django 1.1 begins", 'posted': '2008-09-03'}) + self.assertTrue(form.is_valid()) + form = DerivedPostForm({'title': "Django 1.0 is released", 'posted': '2008-09-04'}) + self.assertTrue(form.is_valid()) + form = DerivedPostForm({'slug': "Django 1.0", 'posted': '2008-01-01'}) + self.assertFalse(form.is_valid()) + self.assertEqual(len(form.errors), 1) + self.assertEqual(form.errors['slug'], [u'Slug must be unique for Posted year.']) + form = DerivedPostForm({'subtitle': "Finally", 'posted': '2008-09-30'}) + self.assertFalse(form.is_valid()) + self.assertEqual(form.errors['subtitle'], [u'Subtitle must be unique for Posted month.']) + form = DerivedPostForm({'subtitle': "Finally", "title": "Django 1.0 is released", + "slug": "Django 1.0", 'posted': '2008-09-03'}, instance=p) + self.assertTrue(form.is_valid()) + diff --git a/parts/django/tests/modeltests/model_formsets/__init__.py b/parts/django/tests/modeltests/model_formsets/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/modeltests/model_formsets/__init__.py diff --git a/parts/django/tests/modeltests/model_formsets/models.py b/parts/django/tests/modeltests/model_formsets/models.py new file mode 100644 index 0000000..3eca696 --- /dev/null +++ b/parts/django/tests/modeltests/model_formsets/models.py @@ -0,0 +1,193 @@ +import datetime +from django.db import models + +class Author(models.Model): + name = models.CharField(max_length=100) + + class Meta: + ordering = ('name',) + + def __unicode__(self): + return self.name + +class BetterAuthor(Author): + write_speed = models.IntegerField() + +class Book(models.Model): + author = models.ForeignKey(Author) + title = models.CharField(max_length=100) + + class Meta: + unique_together = ( + ('author', 'title'), + ) + ordering = ['id'] + + def __unicode__(self): + return self.title + +class BookWithCustomPK(models.Model): + my_pk = models.DecimalField(max_digits=5, decimal_places=0, primary_key=True) + author = models.ForeignKey(Author) + title = models.CharField(max_length=100) + + def __unicode__(self): + return u'%s: %s' % (self.my_pk, self.title) + +class Editor(models.Model): + name = models.CharField(max_length=100) + +class BookWithOptionalAltEditor(models.Model): + author = models.ForeignKey(Author) + # Optional secondary author + alt_editor = models.ForeignKey(Editor, blank=True, null=True) + title = models.CharField(max_length=100) + + class Meta: + unique_together = ( + ('author', 'title', 'alt_editor'), + ) + + def __unicode__(self): + return self.title + +class AlternateBook(Book): + notes = models.CharField(max_length=100) + + def __unicode__(self): + return u'%s - %s' % (self.title, self.notes) + +class AuthorMeeting(models.Model): + name = models.CharField(max_length=100) + authors = models.ManyToManyField(Author) + created = models.DateField(editable=False) + + def __unicode__(self): + return self.name + +class CustomPrimaryKey(models.Model): + my_pk = models.CharField(max_length=10, primary_key=True) + some_field = models.CharField(max_length=100) + + +# models for inheritance tests. + +class Place(models.Model): + name = models.CharField(max_length=50) + city = models.CharField(max_length=50) + + def __unicode__(self): + return self.name + +class Owner(models.Model): + auto_id = models.AutoField(primary_key=True) + name = models.CharField(max_length=100) + place = models.ForeignKey(Place) + + def __unicode__(self): + return "%s at %s" % (self.name, self.place) + +class Location(models.Model): + place = models.ForeignKey(Place, unique=True) + # this is purely for testing the data doesn't matter here :) + lat = models.CharField(max_length=100) + lon = models.CharField(max_length=100) + +class OwnerProfile(models.Model): + owner = models.OneToOneField(Owner, primary_key=True) + age = models.PositiveIntegerField() + + def __unicode__(self): + return "%s is %d" % (self.owner.name, self.age) + +class Restaurant(Place): + serves_pizza = models.BooleanField() + + def __unicode__(self): + return self.name + +class Product(models.Model): + slug = models.SlugField(unique=True) + + def __unicode__(self): + return self.slug + +class Price(models.Model): + price = models.DecimalField(max_digits=10, decimal_places=2) + quantity = models.PositiveIntegerField() + + def __unicode__(self): + return u"%s for %s" % (self.quantity, self.price) + + class Meta: + unique_together = (('price', 'quantity'),) + +class MexicanRestaurant(Restaurant): + serves_tacos = models.BooleanField() + +class ClassyMexicanRestaurant(MexicanRestaurant): + restaurant = models.OneToOneField(MexicanRestaurant, parent_link=True, primary_key=True) + tacos_are_yummy = models.BooleanField() + +# models for testing unique_together validation when a fk is involved and +# using inlineformset_factory. +class Repository(models.Model): + name = models.CharField(max_length=25) + + def __unicode__(self): + return self.name + +class Revision(models.Model): + repository = models.ForeignKey(Repository) + revision = models.CharField(max_length=40) + + class Meta: + unique_together = (("repository", "revision"),) + + def __unicode__(self): + return u"%s (%s)" % (self.revision, unicode(self.repository)) + +# models for testing callable defaults (see bug #7975). If you define a model +# with a callable default value, you cannot rely on the initial value in a +# form. +class Person(models.Model): + name = models.CharField(max_length=128) + +class Membership(models.Model): + person = models.ForeignKey(Person) + date_joined = models.DateTimeField(default=datetime.datetime.now) + karma = models.IntegerField() + +# models for testing a null=True fk to a parent +class Team(models.Model): + name = models.CharField(max_length=100) + +class Player(models.Model): + team = models.ForeignKey(Team, null=True) + name = models.CharField(max_length=100) + + def __unicode__(self): + return self.name + +# Models for testing custom ModelForm save methods in formsets and inline formsets +class Poet(models.Model): + name = models.CharField(max_length=100) + + def __unicode__(self): + return self.name + +class Poem(models.Model): + poet = models.ForeignKey(Poet) + name = models.CharField(max_length=100) + + def __unicode__(self): + return self.name + +class Post(models.Model): + title = models.CharField(max_length=50, unique_for_date='posted', blank=True) + slug = models.CharField(max_length=50, unique_for_year='posted', blank=True) + subtitle = models.CharField(max_length=50, unique_for_month='posted', blank=True) + posted = models.DateField() + + def __unicode__(self): + return self.name diff --git a/parts/django/tests/modeltests/model_formsets/tests.py b/parts/django/tests/modeltests/model_formsets/tests.py new file mode 100644 index 0000000..c856a5f --- /dev/null +++ b/parts/django/tests/modeltests/model_formsets/tests.py @@ -0,0 +1,1159 @@ +import datetime +import re +from datetime import date +from decimal import Decimal + +from django import forms +from django.db import models +from django.forms.models import (_get_foreign_key, inlineformset_factory, + modelformset_factory, modelformset_factory) +from django.test import TestCase + +from modeltests.model_formsets.models import ( + Author, BetterAuthor, Book, BookWithCustomPK, Editor, + BookWithOptionalAltEditor, AlternateBook, AuthorMeeting, CustomPrimaryKey, + Place, Owner, Location, OwnerProfile, Restaurant, Product, Price, + MexicanRestaurant, ClassyMexicanRestaurant, Repository, Revision, + Person, Membership, Team, Player, Poet, Poem, Post) + +class DeletionTests(TestCase): + def test_deletion(self): + PoetFormSet = modelformset_factory(Poet, can_delete=True) + poet = Poet.objects.create(name='test') + data = { + 'form-TOTAL_FORMS': u'1', + 'form-INITIAL_FORMS': u'1', + 'form-MAX_NUM_FORMS': u'0', + 'form-0-id': str(poet.pk), + 'form-0-name': u'test', + 'form-0-DELETE': u'on', + } + formset = PoetFormSet(data, queryset=Poet.objects.all()) + formset.save() + self.assertTrue(formset.is_valid()) + self.assertEqual(Poet.objects.count(), 0) + + def test_add_form_deletion_when_invalid(self): + """ + Make sure that an add form that is filled out, but marked for deletion + doesn't cause validation errors. + """ + PoetFormSet = modelformset_factory(Poet, can_delete=True) + data = { + 'form-TOTAL_FORMS': u'1', + 'form-INITIAL_FORMS': u'0', + 'form-MAX_NUM_FORMS': u'0', + 'form-0-id': u'', + 'form-0-name': u'x' * 1000, + } + formset = PoetFormSet(data, queryset=Poet.objects.all()) + # Make sure this form doesn't pass validation. + self.assertEqual(formset.is_valid(), False) + self.assertEqual(Poet.objects.count(), 0) + + # Then make sure that it *does* pass validation and delete the object, + # even though the data isn't actually valid. + data['form-0-DELETE'] = 'on' + formset = PoetFormSet(data, queryset=Poet.objects.all()) + self.assertEqual(formset.is_valid(), True) + formset.save() + self.assertEqual(Poet.objects.count(), 0) + + def test_change_form_deletion_when_invalid(self): + """ + Make sure that an add form that is filled out, but marked for deletion + doesn't cause validation errors. + """ + PoetFormSet = modelformset_factory(Poet, can_delete=True) + poet = Poet.objects.create(name='test') + data = { + 'form-TOTAL_FORMS': u'1', + 'form-INITIAL_FORMS': u'1', + 'form-MAX_NUM_FORMS': u'0', + 'form-0-id': u'1', + 'form-0-name': u'x' * 1000, + } + formset = PoetFormSet(data, queryset=Poet.objects.all()) + # Make sure this form doesn't pass validation. + self.assertEqual(formset.is_valid(), False) + self.assertEqual(Poet.objects.count(), 1) + + # Then make sure that it *does* pass validation and delete the object, + # even though the data isn't actually valid. + data['form-0-DELETE'] = 'on' + formset = PoetFormSet(data, queryset=Poet.objects.all()) + self.assertEqual(formset.is_valid(), True) + formset.save() + self.assertEqual(Poet.objects.count(), 0) + +class ModelFormsetTest(TestCase): + def test_simple_save(self): + qs = Author.objects.all() + AuthorFormSet = modelformset_factory(Author, extra=3) + + formset = AuthorFormSet(queryset=qs) + self.assertEqual(len(formset.forms), 3) + self.assertEqual(formset.forms[0].as_p(), + '<p><label for="id_form-0-name">Name:</label> <input id="id_form-0-name" type="text" name="form-0-name" maxlength="100" /><input type="hidden" name="form-0-id" id="id_form-0-id" /></p>') + self.assertEqual(formset.forms[1].as_p(), + '<p><label for="id_form-1-name">Name:</label> <input id="id_form-1-name" type="text" name="form-1-name" maxlength="100" /><input type="hidden" name="form-1-id" id="id_form-1-id" /></p>') + self.assertEqual(formset.forms[2].as_p(), + '<p><label for="id_form-2-name">Name:</label> <input id="id_form-2-name" type="text" name="form-2-name" maxlength="100" /><input type="hidden" name="form-2-id" id="id_form-2-id" /></p>') + + data = { + 'form-TOTAL_FORMS': '3', # the number of forms rendered + 'form-INITIAL_FORMS': '0', # the number of forms with initial data + 'form-MAX_NUM_FORMS': '', # the max number of forms + 'form-0-name': 'Charles Baudelaire', + 'form-1-name': 'Arthur Rimbaud', + 'form-2-name': '', + } + + formset = AuthorFormSet(data=data, queryset=qs) + self.assertTrue(formset.is_valid()) + + saved = formset.save() + self.assertEqual(len(saved), 2) + author1, author2 = saved + self.assertEqual(author1, Author.objects.get(name='Charles Baudelaire')) + self.assertEqual(author2, Author.objects.get(name='Arthur Rimbaud')) + + authors = list(Author.objects.order_by('name')) + self.assertEqual(authors, [author2, author1]) + + # Gah! We forgot Paul Verlaine. Let's create a formset to edit the + # existing authors with an extra form to add him. We *could* pass in a + # queryset to restrict the Author objects we edit, but in this case + # we'll use it to display them in alphabetical order by name. + + qs = Author.objects.order_by('name') + AuthorFormSet = modelformset_factory(Author, extra=1, can_delete=False) + + formset = AuthorFormSet(queryset=qs) + self.assertEqual(len(formset.forms), 3) + self.assertEqual(formset.forms[0].as_p(), + '<p><label for="id_form-0-name">Name:</label> <input id="id_form-0-name" type="text" name="form-0-name" value="Arthur Rimbaud" maxlength="100" /><input type="hidden" name="form-0-id" value="%d" id="id_form-0-id" /></p>' % author2.id) + self.assertEqual(formset.forms[1].as_p(), + '<p><label for="id_form-1-name">Name:</label> <input id="id_form-1-name" type="text" name="form-1-name" value="Charles Baudelaire" maxlength="100" /><input type="hidden" name="form-1-id" value="%d" id="id_form-1-id" /></p>' % author1.id) + self.assertEqual(formset.forms[2].as_p(), + '<p><label for="id_form-2-name">Name:</label> <input id="id_form-2-name" type="text" name="form-2-name" maxlength="100" /><input type="hidden" name="form-2-id" id="id_form-2-id" /></p>') + + data = { + 'form-TOTAL_FORMS': '3', # the number of forms rendered + 'form-INITIAL_FORMS': '2', # the number of forms with initial data + 'form-MAX_NUM_FORMS': '', # the max number of forms + 'form-0-id': str(author2.id), + 'form-0-name': 'Arthur Rimbaud', + 'form-1-id': str(author1.id), + 'form-1-name': 'Charles Baudelaire', + 'form-2-name': 'Paul Verlaine', + } + + formset = AuthorFormSet(data=data, queryset=qs) + self.assertTrue(formset.is_valid()) + + # Only changed or new objects are returned from formset.save() + saved = formset.save() + self.assertEqual(len(saved), 1) + author3 = saved[0] + self.assertEqual(author3, Author.objects.get(name='Paul Verlaine')) + + authors = list(Author.objects.order_by('name')) + self.assertEqual(authors, [author2, author1, author3]) + + # This probably shouldn't happen, but it will. If an add form was + # marked for deletion, make sure we don't save that form. + + qs = Author.objects.order_by('name') + AuthorFormSet = modelformset_factory(Author, extra=1, can_delete=True) + + formset = AuthorFormSet(queryset=qs) + self.assertEqual(len(formset.forms), 4) + self.assertEqual(formset.forms[0].as_p(), + '<p><label for="id_form-0-name">Name:</label> <input id="id_form-0-name" type="text" name="form-0-name" value="Arthur Rimbaud" maxlength="100" /></p>\n' + '<p><label for="id_form-0-DELETE">Delete:</label> <input type="checkbox" name="form-0-DELETE" id="id_form-0-DELETE" /><input type="hidden" name="form-0-id" value="%d" id="id_form-0-id" /></p>' % author2.id) + self.assertEqual(formset.forms[1].as_p(), + '<p><label for="id_form-1-name">Name:</label> <input id="id_form-1-name" type="text" name="form-1-name" value="Charles Baudelaire" maxlength="100" /></p>\n' + '<p><label for="id_form-1-DELETE">Delete:</label> <input type="checkbox" name="form-1-DELETE" id="id_form-1-DELETE" /><input type="hidden" name="form-1-id" value="%d" id="id_form-1-id" /></p>' % author1.id) + self.assertEqual(formset.forms[2].as_p(), + '<p><label for="id_form-2-name">Name:</label> <input id="id_form-2-name" type="text" name="form-2-name" value="Paul Verlaine" maxlength="100" /></p>\n' + '<p><label for="id_form-2-DELETE">Delete:</label> <input type="checkbox" name="form-2-DELETE" id="id_form-2-DELETE" /><input type="hidden" name="form-2-id" value="%d" id="id_form-2-id" /></p>' % author3.id) + self.assertEqual(formset.forms[3].as_p(), + '<p><label for="id_form-3-name">Name:</label> <input id="id_form-3-name" type="text" name="form-3-name" maxlength="100" /></p>\n' + '<p><label for="id_form-3-DELETE">Delete:</label> <input type="checkbox" name="form-3-DELETE" id="id_form-3-DELETE" /><input type="hidden" name="form-3-id" id="id_form-3-id" /></p>') + + data = { + 'form-TOTAL_FORMS': '4', # the number of forms rendered + 'form-INITIAL_FORMS': '3', # the number of forms with initial data + 'form-MAX_NUM_FORMS': '', # the max number of forms + 'form-0-id': str(author2.id), + 'form-0-name': 'Arthur Rimbaud', + 'form-1-id': str(author1.id), + 'form-1-name': 'Charles Baudelaire', + 'form-2-id': str(author3.id), + 'form-2-name': 'Paul Verlaine', + 'form-3-name': 'Walt Whitman', + 'form-3-DELETE': 'on', + } + + formset = AuthorFormSet(data=data, queryset=qs) + self.assertTrue(formset.is_valid()) + + # No objects were changed or saved so nothing will come back. + + self.assertEqual(formset.save(), []) + + authors = list(Author.objects.order_by('name')) + self.assertEqual(authors, [author2, author1, author3]) + + # Let's edit a record to ensure save only returns that one record. + + data = { + 'form-TOTAL_FORMS': '4', # the number of forms rendered + 'form-INITIAL_FORMS': '3', # the number of forms with initial data + 'form-MAX_NUM_FORMS': '', # the max number of forms + 'form-0-id': str(author2.id), + 'form-0-name': 'Walt Whitman', + 'form-1-id': str(author1.id), + 'form-1-name': 'Charles Baudelaire', + 'form-2-id': str(author3.id), + 'form-2-name': 'Paul Verlaine', + 'form-3-name': '', + 'form-3-DELETE': '', + } + + formset = AuthorFormSet(data=data, queryset=qs) + self.assertTrue(formset.is_valid()) + + # One record has changed. + + saved = formset.save() + self.assertEqual(len(saved), 1) + self.assertEqual(saved[0], Author.objects.get(name='Walt Whitman')) + + def test_commit_false(self): + # Test the behavior of commit=False and save_m2m + + author1 = Author.objects.create(name='Charles Baudelaire') + author2 = Author.objects.create(name='Paul Verlaine') + author3 = Author.objects.create(name='Walt Whitman') + + meeting = AuthorMeeting.objects.create(created=date.today()) + meeting.authors = Author.objects.all() + + # create an Author instance to add to the meeting. + + author4 = Author.objects.create(name=u'John Steinbeck') + + AuthorMeetingFormSet = modelformset_factory(AuthorMeeting, extra=1, can_delete=True) + data = { + 'form-TOTAL_FORMS': '2', # the number of forms rendered + 'form-INITIAL_FORMS': '1', # the number of forms with initial data + 'form-MAX_NUM_FORMS': '', # the max number of forms + 'form-0-id': '1', + 'form-0-name': '2nd Tuesday of the Week Meeting', + 'form-0-authors': [2, 1, 3, 4], + 'form-1-name': '', + 'form-1-authors': '', + 'form-1-DELETE': '', + } + formset = AuthorMeetingFormSet(data=data, queryset=AuthorMeeting.objects.all()) + self.assertTrue(formset.is_valid()) + + instances = formset.save(commit=False) + for instance in instances: + instance.created = date.today() + instance.save() + formset.save_m2m() + self.assertQuerysetEqual(instances[0].authors.all(), [ + '<Author: Charles Baudelaire>', + '<Author: John Steinbeck>', + '<Author: Paul Verlaine>', + '<Author: Walt Whitman>', + ]) + + def test_max_num(self): + # Test the behavior of max_num with model formsets. It should allow + # all existing related objects/inlines for a given object to be + # displayed, but not allow the creation of new inlines beyond max_num. + + author1 = Author.objects.create(name='Charles Baudelaire') + author2 = Author.objects.create(name='Paul Verlaine') + author3 = Author.objects.create(name='Walt Whitman') + + qs = Author.objects.order_by('name') + + AuthorFormSet = modelformset_factory(Author, max_num=None, extra=3) + formset = AuthorFormSet(queryset=qs) + self.assertEqual(len(formset.forms), 6) + self.assertEqual(len(formset.extra_forms), 3) + + AuthorFormSet = modelformset_factory(Author, max_num=4, extra=3) + formset = AuthorFormSet(queryset=qs) + self.assertEqual(len(formset.forms), 4) + self.assertEqual(len(formset.extra_forms), 1) + + AuthorFormSet = modelformset_factory(Author, max_num=0, extra=3) + formset = AuthorFormSet(queryset=qs) + self.assertEqual(len(formset.forms), 3) + self.assertEqual(len(formset.extra_forms), 0) + + AuthorFormSet = modelformset_factory(Author, max_num=None) + formset = AuthorFormSet(queryset=qs) + self.assertQuerysetEqual(formset.get_queryset(), [ + '<Author: Charles Baudelaire>', + '<Author: Paul Verlaine>', + '<Author: Walt Whitman>', + ]) + + AuthorFormSet = modelformset_factory(Author, max_num=0) + formset = AuthorFormSet(queryset=qs) + self.assertQuerysetEqual(formset.get_queryset(), [ + '<Author: Charles Baudelaire>', + '<Author: Paul Verlaine>', + '<Author: Walt Whitman>', + ]) + + AuthorFormSet = modelformset_factory(Author, max_num=4) + formset = AuthorFormSet(queryset=qs) + self.assertQuerysetEqual(formset.get_queryset(), [ + '<Author: Charles Baudelaire>', + '<Author: Paul Verlaine>', + '<Author: Walt Whitman>', + ]) + + def test_custom_save_method(self): + class PoetForm(forms.ModelForm): + def save(self, commit=True): + # change the name to "Vladimir Mayakovsky" just to be a jerk. + author = super(PoetForm, self).save(commit=False) + author.name = u"Vladimir Mayakovsky" + if commit: + author.save() + return author + + PoetFormSet = modelformset_factory(Poet, form=PoetForm) + + data = { + 'form-TOTAL_FORMS': '3', # the number of forms rendered + 'form-INITIAL_FORMS': '0', # the number of forms with initial data + 'form-MAX_NUM_FORMS': '', # the max number of forms + 'form-0-name': 'Walt Whitman', + 'form-1-name': 'Charles Baudelaire', + 'form-2-name': '', + } + + qs = Poet.objects.all() + formset = PoetFormSet(data=data, queryset=qs) + self.assertTrue(formset.is_valid()) + + poets = formset.save() + self.assertEqual(len(poets), 2) + poet1, poet2 = poets + self.assertEqual(poet1.name, 'Vladimir Mayakovsky') + self.assertEqual(poet2.name, 'Vladimir Mayakovsky') + + def test_model_inheritance(self): + BetterAuthorFormSet = modelformset_factory(BetterAuthor) + formset = BetterAuthorFormSet() + self.assertEqual(len(formset.forms), 1) + self.assertEqual(formset.forms[0].as_p(), + '<p><label for="id_form-0-name">Name:</label> <input id="id_form-0-name" type="text" name="form-0-name" maxlength="100" /></p>\n' + '<p><label for="id_form-0-write_speed">Write speed:</label> <input type="text" name="form-0-write_speed" id="id_form-0-write_speed" /><input type="hidden" name="form-0-author_ptr" id="id_form-0-author_ptr" /></p>') + + data = { + 'form-TOTAL_FORMS': '1', # the number of forms rendered + 'form-INITIAL_FORMS': '0', # the number of forms with initial data + 'form-MAX_NUM_FORMS': '', # the max number of forms + 'form-0-author_ptr': '', + 'form-0-name': 'Ernest Hemingway', + 'form-0-write_speed': '10', + } + + formset = BetterAuthorFormSet(data) + self.assertTrue(formset.is_valid()) + saved = formset.save() + self.assertEqual(len(saved), 1) + author1, = saved + self.assertEqual(author1, BetterAuthor.objects.get(name='Ernest Hemingway')) + hemingway_id = BetterAuthor.objects.get(name="Ernest Hemingway").pk + + formset = BetterAuthorFormSet() + self.assertEqual(len(formset.forms), 2) + self.assertEqual(formset.forms[0].as_p(), + '<p><label for="id_form-0-name">Name:</label> <input id="id_form-0-name" type="text" name="form-0-name" value="Ernest Hemingway" maxlength="100" /></p>\n' + '<p><label for="id_form-0-write_speed">Write speed:</label> <input type="text" name="form-0-write_speed" value="10" id="id_form-0-write_speed" /><input type="hidden" name="form-0-author_ptr" value="%d" id="id_form-0-author_ptr" /></p>' % hemingway_id) + self.assertEqual(formset.forms[1].as_p(), + '<p><label for="id_form-1-name">Name:</label> <input id="id_form-1-name" type="text" name="form-1-name" maxlength="100" /></p>\n' + '<p><label for="id_form-1-write_speed">Write speed:</label> <input type="text" name="form-1-write_speed" id="id_form-1-write_speed" /><input type="hidden" name="form-1-author_ptr" id="id_form-1-author_ptr" /></p>') + + data = { + 'form-TOTAL_FORMS': '2', # the number of forms rendered + 'form-INITIAL_FORMS': '1', # the number of forms with initial data + 'form-MAX_NUM_FORMS': '', # the max number of forms + 'form-0-author_ptr': hemingway_id, + 'form-0-name': 'Ernest Hemingway', + 'form-0-write_speed': '10', + 'form-1-author_ptr': '', + 'form-1-name': '', + 'form-1-write_speed': '', + } + + formset = BetterAuthorFormSet(data) + self.assertTrue(formset.is_valid()) + self.assertEqual(formset.save(), []) + + def test_inline_formsets(self): + # We can also create a formset that is tied to a parent model. This is + # how the admin system's edit inline functionality works. + + AuthorBooksFormSet = inlineformset_factory(Author, Book, can_delete=False, extra=3) + author = Author.objects.create(name='Charles Baudelaire') + + formset = AuthorBooksFormSet(instance=author) + self.assertEqual(len(formset.forms), 3) + self.assertEqual(formset.forms[0].as_p(), + '<p><label for="id_book_set-0-title">Title:</label> <input id="id_book_set-0-title" type="text" name="book_set-0-title" maxlength="100" /><input type="hidden" name="book_set-0-author" value="%d" id="id_book_set-0-author" /><input type="hidden" name="book_set-0-id" id="id_book_set-0-id" /></p>' % author.id) + self.assertEqual(formset.forms[1].as_p(), + '<p><label for="id_book_set-1-title">Title:</label> <input id="id_book_set-1-title" type="text" name="book_set-1-title" maxlength="100" /><input type="hidden" name="book_set-1-author" value="%d" id="id_book_set-1-author" /><input type="hidden" name="book_set-1-id" id="id_book_set-1-id" /></p>' % author.id) + self.assertEqual(formset.forms[2].as_p(), + '<p><label for="id_book_set-2-title">Title:</label> <input id="id_book_set-2-title" type="text" name="book_set-2-title" maxlength="100" /><input type="hidden" name="book_set-2-author" value="%d" id="id_book_set-2-author" /><input type="hidden" name="book_set-2-id" id="id_book_set-2-id" /></p>' % author.id) + + data = { + 'book_set-TOTAL_FORMS': '3', # the number of forms rendered + 'book_set-INITIAL_FORMS': '0', # the number of forms with initial data + 'book_set-MAX_NUM_FORMS': '', # the max number of forms + 'book_set-0-title': 'Les Fleurs du Mal', + 'book_set-1-title': '', + 'book_set-2-title': '', + } + + formset = AuthorBooksFormSet(data, instance=author) + self.assertTrue(formset.is_valid()) + + saved = formset.save() + self.assertEqual(len(saved), 1) + book1, = saved + self.assertEqual(book1, Book.objects.get(title='Les Fleurs du Mal')) + self.assertQuerysetEqual(author.book_set.all(), ['<Book: Les Fleurs du Mal>']) + + # Now that we've added a book to Charles Baudelaire, let's try adding + # another one. This time though, an edit form will be available for + # every existing book. + + AuthorBooksFormSet = inlineformset_factory(Author, Book, can_delete=False, extra=2) + author = Author.objects.get(name='Charles Baudelaire') + + formset = AuthorBooksFormSet(instance=author) + self.assertEqual(len(formset.forms), 3) + self.assertEqual(formset.forms[0].as_p(), + '<p><label for="id_book_set-0-title">Title:</label> <input id="id_book_set-0-title" type="text" name="book_set-0-title" value="Les Fleurs du Mal" maxlength="100" /><input type="hidden" name="book_set-0-author" value="%d" id="id_book_set-0-author" /><input type="hidden" name="book_set-0-id" value="%d" id="id_book_set-0-id" /></p>' % (author.id, book1.id)) + self.assertEqual(formset.forms[1].as_p(), + '<p><label for="id_book_set-1-title">Title:</label> <input id="id_book_set-1-title" type="text" name="book_set-1-title" maxlength="100" /><input type="hidden" name="book_set-1-author" value="%d" id="id_book_set-1-author" /><input type="hidden" name="book_set-1-id" id="id_book_set-1-id" /></p>' % author.id) + self.assertEqual(formset.forms[2].as_p(), + '<p><label for="id_book_set-2-title">Title:</label> <input id="id_book_set-2-title" type="text" name="book_set-2-title" maxlength="100" /><input type="hidden" name="book_set-2-author" value="%d" id="id_book_set-2-author" /><input type="hidden" name="book_set-2-id" id="id_book_set-2-id" /></p>' % author.id) + + data = { + 'book_set-TOTAL_FORMS': '3', # the number of forms rendered + 'book_set-INITIAL_FORMS': '1', # the number of forms with initial data + 'book_set-MAX_NUM_FORMS': '', # the max number of forms + 'book_set-0-id': '1', + 'book_set-0-title': 'Les Fleurs du Mal', + 'book_set-1-title': 'Les Paradis Artificiels', + 'book_set-2-title': '', + } + + formset = AuthorBooksFormSet(data, instance=author) + self.assertTrue(formset.is_valid()) + + saved = formset.save() + self.assertEqual(len(saved), 1) + book2, = saved + self.assertEqual(book2, Book.objects.get(title='Les Paradis Artificiels')) + + # As you can see, 'Les Paradis Artificiels' is now a book belonging to + # Charles Baudelaire. + self.assertQuerysetEqual(author.book_set.order_by('title'), [ + '<Book: Les Fleurs du Mal>', + '<Book: Les Paradis Artificiels>', + ]) + + def test_inline_formsets_save_as_new(self): + # The save_as_new parameter lets you re-associate the data to a new + # instance. This is used in the admin for save_as functionality. + AuthorBooksFormSet = inlineformset_factory(Author, Book, can_delete=False, extra=2) + author = Author.objects.create(name='Charles Baudelaire') + + data = { + 'book_set-TOTAL_FORMS': '3', # the number of forms rendered + 'book_set-INITIAL_FORMS': '2', # the number of forms with initial data + 'book_set-MAX_NUM_FORMS': '', # the max number of forms + 'book_set-0-id': '1', + 'book_set-0-title': 'Les Fleurs du Mal', + 'book_set-1-id': '2', + 'book_set-1-title': 'Les Paradis Artificiels', + 'book_set-2-title': '', + } + + formset = AuthorBooksFormSet(data, instance=Author(), save_as_new=True) + self.assertTrue(formset.is_valid()) + + new_author = Author.objects.create(name='Charles Baudelaire') + formset = AuthorBooksFormSet(data, instance=new_author, save_as_new=True) + saved = formset.save() + self.assertEqual(len(saved), 2) + book1, book2 = saved + self.assertEqual(book1.title, 'Les Fleurs du Mal') + self.assertEqual(book2.title, 'Les Paradis Artificiels') + + # Test using a custom prefix on an inline formset. + + formset = AuthorBooksFormSet(prefix="test") + self.assertEqual(len(formset.forms), 2) + self.assertEqual(formset.forms[0].as_p(), + '<p><label for="id_test-0-title">Title:</label> <input id="id_test-0-title" type="text" name="test-0-title" maxlength="100" /><input type="hidden" name="test-0-author" id="id_test-0-author" /><input type="hidden" name="test-0-id" id="id_test-0-id" /></p>') + self.assertEqual(formset.forms[1].as_p(), + '<p><label for="id_test-1-title">Title:</label> <input id="id_test-1-title" type="text" name="test-1-title" maxlength="100" /><input type="hidden" name="test-1-author" id="id_test-1-author" /><input type="hidden" name="test-1-id" id="id_test-1-id" /></p>') + + def test_inline_formsets_with_custom_pk(self): + # Test inline formsets where the inline-edited object has a custom + # primary key that is not the fk to the parent object. + + AuthorBooksFormSet2 = inlineformset_factory(Author, BookWithCustomPK, can_delete=False, extra=1) + author = Author.objects.create(pk=1, name='Charles Baudelaire') + + formset = AuthorBooksFormSet2(instance=author) + self.assertEqual(len(formset.forms), 1) + self.assertEqual(formset.forms[0].as_p(), + '<p><label for="id_bookwithcustompk_set-0-my_pk">My pk:</label> <input type="text" name="bookwithcustompk_set-0-my_pk" id="id_bookwithcustompk_set-0-my_pk" /></p>\n' + '<p><label for="id_bookwithcustompk_set-0-title">Title:</label> <input id="id_bookwithcustompk_set-0-title" type="text" name="bookwithcustompk_set-0-title" maxlength="100" /><input type="hidden" name="bookwithcustompk_set-0-author" value="1" id="id_bookwithcustompk_set-0-author" /></p>') + + data = { + 'bookwithcustompk_set-TOTAL_FORMS': '1', # the number of forms rendered + 'bookwithcustompk_set-INITIAL_FORMS': '0', # the number of forms with initial data + 'bookwithcustompk_set-MAX_NUM_FORMS': '', # the max number of forms + 'bookwithcustompk_set-0-my_pk': '77777', + 'bookwithcustompk_set-0-title': 'Les Fleurs du Mal', + } + + formset = AuthorBooksFormSet2(data, instance=author) + self.assertTrue(formset.is_valid()) + + saved = formset.save() + self.assertEqual(len(saved), 1) + book1, = saved + self.assertEqual(book1.pk, 77777) + + book1 = author.bookwithcustompk_set.get() + self.assertEqual(book1.title, 'Les Fleurs du Mal') + + def test_inline_formsets_with_multi_table_inheritance(self): + # Test inline formsets where the inline-edited object uses multi-table + # inheritance, thus has a non AutoField yet auto-created primary key. + + AuthorBooksFormSet3 = inlineformset_factory(Author, AlternateBook, can_delete=False, extra=1) + author = Author.objects.create(pk=1, name='Charles Baudelaire') + + formset = AuthorBooksFormSet3(instance=author) + self.assertEqual(len(formset.forms), 1) + self.assertEqual(formset.forms[0].as_p(), + '<p><label for="id_alternatebook_set-0-title">Title:</label> <input id="id_alternatebook_set-0-title" type="text" name="alternatebook_set-0-title" maxlength="100" /></p>\n' + '<p><label for="id_alternatebook_set-0-notes">Notes:</label> <input id="id_alternatebook_set-0-notes" type="text" name="alternatebook_set-0-notes" maxlength="100" /><input type="hidden" name="alternatebook_set-0-author" value="1" id="id_alternatebook_set-0-author" /><input type="hidden" name="alternatebook_set-0-book_ptr" id="id_alternatebook_set-0-book_ptr" /></p>') + + data = { + 'alternatebook_set-TOTAL_FORMS': '1', # the number of forms rendered + 'alternatebook_set-INITIAL_FORMS': '0', # the number of forms with initial data + 'alternatebook_set-MAX_NUM_FORMS': '', # the max number of forms + 'alternatebook_set-0-title': 'Flowers of Evil', + 'alternatebook_set-0-notes': 'English translation of Les Fleurs du Mal' + } + + formset = AuthorBooksFormSet3(data, instance=author) + self.assertTrue(formset.is_valid()) + + saved = formset.save() + self.assertEqual(len(saved), 1) + book1, = saved + self.assertEqual(book1.title, 'Flowers of Evil') + self.assertEqual(book1.notes, 'English translation of Les Fleurs du Mal') + + # Test inline formsets where the inline-edited object has a + # unique_together constraint with a nullable member + + AuthorBooksFormSet4 = inlineformset_factory(Author, BookWithOptionalAltEditor, can_delete=False, extra=2) + + data = { + 'bookwithoptionalalteditor_set-TOTAL_FORMS': '2', # the number of forms rendered + 'bookwithoptionalalteditor_set-INITIAL_FORMS': '0', # the number of forms with initial data + 'bookwithoptionalalteditor_set-MAX_NUM_FORMS': '', # the max number of forms + 'bookwithoptionalalteditor_set-0-author': '1', + 'bookwithoptionalalteditor_set-0-title': 'Les Fleurs du Mal', + 'bookwithoptionalalteditor_set-1-author': '1', + 'bookwithoptionalalteditor_set-1-title': 'Les Fleurs du Mal', + } + formset = AuthorBooksFormSet4(data, instance=author) + self.assertTrue(formset.is_valid()) + + saved = formset.save() + self.assertEqual(len(saved), 2) + book1, book2 = saved + self.assertEqual(book1.author_id, 1) + self.assertEqual(book1.title, 'Les Fleurs du Mal') + self.assertEqual(book2.author_id, 1) + self.assertEqual(book2.title, 'Les Fleurs du Mal') + + def test_inline_formsets_with_custom_save_method(self): + AuthorBooksFormSet = inlineformset_factory(Author, Book, can_delete=False, extra=2) + author = Author.objects.create(pk=1, name='Charles Baudelaire') + book1 = Book.objects.create(pk=1, author=author, title='Les Paradis Artificiels') + book2 = Book.objects.create(pk=2, author=author, title='Les Fleurs du Mal') + book3 = Book.objects.create(pk=3, author=author, title='Flowers of Evil') + + class PoemForm(forms.ModelForm): + def save(self, commit=True): + # change the name to "Brooklyn Bridge" just to be a jerk. + poem = super(PoemForm, self).save(commit=False) + poem.name = u"Brooklyn Bridge" + if commit: + poem.save() + return poem + + PoemFormSet = inlineformset_factory(Poet, Poem, form=PoemForm) + + data = { + 'poem_set-TOTAL_FORMS': '3', # the number of forms rendered + 'poem_set-INITIAL_FORMS': '0', # the number of forms with initial data + 'poem_set-MAX_NUM_FORMS': '', # the max number of forms + 'poem_set-0-name': 'The Cloud in Trousers', + 'poem_set-1-name': 'I', + 'poem_set-2-name': '', + } + + poet = Poet.objects.create(name='Vladimir Mayakovsky') + formset = PoemFormSet(data=data, instance=poet) + self.assertTrue(formset.is_valid()) + + saved = formset.save() + self.assertEqual(len(saved), 2) + poem1, poem2 = saved + self.assertEqual(poem1.name, 'Brooklyn Bridge') + self.assertEqual(poem2.name, 'Brooklyn Bridge') + + # We can provide a custom queryset to our InlineFormSet: + + custom_qs = Book.objects.order_by('-title') + formset = AuthorBooksFormSet(instance=author, queryset=custom_qs) + self.assertEqual(len(formset.forms), 5) + self.assertEqual(formset.forms[0].as_p(), + '<p><label for="id_book_set-0-title">Title:</label> <input id="id_book_set-0-title" type="text" name="book_set-0-title" value="Les Paradis Artificiels" maxlength="100" /><input type="hidden" name="book_set-0-author" value="1" id="id_book_set-0-author" /><input type="hidden" name="book_set-0-id" value="1" id="id_book_set-0-id" /></p>') + self.assertEqual(formset.forms[1].as_p(), + '<p><label for="id_book_set-1-title">Title:</label> <input id="id_book_set-1-title" type="text" name="book_set-1-title" value="Les Fleurs du Mal" maxlength="100" /><input type="hidden" name="book_set-1-author" value="1" id="id_book_set-1-author" /><input type="hidden" name="book_set-1-id" value="2" id="id_book_set-1-id" /></p>') + self.assertEqual(formset.forms[2].as_p(), + '<p><label for="id_book_set-2-title">Title:</label> <input id="id_book_set-2-title" type="text" name="book_set-2-title" value="Flowers of Evil" maxlength="100" /><input type="hidden" name="book_set-2-author" value="1" id="id_book_set-2-author" /><input type="hidden" name="book_set-2-id" value="3" id="id_book_set-2-id" /></p>') + self.assertEqual(formset.forms[3].as_p(), + '<p><label for="id_book_set-3-title">Title:</label> <input id="id_book_set-3-title" type="text" name="book_set-3-title" maxlength="100" /><input type="hidden" name="book_set-3-author" value="1" id="id_book_set-3-author" /><input type="hidden" name="book_set-3-id" id="id_book_set-3-id" /></p>') + self.assertEqual(formset.forms[4].as_p(), + '<p><label for="id_book_set-4-title">Title:</label> <input id="id_book_set-4-title" type="text" name="book_set-4-title" maxlength="100" /><input type="hidden" name="book_set-4-author" value="1" id="id_book_set-4-author" /><input type="hidden" name="book_set-4-id" id="id_book_set-4-id" /></p>') + + data = { + 'book_set-TOTAL_FORMS': '5', # the number of forms rendered + 'book_set-INITIAL_FORMS': '3', # the number of forms with initial data + 'book_set-MAX_NUM_FORMS': '', # the max number of forms + 'book_set-0-id': str(book1.id), + 'book_set-0-title': 'Les Paradis Artificiels', + 'book_set-1-id': str(book2.id), + 'book_set-1-title': 'Les Fleurs du Mal', + 'book_set-2-id': str(book3.id), + 'book_set-2-title': 'Flowers of Evil', + 'book_set-3-title': 'Revue des deux mondes', + 'book_set-4-title': '', + } + formset = AuthorBooksFormSet(data, instance=author, queryset=custom_qs) + self.assertTrue(formset.is_valid()) + + custom_qs = Book.objects.filter(title__startswith='F') + formset = AuthorBooksFormSet(instance=author, queryset=custom_qs) + self.assertEqual(formset.forms[0].as_p(), + '<p><label for="id_book_set-0-title">Title:</label> <input id="id_book_set-0-title" type="text" name="book_set-0-title" value="Flowers of Evil" maxlength="100" /><input type="hidden" name="book_set-0-author" value="1" id="id_book_set-0-author" /><input type="hidden" name="book_set-0-id" value="3" id="id_book_set-0-id" /></p>') + self.assertEqual(formset.forms[1].as_p(), + '<p><label for="id_book_set-1-title">Title:</label> <input id="id_book_set-1-title" type="text" name="book_set-1-title" maxlength="100" /><input type="hidden" name="book_set-1-author" value="1" id="id_book_set-1-author" /><input type="hidden" name="book_set-1-id" id="id_book_set-1-id" /></p>') + self.assertEqual(formset.forms[2].as_p(), + '<p><label for="id_book_set-2-title">Title:</label> <input id="id_book_set-2-title" type="text" name="book_set-2-title" maxlength="100" /><input type="hidden" name="book_set-2-author" value="1" id="id_book_set-2-author" /><input type="hidden" name="book_set-2-id" id="id_book_set-2-id" /></p>') + + data = { + 'book_set-TOTAL_FORMS': '3', # the number of forms rendered + 'book_set-INITIAL_FORMS': '1', # the number of forms with initial data + 'book_set-MAX_NUM_FORMS': '', # the max number of forms + 'book_set-0-id': str(book3.id), + 'book_set-0-title': 'Flowers of Evil', + 'book_set-1-title': 'Revue des deux mondes', + 'book_set-2-title': '', + } + formset = AuthorBooksFormSet(data, instance=author, queryset=custom_qs) + self.assertTrue(formset.is_valid()) + + def test_custom_pk(self): + # We need to ensure that it is displayed + + CustomPrimaryKeyFormSet = modelformset_factory(CustomPrimaryKey) + formset = CustomPrimaryKeyFormSet() + self.assertEqual(len(formset.forms), 1) + self.assertEqual(formset.forms[0].as_p(), + '<p><label for="id_form-0-my_pk">My pk:</label> <input id="id_form-0-my_pk" type="text" name="form-0-my_pk" maxlength="10" /></p>\n' + '<p><label for="id_form-0-some_field">Some field:</label> <input id="id_form-0-some_field" type="text" name="form-0-some_field" maxlength="100" /></p>') + + # Custom primary keys with ForeignKey, OneToOneField and AutoField ############ + + place = Place.objects.create(pk=1, name=u'Giordanos', city=u'Chicago') + + FormSet = inlineformset_factory(Place, Owner, extra=2, can_delete=False) + formset = FormSet(instance=place) + self.assertEqual(len(formset.forms), 2) + self.assertEqual(formset.forms[0].as_p(), + '<p><label for="id_owner_set-0-name">Name:</label> <input id="id_owner_set-0-name" type="text" name="owner_set-0-name" maxlength="100" /><input type="hidden" name="owner_set-0-place" value="1" id="id_owner_set-0-place" /><input type="hidden" name="owner_set-0-auto_id" id="id_owner_set-0-auto_id" /></p>') + self.assertEqual(formset.forms[1].as_p(), + '<p><label for="id_owner_set-1-name">Name:</label> <input id="id_owner_set-1-name" type="text" name="owner_set-1-name" maxlength="100" /><input type="hidden" name="owner_set-1-place" value="1" id="id_owner_set-1-place" /><input type="hidden" name="owner_set-1-auto_id" id="id_owner_set-1-auto_id" /></p>') + + data = { + 'owner_set-TOTAL_FORMS': '2', + 'owner_set-INITIAL_FORMS': '0', + 'owner_set-MAX_NUM_FORMS': '', + 'owner_set-0-auto_id': '', + 'owner_set-0-name': u'Joe Perry', + 'owner_set-1-auto_id': '', + 'owner_set-1-name': '', + } + formset = FormSet(data, instance=place) + self.assertTrue(formset.is_valid()) + saved = formset.save() + self.assertEqual(len(saved), 1) + owner, = saved + self.assertEqual(owner.name, 'Joe Perry') + self.assertEqual(owner.place.name, 'Giordanos') + + formset = FormSet(instance=place) + self.assertEqual(len(formset.forms), 3) + self.assertEqual(formset.forms[0].as_p(), + '<p><label for="id_owner_set-0-name">Name:</label> <input id="id_owner_set-0-name" type="text" name="owner_set-0-name" value="Joe Perry" maxlength="100" /><input type="hidden" name="owner_set-0-place" value="1" id="id_owner_set-0-place" /><input type="hidden" name="owner_set-0-auto_id" value="1" id="id_owner_set-0-auto_id" /></p>') + self.assertEqual(formset.forms[1].as_p(), + '<p><label for="id_owner_set-1-name">Name:</label> <input id="id_owner_set-1-name" type="text" name="owner_set-1-name" maxlength="100" /><input type="hidden" name="owner_set-1-place" value="1" id="id_owner_set-1-place" /><input type="hidden" name="owner_set-1-auto_id" id="id_owner_set-1-auto_id" /></p>') + self.assertEqual(formset.forms[2].as_p(), + '<p><label for="id_owner_set-2-name">Name:</label> <input id="id_owner_set-2-name" type="text" name="owner_set-2-name" maxlength="100" /><input type="hidden" name="owner_set-2-place" value="1" id="id_owner_set-2-place" /><input type="hidden" name="owner_set-2-auto_id" id="id_owner_set-2-auto_id" /></p>') + + data = { + 'owner_set-TOTAL_FORMS': '3', + 'owner_set-INITIAL_FORMS': '1', + 'owner_set-MAX_NUM_FORMS': '', + 'owner_set-0-auto_id': u'1', + 'owner_set-0-name': u'Joe Perry', + 'owner_set-1-auto_id': '', + 'owner_set-1-name': u'Jack Berry', + 'owner_set-2-auto_id': '', + 'owner_set-2-name': '', + } + formset = FormSet(data, instance=place) + self.assertTrue(formset.is_valid()) + saved = formset.save() + self.assertEqual(len(saved), 1) + owner, = saved + self.assertEqual(owner.name, 'Jack Berry') + self.assertEqual(owner.place.name, 'Giordanos') + + # Ensure a custom primary key that is a ForeignKey or OneToOneField get rendered for the user to choose. + + FormSet = modelformset_factory(OwnerProfile) + formset = FormSet() + self.assertEqual(formset.forms[0].as_p(), + '<p><label for="id_form-0-owner">Owner:</label> <select name="form-0-owner" id="id_form-0-owner">\n' + '<option value="" selected="selected">---------</option>\n' + '<option value="1">Joe Perry at Giordanos</option>\n' + '<option value="2">Jack Berry at Giordanos</option>\n' + '</select></p>\n' + '<p><label for="id_form-0-age">Age:</label> <input type="text" name="form-0-age" id="id_form-0-age" /></p>') + + owner = Owner.objects.get(name=u'Joe Perry') + FormSet = inlineformset_factory(Owner, OwnerProfile, max_num=1, can_delete=False) + self.assertEqual(FormSet.max_num, 1) + + formset = FormSet(instance=owner) + self.assertEqual(len(formset.forms), 1) + self.assertEqual(formset.forms[0].as_p(), + '<p><label for="id_ownerprofile-0-age">Age:</label> <input type="text" name="ownerprofile-0-age" id="id_ownerprofile-0-age" /><input type="hidden" name="ownerprofile-0-owner" value="1" id="id_ownerprofile-0-owner" /></p>') + + data = { + 'ownerprofile-TOTAL_FORMS': '1', + 'ownerprofile-INITIAL_FORMS': '0', + 'ownerprofile-MAX_NUM_FORMS': '1', + 'ownerprofile-0-owner': '', + 'ownerprofile-0-age': u'54', + } + formset = FormSet(data, instance=owner) + self.assertTrue(formset.is_valid()) + saved = formset.save() + self.assertEqual(len(saved), 1) + profile1, = saved + self.assertEqual(profile1.owner, owner) + self.assertEqual(profile1.age, 54) + + formset = FormSet(instance=owner) + self.assertEqual(len(formset.forms), 1) + self.assertEqual(formset.forms[0].as_p(), + '<p><label for="id_ownerprofile-0-age">Age:</label> <input type="text" name="ownerprofile-0-age" value="54" id="id_ownerprofile-0-age" /><input type="hidden" name="ownerprofile-0-owner" value="1" id="id_ownerprofile-0-owner" /></p>') + + data = { + 'ownerprofile-TOTAL_FORMS': '1', + 'ownerprofile-INITIAL_FORMS': '1', + 'ownerprofile-MAX_NUM_FORMS': '1', + 'ownerprofile-0-owner': u'1', + 'ownerprofile-0-age': u'55', + } + formset = FormSet(data, instance=owner) + self.assertTrue(formset.is_valid()) + saved = formset.save() + self.assertEqual(len(saved), 1) + profile1, = saved + self.assertEqual(profile1.owner, owner) + self.assertEqual(profile1.age, 55) + + def test_unique_true_enforces_max_num_one(self): + # ForeignKey with unique=True should enforce max_num=1 + + place = Place.objects.create(pk=1, name=u'Giordanos', city=u'Chicago') + + FormSet = inlineformset_factory(Place, Location, can_delete=False) + self.assertEqual(FormSet.max_num, 1) + + formset = FormSet(instance=place) + self.assertEqual(len(formset.forms), 1) + self.assertEqual(formset.forms[0].as_p(), + '<p><label for="id_location_set-0-lat">Lat:</label> <input id="id_location_set-0-lat" type="text" name="location_set-0-lat" maxlength="100" /></p>\n' + '<p><label for="id_location_set-0-lon">Lon:</label> <input id="id_location_set-0-lon" type="text" name="location_set-0-lon" maxlength="100" /><input type="hidden" name="location_set-0-place" value="1" id="id_location_set-0-place" /><input type="hidden" name="location_set-0-id" id="id_location_set-0-id" /></p>') + + def test_foreign_keys_in_parents(self): + self.assertEqual(type(_get_foreign_key(Restaurant, Owner)), models.ForeignKey) + self.assertEqual(type(_get_foreign_key(MexicanRestaurant, Owner)), models.ForeignKey) + + def test_unique_validation(self): + FormSet = modelformset_factory(Product, extra=1) + data = { + 'form-TOTAL_FORMS': '1', + 'form-INITIAL_FORMS': '0', + 'form-MAX_NUM_FORMS': '', + 'form-0-slug': 'car-red', + } + formset = FormSet(data) + self.assertTrue(formset.is_valid()) + saved = formset.save() + self.assertEqual(len(saved), 1) + product1, = saved + self.assertEqual(product1.slug, 'car-red') + + data = { + 'form-TOTAL_FORMS': '1', + 'form-INITIAL_FORMS': '0', + 'form-MAX_NUM_FORMS': '', + 'form-0-slug': 'car-red', + } + formset = FormSet(data) + self.assertFalse(formset.is_valid()) + self.assertEqual(formset.errors, [{'slug': [u'Product with this Slug already exists.']}]) + + def test_unique_together_validation(self): + FormSet = modelformset_factory(Price, extra=1) + data = { + 'form-TOTAL_FORMS': '1', + 'form-INITIAL_FORMS': '0', + 'form-MAX_NUM_FORMS': '', + 'form-0-price': u'12.00', + 'form-0-quantity': '1', + } + formset = FormSet(data) + self.assertTrue(formset.is_valid()) + saved = formset.save() + self.assertEqual(len(saved), 1) + price1, = saved + self.assertEqual(price1.price, Decimal('12.00')) + self.assertEqual(price1.quantity, 1) + + data = { + 'form-TOTAL_FORMS': '1', + 'form-INITIAL_FORMS': '0', + 'form-MAX_NUM_FORMS': '', + 'form-0-price': u'12.00', + 'form-0-quantity': '1', + } + formset = FormSet(data) + self.assertFalse(formset.is_valid()) + self.assertEqual(formset.errors, [{'__all__': [u'Price with this Price and Quantity already exists.']}]) + + def test_unique_together_with_inlineformset_factory(self): + # Also see bug #8882. + + repository = Repository.objects.create(name=u'Test Repo') + FormSet = inlineformset_factory(Repository, Revision, extra=1) + data = { + 'revision_set-TOTAL_FORMS': '1', + 'revision_set-INITIAL_FORMS': '0', + 'revision_set-MAX_NUM_FORMS': '', + 'revision_set-0-repository': repository.pk, + 'revision_set-0-revision': '146239817507f148d448db38840db7c3cbf47c76', + 'revision_set-0-DELETE': '', + } + formset = FormSet(data, instance=repository) + self.assertTrue(formset.is_valid()) + saved = formset.save() + self.assertEqual(len(saved), 1) + revision1, = saved + self.assertEqual(revision1.repository, repository) + self.assertEqual(revision1.revision, '146239817507f148d448db38840db7c3cbf47c76') + + # attempt to save the same revision against against the same repo. + data = { + 'revision_set-TOTAL_FORMS': '1', + 'revision_set-INITIAL_FORMS': '0', + 'revision_set-MAX_NUM_FORMS': '', + 'revision_set-0-repository': repository.pk, + 'revision_set-0-revision': '146239817507f148d448db38840db7c3cbf47c76', + 'revision_set-0-DELETE': '', + } + formset = FormSet(data, instance=repository) + self.assertFalse(formset.is_valid()) + self.assertEqual(formset.errors, [{'__all__': [u'Revision with this Repository and Revision already exists.']}]) + + # unique_together with inlineformset_factory with overridden form fields + # Also see #9494 + + FormSet = inlineformset_factory(Repository, Revision, fields=('revision',), extra=1) + data = { + 'revision_set-TOTAL_FORMS': '1', + 'revision_set-INITIAL_FORMS': '0', + 'revision_set-MAX_NUM_FORMS': '', + 'revision_set-0-repository': repository.pk, + 'revision_set-0-revision': '146239817507f148d448db38840db7c3cbf47c76', + 'revision_set-0-DELETE': '', + } + formset = FormSet(data, instance=repository) + self.assertFalse(formset.is_valid()) + + def test_callable_defaults(self): + # Use of callable defaults (see bug #7975). + + person = Person.objects.create(name='Ringo') + FormSet = inlineformset_factory(Person, Membership, can_delete=False, extra=1) + formset = FormSet(instance=person) + + # Django will render a hidden field for model fields that have a callable + # default. This is required to ensure the value is tested for change correctly + # when determine what extra forms have changed to save. + + self.assertEquals(len(formset.forms), 1) # this formset only has one form + form = formset.forms[0] + now = form.fields['date_joined'].initial() + result = form.as_p() + result = re.sub(r'\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d+)?', '__DATETIME__', result) + self.assertEqual(result, + '<p><label for="id_membership_set-0-date_joined">Date joined:</label> <input type="text" name="membership_set-0-date_joined" value="__DATETIME__" id="id_membership_set-0-date_joined" /><input type="hidden" name="initial-membership_set-0-date_joined" value="__DATETIME__" id="initial-membership_set-0-id_membership_set-0-date_joined" /></p>\n' + '<p><label for="id_membership_set-0-karma">Karma:</label> <input type="text" name="membership_set-0-karma" id="id_membership_set-0-karma" /><input type="hidden" name="membership_set-0-person" value="1" id="id_membership_set-0-person" /><input type="hidden" name="membership_set-0-id" id="id_membership_set-0-id" /></p>') + + # test for validation with callable defaults. Validations rely on hidden fields + + data = { + 'membership_set-TOTAL_FORMS': '1', + 'membership_set-INITIAL_FORMS': '0', + 'membership_set-MAX_NUM_FORMS': '', + 'membership_set-0-date_joined': unicode(now.strftime('%Y-%m-%d %H:%M:%S')), + 'initial-membership_set-0-date_joined': unicode(now.strftime('%Y-%m-%d %H:%M:%S')), + 'membership_set-0-karma': '', + } + formset = FormSet(data, instance=person) + self.assertTrue(formset.is_valid()) + + # now test for when the data changes + + one_day_later = now + datetime.timedelta(days=1) + filled_data = { + 'membership_set-TOTAL_FORMS': '1', + 'membership_set-INITIAL_FORMS': '0', + 'membership_set-MAX_NUM_FORMS': '', + 'membership_set-0-date_joined': unicode(one_day_later.strftime('%Y-%m-%d %H:%M:%S')), + 'initial-membership_set-0-date_joined': unicode(now.strftime('%Y-%m-%d %H:%M:%S')), + 'membership_set-0-karma': '', + } + formset = FormSet(filled_data, instance=person) + self.assertFalse(formset.is_valid()) + + # now test with split datetime fields + + class MembershipForm(forms.ModelForm): + date_joined = forms.SplitDateTimeField(initial=now) + class Meta: + model = Membership + def __init__(self, **kwargs): + super(MembershipForm, self).__init__(**kwargs) + self.fields['date_joined'].widget = forms.SplitDateTimeWidget() + + FormSet = inlineformset_factory(Person, Membership, form=MembershipForm, can_delete=False, extra=1) + data = { + 'membership_set-TOTAL_FORMS': '1', + 'membership_set-INITIAL_FORMS': '0', + 'membership_set-MAX_NUM_FORMS': '', + 'membership_set-0-date_joined_0': unicode(now.strftime('%Y-%m-%d')), + 'membership_set-0-date_joined_1': unicode(now.strftime('%H:%M:%S')), + 'initial-membership_set-0-date_joined': unicode(now.strftime('%Y-%m-%d %H:%M:%S')), + 'membership_set-0-karma': '', + } + formset = FormSet(data, instance=person) + self.assertTrue(formset.is_valid()) + + def test_inlineformset_factory_with_null_fk(self): + # inlineformset_factory tests with fk having null=True. see #9462. + # create some data that will exbit the issue + team = Team.objects.create(name=u"Red Vipers") + Player(name="Timmy").save() + Player(name="Bobby", team=team).save() + + PlayerInlineFormSet = inlineformset_factory(Team, Player) + formset = PlayerInlineFormSet() + self.assertQuerysetEqual(formset.get_queryset(), []) + + formset = PlayerInlineFormSet(instance=team) + players = formset.get_queryset() + self.assertEqual(len(players), 1) + player1, = players + self.assertEqual(player1.team, team) + self.assertEqual(player1.name, 'Bobby') + + def test_model_formset_with_custom_pk(self): + # a formset for a Model that has a custom primary key that still needs to be + # added to the formset automatically + FormSet = modelformset_factory(ClassyMexicanRestaurant, fields=["tacos_are_yummy"]) + self.assertEqual(sorted(FormSet().forms[0].fields.keys()), ['restaurant', 'tacos_are_yummy']) + + def test_prevent_duplicates_from_with_the_same_formset(self): + FormSet = modelformset_factory(Product, extra=2) + data = { + 'form-TOTAL_FORMS': 2, + 'form-INITIAL_FORMS': 0, + 'form-MAX_NUM_FORMS': '', + 'form-0-slug': 'red_car', + 'form-1-slug': 'red_car', + } + formset = FormSet(data) + self.assertFalse(formset.is_valid()) + self.assertEqual(formset._non_form_errors, + [u'Please correct the duplicate data for slug.']) + + FormSet = modelformset_factory(Price, extra=2) + data = { + 'form-TOTAL_FORMS': 2, + 'form-INITIAL_FORMS': 0, + 'form-MAX_NUM_FORMS': '', + 'form-0-price': '25', + 'form-0-quantity': '7', + 'form-1-price': '25', + 'form-1-quantity': '7', + } + formset = FormSet(data) + self.assertFalse(formset.is_valid()) + self.assertEqual(formset._non_form_errors, + [u'Please correct the duplicate data for price and quantity, which must be unique.']) + + # Only the price field is specified, this should skip any unique checks since + # the unique_together is not fulfilled. This will fail with a KeyError if broken. + FormSet = modelformset_factory(Price, fields=("price",), extra=2) + data = { + 'form-TOTAL_FORMS': '2', + 'form-INITIAL_FORMS': '0', + 'form-MAX_NUM_FORMS': '', + 'form-0-price': '24', + 'form-1-price': '24', + } + formset = FormSet(data) + self.assertTrue(formset.is_valid()) + + FormSet = inlineformset_factory(Author, Book, extra=0) + author = Author.objects.create(pk=1, name='Charles Baudelaire') + book1 = Book.objects.create(pk=1, author=author, title='Les Paradis Artificiels') + book2 = Book.objects.create(pk=2, author=author, title='Les Fleurs du Mal') + book3 = Book.objects.create(pk=3, author=author, title='Flowers of Evil') + + book_ids = author.book_set.order_by('id').values_list('id', flat=True) + data = { + 'book_set-TOTAL_FORMS': '2', + 'book_set-INITIAL_FORMS': '2', + 'book_set-MAX_NUM_FORMS': '', + + 'book_set-0-title': 'The 2008 Election', + 'book_set-0-author': str(author.id), + 'book_set-0-id': str(book_ids[0]), + + 'book_set-1-title': 'The 2008 Election', + 'book_set-1-author': str(author.id), + 'book_set-1-id': str(book_ids[1]), + } + formset = FormSet(data=data, instance=author) + self.assertFalse(formset.is_valid()) + self.assertEqual(formset._non_form_errors, + [u'Please correct the duplicate data for title.']) + self.assertEqual(formset.errors, + [{}, {'__all__': u'Please correct the duplicate values below.'}]) + + FormSet = modelformset_factory(Post, extra=2) + data = { + 'form-TOTAL_FORMS': '2', + 'form-INITIAL_FORMS': '0', + 'form-MAX_NUM_FORMS': '', + + 'form-0-title': 'blah', + 'form-0-slug': 'Morning', + 'form-0-subtitle': 'foo', + 'form-0-posted': '2009-01-01', + 'form-1-title': 'blah', + 'form-1-slug': 'Morning in Prague', + 'form-1-subtitle': 'rawr', + 'form-1-posted': '2009-01-01' + } + formset = FormSet(data) + self.assertFalse(formset.is_valid()) + self.assertEqual(formset._non_form_errors, + [u'Please correct the duplicate data for title which must be unique for the date in posted.']) + self.assertEqual(formset.errors, + [{}, {'__all__': u'Please correct the duplicate values below.'}]) + + data = { + 'form-TOTAL_FORMS': '2', + 'form-INITIAL_FORMS': '0', + 'form-MAX_NUM_FORMS': '', + + 'form-0-title': 'foo', + 'form-0-slug': 'Morning in Prague', + 'form-0-subtitle': 'foo', + 'form-0-posted': '2009-01-01', + 'form-1-title': 'blah', + 'form-1-slug': 'Morning in Prague', + 'form-1-subtitle': 'rawr', + 'form-1-posted': '2009-08-02' + } + formset = FormSet(data) + self.assertFalse(formset.is_valid()) + self.assertEqual(formset._non_form_errors, + [u'Please correct the duplicate data for slug which must be unique for the year in posted.']) + + data = { + 'form-TOTAL_FORMS': '2', + 'form-INITIAL_FORMS': '0', + 'form-MAX_NUM_FORMS': '', + + 'form-0-title': 'foo', + 'form-0-slug': 'Morning in Prague', + 'form-0-subtitle': 'rawr', + 'form-0-posted': '2008-08-01', + 'form-1-title': 'blah', + 'form-1-slug': 'Prague', + 'form-1-subtitle': 'rawr', + 'form-1-posted': '2009-08-02' + } + formset = FormSet(data) + self.assertFalse(formset.is_valid()) + self.assertEqual(formset._non_form_errors, + [u'Please correct the duplicate data for subtitle which must be unique for the month in posted.']) diff --git a/parts/django/tests/modeltests/model_inheritance/__init__.py b/parts/django/tests/modeltests/model_inheritance/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/modeltests/model_inheritance/__init__.py diff --git a/parts/django/tests/modeltests/model_inheritance/models.py b/parts/django/tests/modeltests/model_inheritance/models.py new file mode 100644 index 0000000..6cee512 --- /dev/null +++ b/parts/django/tests/modeltests/model_inheritance/models.py @@ -0,0 +1,145 @@ +""" +XX. Model inheritance + +Model inheritance exists in two varieties: + - abstract base classes which are a way of specifying common + information inherited by the subclasses. They don't exist as a separate + model. + - non-abstract base classes (the default), which are models in their own + right with their own database tables and everything. Their subclasses + have references back to them, created automatically. + +Both styles are demonstrated here. +""" + +from django.db import models + +# +# Abstract base classes +# + +class CommonInfo(models.Model): + name = models.CharField(max_length=50) + age = models.PositiveIntegerField() + + class Meta: + abstract = True + ordering = ['name'] + + def __unicode__(self): + return u'%s %s' % (self.__class__.__name__, self.name) + +class Worker(CommonInfo): + job = models.CharField(max_length=50) + +class Student(CommonInfo): + school_class = models.CharField(max_length=10) + + class Meta: + pass + +class StudentWorker(Student, Worker): + pass + +# +# Abstract base classes with related models +# + +class Post(models.Model): + title = models.CharField(max_length=50) + +class Attachment(models.Model): + post = models.ForeignKey(Post, related_name='attached_%(class)s_set') + content = models.TextField() + + class Meta: + abstract = True + + def __unicode__(self): + return self.content + +class Comment(Attachment): + is_spam = models.BooleanField() + +class Link(Attachment): + url = models.URLField() + +# +# Multi-table inheritance +# + +class Chef(models.Model): + name = models.CharField(max_length=50) + + def __unicode__(self): + return u"%s the chef" % self.name + +class Place(models.Model): + name = models.CharField(max_length=50) + address = models.CharField(max_length=80) + + def __unicode__(self): + return u"%s the place" % self.name + +class Rating(models.Model): + rating = models.IntegerField(null=True, blank=True) + + class Meta: + abstract = True + ordering = ['-rating'] + +class Restaurant(Place, Rating): + serves_hot_dogs = models.BooleanField() + serves_pizza = models.BooleanField() + chef = models.ForeignKey(Chef, null=True, blank=True) + + class Meta(Rating.Meta): + db_table = 'my_restaurant' + + def __unicode__(self): + return u"%s the restaurant" % self.name + +class ItalianRestaurant(Restaurant): + serves_gnocchi = models.BooleanField() + + def __unicode__(self): + return u"%s the italian restaurant" % self.name + +class Supplier(Place): + customers = models.ManyToManyField(Restaurant, related_name='provider') + + def __unicode__(self): + return u"%s the supplier" % self.name + +class ParkingLot(Place): + # An explicit link to the parent (we can control the attribute name). + parent = models.OneToOneField(Place, primary_key=True, parent_link=True) + main_site = models.ForeignKey(Place, related_name='lot') + + def __unicode__(self): + return u"%s the parking lot" % self.name + +# +# Abstract base classes with related models where the sub-class has the +# same name in a different app and inherits from the same abstract base +# class. +# NOTE: The actual API tests for the following classes are in +# model_inheritance_same_model_name/models.py - They are defined +# here in order to have the name conflict between apps +# + +class Title(models.Model): + title = models.CharField(max_length=50) + +class NamedURL(models.Model): + title = models.ForeignKey(Title, related_name='attached_%(app_label)s_%(class)s_set') + url = models.URLField() + + class Meta: + abstract = True + +class Copy(NamedURL): + content = models.TextField() + + def __unicode__(self): + return self.content diff --git a/parts/django/tests/modeltests/model_inheritance/tests.py b/parts/django/tests/modeltests/model_inheritance/tests.py new file mode 100644 index 0000000..80dd0de --- /dev/null +++ b/parts/django/tests/modeltests/model_inheritance/tests.py @@ -0,0 +1,281 @@ +from operator import attrgetter + +from django.conf import settings +from django.core.exceptions import FieldError +from django.db import connection +from django.test import TestCase + +from models import (Chef, CommonInfo, ItalianRestaurant, ParkingLot, Place, + Post, Restaurant, Student, StudentWorker, Supplier, Worker) + + +class ModelInheritanceTests(TestCase): + def test_abstract(self): + # The Student and Worker models both have 'name' and 'age' fields on + # them and inherit the __unicode__() method, just as with normal Python + # subclassing. This is useful if you want to factor out common + # information for programming purposes, but still completely + # independent separate models at the database level. + w1 = Worker.objects.create(name="Fred", age=35, job="Quarry worker") + w2 = Worker.objects.create(name="Barney", age=34, job="Quarry worker") + + s = Student.objects.create(name="Pebbles", age=5, school_class="1B") + + self.assertEqual(unicode(w1), "Worker Fred") + self.assertEqual(unicode(s), "Student Pebbles") + + # The children inherit the Meta class of their parents (if they don't + # specify their own). + self.assertQuerysetEqual( + Worker.objects.values("name"), [ + {"name": "Barney"}, + {"name": "Fred"}, + ], + lambda o: o + ) + + # Since Student does not subclass CommonInfo's Meta, it has the effect + # of completely overriding it. So ordering by name doesn't take place + # for Students. + self.assertEqual(Student._meta.ordering, []) + + # However, the CommonInfo class cannot be used as a normal model (it + # doesn't exist as a model). + self.assertRaises(AttributeError, lambda: CommonInfo.objects.all()) + + # A StudentWorker which does not exist is both a Student and Worker + # which does not exist. + self.assertRaises(Student.DoesNotExist, + StudentWorker.objects.get, pk=12321321 + ) + self.assertRaises(Worker.DoesNotExist, + StudentWorker.objects.get, pk=12321321 + ) + + # MultipleObjectsReturned is also inherited. + # This is written out "long form", rather than using __init__/create() + # because of a bug with diamond inheritance (#10808) + sw1 = StudentWorker() + sw1.name = "Wilma" + sw1.age = 35 + sw1.save() + sw2 = StudentWorker() + sw2.name = "Betty" + sw2.age = 24 + sw2.save() + + self.assertRaises(Student.MultipleObjectsReturned, + StudentWorker.objects.get, pk__lt=sw2.pk + 100 + ) + self.assertRaises(Worker.MultipleObjectsReturned, + StudentWorker.objects.get, pk__lt=sw2.pk + 100 + ) + + def test_multiple_table(self): + post = Post.objects.create(title="Lorem Ipsum") + # The Post model has distinct accessors for the Comment and Link models. + post.attached_comment_set.create(content="Save $ on V1agr@", is_spam=True) + post.attached_link_set.create( + content="The Web framework for perfections with deadlines.", + url="http://www.djangoproject.com/" + ) + + # The Post model doesn't have an attribute called + # 'attached_%(class)s_set'. + self.assertRaises(AttributeError, + getattr, post, "attached_%(class)s_set" + ) + + # The Place/Restaurant/ItalianRestaurant models all exist as + # independent models. However, the subclasses also have transparent + # access to the fields of their ancestors. + # Create a couple of Places. + p1 = Place.objects.create(name="Master Shakes", address="666 W. Jersey") + p2 = Place.objects.create(name="Ace Harware", address="1013 N. Ashland") + + # Test constructor for Restaurant. + r = Restaurant.objects.create( + name="Demon Dogs", + address="944 W. Fullerton", + serves_hot_dogs=True, + serves_pizza=False, + rating=2 + ) + # Test the constructor for ItalianRestaurant. + c = Chef.objects.create(name="Albert") + ir = ItalianRestaurant.objects.create( + name="Ristorante Miron", + address="1234 W. Ash", + serves_hot_dogs=False, + serves_pizza=False, + serves_gnocchi=True, + rating=4, + chef=c + ) + self.assertQuerysetEqual( + ItalianRestaurant.objects.filter(address="1234 W. Ash"), [ + "Ristorante Miron", + ], + attrgetter("name") + ) + ir.address = "1234 W. Elm" + ir.save() + self.assertQuerysetEqual( + ItalianRestaurant.objects.filter(address="1234 W. Elm"), [ + "Ristorante Miron", + ], + attrgetter("name") + ) + + # Make sure Restaurant and ItalianRestaurant have the right fields in + # the right order. + self.assertEqual( + [f.name for f in Restaurant._meta.fields], + ["id", "name", "address", "place_ptr", "rating", "serves_hot_dogs", "serves_pizza", "chef"] + ) + self.assertEqual( + [f.name for f in ItalianRestaurant._meta.fields], + ["id", "name", "address", "place_ptr", "rating", "serves_hot_dogs", "serves_pizza", "chef", "restaurant_ptr", "serves_gnocchi"], + ) + self.assertEqual(Restaurant._meta.ordering, ["-rating"]) + + # Even though p.supplier for a Place 'p' (a parent of a Supplier), a + # Restaurant object cannot access that reverse relation, since it's not + # part of the Place-Supplier Hierarchy. + self.assertQuerysetEqual(Place.objects.filter(supplier__name="foo"), []) + self.assertRaises(FieldError, + Restaurant.objects.filter, supplier__name="foo" + ) + + # Parent fields can be used directly in filters on the child model. + self.assertQuerysetEqual( + Restaurant.objects.filter(name="Demon Dogs"), [ + "Demon Dogs", + ], + attrgetter("name") + ) + self.assertQuerysetEqual( + ItalianRestaurant.objects.filter(address="1234 W. Elm"), [ + "Ristorante Miron", + ], + attrgetter("name") + ) + + # Filters against the parent model return objects of the parent's type. + p = Place.objects.get(name="Demon Dogs") + self.assertTrue(type(p) is Place) + + # Since the parent and child are linked by an automatically created + # OneToOneField, you can get from the parent to the child by using the + # child's name. + self.assertEqual( + p.restaurant, Restaurant.objects.get(name="Demon Dogs") + ) + self.assertEqual( + Place.objects.get(name="Ristorante Miron").restaurant.italianrestaurant, + ItalianRestaurant.objects.get(name="Ristorante Miron") + ) + self.assertEqual( + Restaurant.objects.get(name="Ristorante Miron").italianrestaurant, + ItalianRestaurant.objects.get(name="Ristorante Miron") + ) + + # This won't work because the Demon Dogs restaurant is not an Italian + # restaurant. + self.assertRaises(ItalianRestaurant.DoesNotExist, + lambda: p.restaurant.italianrestaurant + ) + # An ItalianRestaurant which does not exist is also a Place which does + # not exist. + self.assertRaises(Place.DoesNotExist, + ItalianRestaurant.objects.get, name="The Noodle Void" + ) + # MultipleObjectsReturned is also inherited. + self.assertRaises(Place.MultipleObjectsReturned, + Restaurant.objects.get, id__lt=12321 + ) + + # Related objects work just as they normally do. + s1 = Supplier.objects.create(name="Joe's Chickens", address="123 Sesame St") + s1.customers = [r, ir] + s2 = Supplier.objects.create(name="Luigi's Pasta", address="456 Sesame St") + s2.customers = [ir] + + # This won't work because the Place we select is not a Restaurant (it's + # a Supplier). + p = Place.objects.get(name="Joe's Chickens") + self.assertRaises(Restaurant.DoesNotExist, + lambda: p.restaurant + ) + + self.assertEqual(p.supplier, s1) + self.assertQuerysetEqual( + ir.provider.order_by("-name"), [ + "Luigi's Pasta", + "Joe's Chickens" + ], + attrgetter("name") + ) + self.assertQuerysetEqual( + Restaurant.objects.filter(provider__name__contains="Chickens"), [ + "Ristorante Miron", + "Demon Dogs", + ], + attrgetter("name") + ) + self.assertQuerysetEqual( + ItalianRestaurant.objects.filter(provider__name__contains="Chickens"), [ + "Ristorante Miron", + ], + attrgetter("name"), + ) + + park1 = ParkingLot.objects.create( + name="Main St", address="111 Main St", main_site=s1 + ) + park2 = ParkingLot.objects.create( + name="Well Lit", address="124 Sesame St", main_site=ir + ) + + self.assertEqual( + Restaurant.objects.get(lot__name="Well Lit").name, + "Ristorante Miron" + ) + + # The update() command can update fields in parent and child classes at + # once (although it executed multiple SQL queries to do so). + rows = Restaurant.objects.filter( + serves_hot_dogs=True, name__contains="D" + ).update( + name="Demon Puppies", serves_hot_dogs=False + ) + self.assertEqual(rows, 1) + + r1 = Restaurant.objects.get(pk=r.pk) + self.assertFalse(r1.serves_hot_dogs) + self.assertEqual(r1.name, "Demon Puppies") + + # The values() command also works on fields from parent models. + self.assertQuerysetEqual( + ItalianRestaurant.objects.values("name", "rating"), [ + {"rating": 4, "name": "Ristorante Miron"} + ], + lambda o: o + ) + + # select_related works with fields from the parent object as if they + # were a normal part of the model. + old_DEBUG = settings.DEBUG + try: + settings.DEBUG = True + starting_queries = len(connection.queries) + ItalianRestaurant.objects.all()[0].chef + self.assertEqual(len(connection.queries) - starting_queries, 2) + + starting_queries = len(connection.queries) + ItalianRestaurant.objects.select_related("chef")[0].chef + self.assertEqual(len(connection.queries) - starting_queries, 1) + finally: + settings.DEBUG = old_DEBUG + + diff --git a/parts/django/tests/modeltests/model_inheritance_same_model_name/__init__.py b/parts/django/tests/modeltests/model_inheritance_same_model_name/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/modeltests/model_inheritance_same_model_name/__init__.py diff --git a/parts/django/tests/modeltests/model_inheritance_same_model_name/models.py b/parts/django/tests/modeltests/model_inheritance_same_model_name/models.py new file mode 100644 index 0000000..40de027 --- /dev/null +++ b/parts/django/tests/modeltests/model_inheritance_same_model_name/models.py @@ -0,0 +1,19 @@ +""" +XX. Model inheritance + +Model inheritance across apps can result in models with the same name resulting +in the need for an %(app_label)s format string. This app specifically tests +this feature by redefining the Copy model from model_inheritance/models.py +""" + +from django.db import models +from modeltests.model_inheritance.models import NamedURL + +# +# Abstract base classes with related models +# +class Copy(NamedURL): + content = models.TextField() + + def __unicode__(self): + return self.content diff --git a/parts/django/tests/modeltests/model_inheritance_same_model_name/tests.py b/parts/django/tests/modeltests/model_inheritance_same_model_name/tests.py new file mode 100644 index 0000000..3f1e345 --- /dev/null +++ b/parts/django/tests/modeltests/model_inheritance_same_model_name/tests.py @@ -0,0 +1,32 @@ +from django.test import TestCase +from modeltests.model_inheritance.models import Title + +class InheritanceSameModelNameTests(TestCase): + + def setUp(self): + # The Title model has distinct accessors for both + # model_inheritance.Copy and model_inheritance_same_model_name.Copy + # models. + self.title = Title.objects.create(title='Lorem Ipsum') + + def test_inheritance_related_name(self): + from modeltests.model_inheritance.models import Copy + self.assertEquals( + self.title.attached_model_inheritance_copy_set.create( + content='Save $ on V1agr@', + url='http://v1agra.com/', + title='V1agra is spam', + ), Copy.objects.get(content='Save $ on V1agr@')) + + def test_inheritance_with_same_model_name(self): + from modeltests.model_inheritance_same_model_name.models import Copy + self.assertEquals( + self.title.attached_model_inheritance_same_model_name_copy_set.create( + content='The Web framework for perfectionists with deadlines.', + url='http://www.djangoproject.com/', + title='Django Rocks' + ), Copy.objects.get(content='The Web framework for perfectionists with deadlines.')) + + def test_related_name_attribute_exists(self): + # The Post model doesn't have an attribute called 'attached_%(app_label)s_%(class)s_set'. + self.assertEqual(hasattr(self.title, 'attached_%(app_label)s_%(class)s_set'), False) diff --git a/parts/django/tests/modeltests/model_package/__init__.py b/parts/django/tests/modeltests/model_package/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/parts/django/tests/modeltests/model_package/__init__.py @@ -0,0 +1 @@ + diff --git a/parts/django/tests/modeltests/model_package/models/__init__.py b/parts/django/tests/modeltests/model_package/models/__init__.py new file mode 100644 index 0000000..91e1b02 --- /dev/null +++ b/parts/django/tests/modeltests/model_package/models/__init__.py @@ -0,0 +1,3 @@ +# Import all the models from subpackages +from article import Article +from publication import Publication diff --git a/parts/django/tests/modeltests/model_package/models/article.py b/parts/django/tests/modeltests/model_package/models/article.py new file mode 100644 index 0000000..c8fae1c --- /dev/null +++ b/parts/django/tests/modeltests/model_package/models/article.py @@ -0,0 +1,10 @@ +from django.db import models +from django.contrib.sites.models import Site + +class Article(models.Model): + sites = models.ManyToManyField(Site) + headline = models.CharField(max_length=100) + publications = models.ManyToManyField("model_package.Publication", null=True, blank=True,) + + class Meta: + app_label = 'model_package' diff --git a/parts/django/tests/modeltests/model_package/models/publication.py b/parts/django/tests/modeltests/model_package/models/publication.py new file mode 100644 index 0000000..4dc2d6a --- /dev/null +++ b/parts/django/tests/modeltests/model_package/models/publication.py @@ -0,0 +1,7 @@ +from django.db import models + +class Publication(models.Model): + title = models.CharField(max_length=30) + + class Meta: + app_label = 'model_package' diff --git a/parts/django/tests/modeltests/model_package/tests.py b/parts/django/tests/modeltests/model_package/tests.py new file mode 100644 index 0000000..e63e2e6 --- /dev/null +++ b/parts/django/tests/modeltests/model_package/tests.py @@ -0,0 +1,72 @@ +from django.contrib.sites.models import Site +from django.db import models +from django.test import TestCase + +from models.publication import Publication +from models.article import Article + + +class Advertisment(models.Model): + customer = models.CharField(max_length=100) + publications = models.ManyToManyField( + "model_package.Publication", null=True, blank=True + ) + + class Meta: + app_label = 'model_package' + + +class ModelPackageTests(TestCase): + def test_model_packages(self): + p = Publication.objects.create(title="FooBar") + + current_site = Site.objects.get_current() + self.assertEqual(current_site.domain, "example.com") + + # Regression for #12168: models split into subpackages still get M2M + # tables + a = Article.objects.create(headline="a foo headline") + a.publications.add(p) + a.sites.add(current_site) + + a = Article.objects.get(id=a.pk) + self.assertEqual(a.id, a.pk) + self.assertEqual(a.sites.count(), 1) + + # Regression for #12245 - Models can exist in the test package, too + ad = Advertisment.objects.create(customer="Lawrence Journal-World") + ad.publications.add(p) + + ad = Advertisment.objects.get(id=ad.pk) + self.assertEqual(ad.publications.count(), 1) + + # Regression for #12386 - field names on the autogenerated intermediate + # class that are specified as dotted strings don't retain any path + # component for the field or column name + self.assertEqual( + Article.publications.through._meta.fields[1].name, 'article' + ) + self.assertEqual( + Article.publications.through._meta.fields[1].get_attname_column(), + ('article_id', 'article_id') + ) + self.assertEqual( + Article.publications.through._meta.fields[2].name, 'publication' + ) + self.assertEqual( + Article.publications.through._meta.fields[2].get_attname_column(), + ('publication_id', 'publication_id') + ) + + # The oracle backend truncates the name to 'model_package_article_publ233f'. + self.assertTrue( + Article._meta.get_field('publications').m2m_db_table() in ('model_package_article_publications', 'model_package_article_publ233f') + ) + + self.assertEqual( + Article._meta.get_field('publications').m2m_column_name(), 'article_id' + ) + self.assertEqual( + Article._meta.get_field('publications').m2m_reverse_name(), + 'publication_id' + ) diff --git a/parts/django/tests/modeltests/mutually_referential/__init__.py b/parts/django/tests/modeltests/mutually_referential/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/modeltests/mutually_referential/__init__.py diff --git a/parts/django/tests/modeltests/mutually_referential/models.py b/parts/django/tests/modeltests/mutually_referential/models.py new file mode 100644 index 0000000..db05cbc --- /dev/null +++ b/parts/django/tests/modeltests/mutually_referential/models.py @@ -0,0 +1,19 @@ +""" +24. Mutually referential many-to-one relationships + +Strings can be used instead of model literals to set up "lazy" relations. +""" + +from django.db.models import * + +class Parent(Model): + name = CharField(max_length=100) + + # Use a simple string for forward declarations. + bestchild = ForeignKey("Child", null=True, related_name="favoured_by") + +class Child(Model): + name = CharField(max_length=100) + + # You can also explicitally specify the related app. + parent = ForeignKey("mutually_referential.Parent") diff --git a/parts/django/tests/modeltests/mutually_referential/tests.py b/parts/django/tests/modeltests/mutually_referential/tests.py new file mode 100644 index 0000000..101d67c --- /dev/null +++ b/parts/django/tests/modeltests/mutually_referential/tests.py @@ -0,0 +1,20 @@ +from django.test import TestCase +from models import Parent, Child + +class MutuallyReferentialTests(TestCase): + + def test_mutually_referential(self): + # Create a Parent + q = Parent(name='Elizabeth') + q.save() + + # Create some children + c = q.child_set.create(name='Charles') + e = q.child_set.create(name='Edward') + + # Set the best child + # No assertion require here; if basic assignment and + # deletion works, the test passes. + q.bestchild = c + q.save() + q.delete() diff --git a/parts/django/tests/modeltests/one_to_one/__init__.py b/parts/django/tests/modeltests/one_to_one/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/modeltests/one_to_one/__init__.py diff --git a/parts/django/tests/modeltests/one_to_one/models.py b/parts/django/tests/modeltests/one_to_one/models.py new file mode 100644 index 0000000..f263735 --- /dev/null +++ b/parts/django/tests/modeltests/one_to_one/models.py @@ -0,0 +1,47 @@ +""" +10. One-to-one relationships + +To define a one-to-one relationship, use ``OneToOneField()``. + +In this example, a ``Place`` optionally can be a ``Restaurant``. +""" + +from django.db import models, transaction, IntegrityError + +class Place(models.Model): + name = models.CharField(max_length=50) + address = models.CharField(max_length=80) + + def __unicode__(self): + return u"%s the place" % self.name + +class Restaurant(models.Model): + place = models.OneToOneField(Place, primary_key=True) + serves_hot_dogs = models.BooleanField() + serves_pizza = models.BooleanField() + + def __unicode__(self): + return u"%s the restaurant" % self.place.name + +class Waiter(models.Model): + restaurant = models.ForeignKey(Restaurant) + name = models.CharField(max_length=50) + + def __unicode__(self): + return u"%s the waiter at %s" % (self.name, self.restaurant) + +class ManualPrimaryKey(models.Model): + primary_key = models.CharField(max_length=10, primary_key=True) + name = models.CharField(max_length = 50) + +class RelatedModel(models.Model): + link = models.OneToOneField(ManualPrimaryKey) + name = models.CharField(max_length = 50) + +class MultiModel(models.Model): + link1 = models.OneToOneField(Place) + link2 = models.OneToOneField(ManualPrimaryKey) + name = models.CharField(max_length=50) + + def __unicode__(self): + return u"Multimodel %s" % self.name diff --git a/parts/django/tests/modeltests/one_to_one/tests.py b/parts/django/tests/modeltests/one_to_one/tests.py new file mode 100644 index 0000000..c3e1704 --- /dev/null +++ b/parts/django/tests/modeltests/one_to_one/tests.py @@ -0,0 +1,119 @@ +from django.test import TestCase +from django.db import transaction, IntegrityError +from models import Place, Restaurant, Waiter, ManualPrimaryKey, RelatedModel, MultiModel + +class OneToOneTests(TestCase): + + def setUp(self): + self.p1 = Place(name='Demon Dogs', address='944 W. Fullerton') + self.p1.save() + self.p2 = Place(name='Ace Hardware', address='1013 N. Ashland') + self.p2.save() + self.r = Restaurant(place=self.p1, serves_hot_dogs=True, serves_pizza=False) + self.r.save() + + def test_getter(self): + # A Restaurant can access its place. + self.assertEqual(repr(self.r.place), '<Place: Demon Dogs the place>') + # A Place can access its restaurant, if available. + self.assertEqual(repr(self.p1.restaurant), '<Restaurant: Demon Dogs the restaurant>') + # p2 doesn't have an associated restaurant. + self.assertRaises(Restaurant.DoesNotExist, getattr, self.p2, 'restaurant') + + def test_setter(self): + # Set the place using assignment notation. Because place is the primary + # key on Restaurant, the save will create a new restaurant + self.r.place = self.p2 + self.r.save() + self.assertEqual(repr(self.p2.restaurant), '<Restaurant: Ace Hardware the restaurant>') + self.assertEqual(repr(self.r.place), '<Place: Ace Hardware the place>') + self.assertEqual(self.p2.pk, self.r.pk) + # Set the place back again, using assignment in the reverse direction. + self.p1.restaurant = self.r + self.assertEqual(repr(self.p1.restaurant), '<Restaurant: Demon Dogs the restaurant>') + r = Restaurant.objects.get(pk=self.p1.id) + self.assertEqual(repr(r.place), '<Place: Demon Dogs the place>') + + def test_manager_all(self): + # Restaurant.objects.all() just returns the Restaurants, not the Places. + self.assertQuerysetEqual(Restaurant.objects.all(), [ + '<Restaurant: Demon Dogs the restaurant>', + ]) + # Place.objects.all() returns all Places, regardless of whether they + # have Restaurants. + self.assertQuerysetEqual(Place.objects.order_by('name'), [ + '<Place: Ace Hardware the place>', + '<Place: Demon Dogs the place>', + ]) + + def test_manager_get(self): + def assert_get_restaurant(**params): + self.assertEqual(repr(Restaurant.objects.get(**params)), + '<Restaurant: Demon Dogs the restaurant>') + assert_get_restaurant(place__id__exact=self.p1.pk) + assert_get_restaurant(place__id=self.p1.pk) + assert_get_restaurant(place__exact=self.p1.pk) + assert_get_restaurant(place__exact=self.p1) + assert_get_restaurant(place=self.p1.pk) + assert_get_restaurant(place=self.p1) + assert_get_restaurant(pk=self.p1.pk) + assert_get_restaurant(place__pk__exact=self.p1.pk) + assert_get_restaurant(place__pk=self.p1.pk) + assert_get_restaurant(place__name__startswith="Demon") + + def assert_get_place(**params): + self.assertEqual(repr(Place.objects.get(**params)), + '<Place: Demon Dogs the place>') + assert_get_place(restaurant__place__exact=self.p1.pk) + assert_get_place(restaurant__place__exact=self.p1) + assert_get_place(restaurant__place__pk=self.p1.pk) + assert_get_place(restaurant__exact=self.p1.pk) + assert_get_place(restaurant__exact=self.r) + assert_get_place(restaurant__pk=self.p1.pk) + assert_get_place(restaurant=self.p1.pk) + assert_get_place(restaurant=self.r) + assert_get_place(id__exact=self.p1.pk) + assert_get_place(pk=self.p1.pk) + + def test_foreign_key(self): + # Add a Waiter to the Restaurant. + w = self.r.waiter_set.create(name='Joe') + w.save() + self.assertEqual(repr(w), '<Waiter: Joe the waiter at Demon Dogs the restaurant>') + # Query the waiters + def assert_filter_waiters(**params): + self.assertQuerysetEqual(Waiter.objects.filter(**params), [ + '<Waiter: Joe the waiter at Demon Dogs the restaurant>' + ]) + assert_filter_waiters(restaurant__place__exact=self.p1.pk) + assert_filter_waiters(restaurant__place__exact=self.p1) + assert_filter_waiters(restaurant__place__pk=self.p1.pk) + assert_filter_waiters(restaurant__exact=self.p1.pk) + assert_filter_waiters(restaurant__exact=self.p1) + assert_filter_waiters(restaurant__pk=self.p1.pk) + assert_filter_waiters(restaurant=self.p1.pk) + assert_filter_waiters(restaurant=self.r) + assert_filter_waiters(id__exact=self.p1.pk) + assert_filter_waiters(pk=self.p1.pk) + # Delete the restaurant; the waiter should also be removed + r = Restaurant.objects.get(pk=self.p1.pk) + r.delete() + self.assertEqual(Waiter.objects.count(), 0) + + def test_multiple_o2o(self): + # One-to-one fields still work if you create your own primary key + o1 = ManualPrimaryKey(primary_key="abc123", name="primary") + o1.save() + o2 = RelatedModel(link=o1, name="secondary") + o2.save() + + # You can have multiple one-to-one fields on a model, too. + x1 = MultiModel(link1=self.p1, link2=o1, name="x1") + x1.save() + self.assertEqual(repr(o1.multimodel), '<MultiModel: Multimodel x1>') + # This will fail because each one-to-one field must be unique (and + # link2=o1 was used for x1, above). + sid = transaction.savepoint() + mm = MultiModel(link1=self.p2, link2=o1, name="x1") + self.assertRaises(IntegrityError, mm.save) + transaction.savepoint_rollback(sid) diff --git a/parts/django/tests/modeltests/or_lookups/__init__.py b/parts/django/tests/modeltests/or_lookups/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/modeltests/or_lookups/__init__.py diff --git a/parts/django/tests/modeltests/or_lookups/models.py b/parts/django/tests/modeltests/or_lookups/models.py new file mode 100644 index 0000000..7f14ba5 --- /dev/null +++ b/parts/django/tests/modeltests/or_lookups/models.py @@ -0,0 +1,22 @@ +""" +19. OR lookups + +To perform an OR lookup, or a lookup that combines ANDs and ORs, combine +``QuerySet`` objects using ``&`` and ``|`` operators. + +Alternatively, use positional arguments, and pass one or more expressions of +clauses using the variable ``django.db.models.Q`` (or any object with an +``add_to_query`` method). +""" + +from django.db import models + +class Article(models.Model): + headline = models.CharField(max_length=50) + pub_date = models.DateTimeField() + + class Meta: + ordering = ('pub_date',) + + def __unicode__(self): + return self.headline diff --git a/parts/django/tests/modeltests/or_lookups/tests.py b/parts/django/tests/modeltests/or_lookups/tests.py new file mode 100644 index 0000000..ad218cd --- /dev/null +++ b/parts/django/tests/modeltests/or_lookups/tests.py @@ -0,0 +1,232 @@ +from datetime import datetime +from operator import attrgetter + +from django.db.models import Q +from django.test import TestCase + +from models import Article + + +class OrLookupsTests(TestCase): + + def setUp(self): + self.a1 = Article.objects.create( + headline='Hello', pub_date=datetime(2005, 11, 27) + ).pk + self.a2 = Article.objects.create( + headline='Goodbye', pub_date=datetime(2005, 11, 28) + ).pk + self.a3 = Article.objects.create( + headline='Hello and goodbye', pub_date=datetime(2005, 11, 29) + ).pk + + def test_filter_or(self): + self.assertQuerysetEqual( + Article.objects.filter(headline__startswith='Hello') | Article.objects.filter(headline__startswith='Goodbye'), [ + 'Hello', + 'Goodbye', + 'Hello and goodbye' + ], + attrgetter("headline") + ) + + self.assertQuerysetEqual( + Article.objects.filter(headline__contains='Hello') | Article.objects.filter(headline__contains='bye'), [ + 'Hello', + 'Goodbye', + 'Hello and goodbye' + ], + attrgetter("headline") + ) + + self.assertQuerysetEqual( + Article.objects.filter(headline__iexact='Hello') | Article.objects.filter(headline__contains='ood'), [ + 'Hello', + 'Goodbye', + 'Hello and goodbye' + ], + attrgetter("headline") + ) + + self.assertQuerysetEqual( + Article.objects.filter(Q(headline__startswith='Hello') | Q(headline__startswith='Goodbye')), [ + 'Hello', + 'Goodbye', + 'Hello and goodbye' + ], + attrgetter("headline") + ) + + + def test_stages(self): + # You can shorten this syntax with code like the following, which is + # especially useful if building the query in stages: + articles = Article.objects.all() + self.assertQuerysetEqual( + articles.filter(headline__startswith='Hello') & articles.filter(headline__startswith='Goodbye'), + [] + ) + self.assertQuerysetEqual( + articles.filter(headline__startswith='Hello') & articles.filter(headline__contains='bye'), [ + 'Hello and goodbye' + ], + attrgetter("headline") + ) + + def test_pk_q(self): + self.assertQuerysetEqual( + Article.objects.filter(Q(pk=self.a1) | Q(pk=self.a2)), [ + 'Hello', + 'Goodbye' + ], + attrgetter("headline") + ) + + self.assertQuerysetEqual( + Article.objects.filter(Q(pk=self.a1) | Q(pk=self.a2) | Q(pk=self.a3)), [ + 'Hello', + 'Goodbye', + 'Hello and goodbye' + ], + attrgetter("headline"), + ) + + def test_pk_in(self): + self.assertQuerysetEqual( + Article.objects.filter(pk__in=[self.a1, self.a2, self.a3]), [ + 'Hello', + 'Goodbye', + 'Hello and goodbye' + ], + attrgetter("headline"), + ) + + self.assertQuerysetEqual( + Article.objects.filter(pk__in=(self.a1, self.a2, self.a3)), [ + 'Hello', + 'Goodbye', + 'Hello and goodbye' + ], + attrgetter("headline"), + ) + + self.assertQuerysetEqual( + Article.objects.filter(pk__in=[self.a1, self.a2, self.a3, 40000]), [ + 'Hello', + 'Goodbye', + 'Hello and goodbye' + ], + attrgetter("headline"), + ) + + def test_q_negated(self): + # Q objects can be negated + self.assertQuerysetEqual( + Article.objects.filter(Q(pk=self.a1) | ~Q(pk=self.a2)), [ + 'Hello', + 'Hello and goodbye' + ], + attrgetter("headline") + ) + + self.assertQuerysetEqual( + Article.objects.filter(~Q(pk=self.a1) & ~Q(pk=self.a2)), [ + 'Hello and goodbye' + ], + attrgetter("headline"), + ) + # This allows for more complex queries than filter() and exclude() + # alone would allow + self.assertQuerysetEqual( + Article.objects.filter(Q(pk=self.a1) & (~Q(pk=self.a2) | Q(pk=self.a3))), [ + 'Hello' + ], + attrgetter("headline"), + ) + + def test_complex_filter(self): + # The 'complex_filter' method supports framework features such as + # 'limit_choices_to' which normally take a single dictionary of lookup + # arguments but need to support arbitrary queries via Q objects too. + self.assertQuerysetEqual( + Article.objects.complex_filter({'pk': self.a1}), [ + 'Hello' + ], + attrgetter("headline"), + ) + + self.assertQuerysetEqual( + Article.objects.complex_filter(Q(pk=self.a1) | Q(pk=self.a2)), [ + 'Hello', + 'Goodbye' + ], + attrgetter("headline"), + ) + + def test_empty_in(self): + # Passing "in" an empty list returns no results ... + self.assertQuerysetEqual( + Article.objects.filter(pk__in=[]), + [] + ) + # ... but can return results if we OR it with another query. + self.assertQuerysetEqual( + Article.objects.filter(Q(pk__in=[]) | Q(headline__icontains='goodbye')), [ + 'Goodbye', + 'Hello and goodbye' + ], + attrgetter("headline"), + ) + + def test_q_and(self): + # Q arg objects are ANDed + self.assertQuerysetEqual( + Article.objects.filter(Q(headline__startswith='Hello'), Q(headline__contains='bye')), [ + 'Hello and goodbye' + ], + attrgetter("headline") + ) + # Q arg AND order is irrelevant + self.assertQuerysetEqual( + Article.objects.filter(Q(headline__contains='bye'), headline__startswith='Hello'), [ + 'Hello and goodbye' + ], + attrgetter("headline"), + ) + + self.assertQuerysetEqual( + Article.objects.filter(Q(headline__startswith='Hello') & Q(headline__startswith='Goodbye')), + [] + ) + + def test_q_exclude(self): + self.assertQuerysetEqual( + Article.objects.exclude(Q(headline__startswith='Hello')), [ + 'Goodbye' + ], + attrgetter("headline") + ) + + def test_other_arg_queries(self): + # Try some arg queries with operations other than filter. + self.assertEqual( + Article.objects.get(Q(headline__startswith='Hello'), Q(headline__contains='bye')).headline, + 'Hello and goodbye' + ) + + self.assertEqual( + Article.objects.filter(Q(headline__startswith='Hello') | Q(headline__contains='bye')).count(), + 3 + ) + + self.assertQuerysetEqual( + Article.objects.filter(Q(headline__startswith='Hello'), Q(headline__contains='bye')).values(), [ + {"headline": "Hello and goodbye", "id": self.a3, "pub_date": datetime(2005, 11, 29)}, + ], + lambda o: o, + ) + + self.assertEqual( + Article.objects.filter(Q(headline__startswith='Hello')).in_bulk([self.a1, self.a2]), + {self.a1: Article.objects.get(pk=self.a1)} + ) diff --git a/parts/django/tests/modeltests/order_with_respect_to/__init__.py b/parts/django/tests/modeltests/order_with_respect_to/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/modeltests/order_with_respect_to/__init__.py diff --git a/parts/django/tests/modeltests/order_with_respect_to/models.py b/parts/django/tests/modeltests/order_with_respect_to/models.py new file mode 100644 index 0000000..59f01d4 --- /dev/null +++ b/parts/django/tests/modeltests/order_with_respect_to/models.py @@ -0,0 +1,29 @@ +""" +Tests for the order_with_respect_to Meta attribute. +""" + +from django.db import models + + +class Question(models.Model): + text = models.CharField(max_length=200) + +class Answer(models.Model): + text = models.CharField(max_length=200) + question = models.ForeignKey(Question) + + class Meta: + order_with_respect_to = 'question' + + def __unicode__(self): + return unicode(self.text) + +class Post(models.Model): + title = models.CharField(max_length=200) + parent = models.ForeignKey("self", related_name="children", null=True) + + class Meta: + order_with_respect_to = "parent" + + def __unicode__(self): + return self.title diff --git a/parts/django/tests/modeltests/order_with_respect_to/tests.py b/parts/django/tests/modeltests/order_with_respect_to/tests.py new file mode 100644 index 0000000..328d968 --- /dev/null +++ b/parts/django/tests/modeltests/order_with_respect_to/tests.py @@ -0,0 +1,71 @@ +from operator import attrgetter + +from django.test import TestCase + +from models import Post, Question, Answer + + +class OrderWithRespectToTests(TestCase): + def test_basic(self): + q1 = Question.objects.create(text="Which Beatle starts with the letter 'R'?") + q2 = Question.objects.create(text="What is your name?") + + Answer.objects.create(text="John", question=q1) + Answer.objects.create(text="Jonno", question=q2) + Answer.objects.create(text="Paul", question=q1) + Answer.objects.create(text="Paulo", question=q2) + Answer.objects.create(text="George", question=q1) + Answer.objects.create(text="Ringo", question=q1) + + # The answers will always be ordered in the order they were inserted. + self.assertQuerysetEqual( + q1.answer_set.all(), [ + "John", "Paul", "George", "Ringo", + ], + attrgetter("text"), + ) + + # We can retrieve the answers related to a particular object, in the + # order they were created, once we have a particular object. + a1 = Answer.objects.filter(question=q1)[0] + self.assertEqual(a1.text, "John") + a2 = a1.get_next_in_order() + self.assertEqual(a2.text, "Paul") + a4 = list(Answer.objects.filter(question=q1))[-1] + self.assertEqual(a4.text, "Ringo") + self.assertEqual(a4.get_previous_in_order().text, "George") + + # Determining (and setting) the ordering for a particular item is also + # possible. + id_list = [o.pk for o in q1.answer_set.all()] + self.assertEqual(a2.question.get_answer_order(), id_list) + + a5 = Answer.objects.create(text="Number five", question=q1) + + # It doesn't matter which answer we use to check the order, it will + # always be the same. + self.assertEqual( + a2.question.get_answer_order(), a5.question.get_answer_order() + ) + + # The ordering can be altered: + id_list = [o.pk for o in q1.answer_set.all()] + x = id_list.pop() + id_list.insert(-1, x) + self.assertNotEqual(a5.question.get_answer_order(), id_list) + a5.question.set_answer_order(id_list) + self.assertQuerysetEqual( + q1.answer_set.all(), [ + "John", "Paul", "George", "Number five", "Ringo" + ], + attrgetter("text") + ) + + def test_recursive_ordering(self): + p1 = Post.objects.create(title='1') + p2 = Post.objects.create(title='2') + p1_1 = Post.objects.create(title="1.1", parent=p1) + p1_2 = Post.objects.create(title="1.2", parent=p1) + p2_1 = Post.objects.create(title="2.1", parent=p2) + p1_3 = Post.objects.create(title="1.3", parent=p1) + self.assertEqual(p1.get_post_order(), [p1_1.pk, p1_2.pk, p1_3.pk]) diff --git a/parts/django/tests/modeltests/ordering/__init__.py b/parts/django/tests/modeltests/ordering/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/modeltests/ordering/__init__.py diff --git a/parts/django/tests/modeltests/ordering/models.py b/parts/django/tests/modeltests/ordering/models.py new file mode 100644 index 0000000..25d3c2c --- /dev/null +++ b/parts/django/tests/modeltests/ordering/models.py @@ -0,0 +1,26 @@ +""" +6. Specifying ordering + +Specify default ordering for a model using the ``ordering`` attribute, which +should be a list or tuple of field names. This tells Django how to order +``QuerySet`` results. + +If a field name in ``ordering`` starts with a hyphen, that field will be +ordered in descending order. Otherwise, it'll be ordered in ascending order. +The special-case field name ``"?"`` specifies random order. + +The ordering attribute is not required. If you leave it off, ordering will be +undefined -- not random, just undefined. +""" + +from django.db import models + + +class Article(models.Model): + headline = models.CharField(max_length=100) + pub_date = models.DateTimeField() + class Meta: + ordering = ('-pub_date', 'headline') + + def __unicode__(self): + return self.headline diff --git a/parts/django/tests/modeltests/ordering/tests.py b/parts/django/tests/modeltests/ordering/tests.py new file mode 100644 index 0000000..77862c5 --- /dev/null +++ b/parts/django/tests/modeltests/ordering/tests.py @@ -0,0 +1,137 @@ +from datetime import datetime +from operator import attrgetter + +from django.test import TestCase + +from models import Article + + +class OrderingTests(TestCase): + def test_basic(self): + a1 = Article.objects.create( + headline="Article 1", pub_date=datetime(2005, 7, 26) + ) + a2 = Article.objects.create( + headline="Article 2", pub_date=datetime(2005, 7, 27) + ) + a3 = Article.objects.create( + headline="Article 3", pub_date=datetime(2005, 7, 27) + ) + a4 = Article.objects.create( + headline="Article 4", pub_date=datetime(2005, 7, 28) + ) + + # By default, Article.objects.all() orders by pub_date descending, then + # headline ascending. + self.assertQuerysetEqual( + Article.objects.all(), [ + "Article 4", + "Article 2", + "Article 3", + "Article 1", + ], + attrgetter("headline") + ) + + # Override ordering with order_by, which is in the same format as the + # ordering attribute in models. + self.assertQuerysetEqual( + Article.objects.order_by("headline"), [ + "Article 1", + "Article 2", + "Article 3", + "Article 4", + ], + attrgetter("headline") + ) + self.assertQuerysetEqual( + Article.objects.order_by("pub_date", "-headline"), [ + "Article 1", + "Article 3", + "Article 2", + "Article 4", + ], + attrgetter("headline") + ) + + # Only the last order_by has any effect (since they each override any + # previous ordering). + self.assertQuerysetEqual( + Article.objects.order_by("id"), [ + "Article 1", + "Article 2", + "Article 3", + "Article 4", + ], + attrgetter("headline") + ) + self.assertQuerysetEqual( + Article.objects.order_by("id").order_by("-headline"), [ + "Article 4", + "Article 3", + "Article 2", + "Article 1", + ], + attrgetter("headline") + ) + + # Use the 'stop' part of slicing notation to limit the results. + self.assertQuerysetEqual( + Article.objects.order_by("headline")[:2], [ + "Article 1", + "Article 2", + ], + attrgetter("headline") + ) + + # Use the 'stop' and 'start' parts of slicing notation to offset the + # result list. + self.assertQuerysetEqual( + Article.objects.order_by("headline")[1:3], [ + "Article 2", + "Article 3", + ], + attrgetter("headline") + ) + + # Getting a single item should work too: + self.assertEqual(Article.objects.all()[0], a4) + + # Use '?' to order randomly. + self.assertEqual( + len(list(Article.objects.order_by("?"))), 4 + ) + + # Ordering can be reversed using the reverse() method on a queryset. + # This allows you to extract things like "the last two items" (reverse + # and then take the first two). + self.assertQuerysetEqual( + Article.objects.all().reverse()[:2], [ + "Article 1", + "Article 3", + ], + attrgetter("headline") + ) + + # Ordering can be based on fields included from an 'extra' clause + self.assertQuerysetEqual( + Article.objects.extra(select={"foo": "pub_date"}, order_by=["foo", "headline"]), [ + "Article 1", + "Article 2", + "Article 3", + "Article 4", + ], + attrgetter("headline") + ) + + # If the extra clause uses an SQL keyword for a name, it will be + # protected by quoting. + self.assertQuerysetEqual( + Article.objects.extra(select={"order": "pub_date"}, order_by=["order", "headline"]), [ + "Article 1", + "Article 2", + "Article 3", + "Article 4", + ], + attrgetter("headline") + ) diff --git a/parts/django/tests/modeltests/pagination/__init__.py b/parts/django/tests/modeltests/pagination/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/modeltests/pagination/__init__.py diff --git a/parts/django/tests/modeltests/pagination/models.py b/parts/django/tests/modeltests/pagination/models.py new file mode 100644 index 0000000..48484dd --- /dev/null +++ b/parts/django/tests/modeltests/pagination/models.py @@ -0,0 +1,17 @@ +""" +30. Object pagination + +Django provides a framework for paginating a list of objects in a few lines +of code. This is often useful for dividing search results or long lists of +objects into easily readable pages. +""" + +from django.db import models + + +class Article(models.Model): + headline = models.CharField(max_length=100, default='Default headline') + pub_date = models.DateTimeField() + + def __unicode__(self): + return self.headline diff --git a/parts/django/tests/modeltests/pagination/tests.py b/parts/django/tests/modeltests/pagination/tests.py new file mode 100644 index 0000000..eaee466 --- /dev/null +++ b/parts/django/tests/modeltests/pagination/tests.py @@ -0,0 +1,132 @@ +from datetime import datetime +from operator import attrgetter + +from django.core.paginator import Paginator, InvalidPage, EmptyPage +from django.test import TestCase + +from models import Article + + +class CountContainer(object): + def count(self): + return 42 + +class LenContainer(object): + def __len__(self): + return 42 + +class PaginationTests(TestCase): + def setUp(self): + # Prepare a list of objects for pagination. + for x in range(1, 10): + a = Article(headline='Article %s' % x, pub_date=datetime(2005, 7, 29)) + a.save() + + def test_paginator(self): + paginator = Paginator(Article.objects.all(), 5) + self.assertEqual(9, paginator.count) + self.assertEqual(2, paginator.num_pages) + self.assertEqual([1, 2], paginator.page_range) + + def test_first_page(self): + paginator = Paginator(Article.objects.all(), 5) + p = paginator.page(1) + self.assertEqual(u"<Page 1 of 2>", unicode(p)) + self.assertQuerysetEqual(p.object_list, [ + "<Article: Article 1>", + "<Article: Article 2>", + "<Article: Article 3>", + "<Article: Article 4>", + "<Article: Article 5>" + ] + ) + self.assertTrue(p.has_next()) + self.assertFalse(p.has_previous()) + self.assertTrue(p.has_other_pages()) + self.assertEqual(2, p.next_page_number()) + self.assertEqual(0, p.previous_page_number()) + self.assertEqual(1, p.start_index()) + self.assertEqual(5, p.end_index()) + + def test_last_page(self): + paginator = Paginator(Article.objects.all(), 5) + p = paginator.page(2) + self.assertEqual(u"<Page 2 of 2>", unicode(p)) + self.assertQuerysetEqual(p.object_list, [ + "<Article: Article 6>", + "<Article: Article 7>", + "<Article: Article 8>", + "<Article: Article 9>" + ] + ) + self.assertFalse(p.has_next()) + self.assertTrue(p.has_previous()) + self.assertTrue(p.has_other_pages()) + self.assertEqual(3, p.next_page_number()) + self.assertEqual(1, p.previous_page_number()) + self.assertEqual(6, p.start_index()) + self.assertEqual(9, p.end_index()) + + def test_empty_page(self): + paginator = Paginator(Article.objects.all(), 5) + self.assertRaises(EmptyPage, paginator.page, 0) + self.assertRaises(EmptyPage, paginator.page, 3) + + # Empty paginators with allow_empty_first_page=True. + paginator = Paginator(Article.objects.filter(id=0), 5, allow_empty_first_page=True) + self.assertEqual(0, paginator.count) + self.assertEqual(1, paginator.num_pages) + self.assertEqual([1], paginator.page_range) + + # Empty paginators with allow_empty_first_page=False. + paginator = Paginator(Article.objects.filter(id=0), 5, allow_empty_first_page=False) + self.assertEqual(0, paginator.count) + self.assertEqual(0, paginator.num_pages) + self.assertEqual([], paginator.page_range) + + def test_invalid_page(self): + paginator = Paginator(Article.objects.all(), 5) + self.assertRaises(InvalidPage, paginator.page, 7) + + def test_orphans(self): + # Add a few more records to test out the orphans feature. + for x in range(10, 13): + Article(headline="Article %s" % x, pub_date=datetime(2006, 10, 6)).save() + + # With orphans set to 3 and 10 items per page, we should get all 12 items on a single page. + paginator = Paginator(Article.objects.all(), 10, orphans=3) + self.assertEqual(1, paginator.num_pages) + + # With orphans only set to 1, we should get two pages. + paginator = Paginator(Article.objects.all(), 10, orphans=1) + self.assertEqual(2, paginator.num_pages) + + def test_paginate_list(self): + # Paginators work with regular lists/tuples, too -- not just with QuerySets. + paginator = Paginator([1, 2, 3, 4, 5, 6, 7, 8, 9], 5) + self.assertEqual(9, paginator.count) + self.assertEqual(2, paginator.num_pages) + self.assertEqual([1, 2], paginator.page_range) + p = paginator.page(1) + self.assertEqual(u"<Page 1 of 2>", unicode(p)) + self.assertEqual([1, 2, 3, 4, 5], p.object_list) + self.assertTrue(p.has_next()) + self.assertFalse(p.has_previous()) + self.assertTrue(p.has_other_pages()) + self.assertEqual(2, p.next_page_number()) + self.assertEqual(0, p.previous_page_number()) + self.assertEqual(1, p.start_index()) + self.assertEqual(5, p.end_index()) + + def test_paginate_misc_classes(self): + # Paginator can be passed other objects with a count() method. + paginator = Paginator(CountContainer(), 10) + self.assertEqual(42, paginator.count) + self.assertEqual(5, paginator.num_pages) + self.assertEqual([1, 2, 3, 4, 5], paginator.page_range) + + # Paginator can be passed other objects that implement __len__. + paginator = Paginator(LenContainer(), 10) + self.assertEqual(42, paginator.count) + self.assertEqual(5, paginator.num_pages) + self.assertEqual([1, 2, 3, 4, 5], paginator.page_range) diff --git a/parts/django/tests/modeltests/properties/__init__.py b/parts/django/tests/modeltests/properties/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/modeltests/properties/__init__.py diff --git a/parts/django/tests/modeltests/properties/models.py b/parts/django/tests/modeltests/properties/models.py new file mode 100644 index 0000000..390efe3 --- /dev/null +++ b/parts/django/tests/modeltests/properties/models.py @@ -0,0 +1,21 @@ +""" +22. Using properties on models + +Use properties on models just like on any other Python object. +""" + +from django.db import models + +class Person(models.Model): + first_name = models.CharField(max_length=30) + last_name = models.CharField(max_length=30) + + def _get_full_name(self): + return "%s %s" % (self.first_name, self.last_name) + + def _set_full_name(self, combined_name): + self.first_name, self.last_name = combined_name.split(' ', 1) + + full_name = property(_get_full_name) + + full_name_2 = property(_get_full_name, _set_full_name) diff --git a/parts/django/tests/modeltests/properties/tests.py b/parts/django/tests/modeltests/properties/tests.py new file mode 100644 index 0000000..e31ac58 --- /dev/null +++ b/parts/django/tests/modeltests/properties/tests.py @@ -0,0 +1,20 @@ +from django.test import TestCase +from models import Person + +class PropertyTests(TestCase): + + def setUp(self): + self.a = Person(first_name='John', last_name='Lennon') + self.a.save() + + def test_getter(self): + self.assertEqual(self.a.full_name, 'John Lennon') + + def test_setter(self): + # The "full_name" property hasn't provided a "set" method. + self.assertRaises(AttributeError, setattr, self.a, 'full_name', 'Paul McCartney') + + # But "full_name_2" has, and it can be used to initialise the class. + a2 = Person(full_name_2 = 'Paul McCartney') + a2.save() + self.assertEqual(a2.first_name, 'Paul') diff --git a/parts/django/tests/modeltests/proxy_model_inheritance/__init__.py b/parts/django/tests/modeltests/proxy_model_inheritance/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/modeltests/proxy_model_inheritance/__init__.py diff --git a/parts/django/tests/modeltests/proxy_model_inheritance/app1/__init__.py b/parts/django/tests/modeltests/proxy_model_inheritance/app1/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/modeltests/proxy_model_inheritance/app1/__init__.py diff --git a/parts/django/tests/modeltests/proxy_model_inheritance/app1/models.py b/parts/django/tests/modeltests/proxy_model_inheritance/app1/models.py new file mode 100644 index 0000000..59a9ac7 --- /dev/null +++ b/parts/django/tests/modeltests/proxy_model_inheritance/app1/models.py @@ -0,0 +1,5 @@ +from app2.models import NiceModel + +class ProxyModel(NiceModel): + class Meta: + proxy = True diff --git a/parts/django/tests/modeltests/proxy_model_inheritance/app2/__init__.py b/parts/django/tests/modeltests/proxy_model_inheritance/app2/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/modeltests/proxy_model_inheritance/app2/__init__.py diff --git a/parts/django/tests/modeltests/proxy_model_inheritance/app2/models.py b/parts/django/tests/modeltests/proxy_model_inheritance/app2/models.py new file mode 100644 index 0000000..549cd07 --- /dev/null +++ b/parts/django/tests/modeltests/proxy_model_inheritance/app2/models.py @@ -0,0 +1,4 @@ +from django.db import models + +class NiceModel(models.Model): + pass diff --git a/parts/django/tests/modeltests/proxy_model_inheritance/models.py b/parts/django/tests/modeltests/proxy_model_inheritance/models.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/modeltests/proxy_model_inheritance/models.py diff --git a/parts/django/tests/modeltests/proxy_model_inheritance/tests.py b/parts/django/tests/modeltests/proxy_model_inheritance/tests.py new file mode 100644 index 0000000..b682851 --- /dev/null +++ b/parts/django/tests/modeltests/proxy_model_inheritance/tests.py @@ -0,0 +1,36 @@ +""" +XX. Proxy model inheritance + +Proxy model inheritance across apps can result in syncdb not creating the table +for the proxied model (as described in #12286). This test creates two dummy +apps and calls syncdb, then verifies that the table has been created. +""" + +import os +import sys + +from django.conf import settings, Settings +from django.core.management import call_command +from django.db.models.loading import load_app +from django.test import TransactionTestCase + +class ProxyModelInheritanceTests(TransactionTestCase): + + def setUp(self): + self.old_sys_path = sys.path[:] + sys.path.append(os.path.dirname(os.path.abspath(__file__))) + self.old_installed_apps = settings.INSTALLED_APPS + settings.INSTALLED_APPS = ('app1', 'app2') + map(load_app, settings.INSTALLED_APPS) + call_command('syncdb', verbosity=0) + global ProxyModel, NiceModel + from app1.models import ProxyModel + from app2.models import NiceModel + + def tearDown(self): + settings.INSTALLED_APPS = self.old_installed_apps + sys.path = self.old_sys_path + + def test_table_exists(self): + self.assertEquals(NiceModel.objects.all().count(), 0) + self.assertEquals(ProxyModel.objects.all().count(), 0) diff --git a/parts/django/tests/modeltests/proxy_models/__init__.py b/parts/django/tests/modeltests/proxy_models/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/modeltests/proxy_models/__init__.py diff --git a/parts/django/tests/modeltests/proxy_models/fixtures/mypeople.json b/parts/django/tests/modeltests/proxy_models/fixtures/mypeople.json new file mode 100644 index 0000000..d20c8f2 --- /dev/null +++ b/parts/django/tests/modeltests/proxy_models/fixtures/mypeople.json @@ -0,0 +1,9 @@ +[ + { + "pk": 100, + "model": "proxy_models.myperson", + "fields": { + "name": "Elvis Presley" + } + } +]
\ No newline at end of file diff --git a/parts/django/tests/modeltests/proxy_models/models.py b/parts/django/tests/modeltests/proxy_models/models.py new file mode 100644 index 0000000..90d54d9 --- /dev/null +++ b/parts/django/tests/modeltests/proxy_models/models.py @@ -0,0 +1,164 @@ +""" +By specifying the 'proxy' Meta attribute, model subclasses can specify that +they will take data directly from the table of their base class table rather +than using a new table of their own. This allows them to act as simple proxies, +providing a modified interface to the data from the base class. +""" + +from django.contrib.contenttypes.models import ContentType +from django.db import models + + +# A couple of managers for testing managing overriding in proxy model cases. + +class PersonManager(models.Manager): + def get_query_set(self): + return super(PersonManager, self).get_query_set().exclude(name="fred") + +class SubManager(models.Manager): + def get_query_set(self): + return super(SubManager, self).get_query_set().exclude(name="wilma") + +class Person(models.Model): + """ + A simple concrete base class. + """ + name = models.CharField(max_length=50) + + objects = PersonManager() + + def __unicode__(self): + return self.name + +class Abstract(models.Model): + """ + A simple abstract base class, to be used for error checking. + """ + data = models.CharField(max_length=10) + + class Meta: + abstract = True + +class MyPerson(Person): + """ + A proxy subclass, this should not get a new table. Overrides the default + manager. + """ + class Meta: + proxy = True + ordering = ["name"] + + objects = SubManager() + other = PersonManager() + + def has_special_name(self): + return self.name.lower() == "special" + +class ManagerMixin(models.Model): + excluder = SubManager() + + class Meta: + abstract = True + +class OtherPerson(Person, ManagerMixin): + """ + A class with the default manager from Person, plus an secondary manager. + """ + class Meta: + proxy = True + ordering = ["name"] + +class StatusPerson(MyPerson): + """ + A non-proxy subclass of a proxy, it should get a new table. + """ + status = models.CharField(max_length=80) + +# We can even have proxies of proxies (and subclass of those). +class MyPersonProxy(MyPerson): + class Meta: + proxy = True + +class LowerStatusPerson(MyPersonProxy): + status = models.CharField(max_length=80) + +class User(models.Model): + name = models.CharField(max_length=100) + + def __unicode__(self): + return self.name + +class UserProxy(User): + class Meta: + proxy = True + +class UserProxyProxy(UserProxy): + class Meta: + proxy = True + +# We can still use `select_related()` to include related models in our querysets. +class Country(models.Model): + name = models.CharField(max_length=50) + +class State(models.Model): + name = models.CharField(max_length=50) + country = models.ForeignKey(Country) + + def __unicode__(self): + return self.name + +class StateProxy(State): + class Meta: + proxy = True + +# Proxy models still works with filters (on related fields) +# and select_related, even when mixed with model inheritance +class BaseUser(models.Model): + name = models.CharField(max_length=255) + +class TrackerUser(BaseUser): + status = models.CharField(max_length=50) + +class ProxyTrackerUser(TrackerUser): + class Meta: + proxy = True + + +class Issue(models.Model): + summary = models.CharField(max_length=255) + assignee = models.ForeignKey(TrackerUser) + + def __unicode__(self): + return ':'.join((self.__class__.__name__,self.summary,)) + +class Bug(Issue): + version = models.CharField(max_length=50) + reporter = models.ForeignKey(BaseUser) + +class ProxyBug(Bug): + """ + Proxy of an inherited class + """ + class Meta: + proxy = True + + +class ProxyProxyBug(ProxyBug): + """ + A proxy of proxy model with related field + """ + class Meta: + proxy = True + +class Improvement(Issue): + """ + A model that has relation to a proxy model + or to a proxy of proxy model + """ + version = models.CharField(max_length=50) + reporter = models.ForeignKey(ProxyTrackerUser) + associated_bug = models.ForeignKey(ProxyProxyBug) + +class ProxyImprovement(Improvement): + class Meta: + proxy = True
\ No newline at end of file diff --git a/parts/django/tests/modeltests/proxy_models/tests.py b/parts/django/tests/modeltests/proxy_models/tests.py new file mode 100644 index 0000000..346a2a3 --- /dev/null +++ b/parts/django/tests/modeltests/proxy_models/tests.py @@ -0,0 +1,314 @@ +from django.test import TestCase +from django.db import models, DEFAULT_DB_ALIAS +from django.db.models import signals +from django.core import management +from django.core.exceptions import FieldError + +from django.contrib.contenttypes.models import ContentType + +from models import MyPerson, Person, StatusPerson, LowerStatusPerson +from models import MyPersonProxy, Abstract, OtherPerson, User, UserProxy +from models import UserProxyProxy, Country, State, StateProxy, TrackerUser +from models import BaseUser, Bug, ProxyTrackerUser, Improvement, ProxyProxyBug +from models import ProxyBug, ProxyImprovement + +class ProxyModelTests(TestCase): + def test_same_manager_queries(self): + """ + The MyPerson model should be generating the same database queries as + the Person model (when the same manager is used in each case). + """ + my_person_sql = MyPerson.other.all().query.get_compiler( + DEFAULT_DB_ALIAS).as_sql() + person_sql = Person.objects.order_by("name").query.get_compiler( + DEFAULT_DB_ALIAS).as_sql() + self.assertEqual(my_person_sql, person_sql) + + def test_inheretance_new_table(self): + """ + The StatusPerson models should have its own table (it's using ORM-level + inheritance). + """ + sp_sql = StatusPerson.objects.all().query.get_compiler( + DEFAULT_DB_ALIAS).as_sql() + p_sql = Person.objects.all().query.get_compiler( + DEFAULT_DB_ALIAS).as_sql() + self.assertNotEqual(sp_sql, p_sql) + + def test_basic_proxy(self): + """ + Creating a Person makes them accessible through the MyPerson proxy. + """ + Person.objects.create(name="Foo McBar") + self.assertEqual(len(Person.objects.all()), 1) + self.assertEqual(len(MyPerson.objects.all()), 1) + self.assertEqual(MyPerson.objects.get(name="Foo McBar").id, 1) + self.assertFalse(MyPerson.objects.get(id=1).has_special_name()) + + def test_no_proxy(self): + """ + Person is not proxied by StatusPerson subclass. + """ + Person.objects.create(name="Foo McBar") + self.assertEqual(list(StatusPerson.objects.all()), []) + + def test_basic_proxy_reverse(self): + """ + A new MyPerson also shows up as a standard Person. + """ + MyPerson.objects.create(name="Bazza del Frob") + self.assertEqual(len(MyPerson.objects.all()), 1) + self.assertEqual(len(Person.objects.all()), 1) + + LowerStatusPerson.objects.create(status="low", name="homer") + lsps = [lsp.name for lsp in LowerStatusPerson.objects.all()] + self.assertEqual(lsps, ["homer"]) + + def test_correct_type_proxy_of_proxy(self): + """ + Correct type when querying a proxy of proxy + """ + Person.objects.create(name="Foo McBar") + MyPerson.objects.create(name="Bazza del Frob") + LowerStatusPerson.objects.create(status="low", name="homer") + pp = sorted([mpp.name for mpp in MyPersonProxy.objects.all()]) + self.assertEqual(pp, ['Bazza del Frob', 'Foo McBar', 'homer']) + + def test_proxy_included_in_ancestors(self): + """ + Proxy models are included in the ancestors for a model's DoesNotExist + and MultipleObjectsReturned + """ + Person.objects.create(name="Foo McBar") + MyPerson.objects.create(name="Bazza del Frob") + LowerStatusPerson.objects.create(status="low", name="homer") + max_id = Person.objects.aggregate(max_id=models.Max('id'))['max_id'] + + self.assertRaises(Person.DoesNotExist, + MyPersonProxy.objects.get, + name='Zathras' + ) + self.assertRaises(Person.MultipleObjectsReturned, + MyPersonProxy.objects.get, + id__lt=max_id+1 + ) + self.assertRaises(Person.DoesNotExist, + StatusPerson.objects.get, + name='Zathras' + ) + + sp1 = StatusPerson.objects.create(name='Bazza Jr.') + sp2 = StatusPerson.objects.create(name='Foo Jr.') + max_id = Person.objects.aggregate(max_id=models.Max('id'))['max_id'] + + self.assertRaises(Person.MultipleObjectsReturned, + StatusPerson.objects.get, + id__lt=max_id+1 + ) + + def test_abc(self): + """ + All base classes must be non-abstract + """ + def build_abc(): + class NoAbstract(Abstract): + class Meta: + proxy = True + self.assertRaises(TypeError, build_abc) + + def test_no_cbc(self): + """ + The proxy must actually have one concrete base class + """ + def build_no_cbc(): + class TooManyBases(Person, Abstract): + class Meta: + proxy = True + self.assertRaises(TypeError, build_no_cbc) + + def test_no_base_classes(self): + def build_no_base_classes(): + class NoBaseClasses(models.Model): + class Meta: + proxy = True + self.assertRaises(TypeError, build_no_base_classes) + + def test_new_fields(self): + def build_new_fields(): + class NoNewFields(Person): + newfield = models.BooleanField() + class Meta: + proxy = True + self.assertRaises(FieldError, build_new_fields) + + def test_myperson_manager(self): + Person.objects.create(name="fred") + Person.objects.create(name="wilma") + Person.objects.create(name="barney") + + resp = [p.name for p in MyPerson.objects.all()] + self.assertEqual(resp, ['barney', 'fred']) + + resp = [p.name for p in MyPerson._default_manager.all()] + self.assertEqual(resp, ['barney', 'fred']) + + def test_otherperson_manager(self): + Person.objects.create(name="fred") + Person.objects.create(name="wilma") + Person.objects.create(name="barney") + + resp = [p.name for p in OtherPerson.objects.all()] + self.assertEqual(resp, ['barney', 'wilma']) + + resp = [p.name for p in OtherPerson.excluder.all()] + self.assertEqual(resp, ['barney', 'fred']) + + resp = [p.name for p in OtherPerson._default_manager.all()] + self.assertEqual(resp, ['barney', 'wilma']) + + def test_proxy_model_signals(self): + """ + Test save signals for proxy models + """ + output = [] + + def make_handler(model, event): + def _handler(*args, **kwargs): + output.append('%s %s save' % (model, event)) + return _handler + + h1 = make_handler('MyPerson', 'pre') + h2 = make_handler('MyPerson', 'post') + h3 = make_handler('Person', 'pre') + h4 = make_handler('Person', 'post') + + signals.pre_save.connect(h1, sender=MyPerson) + signals.post_save.connect(h2, sender=MyPerson) + signals.pre_save.connect(h3, sender=Person) + signals.post_save.connect(h4, sender=Person) + + dino = MyPerson.objects.create(name=u"dino") + self.assertEqual(output, [ + 'MyPerson pre save', + 'MyPerson post save' + ]) + + output = [] + + h5 = make_handler('MyPersonProxy', 'pre') + h6 = make_handler('MyPersonProxy', 'post') + + signals.pre_save.connect(h5, sender=MyPersonProxy) + signals.post_save.connect(h6, sender=MyPersonProxy) + + dino = MyPersonProxy.objects.create(name=u"pebbles") + + self.assertEqual(output, [ + 'MyPersonProxy pre save', + 'MyPersonProxy post save' + ]) + + signals.pre_save.disconnect(h1, sender=MyPerson) + signals.post_save.disconnect(h2, sender=MyPerson) + signals.pre_save.disconnect(h3, sender=Person) + signals.post_save.disconnect(h4, sender=Person) + signals.pre_save.disconnect(h5, sender=MyPersonProxy) + signals.post_save.disconnect(h6, sender=MyPersonProxy) + + def test_content_type(self): + ctype = ContentType.objects.get_for_model + self.assertTrue(ctype(Person) is ctype(OtherPerson)) + + def test_user_userproxy_userproxyproxy(self): + User.objects.create(name='Bruce') + + resp = [u.name for u in User.objects.all()] + self.assertEqual(resp, ['Bruce']) + + resp = [u.name for u in UserProxy.objects.all()] + self.assertEqual(resp, ['Bruce']) + + resp = [u.name for u in UserProxyProxy.objects.all()] + self.assertEqual(resp, ['Bruce']) + + def test_proxy_delete(self): + """ + Proxy objects can be deleted + """ + User.objects.create(name='Bruce') + u2 = UserProxy.objects.create(name='George') + + resp = [u.name for u in UserProxy.objects.all()] + self.assertEqual(resp, ['Bruce', 'George']) + + u2.delete() + + resp = [u.name for u in UserProxy.objects.all()] + self.assertEqual(resp, ['Bruce']) + + def test_select_related(self): + """ + We can still use `select_related()` to include related models in our + querysets. + """ + country = Country.objects.create(name='Australia') + state = State.objects.create(name='New South Wales', country=country) + + resp = [s.name for s in State.objects.select_related()] + self.assertEqual(resp, ['New South Wales']) + + resp = [s.name for s in StateProxy.objects.select_related()] + self.assertEqual(resp, ['New South Wales']) + + self.assertEqual(StateProxy.objects.get(name='New South Wales').name, + 'New South Wales') + + resp = StateProxy.objects.select_related().get(name='New South Wales') + self.assertEqual(resp.name, 'New South Wales') + + def test_proxy_bug(self): + contributor = TrackerUser.objects.create(name='Contributor', + status='contrib') + someone = BaseUser.objects.create(name='Someone') + Bug.objects.create(summary='fix this', version='1.1beta', + assignee=contributor, reporter=someone) + pcontributor = ProxyTrackerUser.objects.create(name='OtherContributor', + status='proxy') + Improvement.objects.create(summary='improve that', version='1.1beta', + assignee=contributor, reporter=pcontributor, + associated_bug=ProxyProxyBug.objects.all()[0]) + + # Related field filter on proxy + resp = ProxyBug.objects.get(version__icontains='beta') + self.assertEqual(repr(resp), '<ProxyBug: ProxyBug:fix this>') + + # Select related + filter on proxy + resp = ProxyBug.objects.select_related().get(version__icontains='beta') + self.assertEqual(repr(resp), '<ProxyBug: ProxyBug:fix this>') + + # Proxy of proxy, select_related + filter + resp = ProxyProxyBug.objects.select_related().get( + version__icontains='beta' + ) + self.assertEqual(repr(resp), '<ProxyProxyBug: ProxyProxyBug:fix this>') + + # Select related + filter on a related proxy field + resp = ProxyImprovement.objects.select_related().get( + reporter__name__icontains='butor' + ) + self.assertEqual(repr(resp), + '<ProxyImprovement: ProxyImprovement:improve that>' + ) + + # Select related + filter on a related proxy of proxy field + resp = ProxyImprovement.objects.select_related().get( + associated_bug__summary__icontains='fix' + ) + self.assertEqual(repr(resp), + '<ProxyImprovement: ProxyImprovement:improve that>' + ) + + def test_proxy_load_from_fixture(self): + management.call_command('loaddata', 'mypeople.json', verbosity=0, commit=False) + p = MyPerson.objects.get(pk=100) + self.assertEqual(p.name, 'Elvis Presley') diff --git a/parts/django/tests/modeltests/raw_query/__init__.py b/parts/django/tests/modeltests/raw_query/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/modeltests/raw_query/__init__.py diff --git a/parts/django/tests/modeltests/raw_query/fixtures/raw_query_books.json b/parts/django/tests/modeltests/raw_query/fixtures/raw_query_books.json new file mode 100644 index 0000000..35879aa --- /dev/null +++ b/parts/django/tests/modeltests/raw_query/fixtures/raw_query_books.json @@ -0,0 +1,110 @@ +[ + { + "pk": 1, + "model": "raw_query.author", + "fields": { + "dob": "1950-09-20", + "first_name": "Joe", + "last_name": "Smith" + } + }, + { + "pk": 2, + "model": "raw_query.author", + "fields": { + "dob": "1920-04-02", + "first_name": "Jill", + "last_name": "Doe" + } + }, + { + "pk": 3, + "model": "raw_query.author", + "fields": { + "dob": "1986-01-25", + "first_name": "Bob", + "last_name": "Smith" + } + }, + { + "pk": 4, + "model": "raw_query.author", + "fields": { + "dob": "1932-05-10", + "first_name": "Bill", + "last_name": "Jones" + } + }, + { + "pk": 1, + "model": "raw_query.book", + "fields": { + "author": 1, + "title": "The awesome book", + "paperback": false, + "opening_line": "It was a bright cold day in April and the clocks were striking thirteen." + } + }, + { + "pk": 2, + "model": "raw_query.book", + "fields": { + "author": 1, + "title": "The horrible book", + "paperback": true, + "opening_line": "On an evening in the latter part of May a middle-aged man was walking homeward from Shaston to the village of Marlott, in the adjoining Vale of Blakemore, or Blackmoor." + } + }, + { + "pk": 3, + "model": "raw_query.book", + "fields": { + "author": 1, + "title": "Another awesome book", + "paperback": false, + "opening_line": "A squat grey building of only thirty-four stories." + } + }, + { + "pk": 4, + "model": "raw_query.book", + "fields": { + "author": 3, + "title": "Some other book", + "paperback": true, + "opening_line": "It was the day my grandmother exploded." + } + }, + { + "pk": 1, + "model": "raw_query.coffee", + "fields": { + "brand": "dunkin doughnuts" + } + }, + { + "pk": 2, + "model": "raw_query.coffee", + "fields": { + "brand": "starbucks" + } + }, + { + "pk": 1, + "model": "raw_query.reviewer", + "fields": { + "reviewed": [ + 2, + 3, + 4 + ] + } + }, + { + "pk": 2, + "model": "raw_query.reviewer", + "fields": { + "reviewed": [] + } + } +] diff --git a/parts/django/tests/modeltests/raw_query/models.py b/parts/django/tests/modeltests/raw_query/models.py new file mode 100644 index 0000000..bb42b5b --- /dev/null +++ b/parts/django/tests/modeltests/raw_query/models.py @@ -0,0 +1,30 @@ +from django.db import models + +class Author(models.Model): + first_name = models.CharField(max_length=255) + last_name = models.CharField(max_length=255) + dob = models.DateField() + + def __init__(self, *args, **kwargs): + super(Author, self).__init__(*args, **kwargs) + # Protect against annotations being passed to __init__ -- + # this'll make the test suite get angry if annotations aren't + # treated differently than fields. + for k in kwargs: + assert k in [f.attname for f in self._meta.fields], \ + "Author.__init__ got an unexpected paramater: %s" % k + +class Book(models.Model): + title = models.CharField(max_length=255) + author = models.ForeignKey(Author) + paperback = models.BooleanField() + opening_line = models.TextField() + +class Coffee(models.Model): + brand = models.CharField(max_length=255, db_column="name") + +class Reviewer(models.Model): + reviewed = models.ManyToManyField(Book) + +class FriendlyAuthor(Author): + pass diff --git a/parts/django/tests/modeltests/raw_query/tests.py b/parts/django/tests/modeltests/raw_query/tests.py new file mode 100644 index 0000000..a1e7edb --- /dev/null +++ b/parts/django/tests/modeltests/raw_query/tests.py @@ -0,0 +1,236 @@ +from datetime import date + +from django.conf import settings +from django.db import connection +from django.db.models.sql.query import InvalidQuery +from django.test import TestCase + +from models import Author, Book, Coffee, Reviewer, FriendlyAuthor + + +class RawQueryTests(TestCase): + fixtures = ['raw_query_books.json'] + + def assertSuccessfulRawQuery(self, model, query, expected_results, + expected_annotations=(), params=[], translations=None): + """ + Execute the passed query against the passed model and check the output + """ + results = list(model.objects.raw(query, params=params, translations=translations)) + self.assertProcessed(model, results, expected_results, expected_annotations) + self.assertAnnotations(results, expected_annotations) + + def assertProcessed(self, model, results, orig, expected_annotations=()): + """ + Compare the results of a raw query against expected results + """ + self.assertEqual(len(results), len(orig)) + for index, item in enumerate(results): + orig_item = orig[index] + for annotation in expected_annotations: + setattr(orig_item, *annotation) + + for field in model._meta.fields: + # Check that all values on the model are equal + self.assertEquals(getattr(item,field.attname), + getattr(orig_item,field.attname)) + # This includes checking that they are the same type + self.assertEquals(type(getattr(item,field.attname)), + type(getattr(orig_item,field.attname))) + + def assertNoAnnotations(self, results): + """ + Check that the results of a raw query contain no annotations + """ + self.assertAnnotations(results, ()) + + def assertAnnotations(self, results, expected_annotations): + """ + Check that the passed raw query results contain the expected + annotations + """ + if expected_annotations: + for index, result in enumerate(results): + annotation, value = expected_annotations[index] + self.assertTrue(hasattr(result, annotation)) + self.assertEqual(getattr(result, annotation), value) + + def assert_num_queries(self, n, func, *args, **kwargs): + old_DEBUG = settings.DEBUG + settings.DEBUG = True + starting_queries = len(connection.queries) + try: + func(*args, **kwargs) + finally: + settings.DEBUG = old_DEBUG + self.assertEqual(starting_queries + n, len(connection.queries)) + + def testSimpleRawQuery(self): + """ + Basic test of raw query with a simple database query + """ + query = "SELECT * FROM raw_query_author" + authors = Author.objects.all() + self.assertSuccessfulRawQuery(Author, query, authors) + + def testRawQueryLazy(self): + """ + Raw queries are lazy: they aren't actually executed until they're + iterated over. + """ + q = Author.objects.raw('SELECT * FROM raw_query_author') + self.assert_(q.query.cursor is None) + list(q) + self.assert_(q.query.cursor is not None) + + def testFkeyRawQuery(self): + """ + Test of a simple raw query against a model containing a foreign key + """ + query = "SELECT * FROM raw_query_book" + books = Book.objects.all() + self.assertSuccessfulRawQuery(Book, query, books) + + def testDBColumnHandler(self): + """ + Test of a simple raw query against a model containing a field with + db_column defined. + """ + query = "SELECT * FROM raw_query_coffee" + coffees = Coffee.objects.all() + self.assertSuccessfulRawQuery(Coffee, query, coffees) + + def testOrderHandler(self): + """ + Test of raw raw query's tolerance for columns being returned in any + order + """ + selects = ( + ('dob, last_name, first_name, id'), + ('last_name, dob, first_name, id'), + ('first_name, last_name, dob, id'), + ) + + for select in selects: + query = "SELECT %s FROM raw_query_author" % select + authors = Author.objects.all() + self.assertSuccessfulRawQuery(Author, query, authors) + + def testTranslations(self): + """ + Test of raw query's optional ability to translate unexpected result + column names to specific model fields + """ + query = "SELECT first_name AS first, last_name AS last, dob, id FROM raw_query_author" + translations = {'first': 'first_name', 'last': 'last_name'} + authors = Author.objects.all() + self.assertSuccessfulRawQuery(Author, query, authors, translations=translations) + + def testParams(self): + """ + Test passing optional query parameters + """ + query = "SELECT * FROM raw_query_author WHERE first_name = %s" + author = Author.objects.all()[2] + params = [author.first_name] + results = list(Author.objects.raw(query, params=params)) + self.assertProcessed(Author, results, [author]) + self.assertNoAnnotations(results) + self.assertEqual(len(results), 1) + + def testManyToMany(self): + """ + Test of a simple raw query against a model containing a m2m field + """ + query = "SELECT * FROM raw_query_reviewer" + reviewers = Reviewer.objects.all() + self.assertSuccessfulRawQuery(Reviewer, query, reviewers) + + def testExtraConversions(self): + """ + Test to insure that extra translations are ignored. + """ + query = "SELECT * FROM raw_query_author" + translations = {'something': 'else'} + authors = Author.objects.all() + self.assertSuccessfulRawQuery(Author, query, authors, translations=translations) + + def testMissingFields(self): + query = "SELECT id, first_name, dob FROM raw_query_author" + for author in Author.objects.raw(query): + self.assertNotEqual(author.first_name, None) + # last_name isn't given, but it will be retrieved on demand + self.assertNotEqual(author.last_name, None) + + def testMissingFieldsWithoutPK(self): + query = "SELECT first_name, dob FROM raw_query_author" + try: + list(Author.objects.raw(query)) + self.fail('Query without primary key should fail') + except InvalidQuery: + pass + + def testAnnotations(self): + query = "SELECT a.*, count(b.id) as book_count FROM raw_query_author a LEFT JOIN raw_query_book b ON a.id = b.author_id GROUP BY a.id, a.first_name, a.last_name, a.dob ORDER BY a.id" + expected_annotations = ( + ('book_count', 3), + ('book_count', 0), + ('book_count', 1), + ('book_count', 0), + ) + authors = Author.objects.all() + self.assertSuccessfulRawQuery(Author, query, authors, expected_annotations) + + def testInvalidQuery(self): + query = "UPDATE raw_query_author SET first_name='thing' WHERE first_name='Joe'" + self.assertRaises(InvalidQuery, Author.objects.raw, query) + + def testWhiteSpaceQuery(self): + query = " SELECT * FROM raw_query_author" + authors = Author.objects.all() + self.assertSuccessfulRawQuery(Author, query, authors) + + def testMultipleIterations(self): + query = "SELECT * FROM raw_query_author" + normal_authors = Author.objects.all() + raw_authors = Author.objects.raw(query) + + # First Iteration + first_iterations = 0 + for index, raw_author in enumerate(raw_authors): + self.assertEqual(normal_authors[index], raw_author) + first_iterations += 1 + + # Second Iteration + second_iterations = 0 + for index, raw_author in enumerate(raw_authors): + self.assertEqual(normal_authors[index], raw_author) + second_iterations += 1 + + self.assertEqual(first_iterations, second_iterations) + + def testGetItem(self): + # Indexing on RawQuerySets + query = "SELECT * FROM raw_query_author ORDER BY id ASC" + third_author = Author.objects.raw(query)[2] + self.assertEqual(third_author.first_name, 'Bob') + + first_two = Author.objects.raw(query)[0:2] + self.assertEquals(len(first_two), 2) + + self.assertRaises(TypeError, lambda: Author.objects.raw(query)['test']) + + def test_inheritance(self): + # date is the end of the Cuban Missile Crisis, I have no idea when + # Wesley was bron + f = FriendlyAuthor.objects.create(first_name="Wesley", last_name="Chun", + dob=date(1962, 10, 28)) + query = "SELECT * FROM raw_query_friendlyauthor" + self.assertEqual( + [o.pk for o in FriendlyAuthor.objects.raw(query)], [f.pk] + ) + + def test_query_count(self): + self.assert_num_queries(1, + list, Author.objects.raw("SELECT * FROM raw_query_author") + ) diff --git a/parts/django/tests/modeltests/reserved_names/__init__.py b/parts/django/tests/modeltests/reserved_names/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/modeltests/reserved_names/__init__.py diff --git a/parts/django/tests/modeltests/reserved_names/models.py b/parts/django/tests/modeltests/reserved_names/models.py new file mode 100644 index 0000000..d8c1238 --- /dev/null +++ b/parts/django/tests/modeltests/reserved_names/models.py @@ -0,0 +1,25 @@ +""" +18. Using SQL reserved names + +Need to use a reserved SQL name as a column name or table name? Need to include +a hyphen in a column or table name? No problem. Django quotes names +appropriately behind the scenes, so your database won't complain about +reserved-name usage. +""" + +from django.db import models + +class Thing(models.Model): + when = models.CharField(max_length=1, primary_key=True) + join = models.CharField(max_length=1) + like = models.CharField(max_length=1) + drop = models.CharField(max_length=1) + alter = models.CharField(max_length=1) + having = models.CharField(max_length=1) + where = models.DateField(max_length=1) + has_hyphen = models.CharField(max_length=1, db_column='has-hyphen') + class Meta: + db_table = 'select' + + def __unicode__(self): + return self.when
\ No newline at end of file diff --git a/parts/django/tests/modeltests/reserved_names/tests.py b/parts/django/tests/modeltests/reserved_names/tests.py new file mode 100644 index 0000000..b7e4867 --- /dev/null +++ b/parts/django/tests/modeltests/reserved_names/tests.py @@ -0,0 +1,48 @@ +import datetime + +from django.test import TestCase + +from models import Thing + +class ReservedNameTests(TestCase): + def generate(self): + day1 = datetime.date(2005, 1, 1) + t = Thing.objects.create(when='a', join='b', like='c', drop='d', + alter='e', having='f', where=day1, has_hyphen='h') + day2 = datetime.date(2006, 2, 2) + u = Thing.objects.create(when='h', join='i', like='j', drop='k', + alter='l', having='m', where=day2) + + def test_simple(self): + day1 = datetime.date(2005, 1, 1) + t = Thing.objects.create(when='a', join='b', like='c', drop='d', + alter='e', having='f', where=day1, has_hyphen='h') + self.assertEqual(t.when, 'a') + + day2 = datetime.date(2006, 2, 2) + u = Thing.objects.create(when='h', join='i', like='j', drop='k', + alter='l', having='m', where=day2) + self.assertEqual(u.when, 'h') + + def test_order_by(self): + self.generate() + things = [t.when for t in Thing.objects.order_by('when')] + self.assertEqual(things, ['a', 'h']) + + def test_fields(self): + self.generate() + v = Thing.objects.get(pk='a') + self.assertEqual(v.join, 'b') + self.assertEqual(v.where, datetime.date(year=2005, month=1, day=1)) + + def test_dates(self): + self.generate() + resp = Thing.objects.dates('where', 'year') + self.assertEqual(list(resp), [ + datetime.datetime(2005, 1, 1, 0, 0), + datetime.datetime(2006, 1, 1, 0, 0), + ]) + + def test_month_filter(self): + self.generate() + self.assertEqual(Thing.objects.filter(where__month=1)[0].when, 'a') diff --git a/parts/django/tests/modeltests/reverse_lookup/__init__.py b/parts/django/tests/modeltests/reverse_lookup/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/modeltests/reverse_lookup/__init__.py diff --git a/parts/django/tests/modeltests/reverse_lookup/models.py b/parts/django/tests/modeltests/reverse_lookup/models.py new file mode 100644 index 0000000..2ffdc39 --- /dev/null +++ b/parts/django/tests/modeltests/reverse_lookup/models.py @@ -0,0 +1,28 @@ +""" +25. Reverse lookups + +This demonstrates the reverse lookup features of the database API. +""" + +from django.db import models + +class User(models.Model): + name = models.CharField(max_length=200) + + def __unicode__(self): + return self.name + +class Poll(models.Model): + question = models.CharField(max_length=200) + creator = models.ForeignKey(User) + + def __unicode__(self): + return self.question + +class Choice(models.Model): + name = models.CharField(max_length=100) + poll = models.ForeignKey(Poll, related_name="poll_choice") + related_poll = models.ForeignKey(Poll, related_name="related_choice") + + def __unicode__(self): + return self.name diff --git a/parts/django/tests/modeltests/reverse_lookup/tests.py b/parts/django/tests/modeltests/reverse_lookup/tests.py new file mode 100644 index 0000000..9a6e306 --- /dev/null +++ b/parts/django/tests/modeltests/reverse_lookup/tests.py @@ -0,0 +1,49 @@ +from django.test import TestCase +from django.core.exceptions import FieldError + +from models import User, Poll, Choice + +class ReverseLookupTests(TestCase): + + def setUp(self): + john = User.objects.create(name="John Doe") + jim = User.objects.create(name="Jim Bo") + first_poll = Poll.objects.create( + question="What's the first question?", + creator=john + ) + second_poll = Poll.objects.create( + question="What's the second question?", + creator=jim + ) + new_choice = Choice.objects.create( + poll=first_poll, + related_poll=second_poll, + name="This is the answer." + ) + + def test_reverse_by_field(self): + u1 = User.objects.get( + poll__question__exact="What's the first question?" + ) + self.assertEqual(u1.name, "John Doe") + + u2 = User.objects.get( + poll__question__exact="What's the second question?" + ) + self.assertEqual(u2.name, "Jim Bo") + + def test_reverse_by_related_name(self): + p1 = Poll.objects.get(poll_choice__name__exact="This is the answer.") + self.assertEqual(p1.question, "What's the first question?") + + p2 = Poll.objects.get( + related_choice__name__exact="This is the answer.") + self.assertEqual(p2.question, "What's the second question?") + + def test_reverse_field_name_disallowed(self): + """ + If a related_name is given you can't use the field name instead + """ + self.assertRaises(FieldError, Poll.objects.get, + choice__name__exact="This is the answer") diff --git a/parts/django/tests/modeltests/save_delete_hooks/__init__.py b/parts/django/tests/modeltests/save_delete_hooks/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/modeltests/save_delete_hooks/__init__.py diff --git a/parts/django/tests/modeltests/save_delete_hooks/models.py b/parts/django/tests/modeltests/save_delete_hooks/models.py new file mode 100644 index 0000000..515c7f6 --- /dev/null +++ b/parts/django/tests/modeltests/save_delete_hooks/models.py @@ -0,0 +1,32 @@ +""" +13. Adding hooks before/after saving and deleting + +To execute arbitrary code around ``save()`` and ``delete()``, just subclass +the methods. +""" + +from django.db import models + + +class Person(models.Model): + first_name = models.CharField(max_length=20) + last_name = models.CharField(max_length=20) + + def __init__(self, *args, **kwargs): + super(Person, self).__init__(*args, **kwargs) + self.data = [] + + def __unicode__(self): + return u"%s %s" % (self.first_name, self.last_name) + + def save(self, *args, **kwargs): + self.data.append("Before save") + # Call the "real" save() method + super(Person, self).save(*args, **kwargs) + self.data.append("After save") + + def delete(self): + self.data.append("Before deletion") + # Call the "real" delete() method + super(Person, self).delete() + self.data.append("After deletion") diff --git a/parts/django/tests/modeltests/save_delete_hooks/tests.py b/parts/django/tests/modeltests/save_delete_hooks/tests.py new file mode 100644 index 0000000..dc7b8ee --- /dev/null +++ b/parts/django/tests/modeltests/save_delete_hooks/tests.py @@ -0,0 +1,30 @@ +from django.test import TestCase + +from models import Person + + +class SaveDeleteHookTests(TestCase): + def test_basic(self): + p = Person(first_name="John", last_name="Smith") + self.assertEqual(p.data, []) + p.save() + self.assertEqual(p.data, [ + "Before save", + "After save", + ]) + + self.assertQuerysetEqual( + Person.objects.all(), [ + "John Smith", + ], + unicode + ) + + p.delete() + self.assertEqual(p.data, [ + "Before save", + "After save", + "Before deletion", + "After deletion", + ]) + self.assertQuerysetEqual(Person.objects.all(), []) diff --git a/parts/django/tests/modeltests/select_related/__init__.py b/parts/django/tests/modeltests/select_related/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/modeltests/select_related/__init__.py diff --git a/parts/django/tests/modeltests/select_related/models.py b/parts/django/tests/modeltests/select_related/models.py new file mode 100644 index 0000000..3c2e772 --- /dev/null +++ b/parts/django/tests/modeltests/select_related/models.py @@ -0,0 +1,59 @@ +""" +41. Tests for select_related() + +``select_related()`` follows all relationships and pre-caches any foreign key +values so that complex trees can be fetched in a single query. However, this +isn't always a good idea, so the ``depth`` argument control how many "levels" +the select-related behavior will traverse. +""" + +from django.db import models + +# Who remembers high school biology? + +class Domain(models.Model): + name = models.CharField(max_length=50) + def __unicode__(self): + return self.name + +class Kingdom(models.Model): + name = models.CharField(max_length=50) + domain = models.ForeignKey(Domain) + def __unicode__(self): + return self.name + +class Phylum(models.Model): + name = models.CharField(max_length=50) + kingdom = models.ForeignKey(Kingdom) + def __unicode__(self): + return self.name + +class Klass(models.Model): + name = models.CharField(max_length=50) + phylum = models.ForeignKey(Phylum) + def __unicode__(self): + return self.name + +class Order(models.Model): + name = models.CharField(max_length=50) + klass = models.ForeignKey(Klass) + def __unicode__(self): + return self.name + +class Family(models.Model): + name = models.CharField(max_length=50) + order = models.ForeignKey(Order) + def __unicode__(self): + return self.name + +class Genus(models.Model): + name = models.CharField(max_length=50) + family = models.ForeignKey(Family) + def __unicode__(self): + return self.name + +class Species(models.Model): + name = models.CharField(max_length=50) + genus = models.ForeignKey(Genus) + def __unicode__(self): + return self.name
\ No newline at end of file diff --git a/parts/django/tests/modeltests/select_related/tests.py b/parts/django/tests/modeltests/select_related/tests.py new file mode 100644 index 0000000..72b3ab2 --- /dev/null +++ b/parts/django/tests/modeltests/select_related/tests.py @@ -0,0 +1,166 @@ +from django.test import TestCase +from django.conf import settings +from django import db + +from models import Domain, Kingdom, Phylum, Klass, Order, Family, Genus, Species + +class SelectRelatedTests(TestCase): + + def create_tree(self, stringtree): + """ + Helper to create a complete tree. + """ + names = stringtree.split() + models = [Domain, Kingdom, Phylum, Klass, Order, Family, Genus, Species] + assert len(names) == len(models), (names, models) + + parent = None + for name, model in zip(names, models): + try: + obj = model.objects.get(name=name) + except model.DoesNotExist: + obj = model(name=name) + if parent: + setattr(obj, parent.__class__.__name__.lower(), parent) + obj.save() + parent = obj + + def create_base_data(self): + self.create_tree("Eukaryota Animalia Anthropoda Insecta Diptera Drosophilidae Drosophila melanogaster") + self.create_tree("Eukaryota Animalia Chordata Mammalia Primates Hominidae Homo sapiens") + self.create_tree("Eukaryota Plantae Magnoliophyta Magnoliopsida Fabales Fabaceae Pisum sativum") + self.create_tree("Eukaryota Fungi Basidiomycota Homobasidiomycatae Agaricales Amanitacae Amanita muscaria") + + def setUp(self): + # The test runner sets settings.DEBUG to False, but we want to gather + # queries so we'll set it to True here and reset it at the end of the + # test case. + self.create_base_data() + settings.DEBUG = True + db.reset_queries() + + def tearDown(self): + settings.DEBUG = False + + def test_access_fks_without_select_related(self): + """ + Normally, accessing FKs doesn't fill in related objects + """ + fly = Species.objects.get(name="melanogaster") + domain = fly.genus.family.order.klass.phylum.kingdom.domain + self.assertEqual(domain.name, 'Eukaryota') + self.assertEqual(len(db.connection.queries), 8) + + def test_access_fks_with_select_related(self): + """ + A select_related() call will fill in those related objects without any + extra queries + """ + person = Species.objects.select_related(depth=10).get(name="sapiens") + domain = person.genus.family.order.klass.phylum.kingdom.domain + self.assertEqual(domain.name, 'Eukaryota') + self.assertEqual(len(db.connection.queries), 1) + + def test_list_without_select_related(self): + """ + select_related() also of course applies to entire lists, not just + items. This test verifies the expected behavior without select_related. + """ + world = Species.objects.all() + families = [o.genus.family.name for o in world] + self.assertEqual(sorted(families), [ + 'Amanitacae', + 'Drosophilidae', + 'Fabaceae', + 'Hominidae', + ]) + self.assertEqual(len(db.connection.queries), 9) + + def test_list_with_select_related(self): + """ + select_related() also of course applies to entire lists, not just + items. This test verifies the expected behavior with select_related. + """ + world = Species.objects.all().select_related() + families = [o.genus.family.name for o in world] + self.assertEqual(sorted(families), [ + 'Amanitacae', + 'Drosophilidae', + 'Fabaceae', + 'Hominidae', + ]) + self.assertEqual(len(db.connection.queries), 1) + + def test_depth(self, depth=1, expected=7): + """ + The "depth" argument to select_related() will stop the descent at a + particular level. + """ + pea = Species.objects.select_related(depth=depth).get(name="sativum") + self.assertEqual( + pea.genus.family.order.klass.phylum.kingdom.domain.name, + 'Eukaryota' + ) + # Notice: one fewer queries than above because of depth=1 + self.assertEqual(len(db.connection.queries), expected) + + def test_larger_depth(self): + """ + The "depth" argument to select_related() will stop the descent at a + particular level. This tests a larger depth value. + """ + self.test_depth(depth=5, expected=3) + + def test_list_with_depth(self): + """ + The "depth" argument to select_related() will stop the descent at a + particular level. This can be used on lists as well. + """ + world = Species.objects.all().select_related(depth=2) + orders = [o.genus.family.order.name for o in world] + self.assertEqual(sorted(orders), + ['Agaricales', 'Diptera', 'Fabales', 'Primates']) + self.assertEqual(len(db.connection.queries), 5) + + def test_select_related_with_extra(self): + s = Species.objects.all().select_related(depth=1)\ + .extra(select={'a': 'select_related_species.id + 10'})[0] + self.assertEqual(s.id + 10, s.a) + + def test_certain_fields(self): + """ + The optional fields passed to select_related() control which related + models we pull in. This allows for smaller queries and can act as an + alternative (or, in addition to) the depth parameter. + + In this case, we explicitly say to select the 'genus' and + 'genus.family' models, leading to the same number of queries as before. + """ + world = Species.objects.select_related('genus__family') + families = [o.genus.family.name for o in world] + self.assertEqual(sorted(families), + ['Amanitacae', 'Drosophilidae', 'Fabaceae', 'Hominidae']) + self.assertEqual(len(db.connection.queries), 1) + + def test_more_certain_fields(self): + """ + In this case, we explicitly say to select the 'genus' and + 'genus.family' models, leading to the same number of queries as before. + """ + world = Species.objects.filter(genus__name='Amanita')\ + .select_related('genus__family') + orders = [o.genus.family.order.name for o in world] + self.assertEqual(orders, [u'Agaricales']) + self.assertEqual(len(db.connection.queries), 2) + + def test_field_traversal(self): + s = Species.objects.all().select_related('genus__family__order' + ).order_by('id')[0:1].get().genus.family.order.name + self.assertEqual(s, u'Diptera') + self.assertEqual(len(db.connection.queries), 1) + + def test_depth_fields_fails(self): + self.assertRaises(TypeError, + Species.objects.select_related, + 'genus__family__order', depth=4 + ) diff --git a/parts/django/tests/modeltests/serializers/__init__.py b/parts/django/tests/modeltests/serializers/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/modeltests/serializers/__init__.py diff --git a/parts/django/tests/modeltests/serializers/models.py b/parts/django/tests/modeltests/serializers/models.py new file mode 100644 index 0000000..c12e73f --- /dev/null +++ b/parts/django/tests/modeltests/serializers/models.py @@ -0,0 +1,117 @@ +# -*- coding: utf-8 -*- +""" +42. Serialization + +``django.core.serializers`` provides interfaces to converting Django +``QuerySet`` objects to and from "flat" data (i.e. strings). +""" + +from decimal import Decimal +from django.db import models + +class Category(models.Model): + name = models.CharField(max_length=20) + + class Meta: + ordering = ('name',) + + def __unicode__(self): + return self.name + + +class Author(models.Model): + name = models.CharField(max_length=20) + + class Meta: + ordering = ('name',) + + def __unicode__(self): + return self.name + + +class Article(models.Model): + author = models.ForeignKey(Author) + headline = models.CharField(max_length=50) + pub_date = models.DateTimeField() + categories = models.ManyToManyField(Category) + + class Meta: + ordering = ('pub_date',) + + def __unicode__(self): + return self.headline + + +class AuthorProfile(models.Model): + author = models.OneToOneField(Author, primary_key=True) + date_of_birth = models.DateField() + + def __unicode__(self): + return u"Profile of %s" % self.author + + +class Actor(models.Model): + name = models.CharField(max_length=20, primary_key=True) + + class Meta: + ordering = ('name',) + + def __unicode__(self): + return self.name + + +class Movie(models.Model): + actor = models.ForeignKey(Actor) + title = models.CharField(max_length=50) + price = models.DecimalField(max_digits=6, decimal_places=2, default=Decimal('0.00')) + + class Meta: + ordering = ('title',) + + def __unicode__(self): + return self.title + + +class Score(models.Model): + score = models.FloatField() + + +class Team(object): + def __init__(self, title): + self.title = title + + def __unicode__(self): + raise NotImplementedError("Not so simple") + + def __str__(self): + raise NotImplementedError("Not so simple") + + def to_string(self): + return "%s" % self.title + + +class TeamField(models.CharField): + __metaclass__ = models.SubfieldBase + + def __init__(self): + super(TeamField, self).__init__(max_length=100) + + def get_db_prep_save(self, value): + return unicode(value.title) + + def to_python(self, value): + if isinstance(value, Team): + return value + return Team(value) + + def value_to_string(self, obj): + return self._get_val_from_obj(obj).to_string() + + +class Player(models.Model): + name = models.CharField(max_length=50) + rank = models.IntegerField() + team = TeamField() + + def __unicode__(self): + return u'%s (%d) playing for %s' % (self.name, self.rank, self.team.to_string()) diff --git a/parts/django/tests/modeltests/serializers/tests.py b/parts/django/tests/modeltests/serializers/tests.py new file mode 100644 index 0000000..9b648a8 --- /dev/null +++ b/parts/django/tests/modeltests/serializers/tests.py @@ -0,0 +1,417 @@ +# -*- coding: utf-8 -*- +from datetime import datetime +from StringIO import StringIO +from xml.dom import minidom + +from django.core import serializers +from django.db import transaction +from django.test import TestCase, TransactionTestCase, Approximate +from django.utils import simplejson + +from models import Category, Author, Article, AuthorProfile, Actor, \ + Movie, Score, Player, Team + +class SerializersTestBase(object): + @staticmethod + def _comparison_value(value): + return value + + def setUp(self): + sports = Category.objects.create(name="Sports") + music = Category.objects.create(name="Music") + op_ed = Category.objects.create(name="Op-Ed") + + self.joe = Author.objects.create(name="Joe") + self.jane = Author.objects.create(name="Jane") + + self.a1 = Article( + author=self.jane, + headline="Poker has no place on ESPN", + pub_date=datetime(2006, 6, 16, 11, 00) + ) + self.a1.save() + self.a1.categories = [sports, op_ed] + + self.a2 = Article( + author=self.joe, + headline="Time to reform copyright", + pub_date=datetime(2006, 6, 16, 13, 00, 11, 345) + ) + self.a2.save() + self.a2.categories = [music, op_ed] + + def test_serialize(self): + """Tests that basic serialization works.""" + serial_str = serializers.serialize(self.serializer_name, + Article.objects.all()) + self.assertTrue(self._validate_output(serial_str)) + + def test_serializer_roundtrip(self): + """Tests that serialized content can be deserialized.""" + serial_str = serializers.serialize(self.serializer_name, + Article.objects.all()) + models = list(serializers.deserialize(self.serializer_name, serial_str)) + self.assertEqual(len(models), 2) + + def test_altering_serialized_output(self): + """ + Tests the ability to create new objects by + modifying serialized content. + """ + old_headline = "Poker has no place on ESPN" + new_headline = "Poker has no place on television" + serial_str = serializers.serialize(self.serializer_name, + Article.objects.all()) + serial_str = serial_str.replace(old_headline, new_headline) + models = list(serializers.deserialize(self.serializer_name, serial_str)) + + # Prior to saving, old headline is in place + self.assertTrue(Article.objects.filter(headline=old_headline)) + self.assertFalse(Article.objects.filter(headline=new_headline)) + + for model in models: + model.save() + + # After saving, new headline is in place + self.assertTrue(Article.objects.filter(headline=new_headline)) + self.assertFalse(Article.objects.filter(headline=old_headline)) + + def test_one_to_one_as_pk(self): + """ + Tests that if you use your own primary key field + (such as a OneToOneField), it doesn't appear in the + serialized field list - it replaces the pk identifier. + """ + profile = AuthorProfile(author=self.joe, + date_of_birth=datetime(1970,1,1)) + profile.save() + serial_str = serializers.serialize(self.serializer_name, + AuthorProfile.objects.all()) + self.assertFalse(self._get_field_values(serial_str, 'author')) + + for obj in serializers.deserialize(self.serializer_name, serial_str): + self.assertEqual(obj.object.pk, self._comparison_value(self.joe.pk)) + + def test_serialize_field_subset(self): + """Tests that output can be restricted to a subset of fields""" + valid_fields = ('headline','pub_date') + invalid_fields = ("author", "categories") + serial_str = serializers.serialize(self.serializer_name, + Article.objects.all(), + fields=valid_fields) + for field_name in invalid_fields: + self.assertFalse(self._get_field_values(serial_str, field_name)) + + for field_name in valid_fields: + self.assertTrue(self._get_field_values(serial_str, field_name)) + + def test_serialize_unicode(self): + """Tests that unicode makes the roundtrip intact""" + actor_name = u"Za\u017c\u00f3\u0142\u0107" + movie_title = u'G\u0119\u015bl\u0105 ja\u017a\u0144' + ac = Actor(name=actor_name) + mv = Movie(title=movie_title, actor=ac) + ac.save() + mv.save() + + serial_str = serializers.serialize(self.serializer_name, [mv]) + self.assertEqual(self._get_field_values(serial_str, "title")[0], movie_title) + self.assertEqual(self._get_field_values(serial_str, "actor")[0], actor_name) + + obj_list = list(serializers.deserialize(self.serializer_name, serial_str)) + mv_obj = obj_list[0].object + self.assertEqual(mv_obj.title, movie_title) + + def test_serialize_with_null_pk(self): + """ + Tests that serialized data with no primary key results + in a model instance with no id + """ + category = Category(name="Reference") + serial_str = serializers.serialize(self.serializer_name, [category]) + pk_value = self._get_pk_values(serial_str)[0] + self.assertFalse(pk_value) + + cat_obj = list(serializers.deserialize(self.serializer_name, + serial_str))[0].object + self.assertEqual(cat_obj.id, None) + + def test_float_serialization(self): + """Tests that float values serialize and deserialize intact""" + sc = Score(score=3.4) + sc.save() + serial_str = serializers.serialize(self.serializer_name, [sc]) + deserial_objs = list(serializers.deserialize(self.serializer_name, + serial_str)) + self.assertEqual(deserial_objs[0].object.score, Approximate(3.4, places=1)) + + def test_custom_field_serialization(self): + """Tests that custom fields serialize and deserialize intact""" + team_str = "Spartak Moskva" + player = Player() + player.name = "Soslan Djanaev" + player.rank = 1 + player.team = Team(team_str) + player.save() + serial_str = serializers.serialize(self.serializer_name, + Player.objects.all()) + team = self._get_field_values(serial_str, "team") + self.assertTrue(team) + self.assertEqual(team[0], team_str) + + deserial_objs = list(serializers.deserialize(self.serializer_name, serial_str)) + self.assertEqual(deserial_objs[0].object.team.to_string(), + player.team.to_string()) + + def test_pre_1000ad_date(self): + """Tests that year values before 1000AD are properly formatted""" + # Regression for #12524 -- dates before 1000AD get prefixed + # 0's on the year + a = Article.objects.create( + author = self.jane, + headline = "Nobody remembers the early years", + pub_date = datetime(1, 2, 3, 4, 5, 6)) + + serial_str = serializers.serialize(self.serializer_name, [a]) + date_values = self._get_field_values(serial_str, "pub_date") + self.assertEquals(date_values[0], "0001-02-03 04:05:06") + + def test_pkless_serialized_strings(self): + """ + Tests that serialized strings without PKs + can be turned into models + """ + deserial_objs = list(serializers.deserialize(self.serializer_name, + self.pkless_str)) + for obj in deserial_objs: + self.assertFalse(obj.object.id) + obj.save() + self.assertEqual(Category.objects.all().count(), 4) + + +class SerializersTransactionTestBase(object): + def test_forward_refs(self): + """ + Tests that objects ids can be referenced before they are + defined in the serialization data. + """ + # The deserialization process needs to be contained + # within a transaction in order to test forward reference + # handling. + transaction.enter_transaction_management() + transaction.managed(True) + objs = serializers.deserialize(self.serializer_name, self.fwd_ref_str) + for obj in objs: + obj.save() + transaction.commit() + transaction.leave_transaction_management() + + for model_cls in (Category, Author, Article): + self.assertEqual(model_cls.objects.all().count(), 1) + art_obj = Article.objects.all()[0] + self.assertEqual(art_obj.categories.all().count(), 1) + self.assertEqual(art_obj.author.name, "Agnes") + + +class XmlSerializerTestCase(SerializersTestBase, TestCase): + serializer_name = "xml" + pkless_str = """<?xml version="1.0" encoding="utf-8"?> +<django-objects version="1.0"> + <object model="serializers.category"> + <field type="CharField" name="name">Reference</field> + </object> +</django-objects>""" + + @staticmethod + def _comparison_value(value): + # The XML serializer handles everything as strings, so comparisons + # need to be performed on the stringified value + return unicode(value) + + @staticmethod + def _validate_output(serial_str): + try: + minidom.parseString(serial_str) + except Exception: + return False + else: + return True + + @staticmethod + def _get_pk_values(serial_str): + ret_list = [] + dom = minidom.parseString(serial_str) + fields = dom.getElementsByTagName("object") + for field in fields: + ret_list.append(field.getAttribute("pk")) + return ret_list + + @staticmethod + def _get_field_values(serial_str, field_name): + ret_list = [] + dom = minidom.parseString(serial_str) + fields = dom.getElementsByTagName("field") + for field in fields: + if field.getAttribute("name") == field_name: + temp = [] + for child in field.childNodes: + temp.append(child.nodeValue) + ret_list.append("".join(temp)) + return ret_list + +class XmlSerializerTransactionTestCase(SerializersTransactionTestBase, TransactionTestCase): + serializer_name = "xml" + fwd_ref_str = """<?xml version="1.0" encoding="utf-8"?> +<django-objects version="1.0"> + <object pk="1" model="serializers.article"> + <field to="serializers.author" name="author" rel="ManyToOneRel">1</field> + <field type="CharField" name="headline">Forward references pose no problem</field> + <field type="DateTimeField" name="pub_date">2006-06-16 15:00:00</field> + <field to="serializers.category" name="categories" rel="ManyToManyRel"> + <object pk="1"></object> + </field> + </object> + <object pk="1" model="serializers.author"> + <field type="CharField" name="name">Agnes</field> + </object> + <object pk="1" model="serializers.category"> + <field type="CharField" name="name">Reference</field></object> +</django-objects>""" + + +class JsonSerializerTestCase(SerializersTestBase, TestCase): + serializer_name = "json" + pkless_str = """[{"pk": null, "model": "serializers.category", "fields": {"name": "Reference"}}]""" + + @staticmethod + def _validate_output(serial_str): + try: + simplejson.loads(serial_str) + except Exception: + return False + else: + return True + + @staticmethod + def _get_pk_values(serial_str): + ret_list = [] + serial_list = simplejson.loads(serial_str) + for obj_dict in serial_list: + ret_list.append(obj_dict["pk"]) + return ret_list + + @staticmethod + def _get_field_values(serial_str, field_name): + ret_list = [] + serial_list = simplejson.loads(serial_str) + for obj_dict in serial_list: + if field_name in obj_dict["fields"]: + ret_list.append(obj_dict["fields"][field_name]) + return ret_list + +class JsonSerializerTransactionTestCase(SerializersTransactionTestBase, TransactionTestCase): + serializer_name = "json" + fwd_ref_str = """[ + { + "pk": 1, + "model": "serializers.article", + "fields": { + "headline": "Forward references pose no problem", + "pub_date": "2006-06-16 15:00:00", + "categories": [1], + "author": 1 + } + }, + { + "pk": 1, + "model": "serializers.category", + "fields": { + "name": "Reference" + } + }, + { + "pk": 1, + "model": "serializers.author", + "fields": { + "name": "Agnes" + } + }]""" + +try: + import yaml +except ImportError: + pass +else: + class YamlSerializerTestCase(SerializersTestBase, TestCase): + serializer_name = "yaml" + fwd_ref_str = """- fields: + headline: Forward references pose no problem + pub_date: 2006-06-16 15:00:00 + categories: [1] + author: 1 + pk: 1 + model: serializers.article +- fields: + name: Reference + pk: 1 + model: serializers.category +- fields: + name: Agnes + pk: 1 + model: serializers.author""" + + pkless_str = """- fields: + name: Reference + pk: null + model: serializers.category""" + + @staticmethod + def _validate_output(serial_str): + try: + yaml.load(StringIO(serial_str)) + except Exception: + return False + else: + return True + + @staticmethod + def _get_pk_values(serial_str): + ret_list = [] + stream = StringIO(serial_str) + for obj_dict in yaml.load(stream): + ret_list.append(obj_dict["pk"]) + return ret_list + + @staticmethod + def _get_field_values(serial_str, field_name): + ret_list = [] + stream = StringIO(serial_str) + for obj_dict in yaml.load(stream): + if "fields" in obj_dict and field_name in obj_dict["fields"]: + field_value = obj_dict["fields"][field_name] + # yaml.load will return non-string objects for some + # of the fields we are interested in, this ensures that + # everything comes back as a string + if isinstance(field_value, basestring): + ret_list.append(field_value) + else: + ret_list.append(str(field_value)) + return ret_list + + class YamlSerializerTransactionTestCase(SerializersTransactionTestBase, TransactionTestCase): + serializer_name = "yaml" + fwd_ref_str = """- fields: + headline: Forward references pose no problem + pub_date: 2006-06-16 15:00:00 + categories: [1] + author: 1 + pk: 1 + model: serializers.article +- fields: + name: Reference + pk: 1 + model: serializers.category +- fields: + name: Agnes + pk: 1 + model: serializers.author""" diff --git a/parts/django/tests/modeltests/signals/__init__.py b/parts/django/tests/modeltests/signals/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/modeltests/signals/__init__.py diff --git a/parts/django/tests/modeltests/signals/models.py b/parts/django/tests/modeltests/signals/models.py new file mode 100644 index 0000000..f1250b4 --- /dev/null +++ b/parts/django/tests/modeltests/signals/models.py @@ -0,0 +1,13 @@ +""" +Testing signals before/after saving and deleting. +""" + +from django.db import models + + +class Person(models.Model): + first_name = models.CharField(max_length=20) + last_name = models.CharField(max_length=20) + + def __unicode__(self): + return u"%s %s" % (self.first_name, self.last_name) diff --git a/parts/django/tests/modeltests/signals/tests.py b/parts/django/tests/modeltests/signals/tests.py new file mode 100644 index 0000000..27948c6 --- /dev/null +++ b/parts/django/tests/modeltests/signals/tests.py @@ -0,0 +1,148 @@ +from django.db.models import signals +from django.test import TestCase + +from models import Person + + +# #8285: signals can be any callable +class PostDeleteHandler(object): + def __init__(self, data): + self.data = data + + def __call__(self, signal, sender, instance, **kwargs): + self.data.append( + (instance, instance.id is None) + ) + +class MyReceiver(object): + def __init__(self, param): + self.param = param + self._run = False + + def __call__(self, signal, sender, **kwargs): + self._run = True + signal.disconnect(receiver=self, sender=sender) + +class SignalTests(TestCase): + def test_basic(self): + # Save up the number of connected signals so that we can check at the + # end that all the signals we register get properly unregistered (#9989) + pre_signals = ( + len(signals.pre_save.receivers), + len(signals.post_save.receivers), + len(signals.pre_delete.receivers), + len(signals.post_delete.receivers), + ) + + data = [] + + def pre_save_test(signal, sender, instance, **kwargs): + data.append( + (instance, kwargs.get("raw", False)) + ) + signals.pre_save.connect(pre_save_test) + + def post_save_test(signal, sender, instance, **kwargs): + data.append( + (instance, kwargs.get("created"), kwargs.get("raw", False)) + ) + signals.post_save.connect(post_save_test) + + def pre_delete_test(signal, sender, instance, **kwargs): + data.append( + (instance, instance.id is None) + ) + signals.pre_delete.connect(pre_delete_test) + + post_delete_test = PostDeleteHandler(data) + signals.post_delete.connect(post_delete_test) + + p1 = Person(first_name="John", last_name="Smith") + self.assertEqual(data, []) + p1.save() + self.assertEqual(data, [ + (p1, False), + (p1, True, False), + ]) + data[:] = [] + + p1.first_name = "Tom" + p1.save() + self.assertEqual(data, [ + (p1, False), + (p1, False, False), + ]) + data[:] = [] + + # Calling an internal method purely so that we can trigger a "raw" save. + p1.save_base(raw=True) + self.assertEqual(data, [ + (p1, True), + (p1, False, True), + ]) + data[:] = [] + + p1.delete() + self.assertEqual(data, [ + (p1, False), + (p1, False), + ]) + data[:] = [] + + p2 = Person(first_name="James", last_name="Jones") + p2.id = 99999 + p2.save() + self.assertEqual(data, [ + (p2, False), + (p2, True, False), + ]) + data[:] = [] + + p2.id = 99998 + p2.save() + self.assertEqual(data, [ + (p2, False), + (p2, True, False), + ]) + data[:] = [] + + p2.delete() + self.assertEqual(data, [ + (p2, False), + (p2, False) + ]) + + self.assertQuerysetEqual( + Person.objects.all(), [ + "James Jones", + ], + unicode + ) + + signals.post_delete.disconnect(post_delete_test) + signals.pre_delete.disconnect(pre_delete_test) + signals.post_save.disconnect(post_save_test) + signals.pre_save.disconnect(pre_save_test) + + # Check that all our signals got disconnected properly. + post_signals = ( + len(signals.pre_save.receivers), + len(signals.post_save.receivers), + len(signals.pre_delete.receivers), + len(signals.post_delete.receivers), + ) + self.assertEqual(pre_signals, post_signals) + + def test_disconnect_in_dispatch(self): + """ + Test that signals that disconnect when being called don't mess future + dispatching. + """ + a, b = MyReceiver(1), MyReceiver(2) + signals.post_save.connect(sender=Person, receiver=a) + signals.post_save.connect(sender=Person, receiver=b) + p = Person.objects.create(first_name='John', last_name='Smith') + + self.assertTrue(a._run) + self.assertTrue(b._run) + self.assertEqual(signals.post_save.receivers, []) diff --git a/parts/django/tests/modeltests/str/__init__.py b/parts/django/tests/modeltests/str/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/modeltests/str/__init__.py diff --git a/parts/django/tests/modeltests/str/models.py b/parts/django/tests/modeltests/str/models.py new file mode 100644 index 0000000..84b8d67 --- /dev/null +++ b/parts/django/tests/modeltests/str/models.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +""" +2. Adding __str__() or __unicode__() to models + +Although it's not a strict requirement, each model should have a +``_str__()`` or ``__unicode__()`` method to return a "human-readable" +representation of the object. Do this not only for your own sanity when dealing +with the interactive prompt, but also because objects' representations are used +throughout Django's automatically-generated admin. + +Normally, you should write ``__unicode__()`` method, since this will work for +all field types (and Django will automatically provide an appropriate +``__str__()`` method). However, you can write a ``__str__()`` method directly, +if you prefer. You must be careful to encode the results correctly, though. +""" + +from django.db import models + +class Article(models.Model): + headline = models.CharField(max_length=100) + pub_date = models.DateTimeField() + + def __str__(self): + # Caution: this is only safe if you are certain that headline will be + # in ASCII. + return self.headline + +class InternationalArticle(models.Model): + headline = models.CharField(max_length=100) + pub_date = models.DateTimeField() + + def __unicode__(self): + return self.headline
\ No newline at end of file diff --git a/parts/django/tests/modeltests/str/tests.py b/parts/django/tests/modeltests/str/tests.py new file mode 100644 index 0000000..4e4c765 --- /dev/null +++ b/parts/django/tests/modeltests/str/tests.py @@ -0,0 +1,23 @@ + # -*- coding: utf-8 -*- +import datetime + +from django.test import TestCase + +from models import Article, InternationalArticle + +class SimpleTests(TestCase): + def test_basic(self): + a = Article.objects.create( + headline='Area man programs in Python', + pub_date=datetime.datetime(2005, 7, 28) + ) + self.assertEqual(str(a), 'Area man programs in Python') + self.assertEqual(repr(a), '<Article: Area man programs in Python>') + + def test_international(self): + a = InternationalArticle.objects.create( + headline=u'Girl wins €12.500 in lottery', + pub_date=datetime.datetime(2005, 7, 28) + ) + # The default str() output will be the UTF-8 encoded output of __unicode__(). + self.assertEqual(str(a), 'Girl wins \xe2\x82\xac12.500 in lottery')
\ No newline at end of file diff --git a/parts/django/tests/modeltests/test_client/__init__.py b/parts/django/tests/modeltests/test_client/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/modeltests/test_client/__init__.py diff --git a/parts/django/tests/modeltests/test_client/fixtures/testdata.json b/parts/django/tests/modeltests/test_client/fixtures/testdata.json new file mode 100644 index 0000000..0dcf625 --- /dev/null +++ b/parts/django/tests/modeltests/test_client/fixtures/testdata.json @@ -0,0 +1,56 @@ +[ + { + "pk": "1", + "model": "auth.user", + "fields": { + "username": "testclient", + "first_name": "Test", + "last_name": "Client", + "is_active": true, + "is_superuser": false, + "is_staff": false, + "last_login": "2006-12-17 07:03:31", + "groups": [], + "user_permissions": [], + "password": "sha1$6efc0$f93efe9fd7542f25a7be94871ea45aa95de57161", + "email": "testclient@example.com", + "date_joined": "2006-12-17 07:03:31" + } + }, + { + "pk": "2", + "model": "auth.user", + "fields": { + "username": "inactive", + "first_name": "Inactive", + "last_name": "User", + "is_active": false, + "is_superuser": false, + "is_staff": false, + "last_login": "2006-12-17 07:03:31", + "groups": [], + "user_permissions": [], + "password": "sha1$6efc0$f93efe9fd7542f25a7be94871ea45aa95de57161", + "email": "testclient@example.com", + "date_joined": "2006-12-17 07:03:31" + } + }, + { + "pk": "3", + "model": "auth.user", + "fields": { + "username": "staff", + "first_name": "Staff", + "last_name": "Member", + "is_active": true, + "is_superuser": false, + "is_staff": true, + "last_login": "2006-12-17 07:03:31", + "groups": [], + "user_permissions": [], + "password": "sha1$6efc0$f93efe9fd7542f25a7be94871ea45aa95de57161", + "email": "testclient@example.com", + "date_joined": "2006-12-17 07:03:31" + } + } +]
\ No newline at end of file diff --git a/parts/django/tests/modeltests/test_client/models.py b/parts/django/tests/modeltests/test_client/models.py new file mode 100644 index 0000000..654f649 --- /dev/null +++ b/parts/django/tests/modeltests/test_client/models.py @@ -0,0 +1,459 @@ +# coding: utf-8 +""" +39. Testing using the Test Client + +The test client is a class that can act like a simple +browser for testing purposes. + +It allows the user to compose GET and POST requests, and +obtain the response that the server gave to those requests. +The server Response objects are annotated with the details +of the contexts and templates that were rendered during the +process of serving the request. + +``Client`` objects are stateful - they will retain cookie (and +thus session) details for the lifetime of the ``Client`` instance. + +This is not intended as a replacement for Twill, Selenium, or +other browser automation frameworks - it is here to allow +testing against the contexts and templates produced by a view, +rather than the HTML rendered to the end-user. + +""" +from django.test import Client, TestCase +from django.conf import settings +from django.core import mail + +class ClientTest(TestCase): + fixtures = ['testdata.json'] + + def test_get_view(self): + "GET a view" + # The data is ignored, but let's check it doesn't crash the system + # anyway. + data = {'var': u'\xf2'} + response = self.client.get('/test_client/get_view/', data) + + # Check some response details + self.assertContains(response, 'This is a test') + self.assertEqual(response.context['var'], u'\xf2') + self.assertEqual(response.template.name, 'GET Template') + + def test_get_post_view(self): + "GET a view that normally expects POSTs" + response = self.client.get('/test_client/post_view/', {}) + + # Check some response details + self.assertEqual(response.status_code, 200) + self.assertEqual(response.template.name, 'Empty GET Template') + self.assertTemplateUsed(response, 'Empty GET Template') + self.assertTemplateNotUsed(response, 'Empty POST Template') + + def test_empty_post(self): + "POST an empty dictionary to a view" + response = self.client.post('/test_client/post_view/', {}) + + # Check some response details + self.assertEqual(response.status_code, 200) + self.assertEqual(response.template.name, 'Empty POST Template') + self.assertTemplateNotUsed(response, 'Empty GET Template') + self.assertTemplateUsed(response, 'Empty POST Template') + + def test_post(self): + "POST some data to a view" + post_data = { + 'value': 37 + } + response = self.client.post('/test_client/post_view/', post_data) + + # Check some response details + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context['data'], '37') + self.assertEqual(response.template.name, 'POST Template') + self.assertTrue('Data received' in response.content) + + def test_response_headers(self): + "Check the value of HTTP headers returned in a response" + response = self.client.get("/test_client/header_view/") + + self.assertEquals(response['X-DJANGO-TEST'], 'Slartibartfast') + + def test_raw_post(self): + "POST raw data (with a content type) to a view" + test_doc = """<?xml version="1.0" encoding="utf-8"?><library><book><title>Blink</title><author>Malcolm Gladwell</author></book></library>""" + response = self.client.post("/test_client/raw_post_view/", test_doc, + content_type="text/xml") + self.assertEqual(response.status_code, 200) + self.assertEqual(response.template.name, "Book template") + self.assertEqual(response.content, "Blink - Malcolm Gladwell") + + def test_redirect(self): + "GET a URL that redirects elsewhere" + response = self.client.get('/test_client/redirect_view/') + # Check that the response was a 302 (redirect) and that + # assertRedirect() understands to put an implicit http://testserver/ in + # front of non-absolute URLs. + self.assertRedirects(response, '/test_client/get_view/') + + host = 'django.testserver' + client_providing_host = Client(HTTP_HOST=host) + response = client_providing_host.get('/test_client/redirect_view/') + # Check that the response was a 302 (redirect) with absolute URI + self.assertRedirects(response, '/test_client/get_view/', host=host) + + def test_redirect_with_query(self): + "GET a URL that redirects with given GET parameters" + response = self.client.get('/test_client/redirect_view/', {'var': 'value'}) + + # Check if parameters are intact + self.assertRedirects(response, 'http://testserver/test_client/get_view/?var=value') + + def test_permanent_redirect(self): + "GET a URL that redirects permanently elsewhere" + response = self.client.get('/test_client/permanent_redirect_view/') + # Check that the response was a 301 (permanent redirect) + self.assertRedirects(response, 'http://testserver/test_client/get_view/', status_code=301) + + client_providing_host = Client(HTTP_HOST='django.testserver') + response = client_providing_host.get('/test_client/permanent_redirect_view/') + # Check that the response was a 301 (permanent redirect) with absolute URI + self.assertRedirects(response, 'http://django.testserver/test_client/get_view/', status_code=301) + + def test_temporary_redirect(self): + "GET a URL that does a non-permanent redirect" + response = self.client.get('/test_client/temporary_redirect_view/') + # Check that the response was a 302 (non-permanent redirect) + self.assertRedirects(response, 'http://testserver/test_client/get_view/', status_code=302) + + def test_redirect_to_strange_location(self): + "GET a URL that redirects to a non-200 page" + response = self.client.get('/test_client/double_redirect_view/') + + # Check that the response was a 302, and that + # the attempt to get the redirection location returned 301 when retrieved + self.assertRedirects(response, 'http://testserver/test_client/permanent_redirect_view/', target_status_code=301) + + def test_follow_redirect(self): + "A URL that redirects can be followed to termination." + response = self.client.get('/test_client/double_redirect_view/', follow=True) + self.assertRedirects(response, 'http://testserver/test_client/get_view/', status_code=302, target_status_code=200) + self.assertEquals(len(response.redirect_chain), 2) + + def test_redirect_http(self): + "GET a URL that redirects to an http URI" + response = self.client.get('/test_client/http_redirect_view/',follow=True) + self.assertFalse(response.test_was_secure_request) + + def test_redirect_https(self): + "GET a URL that redirects to an https URI" + response = self.client.get('/test_client/https_redirect_view/',follow=True) + self.assertTrue(response.test_was_secure_request) + + def test_notfound_response(self): + "GET a URL that responds as '404:Not Found'" + response = self.client.get('/test_client/bad_view/') + + # Check that the response was a 404, and that the content contains MAGIC + self.assertContains(response, 'MAGIC', status_code=404) + + def test_valid_form(self): + "POST valid data to a form" + post_data = { + 'text': 'Hello World', + 'email': 'foo@example.com', + 'value': 37, + 'single': 'b', + 'multi': ('b','c','e') + } + response = self.client.post('/test_client/form_view/', post_data) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "Valid POST Template") + + def test_valid_form_with_hints(self): + "GET a form, providing hints in the GET data" + hints = { + 'text': 'Hello World', + 'multi': ('b','c','e') + } + response = self.client.get('/test_client/form_view/', data=hints) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "Form GET Template") + # Check that the multi-value data has been rolled out ok + self.assertContains(response, 'Select a valid choice.', 0) + + def test_incomplete_data_form(self): + "POST incomplete data to a form" + post_data = { + 'text': 'Hello World', + 'value': 37 + } + response = self.client.post('/test_client/form_view/', post_data) + self.assertContains(response, 'This field is required.', 3) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "Invalid POST Template") + + self.assertFormError(response, 'form', 'email', 'This field is required.') + self.assertFormError(response, 'form', 'single', 'This field is required.') + self.assertFormError(response, 'form', 'multi', 'This field is required.') + + def test_form_error(self): + "POST erroneous data to a form" + post_data = { + 'text': 'Hello World', + 'email': 'not an email address', + 'value': 37, + 'single': 'b', + 'multi': ('b','c','e') + } + response = self.client.post('/test_client/form_view/', post_data) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "Invalid POST Template") + + self.assertFormError(response, 'form', 'email', 'Enter a valid e-mail address.') + + def test_valid_form_with_template(self): + "POST valid data to a form using multiple templates" + post_data = { + 'text': 'Hello World', + 'email': 'foo@example.com', + 'value': 37, + 'single': 'b', + 'multi': ('b','c','e') + } + response = self.client.post('/test_client/form_view_with_template/', post_data) + self.assertContains(response, 'POST data OK') + self.assertTemplateUsed(response, "form_view.html") + self.assertTemplateUsed(response, 'base.html') + self.assertTemplateNotUsed(response, "Valid POST Template") + + def test_incomplete_data_form_with_template(self): + "POST incomplete data to a form using multiple templates" + post_data = { + 'text': 'Hello World', + 'value': 37 + } + response = self.client.post('/test_client/form_view_with_template/', post_data) + self.assertContains(response, 'POST data has errors') + self.assertTemplateUsed(response, 'form_view.html') + self.assertTemplateUsed(response, 'base.html') + self.assertTemplateNotUsed(response, "Invalid POST Template") + + self.assertFormError(response, 'form', 'email', 'This field is required.') + self.assertFormError(response, 'form', 'single', 'This field is required.') + self.assertFormError(response, 'form', 'multi', 'This field is required.') + + def test_form_error_with_template(self): + "POST erroneous data to a form using multiple templates" + post_data = { + 'text': 'Hello World', + 'email': 'not an email address', + 'value': 37, + 'single': 'b', + 'multi': ('b','c','e') + } + response = self.client.post('/test_client/form_view_with_template/', post_data) + self.assertContains(response, 'POST data has errors') + self.assertTemplateUsed(response, "form_view.html") + self.assertTemplateUsed(response, 'base.html') + self.assertTemplateNotUsed(response, "Invalid POST Template") + + self.assertFormError(response, 'form', 'email', 'Enter a valid e-mail address.') + + def test_unknown_page(self): + "GET an invalid URL" + response = self.client.get('/test_client/unknown_view/') + + # Check that the response was a 404 + self.assertEqual(response.status_code, 404) + + def test_view_with_login(self): + "Request a page that is protected with @login_required" + + # Get the page without logging in. Should result in 302. + response = self.client.get('/test_client/login_protected_view/') + self.assertRedirects(response, 'http://testserver/accounts/login/?next=/test_client/login_protected_view/') + + # Log in + login = self.client.login(username='testclient', password='password') + self.assertTrue(login, 'Could not log in') + + # Request a page that requires a login + response = self.client.get('/test_client/login_protected_view/') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context['user'].username, 'testclient') + + def test_view_with_method_login(self): + "Request a page that is protected with a @login_required method" + + # Get the page without logging in. Should result in 302. + response = self.client.get('/test_client/login_protected_method_view/') + self.assertRedirects(response, 'http://testserver/accounts/login/?next=/test_client/login_protected_method_view/') + + # Log in + login = self.client.login(username='testclient', password='password') + self.assertTrue(login, 'Could not log in') + + # Request a page that requires a login + response = self.client.get('/test_client/login_protected_method_view/') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context['user'].username, 'testclient') + + def test_view_with_login_and_custom_redirect(self): + "Request a page that is protected with @login_required(redirect_field_name='redirect_to')" + + # Get the page without logging in. Should result in 302. + response = self.client.get('/test_client/login_protected_view_custom_redirect/') + self.assertRedirects(response, 'http://testserver/accounts/login/?redirect_to=/test_client/login_protected_view_custom_redirect/') + + # Log in + login = self.client.login(username='testclient', password='password') + self.assertTrue(login, 'Could not log in') + + # Request a page that requires a login + response = self.client.get('/test_client/login_protected_view_custom_redirect/') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context['user'].username, 'testclient') + + def test_view_with_bad_login(self): + "Request a page that is protected with @login, but use bad credentials" + + login = self.client.login(username='otheruser', password='nopassword') + self.assertFalse(login) + + def test_view_with_inactive_login(self): + "Request a page that is protected with @login, but use an inactive login" + + login = self.client.login(username='inactive', password='password') + self.assertFalse(login) + + def test_logout(self): + "Request a logout after logging in" + # Log in + self.client.login(username='testclient', password='password') + + # Request a page that requires a login + response = self.client.get('/test_client/login_protected_view/') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context['user'].username, 'testclient') + + # Log out + self.client.logout() + + # Request a page that requires a login + response = self.client.get('/test_client/login_protected_view/') + self.assertRedirects(response, 'http://testserver/accounts/login/?next=/test_client/login_protected_view/') + + def test_view_with_permissions(self): + "Request a page that is protected with @permission_required" + + # Get the page without logging in. Should result in 302. + response = self.client.get('/test_client/permission_protected_view/') + self.assertRedirects(response, 'http://testserver/accounts/login/?next=/test_client/permission_protected_view/') + + # Log in + login = self.client.login(username='testclient', password='password') + self.assertTrue(login, 'Could not log in') + + # Log in with wrong permissions. Should result in 302. + response = self.client.get('/test_client/permission_protected_view/') + self.assertRedirects(response, 'http://testserver/accounts/login/?next=/test_client/permission_protected_view/') + + # TODO: Log in with right permissions and request the page again + + def test_view_with_method_permissions(self): + "Request a page that is protected with a @permission_required method" + + # Get the page without logging in. Should result in 302. + response = self.client.get('/test_client/permission_protected_method_view/') + self.assertRedirects(response, 'http://testserver/accounts/login/?next=/test_client/permission_protected_method_view/') + + # Log in + login = self.client.login(username='testclient', password='password') + self.assertTrue(login, 'Could not log in') + + # Log in with wrong permissions. Should result in 302. + response = self.client.get('/test_client/permission_protected_method_view/') + self.assertRedirects(response, 'http://testserver/accounts/login/?next=/test_client/permission_protected_method_view/') + + # TODO: Log in with right permissions and request the page again + + def test_session_modifying_view(self): + "Request a page that modifies the session" + # Session value isn't set initially + try: + self.client.session['tobacconist'] + self.fail("Shouldn't have a session value") + except KeyError: + pass + + from django.contrib.sessions.models import Session + response = self.client.post('/test_client/session_view/') + + # Check that the session was modified + self.assertEquals(self.client.session['tobacconist'], 'hovercraft') + + def test_view_with_exception(self): + "Request a page that is known to throw an error" + self.assertRaises(KeyError, self.client.get, "/test_client/broken_view/") + + #Try the same assertion, a different way + try: + self.client.get('/test_client/broken_view/') + self.fail('Should raise an error') + except KeyError: + pass + + def test_mail_sending(self): + "Test that mail is redirected to a dummy outbox during test setup" + + response = self.client.get('/test_client/mail_sending_view/') + self.assertEqual(response.status_code, 200) + + self.assertEqual(len(mail.outbox), 1) + self.assertEqual(mail.outbox[0].subject, 'Test message') + self.assertEqual(mail.outbox[0].body, 'This is a test email') + self.assertEqual(mail.outbox[0].from_email, 'from@example.com') + self.assertEqual(mail.outbox[0].to[0], 'first@example.com') + self.assertEqual(mail.outbox[0].to[1], 'second@example.com') + + def test_mass_mail_sending(self): + "Test that mass mail is redirected to a dummy outbox during test setup" + + response = self.client.get('/test_client/mass_mail_sending_view/') + self.assertEqual(response.status_code, 200) + + self.assertEqual(len(mail.outbox), 2) + self.assertEqual(mail.outbox[0].subject, 'First Test message') + self.assertEqual(mail.outbox[0].body, 'This is the first test email') + self.assertEqual(mail.outbox[0].from_email, 'from@example.com') + self.assertEqual(mail.outbox[0].to[0], 'first@example.com') + self.assertEqual(mail.outbox[0].to[1], 'second@example.com') + + self.assertEqual(mail.outbox[1].subject, 'Second Test message') + self.assertEqual(mail.outbox[1].body, 'This is the second test email') + self.assertEqual(mail.outbox[1].from_email, 'from@example.com') + self.assertEqual(mail.outbox[1].to[0], 'second@example.com') + self.assertEqual(mail.outbox[1].to[1], 'third@example.com') + +class CSRFEnabledClientTests(TestCase): + def setUp(self): + # Enable the CSRF middleware for this test + self.old_MIDDLEWARE_CLASSES = settings.MIDDLEWARE_CLASSES + csrf_middleware_class = 'django.middleware.csrf.CsrfViewMiddleware' + if csrf_middleware_class not in settings.MIDDLEWARE_CLASSES: + settings.MIDDLEWARE_CLASSES += (csrf_middleware_class,) + + def tearDown(self): + settings.MIDDLEWARE_CLASSES = self.old_MIDDLEWARE_CLASSES + + def test_csrf_enabled_client(self): + "A client can be instantiated with CSRF checks enabled" + csrf_client = Client(enforce_csrf_checks=True) + + # The normal client allows the post + response = self.client.post('/test_client/post_view/', {}) + self.assertEqual(response.status_code, 200) + + # The CSRF-enabled client rejects it + response = csrf_client.post('/test_client/post_view/', {}) + self.assertEqual(response.status_code, 403) diff --git a/parts/django/tests/modeltests/test_client/tests.py b/parts/django/tests/modeltests/test_client/tests.py new file mode 100644 index 0000000..09f292e --- /dev/null +++ b/parts/django/tests/modeltests/test_client/tests.py @@ -0,0 +1,20 @@ +# Validate that you can override the default test suite + +import unittest + +def suite(): + """ + Define a suite that deliberately ignores a test defined in + this module. + """ + + testSuite = unittest.TestSuite() + testSuite.addTest(SampleTests('testGoodStuff')) + return testSuite + +class SampleTests(unittest.TestCase): + def testGoodStuff(self): + pass + + def testBadStuff(self): + self.fail("This test shouldn't run") diff --git a/parts/django/tests/modeltests/test_client/urls.py b/parts/django/tests/modeltests/test_client/urls.py new file mode 100644 index 0000000..9e0eabe --- /dev/null +++ b/parts/django/tests/modeltests/test_client/urls.py @@ -0,0 +1,29 @@ +from django.conf.urls.defaults import * +from django.views.generic.simple import redirect_to +import views + +urlpatterns = patterns('', + (r'^get_view/$', views.get_view), + (r'^post_view/$', views.post_view), + (r'^header_view/$', views.view_with_header), + (r'^raw_post_view/$', views.raw_post_view), + (r'^redirect_view/$', views.redirect_view), + (r'^secure_view/$', views.view_with_secure), + (r'^permanent_redirect_view/$', redirect_to, {'url': '/test_client/get_view/'}), + (r'^temporary_redirect_view/$', redirect_to, {'url': '/test_client/get_view/', 'permanent': False}), + (r'^http_redirect_view/$', redirect_to, {'url': '/test_client/secure_view/'}), + (r'^https_redirect_view/$', redirect_to, {'url': 'https://testserver/test_client/secure_view/'}), + (r'^double_redirect_view/$', views.double_redirect_view), + (r'^bad_view/$', views.bad_view), + (r'^form_view/$', views.form_view), + (r'^form_view_with_template/$', views.form_view_with_template), + (r'^login_protected_view/$', views.login_protected_view), + (r'^login_protected_method_view/$', views.login_protected_method_view), + (r'^login_protected_view_custom_redirect/$', views.login_protected_view_changed_redirect), + (r'^permission_protected_view/$', views.permission_protected_view), + (r'^permission_protected_method_view/$', views.permission_protected_method_view), + (r'^session_view/$', views.session_view), + (r'^broken_view/$', views.broken_view), + (r'^mail_sending_view/$', views.mail_sending_view), + (r'^mass_mail_sending_view/$', views.mass_mail_sending_view) +) diff --git a/parts/django/tests/modeltests/test_client/views.py b/parts/django/tests/modeltests/test_client/views.py new file mode 100644 index 0000000..baa9525 --- /dev/null +++ b/parts/django/tests/modeltests/test_client/views.py @@ -0,0 +1,214 @@ +from xml.dom.minidom import parseString + +from django.core import mail +from django.template import Context, Template +from django.http import HttpResponse, HttpResponseRedirect, HttpResponseNotFound +from django.contrib.auth.decorators import login_required, permission_required +from django.forms.forms import Form +from django.forms import fields +from django.shortcuts import render_to_response +from django.utils.decorators import method_decorator + +def get_view(request): + "A simple view that expects a GET request, and returns a rendered template" + t = Template('This is a test. {{ var }} is the value.', name='GET Template') + c = Context({'var': request.GET.get('var', 42)}) + + return HttpResponse(t.render(c)) + +def post_view(request): + """A view that expects a POST, and returns a different template depending + on whether any POST data is available + """ + if request.method == 'POST': + if request.POST: + t = Template('Data received: {{ data }} is the value.', name='POST Template') + c = Context({'data': request.POST['value']}) + else: + t = Template('Viewing POST page.', name='Empty POST Template') + c = Context() + else: + t = Template('Viewing GET page.', name='Empty GET Template') + c = Context() + + return HttpResponse(t.render(c)) + +def view_with_header(request): + "A view that has a custom header" + response = HttpResponse() + response['X-DJANGO-TEST'] = 'Slartibartfast' + return response + +def raw_post_view(request): + """A view which expects raw XML to be posted and returns content extracted + from the XML""" + if request.method == 'POST': + root = parseString(request.raw_post_data) + first_book = root.firstChild.firstChild + title, author = [n.firstChild.nodeValue for n in first_book.childNodes] + t = Template("{{ title }} - {{ author }}", name="Book template") + c = Context({"title": title, "author": author}) + else: + t = Template("GET request.", name="Book GET template") + c = Context() + + return HttpResponse(t.render(c)) + +def redirect_view(request): + "A view that redirects all requests to the GET view" + if request.GET: + from urllib import urlencode + query = '?' + urlencode(request.GET, True) + else: + query = '' + return HttpResponseRedirect('/test_client/get_view/' + query) + +def view_with_secure(request): + "A view that indicates if the request was secure" + response = HttpResponse() + response.test_was_secure_request = request.is_secure() + return response + +def double_redirect_view(request): + "A view that redirects all requests to a redirection view" + return HttpResponseRedirect('/test_client/permanent_redirect_view/') + +def bad_view(request): + "A view that returns a 404 with some error content" + return HttpResponseNotFound('Not found!. This page contains some MAGIC content') + +TestChoices = ( + ('a', 'First Choice'), + ('b', 'Second Choice'), + ('c', 'Third Choice'), + ('d', 'Fourth Choice'), + ('e', 'Fifth Choice') +) + +class TestForm(Form): + text = fields.CharField() + email = fields.EmailField() + value = fields.IntegerField() + single = fields.ChoiceField(choices=TestChoices) + multi = fields.MultipleChoiceField(choices=TestChoices) + +def form_view(request): + "A view that tests a simple form" + if request.method == 'POST': + form = TestForm(request.POST) + if form.is_valid(): + t = Template('Valid POST data.', name='Valid POST Template') + c = Context() + else: + t = Template('Invalid POST data. {{ form.errors }}', name='Invalid POST Template') + c = Context({'form': form}) + else: + form = TestForm(request.GET) + t = Template('Viewing base form. {{ form }}.', name='Form GET Template') + c = Context({'form': form}) + + return HttpResponse(t.render(c)) + +def form_view_with_template(request): + "A view that tests a simple form" + if request.method == 'POST': + form = TestForm(request.POST) + if form.is_valid(): + message = 'POST data OK' + else: + message = 'POST data has errors' + else: + form = TestForm() + message = 'GET form page' + return render_to_response('form_view.html', + { + 'form': form, + 'message': message + } + ) + +def login_protected_view(request): + "A simple view that is login protected." + t = Template('This is a login protected test. Username is {{ user.username }}.', name='Login Template') + c = Context({'user': request.user}) + + return HttpResponse(t.render(c)) +login_protected_view = login_required(login_protected_view) + +def login_protected_view_changed_redirect(request): + "A simple view that is login protected with a custom redirect field set" + t = Template('This is a login protected test. Username is {{ user.username }}.', name='Login Template') + c = Context({'user': request.user}) + + return HttpResponse(t.render(c)) +login_protected_view_changed_redirect = login_required(redirect_field_name="redirect_to")(login_protected_view_changed_redirect) + +def permission_protected_view(request): + "A simple view that is permission protected." + t = Template('This is a permission protected test. ' + 'Username is {{ user.username }}. ' + 'Permissions are {{ user.get_all_permissions }}.' , + name='Permissions Template') + c = Context({'user': request.user}) + return HttpResponse(t.render(c)) +permission_protected_view = permission_required('modeltests.test_perm')(permission_protected_view) + +class _ViewManager(object): + @method_decorator(login_required) + def login_protected_view(self, request): + t = Template('This is a login protected test using a method. ' + 'Username is {{ user.username }}.', + name='Login Method Template') + c = Context({'user': request.user}) + return HttpResponse(t.render(c)) + + @method_decorator(permission_required('modeltests.test_perm')) + def permission_protected_view(self, request): + t = Template('This is a permission protected test using a method. ' + 'Username is {{ user.username }}. ' + 'Permissions are {{ user.get_all_permissions }}.' , + name='Permissions Template') + c = Context({'user': request.user}) + return HttpResponse(t.render(c)) + +_view_manager = _ViewManager() +login_protected_method_view = _view_manager.login_protected_view +permission_protected_method_view = _view_manager.permission_protected_view + +def session_view(request): + "A view that modifies the session" + request.session['tobacconist'] = 'hovercraft' + + t = Template('This is a view that modifies the session.', + name='Session Modifying View Template') + c = Context() + return HttpResponse(t.render(c)) + +def broken_view(request): + """A view which just raises an exception, simulating a broken view.""" + raise KeyError("Oops! Looks like you wrote some bad code.") + +def mail_sending_view(request): + mail.EmailMessage( + "Test message", + "This is a test email", + "from@example.com", + ['first@example.com', 'second@example.com']).send() + return HttpResponse("Mail sent") + +def mass_mail_sending_view(request): + m1 = mail.EmailMessage( + 'First Test message', + 'This is the first test email', + 'from@example.com', + ['first@example.com', 'second@example.com']) + m2 = mail.EmailMessage( + 'Second Test message', + 'This is the second test email', + 'from@example.com', + ['second@example.com', 'third@example.com']) + + c = mail.get_connection() + c.send_messages([m1,m2]) + + return HttpResponse("Mail sent") diff --git a/parts/django/tests/modeltests/transactions/__init__.py b/parts/django/tests/modeltests/transactions/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/modeltests/transactions/__init__.py diff --git a/parts/django/tests/modeltests/transactions/models.py b/parts/django/tests/modeltests/transactions/models.py new file mode 100644 index 0000000..d957fe1 --- /dev/null +++ b/parts/django/tests/modeltests/transactions/models.py @@ -0,0 +1,21 @@ +""" +15. Transactions + +Django handles transactions in three different ways. The default is to commit +each transaction upon a write, but you can decorate a function to get +commit-on-success behavior. Alternatively, you can manage the transaction +manually. +""" + +from django.db import models, DEFAULT_DB_ALIAS + +class Reporter(models.Model): + first_name = models.CharField(max_length=30) + last_name = models.CharField(max_length=30) + email = models.EmailField() + + class Meta: + ordering = ('first_name', 'last_name') + + def __unicode__(self): + return u"%s %s" % (self.first_name, self.last_name)
\ No newline at end of file diff --git a/parts/django/tests/modeltests/transactions/tests.py b/parts/django/tests/modeltests/transactions/tests.py new file mode 100644 index 0000000..9964f5d --- /dev/null +++ b/parts/django/tests/modeltests/transactions/tests.py @@ -0,0 +1,155 @@ +from django.test import TransactionTestCase +from django.db import connection, transaction, IntegrityError, DEFAULT_DB_ALIAS +from django.conf import settings + +from models import Reporter + +PGSQL = 'psycopg2' in settings.DATABASES[DEFAULT_DB_ALIAS]['ENGINE'] +MYSQL = 'mysql' in settings.DATABASES[DEFAULT_DB_ALIAS]['ENGINE'] + +class TransactionTests(TransactionTestCase): + + if not MYSQL: + + def create_a_reporter_then_fail(self, first, last): + a = Reporter(first_name=first, last_name=last) + a.save() + raise Exception("I meant to do that") + + def remove_a_reporter(self, first_name): + r = Reporter.objects.get(first_name="Alice") + r.delete() + + def manually_managed(self): + r = Reporter(first_name="Dirk", last_name="Gently") + r.save() + transaction.commit() + + def manually_managed_mistake(self): + r = Reporter(first_name="Edward", last_name="Woodward") + r.save() + # Oops, I forgot to commit/rollback! + + def execute_bad_sql(self): + cursor = connection.cursor() + cursor.execute("INSERT INTO transactions_reporter (first_name, last_name) VALUES ('Douglas', 'Adams');") + transaction.set_dirty() + + def test_autocommit(self): + """ + The default behavior is to autocommit after each save() action. + """ + self.assertRaises(Exception, + self.create_a_reporter_then_fail, + "Alice", "Smith" + ) + + # The object created before the exception still exists + self.assertEqual(Reporter.objects.count(), 1) + + def test_autocommit_decorator(self): + """ + The autocommit decorator works exactly the same as the default behavior. + """ + autocomitted_create_then_fail = transaction.autocommit( + self.create_a_reporter_then_fail + ) + self.assertRaises(Exception, + autocomitted_create_then_fail, + "Alice", "Smith" + ) + # Again, the object created before the exception still exists + self.assertEqual(Reporter.objects.count(), 1) + + def test_autocommit_decorator_with_using(self): + """ + The autocommit decorator also works with a using argument. + """ + autocomitted_create_then_fail = transaction.autocommit(using='default')( + self.create_a_reporter_then_fail + ) + self.assertRaises(Exception, + autocomitted_create_then_fail, + "Alice", "Smith" + ) + # Again, the object created before the exception still exists + self.assertEqual(Reporter.objects.count(), 1) + + def test_commit_on_success(self): + """ + With the commit_on_success decorator, the transaction is only committed + if the function doesn't throw an exception. + """ + committed_on_success = transaction.commit_on_success( + self.create_a_reporter_then_fail) + self.assertRaises(Exception, committed_on_success, "Dirk", "Gently") + # This time the object never got saved + self.assertEqual(Reporter.objects.count(), 0) + + def test_commit_on_success_with_using(self): + """ + The commit_on_success decorator also works with a using argument. + """ + using_committed_on_success = transaction.commit_on_success(using='default')( + self.create_a_reporter_then_fail + ) + self.assertRaises(Exception, + using_committed_on_success, + "Dirk", "Gently" + ) + # This time the object never got saved + self.assertEqual(Reporter.objects.count(), 0) + + def test_commit_on_success_succeed(self): + """ + If there aren't any exceptions, the data will get saved. + """ + Reporter.objects.create(first_name="Alice", last_name="Smith") + remove_comitted_on_success = transaction.commit_on_success( + self.remove_a_reporter + ) + remove_comitted_on_success("Alice") + self.assertEqual(list(Reporter.objects.all()), []) + + def test_manually_managed(self): + """ + You can manually manage transactions if you really want to, but you + have to remember to commit/rollback. + """ + manually_managed = transaction.commit_manually(self.manually_managed) + manually_managed() + self.assertEqual(Reporter.objects.count(), 1) + + def test_manually_managed_mistake(self): + """ + If you forget, you'll get bad errors. + """ + manually_managed_mistake = transaction.commit_manually( + self.manually_managed_mistake + ) + self.assertRaises(transaction.TransactionManagementError, + manually_managed_mistake) + + def test_manually_managed_with_using(self): + """ + The commit_manually function also works with a using argument. + """ + using_manually_managed_mistake = transaction.commit_manually(using='default')( + self.manually_managed_mistake + ) + self.assertRaises(transaction.TransactionManagementError, + using_manually_managed_mistake + ) + + if PGSQL: + + def test_bad_sql(self): + """ + Regression for #11900: If a function wrapped by commit_on_success + writes a transaction that can't be committed, that transaction should + be rolled back. The bug is only visible using the psycopg2 backend, + though the fix is generally a good idea. + """ + execute_bad_sql = transaction.commit_on_success(self.execute_bad_sql) + self.assertRaises(IntegrityError, execute_bad_sql) + transaction.rollback() diff --git a/parts/django/tests/modeltests/unmanaged_models/__init__.py b/parts/django/tests/modeltests/unmanaged_models/__init__.py new file mode 100644 index 0000000..139597f --- /dev/null +++ b/parts/django/tests/modeltests/unmanaged_models/__init__.py @@ -0,0 +1,2 @@ + + diff --git a/parts/django/tests/modeltests/unmanaged_models/models.py b/parts/django/tests/modeltests/unmanaged_models/models.py new file mode 100644 index 0000000..0c2cf50 --- /dev/null +++ b/parts/django/tests/modeltests/unmanaged_models/models.py @@ -0,0 +1,125 @@ +""" +Models can have a ``managed`` attribute, which specifies whether the SQL code +is generated for the table on various manage.py operations. +""" + +from django.db import models + +# All of these models are creatd in the database by Django. + +class A01(models.Model): + f_a = models.CharField(max_length=10, db_index=True) + f_b = models.IntegerField() + + class Meta: + db_table = 'A01' + + def __unicode__(self): + return self.f_a + +class B01(models.Model): + fk_a = models.ForeignKey(A01) + f_a = models.CharField(max_length=10, db_index=True) + f_b = models.IntegerField() + + class Meta: + db_table = 'B01' + # 'managed' is True by default. This tests we can set it explicitly. + managed = True + + def __unicode__(self): + return self.f_a + +class C01(models.Model): + mm_a = models.ManyToManyField(A01, db_table='D01') + f_a = models.CharField(max_length=10, db_index=True) + f_b = models.IntegerField() + + class Meta: + db_table = 'C01' + + def __unicode__(self): + return self.f_a + +# All of these models use the same tables as the previous set (they are shadows +# of possibly a subset of the columns). There should be no creation errors, +# since we have told Django they aren't managed by Django. + +class A02(models.Model): + f_a = models.CharField(max_length=10, db_index=True) + + class Meta: + db_table = 'A01' + managed = False + + def __unicode__(self): + return self.f_a + +class B02(models.Model): + class Meta: + db_table = 'B01' + managed = False + + fk_a = models.ForeignKey(A02) + f_a = models.CharField(max_length=10, db_index=True) + f_b = models.IntegerField() + + def __unicode__(self): + return self.f_a + +# To re-use the many-to-many intermediate table, we need to manually set up +# things up. +class C02(models.Model): + mm_a = models.ManyToManyField(A02, through="Intermediate") + f_a = models.CharField(max_length=10, db_index=True) + f_b = models.IntegerField() + + class Meta: + db_table = 'C01' + managed = False + + def __unicode__(self): + return self.f_a + +class Intermediate(models.Model): + a02 = models.ForeignKey(A02, db_column="a01_id") + c02 = models.ForeignKey(C02, db_column="c01_id") + + class Meta: + db_table = 'D01' + managed = False + +# +# These next models test the creation (or not) of many to many join tables +# between managed and unmanaged models. A join table between two unmanaged +# models shouldn't be automatically created (see #10647). +# + +# Firstly, we need some models that will create the tables, purely so that the +# tables are created. This is a test setup, not a requirement for unmanaged +# models. +class Proxy1(models.Model): + class Meta: + db_table = "unmanaged_models_proxy1" + +class Proxy2(models.Model): + class Meta: + db_table = "unmanaged_models_proxy2" + +class Unmanaged1(models.Model): + class Meta: + managed = False + db_table = "unmanaged_models_proxy1" + +# Unmanged with an m2m to unmanaged: the intermediary table won't be created. +class Unmanaged2(models.Model): + mm = models.ManyToManyField(Unmanaged1) + + class Meta: + managed = False + db_table = "unmanaged_models_proxy2" + +# Here's an unmanaged model with an m2m to a managed one; the intermediary +# table *will* be created (unless given a custom `through` as for C02 above). +class Managed1(models.Model): + mm = models.ManyToManyField(Unmanaged1) diff --git a/parts/django/tests/modeltests/unmanaged_models/tests.py b/parts/django/tests/modeltests/unmanaged_models/tests.py new file mode 100644 index 0000000..dbbe848 --- /dev/null +++ b/parts/django/tests/modeltests/unmanaged_models/tests.py @@ -0,0 +1,58 @@ +from django.test import TestCase +from django.db import connection +from models import Unmanaged1, Unmanaged2, Managed1 +from models import A01, A02, B01, B02, C01, C02 + +class SimpleTests(TestCase): + + def test_simple(self): + """ + The main test here is that the all the models can be created without + any database errors. We can also do some more simple insertion and + lookup tests whilst we're here to show that the second of models do + refer to the tables from the first set. + """ + # Insert some data into one set of models. + a = A01.objects.create(f_a="foo", f_b=42) + B01.objects.create(fk_a=a, f_a="fred", f_b=1729) + c = C01.objects.create(f_a="barney", f_b=1) + c.mm_a = [a] + + # ... and pull it out via the other set. + a2 = A02.objects.all()[0] + self.assertTrue(isinstance(a2, A02)) + self.assertEqual(a2.f_a, "foo") + + b2 = B02.objects.all()[0] + self.assertTrue(isinstance(b2, B02)) + self.assertEqual(b2.f_a, "fred") + + self.assertTrue(isinstance(b2.fk_a, A02)) + self.assertEqual(b2.fk_a.f_a, "foo") + + self.assertEqual(list(C02.objects.filter(f_a=None)), []) + + resp = list(C02.objects.filter(mm_a=a.id)) + self.assertEqual(len(resp), 1) + + self.assertTrue(isinstance(resp[0], C02)) + self.assertEqual(resp[0].f_a, 'barney') + + +class ManyToManyUnmanagedTests(TestCase): + + def test_many_to_many_between_unmanaged(self): + """ + The intermediary table between two unmanaged models should not be created. + """ + table = Unmanaged2._meta.get_field('mm').m2m_db_table() + tables = connection.introspection.table_names() + self.assert_(table not in tables, "Table '%s' should not exist, but it does." % table) + + def test_many_to_many_between_unmanaged_and_managed(self): + """ + An intermediary table between a managed and an unmanaged model should be created. + """ + table = Managed1._meta.get_field('mm').m2m_db_table() + tables = connection.introspection.table_names() + self.assert_(table in tables, "Table '%s' does not exist." % table) diff --git a/parts/django/tests/modeltests/update/__init__.py b/parts/django/tests/modeltests/update/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/modeltests/update/__init__.py diff --git a/parts/django/tests/modeltests/update/models.py b/parts/django/tests/modeltests/update/models.py new file mode 100644 index 0000000..7b633e2 --- /dev/null +++ b/parts/django/tests/modeltests/update/models.py @@ -0,0 +1,35 @@ +""" +Tests for the update() queryset method that allows in-place, multi-object +updates. +""" + +from django.db import models + +class DataPoint(models.Model): + name = models.CharField(max_length=20) + value = models.CharField(max_length=20) + another_value = models.CharField(max_length=20, blank=True) + + def __unicode__(self): + return unicode(self.name) + +class RelatedPoint(models.Model): + name = models.CharField(max_length=20) + data = models.ForeignKey(DataPoint) + + def __unicode__(self): + return unicode(self.name) + + +class A(models.Model): + x = models.IntegerField(default=10) + +class B(models.Model): + a = models.ForeignKey(A) + y = models.IntegerField(default=10) + +class C(models.Model): + y = models.IntegerField(default=10) + +class D(C): + a = models.ForeignKey(A) diff --git a/parts/django/tests/modeltests/update/tests.py b/parts/django/tests/modeltests/update/tests.py new file mode 100644 index 0000000..d0b6ea3 --- /dev/null +++ b/parts/django/tests/modeltests/update/tests.py @@ -0,0 +1,116 @@ +from django.test import TestCase + +from models import A, B, C, D, DataPoint, RelatedPoint + + +class SimpleTest(TestCase): + def setUp(self): + self.a1 = A.objects.create() + self.a2 = A.objects.create() + for x in range(20): + B.objects.create(a=self.a1) + D.objects.create(a=self.a1) + + def test_nonempty_update(self): + """ + Test that update changes the right number of rows for a nonempty queryset + """ + num_updated = self.a1.b_set.update(y=100) + self.assertEqual(num_updated, 20) + cnt = B.objects.filter(y=100).count() + self.assertEqual(cnt, 20) + + def test_empty_update(self): + """ + Test that update changes the right number of rows for an empty queryset + """ + num_updated = self.a2.b_set.update(y=100) + self.assertEqual(num_updated, 0) + cnt = B.objects.filter(y=100).count() + self.assertEqual(cnt, 0) + + def test_nonempty_update_with_inheritance(self): + """ + Test that update changes the right number of rows for an empty queryset + when the update affects only a base table + """ + num_updated = self.a1.d_set.update(y=100) + self.assertEqual(num_updated, 20) + cnt = D.objects.filter(y=100).count() + self.assertEqual(cnt, 20) + + def test_empty_update_with_inheritance(self): + """ + Test that update changes the right number of rows for an empty queryset + when the update affects only a base table + """ + num_updated = self.a2.d_set.update(y=100) + self.assertEqual(num_updated, 0) + cnt = D.objects.filter(y=100).count() + self.assertEqual(cnt, 0) + +class AdvancedTests(TestCase): + + def setUp(self): + self.d0 = DataPoint.objects.create(name="d0", value="apple") + self.d2 = DataPoint.objects.create(name="d2", value="banana") + self.d3 = DataPoint.objects.create(name="d3", value="banana") + self.r1 = RelatedPoint.objects.create(name="r1", data=self.d3) + + def test_update(self): + """ + Objects are updated by first filtering the candidates into a queryset + and then calling the update() method. It executes immediately and + returns nothing. + """ + resp = DataPoint.objects.filter(value="apple").update(name="d1") + self.assertEqual(resp, 1) + resp = DataPoint.objects.filter(value="apple") + self.assertEqual(list(resp), [self.d0]) + + def test_update_multiple_objects(self): + """ + We can update multiple objects at once. + """ + resp = DataPoint.objects.filter(value="banana").update( + value="pineapple") + self.assertEqual(resp, 2) + self.assertEqual(DataPoint.objects.get(name="d2").value, u'pineapple') + + def test_update_fk(self): + """ + Foreign key fields can also be updated, although you can only update + the object referred to, not anything inside the related object. + """ + resp = RelatedPoint.objects.filter(name="r1").update(data=self.d0) + self.assertEqual(resp, 1) + resp = RelatedPoint.objects.filter(data__name="d0") + self.assertEqual(list(resp), [self.r1]) + + def test_update_multiple_fields(self): + """ + Multiple fields can be updated at once + """ + resp = DataPoint.objects.filter(value="apple").update( + value="fruit", another_value="peach") + self.assertEqual(resp, 1) + d = DataPoint.objects.get(name="d0") + self.assertEqual(d.value, u'fruit') + self.assertEqual(d.another_value, u'peach') + + def test_update_all(self): + """ + In the rare case you want to update every instance of a model, update() + is also a manager method. + """ + self.assertEqual(DataPoint.objects.update(value='thing'), 3) + resp = DataPoint.objects.values('value').distinct() + self.assertEqual(list(resp), [{'value': u'thing'}]) + + def test_update_slice_fail(self): + """ + We do not support update on already sliced query sets. + """ + method = DataPoint.objects.all()[:2].update + self.assertRaises(AssertionError, method, + another_value='another thing') diff --git a/parts/django/tests/modeltests/user_commands/__init__.py b/parts/django/tests/modeltests/user_commands/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/modeltests/user_commands/__init__.py diff --git a/parts/django/tests/modeltests/user_commands/management/__init__.py b/parts/django/tests/modeltests/user_commands/management/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/modeltests/user_commands/management/__init__.py diff --git a/parts/django/tests/modeltests/user_commands/management/commands/__init__.py b/parts/django/tests/modeltests/user_commands/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/modeltests/user_commands/management/commands/__init__.py diff --git a/parts/django/tests/modeltests/user_commands/management/commands/dance.py b/parts/django/tests/modeltests/user_commands/management/commands/dance.py new file mode 100644 index 0000000..acefe09 --- /dev/null +++ b/parts/django/tests/modeltests/user_commands/management/commands/dance.py @@ -0,0 +1,14 @@ +from optparse import make_option +from django.core.management.base import BaseCommand + +class Command(BaseCommand): + help = "Dance around like a madman." + args = '' + requires_model_validation = True + + option_list =[ + make_option("-s", "--style", default="Rock'n'Roll") + ] + + def handle(self, *args, **options): + self.stdout.write("I don't feel like dancing %s." % options["style"]) diff --git a/parts/django/tests/modeltests/user_commands/models.py b/parts/django/tests/modeltests/user_commands/models.py new file mode 100644 index 0000000..f2aa549 --- /dev/null +++ b/parts/django/tests/modeltests/user_commands/models.py @@ -0,0 +1,14 @@ +""" +38. User-registered management commands + +The ``manage.py`` utility provides a number of useful commands for managing a +Django project. If you want to add a utility command of your own, you can. + +The user-defined command ``dance`` is defined in the management/commands +subdirectory of this test application. It is a simple command that responds +with a printed message when invoked. + +For more details on how to define your own ``manage.py`` commands, look at the +``django.core.management.commands`` directory. This directory contains the +definitions for the base Django ``manage.py`` commands. +""" diff --git a/parts/django/tests/modeltests/user_commands/tests.py b/parts/django/tests/modeltests/user_commands/tests.py new file mode 100644 index 0000000..84aa7a5 --- /dev/null +++ b/parts/django/tests/modeltests/user_commands/tests.py @@ -0,0 +1,21 @@ +from StringIO import StringIO + +from django.test import TestCase +from django.core import management +from django.core.management.base import CommandError + +class CommandTests(TestCase): + def test_command(self): + out = StringIO() + management.call_command('dance', stdout=out) + self.assertEquals(out.getvalue(), + "I don't feel like dancing Rock'n'Roll.") + + def test_command_style(self): + out = StringIO() + management.call_command('dance', style='Jive', stdout=out) + self.assertEquals(out.getvalue(), + "I don't feel like dancing Jive.") + + def test_explode(self): + self.assertRaises(CommandError, management.call_command, ('explode',))
\ No newline at end of file diff --git a/parts/django/tests/modeltests/validation/__init__.py b/parts/django/tests/modeltests/validation/__init__.py new file mode 100644 index 0000000..d0a7d19 --- /dev/null +++ b/parts/django/tests/modeltests/validation/__init__.py @@ -0,0 +1,21 @@ +import unittest + +from django.core.exceptions import ValidationError + +class ValidationTestCase(unittest.TestCase): + def assertFailsValidation(self, clean, failed_fields): + self.assertRaises(ValidationError, clean) + try: + clean() + except ValidationError, e: + self.assertEquals(sorted(failed_fields), sorted(e.message_dict.keys())) + + def assertFieldFailsValidationWithMessage(self, clean, field_name, message): + self.assertRaises(ValidationError, clean) + try: + clean() + except ValidationError, e: + self.assertTrue(field_name in e.message_dict) + self.assertEquals(message, e.message_dict[field_name]) + + diff --git a/parts/django/tests/modeltests/validation/models.py b/parts/django/tests/modeltests/validation/models.py new file mode 100644 index 0000000..dd42936 --- /dev/null +++ b/parts/django/tests/modeltests/validation/models.py @@ -0,0 +1,65 @@ +from datetime import datetime +from django.core.exceptions import ValidationError +from django.db import models +from django.test import TestCase + + +def validate_answer_to_universe(value): + if value != 42: + raise ValidationError('This is not the answer to life, universe and everything!', code='not42') + +class ModelToValidate(models.Model): + name = models.CharField(max_length=100) + created = models.DateTimeField(default=datetime.now) + number = models.IntegerField(db_column='number_val') + parent = models.ForeignKey('self', blank=True, null=True, limit_choices_to={'number': 10}) + email = models.EmailField(blank=True) + url = models.URLField(blank=True) + f_with_custom_validator = models.IntegerField(blank=True, null=True, validators=[validate_answer_to_universe]) + + def clean(self): + super(ModelToValidate, self).clean() + if self.number == 11: + raise ValidationError('Invalid number supplied!') + +class UniqueFieldsModel(models.Model): + unique_charfield = models.CharField(max_length=100, unique=True) + unique_integerfield = models.IntegerField(unique=True) + non_unique_field = models.IntegerField() + +class CustomPKModel(models.Model): + my_pk_field = models.CharField(max_length=100, primary_key=True) + +class UniqueTogetherModel(models.Model): + cfield = models.CharField(max_length=100) + ifield = models.IntegerField() + efield = models.EmailField() + + class Meta: + unique_together = (('ifield', 'cfield',), ['ifield', 'efield']) + +class UniqueForDateModel(models.Model): + start_date = models.DateField() + end_date = models.DateTimeField() + count = models.IntegerField(unique_for_date="start_date", unique_for_year="end_date") + order = models.IntegerField(unique_for_month="end_date") + name = models.CharField(max_length=100) + +class CustomMessagesModel(models.Model): + other = models.IntegerField(blank=True, null=True) + number = models.IntegerField(db_column='number_val', + error_messages={'null': 'NULL', 'not42': 'AAARGH', 'not_equal': '%s != me'}, + validators=[validate_answer_to_universe] + ) + +class Author(models.Model): + name = models.CharField(max_length=100) + +class Article(models.Model): + title = models.CharField(max_length=100) + author = models.ForeignKey(Author) + pub_date = models.DateTimeField(blank=True) + + def clean(self): + if self.pub_date is None: + self.pub_date = datetime.now() diff --git a/parts/django/tests/modeltests/validation/test_custom_messages.py b/parts/django/tests/modeltests/validation/test_custom_messages.py new file mode 100644 index 0000000..05bb651 --- /dev/null +++ b/parts/django/tests/modeltests/validation/test_custom_messages.py @@ -0,0 +1,13 @@ +from modeltests.validation import ValidationTestCase +from models import CustomMessagesModel + + +class CustomMessagesTest(ValidationTestCase): + def test_custom_simple_validator_message(self): + cmm = CustomMessagesModel(number=12) + self.assertFieldFailsValidationWithMessage(cmm.full_clean, 'number', ['AAARGH']) + + def test_custom_null_message(self): + cmm = CustomMessagesModel() + self.assertFieldFailsValidationWithMessage(cmm.full_clean, 'number', ['NULL']) + diff --git a/parts/django/tests/modeltests/validation/test_unique.py b/parts/django/tests/modeltests/validation/test_unique.py new file mode 100644 index 0000000..fb77c4d --- /dev/null +++ b/parts/django/tests/modeltests/validation/test_unique.py @@ -0,0 +1,85 @@ +import unittest +import datetime +from django.conf import settings +from django.db import connection +from models import CustomPKModel, UniqueTogetherModel, UniqueFieldsModel, UniqueForDateModel, ModelToValidate + + +class GetUniqueCheckTests(unittest.TestCase): + def test_unique_fields_get_collected(self): + m = UniqueFieldsModel() + self.assertEqual( + ([(UniqueFieldsModel, ('id',)), + (UniqueFieldsModel, ('unique_charfield',)), + (UniqueFieldsModel, ('unique_integerfield',))], + []), + m._get_unique_checks() + ) + + def test_unique_together_gets_picked_up_and_converted_to_tuple(self): + m = UniqueTogetherModel() + self.assertEqual( + ([(UniqueTogetherModel, ('ifield', 'cfield',)), + (UniqueTogetherModel, ('ifield', 'efield')), + (UniqueTogetherModel, ('id',)), ], + []), + m._get_unique_checks() + ) + + def test_primary_key_is_considered_unique(self): + m = CustomPKModel() + self.assertEqual(([(CustomPKModel, ('my_pk_field',))], []), m._get_unique_checks()) + + def test_unique_for_date_gets_picked_up(self): + m = UniqueForDateModel() + self.assertEqual(( + [(UniqueForDateModel, ('id',))], + [(UniqueForDateModel, 'date', 'count', 'start_date'), + (UniqueForDateModel, 'year', 'count', 'end_date'), + (UniqueForDateModel, 'month', 'order', 'end_date')] + ), m._get_unique_checks() + ) + + def test_unique_for_date_exclusion(self): + m = UniqueForDateModel() + self.assertEqual(( + [(UniqueForDateModel, ('id',))], + [(UniqueForDateModel, 'year', 'count', 'end_date'), + (UniqueForDateModel, 'month', 'order', 'end_date')] + ), m._get_unique_checks(exclude='start_date') + ) + +class PerformUniqueChecksTest(unittest.TestCase): + def setUp(self): + # Set debug to True to gain access to connection.queries. + self._old_debug, settings.DEBUG = settings.DEBUG, True + super(PerformUniqueChecksTest, self).setUp() + + def tearDown(self): + # Restore old debug value. + settings.DEBUG = self._old_debug + super(PerformUniqueChecksTest, self).tearDown() + + def test_primary_key_unique_check_not_performed_when_adding_and_pk_not_specified(self): + # Regression test for #12560 + query_count = len(connection.queries) + mtv = ModelToValidate(number=10, name='Some Name') + setattr(mtv, '_adding', True) + mtv.full_clean() + self.assertEqual(query_count, len(connection.queries)) + + def test_primary_key_unique_check_performed_when_adding_and_pk_specified(self): + # Regression test for #12560 + query_count = len(connection.queries) + mtv = ModelToValidate(number=10, name='Some Name', id=123) + setattr(mtv, '_adding', True) + mtv.full_clean() + self.assertEqual(query_count + 1, len(connection.queries)) + + def test_primary_key_unique_check_not_performed_when_not_adding(self): + # Regression test for #12132 + query_count= len(connection.queries) + mtv = ModelToValidate(number=10, name='Some Name') + mtv.full_clean() + self.assertEqual(query_count, len(connection.queries)) + diff --git a/parts/django/tests/modeltests/validation/tests.py b/parts/django/tests/modeltests/validation/tests.py new file mode 100644 index 0000000..0027393 --- /dev/null +++ b/parts/django/tests/modeltests/validation/tests.py @@ -0,0 +1,114 @@ +from django import forms +from django.test import TestCase +from django.core.exceptions import NON_FIELD_ERRORS +from modeltests.validation import ValidationTestCase +from modeltests.validation.models import Author, Article, ModelToValidate + +# Import other tests for this package. +from modeltests.validation.validators import TestModelsWithValidators +from modeltests.validation.test_unique import GetUniqueCheckTests, PerformUniqueChecksTest +from modeltests.validation.test_custom_messages import CustomMessagesTest + + +class BaseModelValidationTests(ValidationTestCase): + + def test_missing_required_field_raises_error(self): + mtv = ModelToValidate(f_with_custom_validator=42) + self.assertFailsValidation(mtv.full_clean, ['name', 'number']) + + def test_with_correct_value_model_validates(self): + mtv = ModelToValidate(number=10, name='Some Name') + self.assertEqual(None, mtv.full_clean()) + + def test_custom_validate_method(self): + mtv = ModelToValidate(number=11) + self.assertFailsValidation(mtv.full_clean, [NON_FIELD_ERRORS, 'name']) + + def test_wrong_FK_value_raises_error(self): + mtv=ModelToValidate(number=10, name='Some Name', parent_id=3) + self.assertFailsValidation(mtv.full_clean, ['parent']) + + def test_correct_FK_value_validates(self): + parent = ModelToValidate.objects.create(number=10, name='Some Name') + mtv = ModelToValidate(number=10, name='Some Name', parent_id=parent.pk) + self.assertEqual(None, mtv.full_clean()) + + def test_limitted_FK_raises_error(self): + # The limit_choices_to on the parent field says that a parent object's + # number attribute must be 10, so this should fail validation. + parent = ModelToValidate.objects.create(number=11, name='Other Name') + mtv = ModelToValidate(number=10, name='Some Name', parent_id=parent.pk) + self.assertFailsValidation(mtv.full_clean, ['parent']) + + def test_wrong_email_value_raises_error(self): + mtv = ModelToValidate(number=10, name='Some Name', email='not-an-email') + self.assertFailsValidation(mtv.full_clean, ['email']) + + def test_correct_email_value_passes(self): + mtv = ModelToValidate(number=10, name='Some Name', email='valid@email.com') + self.assertEqual(None, mtv.full_clean()) + + def test_wrong_url_value_raises_error(self): + mtv = ModelToValidate(number=10, name='Some Name', url='not a url') + self.assertFieldFailsValidationWithMessage(mtv.full_clean, 'url', [u'Enter a valid value.']) + + def test_correct_url_but_nonexisting_gives_404(self): + mtv = ModelToValidate(number=10, name='Some Name', url='http://google.com/we-love-microsoft.html') + self.assertFieldFailsValidationWithMessage(mtv.full_clean, 'url', [u'This URL appears to be a broken link.']) + + def test_correct_url_value_passes(self): + mtv = ModelToValidate(number=10, name='Some Name', url='http://www.djangoproject.com/') + self.assertEqual(None, mtv.full_clean()) # This will fail if there's no Internet connection + + def test_text_greater_that_charfields_max_length_eaises_erros(self): + mtv = ModelToValidate(number=10, name='Some Name'*100) + self.assertFailsValidation(mtv.full_clean, ['name',]) + +class ArticleForm(forms.ModelForm): + class Meta: + model = Article + exclude = ['author'] + +class ModelFormsTests(TestCase): + def setUp(self): + self.author = Author.objects.create(name='Joseph Kocherhans') + + def test_partial_validation(self): + # Make sure the "commit=False and set field values later" idiom still + # works with model validation. + data = { + 'title': 'The state of model validation', + 'pub_date': '2010-1-10 14:49:00' + } + form = ArticleForm(data) + self.assertEqual(form.errors.keys(), []) + article = form.save(commit=False) + article.author = self.author + article.save() + + def test_validation_with_empty_blank_field(self): + # Since a value for pub_date wasn't provided and the field is + # blank=True, model-validation should pass. + # Also, Article.clean() should be run, so pub_date will be filled after + # validation, so the form should save cleanly even though pub_date is + # not allowed to be null. + data = { + 'title': 'The state of model validation', + } + article = Article(author_id=self.author.id) + form = ArticleForm(data, instance=article) + self.assertEqual(form.errors.keys(), []) + self.assertNotEqual(form.instance.pub_date, None) + article = form.save() + + def test_validation_with_invalid_blank_field(self): + # Even though pub_date is set to blank=True, an invalid value was + # provided, so it should fail validation. + data = { + 'title': 'The state of model validation', + 'pub_date': 'never' + } + article = Article(author_id=self.author.id) + form = ArticleForm(data, instance=article) + self.assertEqual(form.errors.keys(), ['pub_date']) + diff --git a/parts/django/tests/modeltests/validation/validators.py b/parts/django/tests/modeltests/validation/validators.py new file mode 100644 index 0000000..3ad2c40 --- /dev/null +++ b/parts/django/tests/modeltests/validation/validators.py @@ -0,0 +1,18 @@ +from unittest import TestCase +from modeltests.validation import ValidationTestCase +from models import * + + +class TestModelsWithValidators(ValidationTestCase): + def test_custom_validator_passes_for_correct_value(self): + mtv = ModelToValidate(number=10, name='Some Name', f_with_custom_validator=42) + self.assertEqual(None, mtv.full_clean()) + + def test_custom_validator_raises_error_for_incorrect_value(self): + mtv = ModelToValidate(number=10, name='Some Name', f_with_custom_validator=12) + self.assertFailsValidation(mtv.full_clean, ['f_with_custom_validator']) + self.assertFieldFailsValidationWithMessage( + mtv.full_clean, + 'f_with_custom_validator', + [u'This is not the answer to life, universe and everything!'] + ) diff --git a/parts/django/tests/modeltests/validators/__init__.py b/parts/django/tests/modeltests/validators/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/modeltests/validators/__init__.py diff --git a/parts/django/tests/modeltests/validators/models.py b/parts/django/tests/modeltests/validators/models.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/modeltests/validators/models.py diff --git a/parts/django/tests/modeltests/validators/tests.py b/parts/django/tests/modeltests/validators/tests.py new file mode 100644 index 0000000..44ad176 --- /dev/null +++ b/parts/django/tests/modeltests/validators/tests.py @@ -0,0 +1,159 @@ +# -*- coding: utf-8 -*- +import re +import types +from unittest import TestCase +from datetime import datetime, timedelta +from django.core.exceptions import ValidationError +from django.core.validators import * + +NOW = datetime.now() + +TEST_DATA = ( + # (validator, value, expected), + (validate_integer, '42', None), + (validate_integer, '-42', None), + (validate_integer, -42, None), + (validate_integer, -42.5, None), + + (validate_integer, None, ValidationError), + (validate_integer, 'a', ValidationError), + + (validate_email, 'email@here.com', None), + (validate_email, 'weirder-email@here.and.there.com', None), + + (validate_email, None, ValidationError), + (validate_email, '', ValidationError), + (validate_email, 'abc', ValidationError), + (validate_email, 'a @x.cz', ValidationError), + (validate_email, 'something@@somewhere.com', ValidationError), + + (validate_slug, 'slug-ok', None), + (validate_slug, 'longer-slug-still-ok', None), + (validate_slug, '--------', None), + (validate_slug, 'nohyphensoranything', None), + + (validate_slug, '', ValidationError), + (validate_slug, ' text ', ValidationError), + (validate_slug, ' ', ValidationError), + (validate_slug, 'some@mail.com', ValidationError), + (validate_slug, '你好', ValidationError), + (validate_slug, '\n', ValidationError), + + (validate_ipv4_address, '1.1.1.1', None), + (validate_ipv4_address, '255.0.0.0', None), + (validate_ipv4_address, '0.0.0.0', None), + + (validate_ipv4_address, '256.1.1.1', ValidationError), + (validate_ipv4_address, '25.1.1.', ValidationError), + (validate_ipv4_address, '25,1,1,1', ValidationError), + (validate_ipv4_address, '25.1 .1.1', ValidationError), + + (validate_comma_separated_integer_list, '1', None), + (validate_comma_separated_integer_list, '1,2,3', None), + (validate_comma_separated_integer_list, '1,2,3,', None), + + (validate_comma_separated_integer_list, '', ValidationError), + (validate_comma_separated_integer_list, 'a,b,c', ValidationError), + (validate_comma_separated_integer_list, '1, 2, 3', ValidationError), + + (MaxValueValidator(10), 10, None), + (MaxValueValidator(10), -10, None), + (MaxValueValidator(10), 0, None), + (MaxValueValidator(NOW), NOW, None), + (MaxValueValidator(NOW), NOW - timedelta(days=1), None), + + (MaxValueValidator(0), 1, ValidationError), + (MaxValueValidator(NOW), NOW + timedelta(days=1), ValidationError), + + (MinValueValidator(-10), -10, None), + (MinValueValidator(-10), 10, None), + (MinValueValidator(-10), 0, None), + (MinValueValidator(NOW), NOW, None), + (MinValueValidator(NOW), NOW + timedelta(days=1), None), + + (MinValueValidator(0), -1, ValidationError), + (MinValueValidator(NOW), NOW - timedelta(days=1), ValidationError), + + (MaxLengthValidator(10), '', None), + (MaxLengthValidator(10), 10*'x', None), + + (MaxLengthValidator(10), 15*'x', ValidationError), + + (MinLengthValidator(10), 15*'x', None), + (MinLengthValidator(10), 10*'x', None), + + (MinLengthValidator(10), '', ValidationError), + + (URLValidator(), 'http://www.djangoproject.com/', None), + (URLValidator(), 'http://localhost/', None), + (URLValidator(), 'http://example.com/', None), + (URLValidator(), 'http://www.example.com/', None), + (URLValidator(), 'http://www.example.com:8000/test', None), + (URLValidator(), 'http://valid-with-hyphens.com/', None), + (URLValidator(), 'http://subdomain.domain.com/', None), + (URLValidator(), 'http://200.8.9.10/', None), + (URLValidator(), 'http://200.8.9.10:8000/test', None), + (URLValidator(), 'http://valid-----hyphens.com/', None), + (URLValidator(), 'http://example.com?something=value', None), + (URLValidator(), 'http://example.com/index.php?something=value&another=value2', None), + + (URLValidator(), 'foo', ValidationError), + (URLValidator(), 'http://', ValidationError), + (URLValidator(), 'http://example', ValidationError), + (URLValidator(), 'http://example.', ValidationError), + (URLValidator(), 'http://.com', ValidationError), + (URLValidator(), 'http://invalid-.com', ValidationError), + (URLValidator(), 'http://-invalid.com', ValidationError), + (URLValidator(), 'http://inv-.alid-.com', ValidationError), + (URLValidator(), 'http://inv-.-alid.com', ValidationError), + + (BaseValidator(True), True, None), + (BaseValidator(True), False, ValidationError), + + (RegexValidator('.*'), '', None), + (RegexValidator(re.compile('.*')), '', None), + (RegexValidator('.*'), 'xxxxx', None), + + (RegexValidator('x'), 'y', ValidationError), + (RegexValidator(re.compile('x')), 'y', ValidationError), +) + +def create_simple_test_method(validator, expected, value, num): + if expected is not None and issubclass(expected, Exception): + test_mask = 'test_%s_raises_error_%d' + def test_func(self): + self.assertRaises(expected, validator, value) + else: + test_mask = 'test_%s_%d' + def test_func(self): + self.assertEqual(expected, validator(value)) + if isinstance(validator, types.FunctionType): + val_name = validator.__name__ + else: + val_name = validator.__class__.__name__ + test_name = test_mask % (val_name, num) + return test_name, test_func + +# Dynamically assemble a test class with the contents of TEST_DATA + +class TestSimpleValidators(TestCase): + def test_single_message(self): + v = ValidationError('Not Valid') + self.assertEquals(str(v), "[u'Not Valid']") + self.assertEquals(repr(v), "ValidationError([u'Not Valid'])") + + def test_message_list(self): + v = ValidationError(['First Problem', 'Second Problem']) + self.assertEquals(str(v), "[u'First Problem', u'Second Problem']") + self.assertEquals(repr(v), "ValidationError([u'First Problem', u'Second Problem'])") + + def test_message_dict(self): + v = ValidationError({'first': 'First Problem'}) + self.assertEquals(str(v), "{'first': 'First Problem'}") + self.assertEquals(repr(v), "ValidationError({'first': 'First Problem'})") + +test_counter = 0 +for validator, value, expected in TEST_DATA: + name, method = create_simple_test_method(validator, expected, value, test_counter) + setattr(TestSimpleValidators, name, method) + test_counter += 1 |