From 42c03cbeae9ba0897cd25d36eca8113954ee1b5f Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Sat, 19 Dec 2009 18:03:58 +0000 Subject: [PATCH] [soc2009/multidb] Merged up to trunk r11917. git-svn-id: http://code.djangoproject.com/svn/django/branches/soc2009/multidb@11920 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/contrib/admin/options.py | 10 +++- django/contrib/contenttypes/views.py | 2 +- django/core/management/commands/loaddata.py | 12 ++++- django/db/backends/postgresql/creation.py | 39 ++++++++++++++ django/db/models/query.py | 4 +- docs/ref/databases.txt | 15 ++++++ docs/topics/auth.txt | 15 +++++- docs/topics/serialization.txt | 2 +- .../fixtures_model_package/__init__.py | 2 + .../fixtures/fixture1.json | 18 +++++++ .../fixtures/fixture2.json | 18 +++++++ .../fixtures/fixture2.xml | 11 ++++ .../fixtures/initial_data.json | 10 ++++ .../fixtures_model_package/models/__init__.py | 54 +++++++++++++++++++ tests/modeltests/lookup/models.py | 6 +++ tests/regressiontests/admin_views/models.py | 16 ++++++ tests/regressiontests/admin_views/tests.py | 27 ++++++++++ .../generic_inline_admin/tests.py | 10 ++-- tests/regressiontests/views/tests/defaults.py | 28 ++++++++-- 19 files changed, 284 insertions(+), 15 deletions(-) create mode 100644 tests/modeltests/fixtures_model_package/__init__.py create mode 100644 tests/modeltests/fixtures_model_package/fixtures/fixture1.json create mode 100644 tests/modeltests/fixtures_model_package/fixtures/fixture2.json create mode 100644 tests/modeltests/fixtures_model_package/fixtures/fixture2.xml create mode 100644 tests/modeltests/fixtures_model_package/fixtures/initial_data.json create mode 100644 tests/modeltests/fixtures_model_package/models/__init__.py diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index 8fa0fbbf01..dd471df363 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -353,6 +353,13 @@ class ModelAdmin(BaseModelAdmin): defaults.update(kwargs) return modelform_factory(self.model, **defaults) + def get_changelist(self, request, **kwargs): + """ + Returns the ChangeList class for use on the changelist page. + """ + from django.contrib.admin.views.main import ChangeList + return ChangeList + def get_changelist_form(self, request, **kwargs): """ Returns a Form class for use in the Formset on the changelist page. @@ -896,7 +903,7 @@ class ModelAdmin(BaseModelAdmin): @csrf_protect def changelist_view(self, request, extra_context=None): "The 'change list' admin view for this model." - from django.contrib.admin.views.main import ChangeList, ERROR_FLAG + from django.contrib.admin.views.main import ERROR_FLAG opts = self.model._meta app_label = opts.app_label if not self.has_change_permission(request, None): @@ -913,6 +920,7 @@ class ModelAdmin(BaseModelAdmin): except ValueError: pass + ChangeList = self.get_changelist(request) try: cl = ChangeList(request, self.model, list_display, self.list_display_links, self.list_filter, self.date_hierarchy, self.search_fields, self.list_select_related, self.list_per_page, self.list_editable, self) diff --git a/django/contrib/contenttypes/views.py b/django/contrib/contenttypes/views.py index 4285be36cc..26961201cd 100644 --- a/django/contrib/contenttypes/views.py +++ b/django/contrib/contenttypes/views.py @@ -9,7 +9,7 @@ def shortcut(request, content_type_id, object_id): try: content_type = ContentType.objects.get(pk=content_type_id) obj = content_type.get_object_for_this_type(pk=object_id) - except ObjectDoesNotExist: + except (ObjectDoesNotExist, ValueError): raise http.Http404("Content type %s object %s doesn't exist" % (content_type_id, object_id)) try: absurl = obj.get_absolute_url() diff --git a/django/core/management/commands/loaddata.py b/django/core/management/commands/loaddata.py index e7dd14fbe7..4967c64cd2 100644 --- a/django/core/management/commands/loaddata.py +++ b/django/core/management/commands/loaddata.py @@ -88,7 +88,17 @@ class Command(BaseCommand): if has_bz2: compression_types['bz2'] = bz2.BZ2File - app_fixtures = [os.path.join(os.path.dirname(app.__file__), 'fixtures') for app in get_apps()] + app_module_paths = [] + for app in get_apps(): + if hasattr(app, '__path__'): + # It's a 'models/' subpackage + for path in app.__path__: + app_module_paths.append(path) + else: + # It's a models.py module + app_module_paths.append(app.__file__) + + app_fixtures = [os.path.join(os.path.dirname(path), 'fixtures') for path in app_module_paths] for fixture_label in fixture_labels: parts = fixture_label.split('.') diff --git a/django/db/backends/postgresql/creation.py b/django/db/backends/postgresql/creation.py index 2d509dabe3..af26d0b78f 100644 --- a/django/db/backends/postgresql/creation.py +++ b/django/db/backends/postgresql/creation.py @@ -34,3 +34,42 @@ class DatabaseCreation(BaseDatabaseCreation): if self.connection.settings_dict['TEST_CHARSET']: return "WITH ENCODING '%s'" % self.connection.settings_dict['TEST_CHARSET'] return '' + + def sql_indexes_for_field(self, model, f, style): + if f.db_index and not f.unique: + qn = self.connection.ops.quote_name + db_table = model._meta.db_table + tablespace = f.db_tablespace or model._meta.db_tablespace + if tablespace: + sql = self.connection.ops.tablespace_sql(tablespace) + if sql: + tablespace_sql = ' ' + sql + else: + tablespace_sql = '' + else: + tablespace_sql = '' + + def get_index_sql(index_name, opclass=''): + return (style.SQL_KEYWORD('CREATE INDEX') + ' ' + + style.SQL_TABLE(qn(index_name)) + ' ' + + style.SQL_KEYWORD('ON') + ' ' + + style.SQL_TABLE(qn(db_table)) + ' ' + + "(%s%s)" % (style.SQL_FIELD(qn(f.column)), opclass) + + "%s;" % tablespace_sql) + + output = [get_index_sql('%s_%s' % (db_table, f.column))] + + # Fields with database column types of `varchar` and `text` need + # a second index that specifies their operator class, which is + # needed when performing correct LIKE queries outside the + # C locale. See #12234. + db_type = f.db_type() + if db_type.startswith('varchar'): + output.append(get_index_sql('%s_%s_like' % (db_table, f.column), + ' varchar_pattern_ops')) + elif db_type.startswith('text'): + output.append(get_index_sql('%s_%s_like' % (db_table, f.column), + ' text_pattern_ops')) + else: + output = [] + return output diff --git a/django/db/models/query.py b/django/db/models/query.py index 7d2b2df28e..d24bbd69ea 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -328,6 +328,8 @@ class QuerySet(object): keyword arguments. """ clone = self.filter(*args, **kwargs) + if self.query.can_filter(): + clone = clone.order_by() num = len(clone) if num == 1: return clone._result_cache[0] @@ -394,7 +396,7 @@ class QuerySet(object): """ assert self.query.can_filter(), \ "Cannot use 'limit' or 'offset' with in_bulk" - assert isinstance(id_list, (tuple, list)), \ + assert isinstance(id_list, (tuple, list, set, frozenset)), \ "in_bulk() must be provided with a list of IDs." if not id_list: return {} diff --git a/docs/ref/databases.txt b/docs/ref/databases.txt index 3cff1c61cb..8b473da821 100644 --- a/docs/ref/databases.txt +++ b/docs/ref/databases.txt @@ -82,6 +82,21 @@ You should also audit your existing code for any instances of this behavior before enabling this feature. It's faster, but it provides less automatic protection for multi-call operations. +Indexes for ``varchar`` and ``text`` columns +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. versionadded:: 1.1.2 + +When specifying ``db_index=True`` on your model fields, Django typically +outputs a single ``CREATE INDEX`` statement. However, if the database type +for the field is either ``varchar`` or ``text`` (e.g., used by ``CharField``, +``FileField``, and ``TextField``), then Django will create +an additional index that uses an appropriate `PostgreSQL operator class`_ +for the column. The extra index is necessary to correctly perfrom +lookups that use the ``LIKE`` operator in their SQL, as is done with the +``contains`` and ``startswith`` lookup types. + +.. _PostgreSQL operator class: http://www.postgresql.org/docs/8.4/static/indexes-opclass.html + .. _mysql-notes: MySQL notes diff --git a/docs/topics/auth.txt b/docs/topics/auth.txt index c85ff604bf..8aae4f5e73 100644 --- a/docs/topics/auth.txt +++ b/docs/topics/auth.txt @@ -225,8 +225,9 @@ Methods .. method:: models.User.has_perm(perm, obj=None) Returns ``True`` if the user has the specified permission, where perm is - in the format ``"."``. - If the user is inactive, this method will always return ``False``. + in the format ``"."``. (see + `permissions`_ section below). If the user is inactive, this method will + always return ``False``. .. versionadded:: 1.2 @@ -1122,6 +1123,8 @@ generic view itself. For example:: def limited_object_detail(*args, **kwargs): return object_detail(*args, **kwargs) +.. _permissions: + Permissions =========== @@ -1164,6 +1167,14 @@ models being installed at that time. Afterward, it will create default permissions for new models each time you run :djadmin:`manage.py syncdb `. +Assuming you have an application with an +:attr:`~django.db.models.Options.app_label` ``foo`` and a model named ``Bar``, +to test for basic permissions you should use: + + * add: ``user.has_perm('foo.add_bar')`` + * change: ``user.has_perm('foo.change_bar')`` + * delete: ``user.has_perm('foo.delete_bar')`` + .. _custom-permissions: Custom permissions diff --git a/docs/topics/serialization.txt b/docs/topics/serialization.txt index b33e4effe3..62b8869313 100644 --- a/docs/topics/serialization.txt +++ b/docs/topics/serialization.txt @@ -264,7 +264,7 @@ name:: class PersonManager(models.Manager): def get_by_natural_key(self, first_name, last_name): - return self.filter(first_name=first_name, last_name=last_name) + return self.get(first_name=first_name, last_name=last_name) class Person(models.Model): objects = PersonManager() diff --git a/tests/modeltests/fixtures_model_package/__init__.py b/tests/modeltests/fixtures_model_package/__init__.py new file mode 100644 index 0000000000..139597f9cb --- /dev/null +++ b/tests/modeltests/fixtures_model_package/__init__.py @@ -0,0 +1,2 @@ + + diff --git a/tests/modeltests/fixtures_model_package/fixtures/fixture1.json b/tests/modeltests/fixtures_model_package/fixtures/fixture1.json new file mode 100644 index 0000000000..7684d84609 --- /dev/null +++ b/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/tests/modeltests/fixtures_model_package/fixtures/fixture2.json b/tests/modeltests/fixtures_model_package/fixtures/fixture2.json new file mode 100644 index 0000000000..4997627385 --- /dev/null +++ b/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/tests/modeltests/fixtures_model_package/fixtures/fixture2.xml b/tests/modeltests/fixtures_model_package/fixtures/fixture2.xml new file mode 100644 index 0000000000..55337cf810 --- /dev/null +++ b/tests/modeltests/fixtures_model_package/fixtures/fixture2.xml @@ -0,0 +1,11 @@ + + + + Poker on TV is great! + 2006-06-16 11:00:00 + + + XML identified as leading cause of cancer + 2006-06-16 16:00:00 + + diff --git a/tests/modeltests/fixtures_model_package/fixtures/initial_data.json b/tests/modeltests/fixtures_model_package/fixtures/initial_data.json new file mode 100644 index 0000000000..66cb5d7b87 --- /dev/null +++ b/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/tests/modeltests/fixtures_model_package/models/__init__.py b/tests/modeltests/fixtures_model_package/models/__init__.py new file mode 100644 index 0000000000..1581102b88 --- /dev/null +++ b/tests/modeltests/fixtures_model_package/models/__init__.py @@ -0,0 +1,54 @@ +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') + +__test__ = {'API_TESTS': """ +>>> from django.core import management +>>> from django.db.models import get_app + +# Reset the database representation of this app. +# This will return the database to a clean initial state. +>>> management.call_command('flush', verbosity=0, interactive=False) + +# Syncdb introduces 1 initial data object from initial_data.json. +>>> Article.objects.all() +[] + +# Load fixture 1. Single JSON file, with two objects. +>>> management.call_command('loaddata', 'fixture1.json', verbosity=0) +>>> Article.objects.all() +[, , ] + +# Load fixture 2. JSON file imported by default. Overwrites some existing objects +>>> management.call_command('loaddata', 'fixture2.json', verbosity=0) +>>> Article.objects.all() +[, , , ] + +# Load a fixture that doesn't exist +>>> management.call_command('loaddata', 'unknown.json', verbosity=0) + +# object list is unaffected +>>> Article.objects.all() +[, , , ] +"""} + + +from django.test import TestCase + +class SampleTestCase(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.assertEquals(str(Article.objects.all()), "[, , , ]") diff --git a/tests/modeltests/lookup/models.py b/tests/modeltests/lookup/models.py index 99eefffd66..512ffd7e7d 100644 --- a/tests/modeltests/lookup/models.py +++ b/tests/modeltests/lookup/models.py @@ -104,6 +104,12 @@ Article 4 >>> Article.objects.in_bulk([3]) {3: } +>>> Article.objects.in_bulk(set([3])) +{3: } +>>> Article.objects.in_bulk(frozenset([3])) +{3: } +>>> Article.objects.in_bulk((3,)) +{3: } >>> Article.objects.in_bulk([1000]) {} >>> Article.objects.in_bulk([]) diff --git a/tests/regressiontests/admin_views/models.py b/tests/regressiontests/admin_views/models.py index 50bc05eef0..97785c50f9 100644 --- a/tests/regressiontests/admin_views/models.py +++ b/tests/regressiontests/admin_views/models.py @@ -4,6 +4,7 @@ import os from django.core.files.storage import FileSystemStorage from django.db import models from django.contrib import admin +from django.contrib.admin.views.main import ChangeList from django.core.mail import EmailMessage class Section(models.Model): @@ -420,6 +421,20 @@ class CategoryInline(admin.StackedInline): class CollectorAdmin(admin.ModelAdmin): inlines = [WidgetInline, DooHickeyInline, GrommetInline, WhatsitInline, FancyDoodadInline, CategoryInline] +class Gadget(models.Model): + name = models.CharField(max_length=100) + + def __unicode__(self): + return self.name + +class CustomChangeList(ChangeList): + def get_query_set(self): + return self.root_query_set.filter(pk=9999) # Does not exist + +class GadgetAdmin(admin.ModelAdmin): + def get_changelist(self, request, **kwargs): + return CustomChangeList + admin.site.register(Article, ArticleAdmin) admin.site.register(CustomArticle, CustomArticleAdmin) admin.site.register(Section, save_as=True, inlines=[ArticleInline]) @@ -443,6 +458,7 @@ admin.site.register(Recommendation, RecommendationAdmin) admin.site.register(Recommender) admin.site.register(Collector, CollectorAdmin) admin.site.register(Category, CategoryAdmin) +admin.site.register(Gadget, GadgetAdmin) # We intentionally register Promo and ChapterXtra1 but not Chapter nor ChapterXtra2. # That way we cover all four cases: diff --git a/tests/regressiontests/admin_views/tests.py b/tests/regressiontests/admin_views/tests.py index 3124071503..8e156899a1 100644 --- a/tests/regressiontests/admin_views/tests.py +++ b/tests/regressiontests/admin_views/tests.py @@ -1203,6 +1203,33 @@ class AdminActionsTest(TestCase): self.failUnlessEqual(Subscriber.objects.count(), 2) +class TestCustomChangeList(TestCase): + fixtures = ['admin-views-users.xml'] + urlbit = 'admin' + + def setUp(self): + result = self.client.login(username='super', password='secret') + self.failUnlessEqual(result, True) + + def tearDown(self): + self.client.logout() + + def test_custom_changelist(self): + """ + Validate that a custom ChangeList class can be used (#9749) + """ + # Insert some data + post_data = {"name": u"First Gadget"} + response = self.client.post('/test_admin/%s/admin_views/gadget/add/' % self.urlbit, post_data) + self.failUnlessEqual(response.status_code, 302) # redirect somewhere + # Hit the page once to get messages out of the queue message list + response = self.client.get('/test_admin/%s/admin_views/gadget/' % self.urlbit) + # Ensure that that data is still not visible on the page + response = self.client.get('/test_admin/%s/admin_views/gadget/' % self.urlbit) + self.failUnlessEqual(response.status_code, 200) + self.assertNotContains(response, 'First Gadget') + + class TestInlineNotEditable(TestCase): fixtures = ['admin-views-users.xml'] diff --git a/tests/regressiontests/generic_inline_admin/tests.py b/tests/regressiontests/generic_inline_admin/tests.py index e1969c2e79..0cf1f4ea69 100644 --- a/tests/regressiontests/generic_inline_admin/tests.py +++ b/tests/regressiontests/generic_inline_admin/tests.py @@ -89,22 +89,22 @@ class GenericAdminViewTest(TestCase): # Works with no queryset formset = EpisodeMediaFormSet(instance=e) self.assertEquals(len(formset.forms), 5) - self.assertEquals(formset.forms[0].as_p(), '

') - self.assertEquals(formset.forms[1].as_p(), '

') + self.assertEquals(formset.forms[0].as_p(), '

' % self.mp3_media_pk) + self.assertEquals(formset.forms[1].as_p(), '

' % self.png_media_pk) self.assertEquals(formset.forms[2].as_p(), '

') # A queryset can be used to alter display ordering formset = EpisodeMediaFormSet(instance=e, queryset=Media.objects.order_by('url')) self.assertEquals(len(formset.forms), 5) - self.assertEquals(formset.forms[0].as_p(), '

') - self.assertEquals(formset.forms[1].as_p(), '

') + self.assertEquals(formset.forms[0].as_p(), '

' % self.png_media_pk) + self.assertEquals(formset.forms[1].as_p(), '

' % self.mp3_media_pk) self.assertEquals(formset.forms[2].as_p(), '

') # Works with a queryset that omits items formset = EpisodeMediaFormSet(instance=e, queryset=Media.objects.filter(url__endswith=".png")) self.assertEquals(len(formset.forms), 4) - self.assertEquals(formset.forms[0].as_p(), '

') + self.assertEquals(formset.forms[0].as_p(), '

' % self.png_media_pk) self.assertEquals(formset.forms[1].as_p(), '

') def testGenericInlineFormsetFactory(self): diff --git a/tests/regressiontests/views/tests/defaults.py b/tests/regressiontests/views/tests/defaults.py index bf490d7cf0..1d1b3c3501 100644 --- a/tests/regressiontests/views/tests/defaults.py +++ b/tests/regressiontests/views/tests/defaults.py @@ -10,8 +10,8 @@ class DefaultsTests(TestCase): """Test django views in django/views/defaults.py""" fixtures = ['testdata.json'] - def test_shorcut_with_absolute_url(self): - "Can view a shortcut an Author object that has with a get_absolute_url method" + def test_shortcut_with_absolute_url(self): + "Can view a shortcut for an Author object that has a get_absolute_url method" for obj in Author.objects.all(): short_url = '/views/shortcut/%s/%s/' % (ContentType.objects.get_for_model(Author).id, obj.pk) response = self.client.get(short_url) @@ -19,12 +19,34 @@ class DefaultsTests(TestCase): status_code=302, target_status_code=404) def test_shortcut_no_absolute_url(self): - "Shortcuts for an object that has with a get_absolute_url method raises 404" + "Shortcuts for an object that has no get_absolute_url method raises 404" for obj in Article.objects.all(): short_url = '/views/shortcut/%s/%s/' % (ContentType.objects.get_for_model(Article).id, obj.pk) response = self.client.get(short_url) self.assertEquals(response.status_code, 404) + def test_wrong_type_pk(self): + short_url = '/views/shortcut/%s/%s/' % (ContentType.objects.get_for_model(Author).id, 'nobody/expects') + response = self.client.get(short_url) + self.assertEquals(response.status_code, 404) + + def test_shortcut_bad_pk(self): + short_url = '/views/shortcut/%s/%s/' % (ContentType.objects.get_for_model(Author).id, '4242424242') + response = self.client.get(short_url) + self.assertEquals(response.status_code, 404) + + def test_nonint_content_type(self): + an_author = Author.objects.all()[0] + short_url = '/views/shortcut/%s/%s/' % ('spam', an_author.pk) + response = self.client.get(short_url) + self.assertEquals(response.status_code, 404) + + def test_bad_content_type(self): + an_author = Author.objects.all()[0] + short_url = '/views/shortcut/%s/%s/' % (4242424242, an_author.pk) + response = self.client.get(short_url) + self.assertEquals(response.status_code, 404) + def test_page_not_found(self): "A 404 status is returned by the page_not_found view" non_existing_urls = ['/views/non_existing_url/', # this is in urls.py