Ticket #12203: 12203-m2m-w-through-modeladmin-fields-option2.diff

File 12203-m2m-w-through-modeladmin-fields-option2.diff, 7.0 KB (added by Ramiro Morales, 9 years ago)

Patch implementing fancy admin validation strategy described by Russell.

  • django/contrib/admin/validation.py

    diff --git a/django/contrib/admin/validation.py b/django/contrib/admin/validation.py
    a b  
    149149            validate_inline(inline, cls, model)
    150150
    151151def validate_inline(cls, parent, parent_model):
    152    
     152
    153153    # model is already verified to exist and be a Model
    154154    if cls.fk_name: # default value is None
    155155        f = get_field(cls, cls.model, cls.model._meta, 'fk_name', cls.fk_name)
     
    200200            raise ImproperlyConfigured('Both fieldsets and fields are specified in %s.' % cls.__name__)
    201201        if len(cls.fields) > len(set(cls.fields)):
    202202            raise ImproperlyConfigured('There are duplicate field(s) in %s.fields' % cls.__name__)
     203        for idx, field in enumerate(cls.fields):
     204            f = get_field(cls, model, opts, 'fields', field)
     205            if isinstance(f, models.ManyToManyField) and f.rel.through and not f.rel.through._meta.auto_created:
     206                if f.rel.through:
     207                    validate_fields_m2m_through_model(cls, model, field, f, idx)
    203208
    204209    # fieldsets
    205210    if cls.fieldsets: # default value is None
     
    272277            for idx, f in enumerate(val):
    273278                get_field(cls, model, opts, "prepopulated_fields['%s'][%d]" % (field, idx), f)
    274279
     280def validate_fields_m2m_through_model(cls, model, field, f, idx):
     281    from_model, to_model, pk, uniquet = None, None, False, False
     282    for i, through_model_fld in enumerate(f.rel.through._meta.fields):
     283        if isinstance(through_model_fld, models.ForeignKey):
     284            rel_to = through_model_fld.rel.to
     285            if rel_to == model:
     286                from_model = through_model_fld.name
     287            elif rel_to == f.rel.to:
     288                to_model = through_model_fld.name
     289            else:
     290                break
     291        elif through_model_fld.primary_key: # XXX hasattr?
     292            if pk:
     293                break
     294            pk = True
     295    if hasattr(f.rel.through._meta, 'unique_together'):
     296        unique_together = f.rel.through._meta.unique_together
     297        if unique_together and isinstance(unique_together, (list, tuple)):
     298            unique_together = unique_together[0]
     299        if from_model in unique_together and \
     300            to_model in unique_together and len(unique_together) == 2:
     301                uniquet = True
     302    if not (i == 2 and from_model and to_model and pk and uniquet):
     303        raise ImproperlyConfigured("%s.fields: Included '%s' ManyToManyField field uses 'through' option but specifies a non suitable intermediate model (%s)." % (cls.__name__, field, f.rel.through.__name__))
     304
    275305def check_isseq(cls, label, obj):
    276306    if not isinstance(obj, (list, tuple)):
    277307        raise ImproperlyConfigured("'%s.%s' must be a list or tuple." % (cls.__name__, label))
  • tests/regressiontests/admin_validation/models.py

    diff --git a/tests/regressiontests/admin_validation/models.py b/tests/regressiontests/admin_validation/models.py
    a b  
    2525    album2 = models.ForeignKey(Album, related_name="album2_set")
    2626    e = models.CharField(max_length=1)
    2727
     28# Validation of inclusion of m2m fields in ModelAdmin.fields. See #12203
    2829
     30class Author(models.Model):
     31    name = models.CharField(max_length=30)
     32
     33# Missing unique_together in the intermediate model:
     34
     35class Book(models.Model):
     36    name = models.CharField(max_length=20)
     37    authors = models.ManyToManyField(Author, through='AuthorsBooks')
     38
     39class AuthorsBooks(models.Model):
     40    author = models.ForeignKey(Author)
     41    book = models.ForeignKey(Book)
     42
     43# Extra data in the intermediate model:
     44
     45class Ballad(models.Model):
     46    name = models.CharField(max_length=20)
     47    authors = models.ManyToManyField(Author, through='AuthorsBallads')
     48
     49class AuthorsBallads(models.Model):
     50    author = models.ForeignKey(Author)
     51    ballad = models.ForeignKey(Ballad)
     52    extra_data = models.IntegerField()
     53
     54    class Meta:
     55        unique_together = ('author', 'ballad')
     56
     57# Intermediate model PK field isn't an independent one:
     58
     59class Act(models.Model):
     60    name = models.CharField(max_length=20)
     61    authors = models.ManyToManyField(Author, through='AuthorsActs')
     62
     63class AuthorsActs(models.Model):
     64    author = models.ForeignKey(Author, primary_key=True)
     65    act = models.ForeignKey(Act)
     66
     67# This through-using m2m complies with all the requirements needed to be
     68# listed in ModelAdmin.fields, should validate succesfully:
     69
     70class Paper(models.Model):
     71    name = models.CharField(max_length=20)
     72    authors = models.ManyToManyField(Author, through='AuthorsPapers')
     73
     74
     75class AuthorsPapers(models.Model):
     76    author = models.ForeignKey(Author)
     77    paper = models.ForeignKey(Paper)
     78
     79    class Meta:
     80        unique_together = ('author', 'paper')
     81
     82# Intermediate model has a manually set PK, should work too:
     83
     84class Post(models.Model):
     85    name = models.CharField(max_length=20)
     86    authors = models.ManyToManyField(Author, through='AuthorsPosts')
     87
     88
     89class AuthorsPosts(models.Model):
     90    mypk = models.AutoField(primary_key=True)
     91    author = models.ForeignKey(Author)
     92    post = models.ForeignKey(Post)
     93
     94    class Meta:
     95        unique_together = ('author', 'post')
     96
     97# Recursive m2m with through, should work too:
     98
     99class Writer(models.Model):
     100    name = models.CharField(max_length=30)
     101    similar = models.ManyToManyField('self', through='Influence', symmetrical=False)
     102
     103
     104class Influence(models.Model):
     105    inspirator = models.ForeignKey(Writer, related_name='influenced_by')
     106    inspired = models.ForeignKey(Writer, related_name='influenced')
    29107
    30108__test__ = {'API_TESTS':"""
    31109
     
    95173
    96174>>> validate_inline(TwoAlbumFKAndAnEInline, None, Album)
    97175
     176# Regression test for #12203 - Fail more gracefully when a M2M field that
     177# specifies the 'through' option is included in the 'fields' ModelAdmin option.
     178
     179>>> class BookAdmin(admin.ModelAdmin):
     180...     fields = ['authors']
     181
     182>>> validate(BookAdmin, Book)
     183Traceback (most recent call last):
     184    ...
     185ImproperlyConfigured: BookAdmin.fields: Included 'authors' ManyToManyField field uses 'through' option but specifies a non suitable intermediate model (AuthorsBooks).
     186
     187>>> class BalladAdmin(admin.ModelAdmin):
     188...     fields = ['authors']
     189
     190>>> validate(BalladAdmin, Ballad)
     191Traceback (most recent call last):
     192    ...
     193ImproperlyConfigured: BalladAdmin.fields: Included 'authors' ManyToManyField field uses 'through' option but specifies a non suitable intermediate model (AuthorsBallads).
     194
     195>>> class ActAdmin(admin.ModelAdmin):
     196...     fields = ['authors']
     197
     198>>> validate(ActAdmin, Act)
     199Traceback (most recent call last):
     200    ...
     201ImproperlyConfigured: ActAdmin.fields: Included 'authors' ManyToManyField field uses 'through' option but specifies a non suitable intermediate model (AuthorsActs).
     202
     203>>> class PaperAdmin(admin.ModelAdmin):
     204...     fields = ['authors']
     205
     206>>> validate(PaperAdmin, Paper)
     207
     208>>> class PostAdmin(admin.ModelAdmin):
     209...     fields = ['authors']
     210
     211>>> validate(PostAdmin, Post)
     212
    98213"""}
Back to Top