Commit 6f6e7d01 authored by Carl Meyer's avatar Carl Meyer
Browse files

Merge pull request #3308 from aericson/ticket_22064

Fixed #22064 -- Add check for related_name
parents 51165401 1e5e2a47
Loading
Loading
Loading
Loading
+21 −0
Original line number Diff line number Diff line
@@ -97,11 +97,32 @@ signals.class_prepared.connect(do_pending_lookups)
class RelatedField(Field):
    def check(self, **kwargs):
        errors = super(RelatedField, self).check(**kwargs)
        errors.extend(self._check_related_name_is_valid())
        errors.extend(self._check_relation_model_exists())
        errors.extend(self._check_referencing_to_swapped_model())
        errors.extend(self._check_clashes())
        return errors

    def _check_related_name_is_valid(self):
        import re
        import keyword
        related_name = self.rel.related_name

        is_valid_id = (related_name and re.match('^[a-zA-Z_][a-zA-Z0-9_]*$', related_name)
                       and not keyword.iskeyword(related_name))
        if related_name and not (is_valid_id or related_name.endswith('+')):
            return [
                checks.Error(
                    "The name '%s' is invalid related_name for field %s.%s" %
                    (self.rel.related_name, self.model._meta.object_name,
                     self.name),
                    hint="Related name must be a valid Python identifier or end with a '+'",
                    obj=self,
                    id='fields.E306',
                )
            ]
        return []

    def _check_relation_model_exists(self):
        rel_is_missing = self.rel.to not in apps.get_models()
        rel_is_string = isinstance(self.rel.to, six.string_types)
+2 −0
Original line number Diff line number Diff line
@@ -117,6 +117,8 @@ Related Fields
  ``<field name>``.
* **fields.E305**: Field name ``<field name>`` clashes with reverse query name
  for ``<field name>``.
* **fields.E306**: Related name must be a valid Python identifier or end with
  a ``'+'``.
* **fields.E310**: None of the fields ``<field1>``, ``<field2>``, ... on model
  ``<model>`` have a ``unique=True`` constraint.
* **fields.E311**: ``<model>`` must set ``unique=True`` because it is
+67 −0
Original line number Diff line number Diff line
@@ -546,6 +546,73 @@ class RelativeFieldTests(IsolatedModelsTestCase):
            errors = field.check(from_model=Model)
            self.assertEqual(errors, [expected_error])

    def test_related_field_has_invalid_related_name(self):
        digit = 0
        illegal_non_alphanumeric = '!'
        whitespace = '\t'

        invalid_related_names = [
            '%s_begins_with_digit' % digit,
            '%s_begins_with_illegal_non_alphanumeric' % illegal_non_alphanumeric,
            '%s_begins_with_whitespace' % whitespace,
            'contains_%s_illegal_non_alphanumeric' % illegal_non_alphanumeric,
            'contains_%s_whitespace' % whitespace,
            'ends_with_with_illegal_non_alphanumeric_%s' % illegal_non_alphanumeric,
            'ends_with_whitespace_%s' % whitespace,
            # Python's keyword
            'with',
        ]

        class Parent(models.Model):
            pass

        for invalid_related_name in invalid_related_names:
            Child = type(str('Child_%s') % str(invalid_related_name), (models.Model,), {
                'parent': models.ForeignKey('Parent', related_name=invalid_related_name),
                '__module__': Parent.__module__,
            })

            field = Child._meta.get_field('parent')
            errors = Child.check()
            expected = [
                Error(
                    "The name '%s' is invalid related_name for field Child_%s.parent"
                    % (invalid_related_name, invalid_related_name),
                    hint="Related name must be a valid Python identifier or end with a '+'",
                    obj=field,
                    id='fields.E306',
                ),
            ]
            self.assertEqual(errors, expected)

    def test_related_field_has_valid_related_name(self):
        lowercase = 'a'
        uppercase = 'A'
        digit = 0

        related_names = [
            '%s_starts_with_lowercase' % lowercase,
            '%s_tarts_with_uppercase' % uppercase,
            '_starts_with_underscore',
            'contains_%s_digit' % digit,
            'ends_with_plus+',
            '_',
            '_+',
            '+',
        ]

        class Parent(models.Model):
            pass

        for related_name in related_names:
            Child = type(str('Child_%s') % str(related_name), (models.Model,), {
                'parent': models.ForeignKey('Parent', related_name=related_name),
                '__module__': Parent.__module__,
            })

            errors = Child.check()
            self.assertFalse(errors)


class AccessorClashTests(IsolatedModelsTestCase):