summaryrefslogtreecommitdiff
path: root/parts/django/tests/modeltests
diff options
context:
space:
mode:
Diffstat (limited to 'parts/django/tests/modeltests')
-rw-r--r--parts/django/tests/modeltests/__init__.py0
-rw-r--r--parts/django/tests/modeltests/aggregation/__init__.py0
-rw-r--r--parts/django/tests/modeltests/aggregation/fixtures/initial_data.json243
-rw-r--r--parts/django/tests/modeltests/aggregation/models.py42
-rw-r--r--parts/django/tests/modeltests/aggregation/tests.py565
-rw-r--r--parts/django/tests/modeltests/basic/__init__.py0
-rw-r--r--parts/django/tests/modeltests/basic/models.py17
-rw-r--r--parts/django/tests/modeltests/basic/tests.py562
-rw-r--r--parts/django/tests/modeltests/choices/__init__.py0
-rw-r--r--parts/django/tests/modeltests/choices/models.py24
-rw-r--r--parts/django/tests/modeltests/choices/tests.py23
-rw-r--r--parts/django/tests/modeltests/custom_columns/__init__.py0
-rw-r--r--parts/django/tests/modeltests/custom_columns/models.py40
-rw-r--r--parts/django/tests/modeltests/custom_columns/tests.py71
-rw-r--r--parts/django/tests/modeltests/custom_managers/__init__.py0
-rw-r--r--parts/django/tests/modeltests/custom_managers/models.py59
-rw-r--r--parts/django/tests/modeltests/custom_managers/tests.py71
-rw-r--r--parts/django/tests/modeltests/custom_methods/__init__.py0
-rw-r--r--parts/django/tests/modeltests/custom_methods/models.py36
-rw-r--r--parts/django/tests/modeltests/custom_methods/tests.py42
-rw-r--r--parts/django/tests/modeltests/custom_pk/__init__.py0
-rw-r--r--parts/django/tests/modeltests/custom_pk/fields.py55
-rw-r--r--parts/django/tests/modeltests/custom_pk/models.py42
-rw-r--r--parts/django/tests/modeltests/custom_pk/tests.py183
-rw-r--r--parts/django/tests/modeltests/defer/__init__.py0
-rw-r--r--parts/django/tests/modeltests/defer/models.py24
-rw-r--r--parts/django/tests/modeltests/defer/tests.py137
-rw-r--r--parts/django/tests/modeltests/delete/__init__.py1
-rw-r--r--parts/django/tests/modeltests/delete/models.py42
-rw-r--r--parts/django/tests/modeltests/delete/tests.py135
-rw-r--r--parts/django/tests/modeltests/empty/__init__.py0
-rw-r--r--parts/django/tests/modeltests/empty/models.py12
-rw-r--r--parts/django/tests/modeltests/empty/tests.py15
-rw-r--r--parts/django/tests/modeltests/expressions/__init__.py0
-rw-r--r--parts/django/tests/modeltests/expressions/models.py27
-rw-r--r--parts/django/tests/modeltests/expressions/tests.py218
-rw-r--r--parts/django/tests/modeltests/field_defaults/__init__.py0
-rw-r--r--parts/django/tests/modeltests/field_defaults/models.py21
-rw-r--r--parts/django/tests/modeltests/field_defaults/tests.py16
-rw-r--r--parts/django/tests/modeltests/field_subclassing/__init__.py0
-rw-r--r--parts/django/tests/modeltests/field_subclassing/fields.py74
-rw-r--r--parts/django/tests/modeltests/field_subclassing/models.py22
-rw-r--r--parts/django/tests/modeltests/field_subclassing/tests.py81
-rw-r--r--parts/django/tests/modeltests/files/__init__.py1
-rw-r--r--parts/django/tests/modeltests/files/models.py34
-rw-r--r--parts/django/tests/modeltests/files/tests.py100
-rw-r--r--parts/django/tests/modeltests/fixtures/__init__.py2
-rw-r--r--parts/django/tests/modeltests/fixtures/fixtures/db_fixture_1.default.json10
-rw-r--r--parts/django/tests/modeltests/fixtures/fixtures/db_fixture_2.default.json.gzbin0 -> 175 bytes
-rw-r--r--parts/django/tests/modeltests/fixtures/fixtures/db_fixture_3.nosuchdb.json10
-rw-r--r--parts/django/tests/modeltests/fixtures/fixtures/fixture1.json34
-rw-r--r--parts/django/tests/modeltests/fixtures/fixtures/fixture2.json18
-rw-r--r--parts/django/tests/modeltests/fixtures/fixtures/fixture2.xml11
-rw-r--r--parts/django/tests/modeltests/fixtures/fixtures/fixture3.xml11
-rw-r--r--parts/django/tests/modeltests/fixtures/fixtures/fixture4.json.zipbin0 -> 282 bytes
-rw-r--r--parts/django/tests/modeltests/fixtures/fixtures/fixture5.json.gzbin0 -> 169 bytes
-rw-r--r--parts/django/tests/modeltests/fixtures/fixtures/fixture5.json.zipbin0 -> 295 bytes
-rw-r--r--parts/django/tests/modeltests/fixtures/fixtures/fixture6.json41
-rw-r--r--parts/django/tests/modeltests/fixtures/fixtures/fixture7.xml27
-rw-r--r--parts/django/tests/modeltests/fixtures/fixtures/fixture8.json32
-rw-r--r--parts/django/tests/modeltests/fixtures/fixtures/fixture9.xml48
-rw-r--r--parts/django/tests/modeltests/fixtures/fixtures/initial_data.json10
-rw-r--r--parts/django/tests/modeltests/fixtures/models.py92
-rw-r--r--parts/django/tests/modeltests/fixtures/tests.py277
-rw-r--r--parts/django/tests/modeltests/fixtures_model_package/__init__.py2
-rw-r--r--parts/django/tests/modeltests/fixtures_model_package/fixtures/fixture1.json18
-rw-r--r--parts/django/tests/modeltests/fixtures_model_package/fixtures/fixture2.json18
-rw-r--r--parts/django/tests/modeltests/fixtures_model_package/fixtures/fixture2.xml11
-rw-r--r--parts/django/tests/modeltests/fixtures_model_package/fixtures/initial_data.json10
-rw-r--r--parts/django/tests/modeltests/fixtures_model_package/models/__init__.py14
-rw-r--r--parts/django/tests/modeltests/fixtures_model_package/tests.py71
-rw-r--r--parts/django/tests/modeltests/force_insert_update/__init__.py0
-rw-r--r--parts/django/tests/modeltests/force_insert_update/models.py13
-rw-r--r--parts/django/tests/modeltests/force_insert_update/tests.py38
-rw-r--r--parts/django/tests/modeltests/generic_relations/__init__.py0
-rw-r--r--parts/django/tests/modeltests/generic_relations/models.py80
-rw-r--r--parts/django/tests/modeltests/generic_relations/tests.py223
-rw-r--r--parts/django/tests/modeltests/get_latest/__init__.py0
-rw-r--r--parts/django/tests/modeltests/get_latest/models.py30
-rw-r--r--parts/django/tests/modeltests/get_latest/tests.py53
-rw-r--r--parts/django/tests/modeltests/get_object_or_404/__init__.py0
-rw-r--r--parts/django/tests/modeltests/get_object_or_404/models.py34
-rw-r--r--parts/django/tests/modeltests/get_object_or_404/tests.py80
-rw-r--r--parts/django/tests/modeltests/get_or_create/__init__.py0
-rw-r--r--parts/django/tests/modeltests/get_or_create/models.py21
-rw-r--r--parts/django/tests/modeltests/get_or_create/tests.py52
-rw-r--r--parts/django/tests/modeltests/invalid_models/__init__.py1
-rw-r--r--parts/django/tests/modeltests/invalid_models/models.py335
-rw-r--r--parts/django/tests/modeltests/lookup/__init__.py0
-rw-r--r--parts/django/tests/modeltests/lookup/models.py16
-rw-r--r--parts/django/tests/modeltests/lookup/tests.py547
-rw-r--r--parts/django/tests/modeltests/m2m_and_m2o/__init__.py0
-rw-r--r--parts/django/tests/modeltests/m2m_and_m2o/models.py21
-rw-r--r--parts/django/tests/modeltests/m2m_and_m2o/tests.py75
-rw-r--r--parts/django/tests/modeltests/m2m_intermediary/__init__.py0
-rw-r--r--parts/django/tests/modeltests/m2m_intermediary/models.py36
-rw-r--r--parts/django/tests/modeltests/m2m_intermediary/tests.py38
-rw-r--r--parts/django/tests/modeltests/m2m_multiple/__init__.py0
-rw-r--r--parts/django/tests/modeltests/m2m_multiple/models.py30
-rw-r--r--parts/django/tests/modeltests/m2m_multiple/tests.py84
-rw-r--r--parts/django/tests/modeltests/m2m_recursive/__init__.py0
-rw-r--r--parts/django/tests/modeltests/m2m_recursive/models.py28
-rw-r--r--parts/django/tests/modeltests/m2m_recursive/tests.py253
-rw-r--r--parts/django/tests/modeltests/m2m_signals/__init__.py1
-rw-r--r--parts/django/tests/modeltests/m2m_signals/models.py36
-rw-r--r--parts/django/tests/modeltests/m2m_signals/tests.py427
-rw-r--r--parts/django/tests/modeltests/m2m_through/__init__.py2
-rw-r--r--parts/django/tests/modeltests/m2m_through/models.py65
-rw-r--r--parts/django/tests/modeltests/m2m_through/tests.py343
-rw-r--r--parts/django/tests/modeltests/m2o_recursive/__init__.py0
-rw-r--r--parts/django/tests/modeltests/m2o_recursive/models.py28
-rw-r--r--parts/django/tests/modeltests/m2o_recursive/tests.py38
-rw-r--r--parts/django/tests/modeltests/many_to_many/__init__.py0
-rw-r--r--parts/django/tests/modeltests/many_to_many/models.py29
-rw-r--r--parts/django/tests/modeltests/many_to_many/tests.py384
-rw-r--r--parts/django/tests/modeltests/many_to_one/__init__.py0
-rw-r--r--parts/django/tests/modeltests/many_to_one/models.py26
-rw-r--r--parts/django/tests/modeltests/many_to_one/tests.py371
-rw-r--r--parts/django/tests/modeltests/many_to_one_null/__init__.py0
-rw-r--r--parts/django/tests/modeltests/many_to_one_null/models.py24
-rw-r--r--parts/django/tests/modeltests/many_to_one_null/tests.py84
-rw-r--r--parts/django/tests/modeltests/model_forms/__init__.py0
-rw-r--r--parts/django/tests/modeltests/model_forms/mforms.py39
-rw-r--r--parts/django/tests/modeltests/model_forms/models.py1575
-rw-r--r--parts/django/tests/modeltests/model_forms/test.pngbin0 -> 482 bytes
-rw-r--r--parts/django/tests/modeltests/model_forms/test2.pngbin0 -> 2072 bytes
-rw-r--r--parts/django/tests/modeltests/model_forms/tests.py185
-rw-r--r--parts/django/tests/modeltests/model_formsets/__init__.py0
-rw-r--r--parts/django/tests/modeltests/model_formsets/models.py193
-rw-r--r--parts/django/tests/modeltests/model_formsets/tests.py1159
-rw-r--r--parts/django/tests/modeltests/model_inheritance/__init__.py0
-rw-r--r--parts/django/tests/modeltests/model_inheritance/models.py145
-rw-r--r--parts/django/tests/modeltests/model_inheritance/tests.py281
-rw-r--r--parts/django/tests/modeltests/model_inheritance_same_model_name/__init__.py0
-rw-r--r--parts/django/tests/modeltests/model_inheritance_same_model_name/models.py19
-rw-r--r--parts/django/tests/modeltests/model_inheritance_same_model_name/tests.py32
-rw-r--r--parts/django/tests/modeltests/model_package/__init__.py1
-rw-r--r--parts/django/tests/modeltests/model_package/models/__init__.py3
-rw-r--r--parts/django/tests/modeltests/model_package/models/article.py10
-rw-r--r--parts/django/tests/modeltests/model_package/models/publication.py7
-rw-r--r--parts/django/tests/modeltests/model_package/tests.py72
-rw-r--r--parts/django/tests/modeltests/mutually_referential/__init__.py0
-rw-r--r--parts/django/tests/modeltests/mutually_referential/models.py19
-rw-r--r--parts/django/tests/modeltests/mutually_referential/tests.py20
-rw-r--r--parts/django/tests/modeltests/one_to_one/__init__.py0
-rw-r--r--parts/django/tests/modeltests/one_to_one/models.py47
-rw-r--r--parts/django/tests/modeltests/one_to_one/tests.py119
-rw-r--r--parts/django/tests/modeltests/or_lookups/__init__.py0
-rw-r--r--parts/django/tests/modeltests/or_lookups/models.py22
-rw-r--r--parts/django/tests/modeltests/or_lookups/tests.py232
-rw-r--r--parts/django/tests/modeltests/order_with_respect_to/__init__.py0
-rw-r--r--parts/django/tests/modeltests/order_with_respect_to/models.py29
-rw-r--r--parts/django/tests/modeltests/order_with_respect_to/tests.py71
-rw-r--r--parts/django/tests/modeltests/ordering/__init__.py0
-rw-r--r--parts/django/tests/modeltests/ordering/models.py26
-rw-r--r--parts/django/tests/modeltests/ordering/tests.py137
-rw-r--r--parts/django/tests/modeltests/pagination/__init__.py0
-rw-r--r--parts/django/tests/modeltests/pagination/models.py17
-rw-r--r--parts/django/tests/modeltests/pagination/tests.py132
-rw-r--r--parts/django/tests/modeltests/properties/__init__.py0
-rw-r--r--parts/django/tests/modeltests/properties/models.py21
-rw-r--r--parts/django/tests/modeltests/properties/tests.py20
-rw-r--r--parts/django/tests/modeltests/proxy_model_inheritance/__init__.py0
-rw-r--r--parts/django/tests/modeltests/proxy_model_inheritance/app1/__init__.py0
-rw-r--r--parts/django/tests/modeltests/proxy_model_inheritance/app1/models.py5
-rw-r--r--parts/django/tests/modeltests/proxy_model_inheritance/app2/__init__.py0
-rw-r--r--parts/django/tests/modeltests/proxy_model_inheritance/app2/models.py4
-rw-r--r--parts/django/tests/modeltests/proxy_model_inheritance/models.py0
-rw-r--r--parts/django/tests/modeltests/proxy_model_inheritance/tests.py36
-rw-r--r--parts/django/tests/modeltests/proxy_models/__init__.py0
-rw-r--r--parts/django/tests/modeltests/proxy_models/fixtures/mypeople.json9
-rw-r--r--parts/django/tests/modeltests/proxy_models/models.py164
-rw-r--r--parts/django/tests/modeltests/proxy_models/tests.py314
-rw-r--r--parts/django/tests/modeltests/raw_query/__init__.py0
-rw-r--r--parts/django/tests/modeltests/raw_query/fixtures/raw_query_books.json110
-rw-r--r--parts/django/tests/modeltests/raw_query/models.py30
-rw-r--r--parts/django/tests/modeltests/raw_query/tests.py236
-rw-r--r--parts/django/tests/modeltests/reserved_names/__init__.py0
-rw-r--r--parts/django/tests/modeltests/reserved_names/models.py25
-rw-r--r--parts/django/tests/modeltests/reserved_names/tests.py48
-rw-r--r--parts/django/tests/modeltests/reverse_lookup/__init__.py0
-rw-r--r--parts/django/tests/modeltests/reverse_lookup/models.py28
-rw-r--r--parts/django/tests/modeltests/reverse_lookup/tests.py49
-rw-r--r--parts/django/tests/modeltests/save_delete_hooks/__init__.py0
-rw-r--r--parts/django/tests/modeltests/save_delete_hooks/models.py32
-rw-r--r--parts/django/tests/modeltests/save_delete_hooks/tests.py30
-rw-r--r--parts/django/tests/modeltests/select_related/__init__.py0
-rw-r--r--parts/django/tests/modeltests/select_related/models.py59
-rw-r--r--parts/django/tests/modeltests/select_related/tests.py166
-rw-r--r--parts/django/tests/modeltests/serializers/__init__.py0
-rw-r--r--parts/django/tests/modeltests/serializers/models.py117
-rw-r--r--parts/django/tests/modeltests/serializers/tests.py417
-rw-r--r--parts/django/tests/modeltests/signals/__init__.py0
-rw-r--r--parts/django/tests/modeltests/signals/models.py13
-rw-r--r--parts/django/tests/modeltests/signals/tests.py148
-rw-r--r--parts/django/tests/modeltests/str/__init__.py0
-rw-r--r--parts/django/tests/modeltests/str/models.py33
-rw-r--r--parts/django/tests/modeltests/str/tests.py23
-rw-r--r--parts/django/tests/modeltests/test_client/__init__.py0
-rw-r--r--parts/django/tests/modeltests/test_client/fixtures/testdata.json56
-rw-r--r--parts/django/tests/modeltests/test_client/models.py459
-rw-r--r--parts/django/tests/modeltests/test_client/tests.py20
-rw-r--r--parts/django/tests/modeltests/test_client/urls.py29
-rw-r--r--parts/django/tests/modeltests/test_client/views.py214
-rw-r--r--parts/django/tests/modeltests/transactions/__init__.py0
-rw-r--r--parts/django/tests/modeltests/transactions/models.py21
-rw-r--r--parts/django/tests/modeltests/transactions/tests.py155
-rw-r--r--parts/django/tests/modeltests/unmanaged_models/__init__.py2
-rw-r--r--parts/django/tests/modeltests/unmanaged_models/models.py125
-rw-r--r--parts/django/tests/modeltests/unmanaged_models/tests.py58
-rw-r--r--parts/django/tests/modeltests/update/__init__.py0
-rw-r--r--parts/django/tests/modeltests/update/models.py35
-rw-r--r--parts/django/tests/modeltests/update/tests.py116
-rw-r--r--parts/django/tests/modeltests/user_commands/__init__.py0
-rw-r--r--parts/django/tests/modeltests/user_commands/management/__init__.py0
-rw-r--r--parts/django/tests/modeltests/user_commands/management/commands/__init__.py0
-rw-r--r--parts/django/tests/modeltests/user_commands/management/commands/dance.py14
-rw-r--r--parts/django/tests/modeltests/user_commands/models.py14
-rw-r--r--parts/django/tests/modeltests/user_commands/tests.py21
-rw-r--r--parts/django/tests/modeltests/validation/__init__.py21
-rw-r--r--parts/django/tests/modeltests/validation/models.py65
-rw-r--r--parts/django/tests/modeltests/validation/test_custom_messages.py13
-rw-r--r--parts/django/tests/modeltests/validation/test_unique.py85
-rw-r--r--parts/django/tests/modeltests/validation/tests.py114
-rw-r--r--parts/django/tests/modeltests/validation/validators.py18
-rw-r--r--parts/django/tests/modeltests/validators/__init__.py0
-rw-r--r--parts/django/tests/modeltests/validators/models.py0
-rw-r--r--parts/django/tests/modeltests/validators/tests.py159
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
new file mode 100644
index 0000000..80e4ba1
--- /dev/null
+++ b/parts/django/tests/modeltests/fixtures/fixtures/db_fixture_2.default.json.gz
Binary files differ
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
new file mode 100644
index 0000000..270cccb
--- /dev/null
+++ b/parts/django/tests/modeltests/fixtures/fixtures/fixture4.json.zip
Binary files differ
diff --git a/parts/django/tests/modeltests/fixtures/fixtures/fixture5.json.gz b/parts/django/tests/modeltests/fixtures/fixtures/fixture5.json.gz
new file mode 100644
index 0000000..bb6baca
--- /dev/null
+++ b/parts/django/tests/modeltests/fixtures/fixtures/fixture5.json.gz
Binary files differ
diff --git a/parts/django/tests/modeltests/fixtures/fixtures/fixture5.json.zip b/parts/django/tests/modeltests/fixtures/fixtures/fixture5.json.zip
new file mode 100644
index 0000000..9380cef
--- /dev/null
+++ b/parts/django/tests/modeltests/fixtures/fixtures/fixture5.json.zip
Binary files differ
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 &quot;Prince&quot;</natural>
+ </field>
+ </object>
+ <object pk="3" model="fixtures.visa">
+ <field type="CharField" name="person">
+ <natural>Artist formerly known as &quot;Prince&quot;</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 &quot;Prince&quot;</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&#39;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&#39;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&#39;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&#39;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&#39;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&#39;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
new file mode 100644
index 0000000..4f17cd0
--- /dev/null
+++ b/parts/django/tests/modeltests/model_forms/test.png
Binary files differ
diff --git a/parts/django/tests/modeltests/model_forms/test2.png b/parts/django/tests/modeltests/model_forms/test2.png
new file mode 100644
index 0000000..10702f7
--- /dev/null
+++ b/parts/django/tests/modeltests/model_forms/test2.png
Binary files differ
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