diff --git a/AUTHORS b/AUTHORS index beb067b94f..2a4494293f 100644 --- a/AUTHORS +++ b/AUTHORS @@ -487,6 +487,7 @@ answer newbie questions, and generally made Django that much better: Tomek Paczkowski Jens Page Guillaume Pannatier + Renaud Parent Jay Parlar Carlos Eduardo de Paula John Paulett diff --git a/django/db/models/options.py b/django/db/models/options.py index 632c71f3f9..39f0ab6f19 100644 --- a/django/db/models/options.py +++ b/django/db/models/options.py @@ -20,7 +20,7 @@ DEFAULT_NAMES = ('verbose_name', 'verbose_name_plural', 'db_table', 'ordering', 'order_with_respect_to', 'app_label', 'db_tablespace', 'abstract', 'managed', 'proxy', 'swappable', 'auto_created', 'index_together', 'apps', 'default_permissions', - 'select_on_save') + 'select_on_save', 'default_related_name') def normalize_together(option_together): @@ -99,6 +99,8 @@ class Options(object): # A custom app registry to use, if you're making a separate model set. self.apps = apps + self.default_related_name = None + @property def app_config(self): # Don't go through get_app_config to avoid triggering imports. diff --git a/django/db/models/related.py b/django/db/models/related.py index 6e4f946ea9..51460eedbe 100644 --- a/django/db/models/related.py +++ b/django/db/models/related.py @@ -57,7 +57,14 @@ class RelatedObject(object): # If this is a symmetrical m2m relation on self, there is no reverse accessor. if getattr(self.field.rel, 'symmetrical', False) and self.model == self.parent_model: return None - return self.field.rel.related_name or (self.opts.model_name + '_set') + if self.field.rel.related_name: + return self.field.rel.related_name + if self.opts.default_related_name: + return self.opts.default_related_name % { + 'model_name': self.opts.model_name.lower(), + 'app_label': self.opts.app_label.lower(), + } + return self.opts.model_name + '_set' else: return self.field.rel.related_name or (self.opts.model_name) diff --git a/docs/ref/models/options.txt b/docs/ref/models/options.txt index 75b6de8040..6cb2e34bd9 100644 --- a/docs/ref/models/options.txt +++ b/docs/ref/models/options.txt @@ -95,6 +95,23 @@ Django quotes column and table names behind the scenes. setting, if set. If the backend doesn't support tablespaces, this option is ignored. +``default_related_name`` +------------------------ + +.. attribute:: Options.default_related_name + +.. versionadded:: 1.8 + + The name that will be used by default for the relation from a related object + back to this one. The default is ``_set``. + + As the reverse name for a field should be unique, be careful if you intend + to subclass your model. To work around name collisions, part of the name + should contain ``'%(app_label)s'`` and ``'%(model_name)s'``, which are + replaced respectively by the name of the application the model is in, + and the name of the model, both lowercased. See the paragraph on + :ref:`related names for abstract models `. + ``get_latest_by`` ----------------- diff --git a/docs/releases/1.8.txt b/docs/releases/1.8.txt index 7f3b4aa6c7..32de9ba631 100644 --- a/docs/releases/1.8.txt +++ b/docs/releases/1.8.txt @@ -178,6 +178,10 @@ Models * Django now logs at most 9000 queries in ``connections.queries``, in order to prevent excessive memory usage in long-running processes in debug mode. +* There is now a model ``Meta`` option to define a + :attr:`default related name ` + for all relational fields of a model. + * Pickling models and querysets across different versions of Django isn't officially supported (it may work, but there's no guarantee). An extra variable that specifies the current Django version is now added to the diff --git a/tests/model_options/models/default_related_name.py b/tests/model_options/models/default_related_name.py new file mode 100644 index 0000000000..57b52647e6 --- /dev/null +++ b/tests/model_options/models/default_related_name.py @@ -0,0 +1,41 @@ +from django.db import models + + +class Author(models.Model): + first_name = models.CharField(max_length=128) + last_name = models.CharField(max_length=128) + + +class Editor(models.Model): + name = models.CharField(max_length=128) + bestselling_author = models.ForeignKey(Author) + + +class Book(models.Model): + title = models.CharField(max_length=128) + authors = models.ManyToManyField(Author) + editor = models.ForeignKey(Editor, related_name="edited_books") + + class Meta: + default_related_name = "books" + + +class Store(models.Model): + name = models.CharField(max_length=128) + address = models.CharField(max_length=128) + + class Meta: + abstract = True + default_related_name = "%(app_label)s_%(model_name)ss" + + +class BookStore(Store): + available_books = models.ManyToManyField(Book) + + +class EditorStore(Store): + editor = models.ForeignKey(Editor) + available_books = models.ManyToManyField(Book) + + class Meta: + default_related_name = "editor_stores" diff --git a/tests/model_options/test_default_related_name.py b/tests/model_options/test_default_related_name.py new file mode 100644 index 0000000000..6cdffe5cf9 --- /dev/null +++ b/tests/model_options/test_default_related_name.py @@ -0,0 +1,46 @@ +from django.test import TestCase + +from .models.default_related_name import Author, Editor, Book + + +class DefaultRelatedNameTests(TestCase): + + def setUp(self): + self.author = Author.objects.create(first_name="Dave", last_name="Loper") + self.editor = Editor.objects.create(name="Test Editions", + bestselling_author=self.author) + self.book = Book.objects.create(title="Test Book", editor=self.editor) + self.book.authors.add(self.author) + self.book.save() + + def test_no_default_related_name(self): + try: + self.author.editor_set + except AttributeError: + self.fail("Author should have an editor_set relation.") + + def test_default_related_name(self): + try: + self.author.books + except AttributeError: + self.fail("Author should have a books relation.") + + def test_related_name_overrides_default_related_name(self): + try: + self.editor.edited_books + except AttributeError: + self.fail("Editor should have a edited_books relation.") + + def test_inheritance(self): + try: + # Here model_options corresponds to the name of the application used + # in this test + self.book.model_options_bookstores + except AttributeError: + self.fail("Book should have a model_options_bookstores relation.") + + def test_inheritance_with_overrided_default_related_name(self): + try: + self.book.editor_stores + except AttributeError: + self.fail("Book should have a editor_stores relation.")