views:

209

answers:

2

I have two models (ModelParent and ModelChild) with same m2m fields on Subject model. ModelChild has a foreign key on ModelParent and ModelChild is defined as inline for ModelParent on admin page.

### models.py ###
  class Subject(Models.Model):
    pass

  class ModelParent(models.Model):
    subjects_parent = ManyToManyField(Subject)

  class ModelChild(models.Model):
    parent = ForeignKey(ModelParent)
    subjects_child = ManyToManyField(Subject)

### admin.py ###
  class ModelChildInline(admin.TabularInline):
      model = ModelChild

  class ModelParentAdmin(admin.ModelAdmin):
    inlines = [ModelChildInline]

  admin.site.register(ModelParent, ModelParentAdmin)

I have one important restriction though, ModelChild's subjects_child field must not reference any subject that subject_parent does with its subjects_parent.

So, if I select the same Subject (in subject_parent and subject_child) on Admin page for both models, how can I validate this? If only one field changes you validate it against the db, but what if both change (subject_parent and subject_child)? How can I validate both forms together before saving?

A: 

The admin classes do not have the clean() method. Their forms do. Each admin class has a parameter called form. You simply extend the default form (it's the normal ModelAdmin form), implement the clean() method and add the form to the admin class. Example:

class SomeForm(ModelForm):
  #some code
  def clean(self):
   #some code
class SomeAdminClass(ModelAdmin):
 #some code
 form = SomeForm
 #more code
Klop
I already done that, but with this I can only validate the forms against the DB _separately_. What if you add the same subject to ModelParent.subjects_parent and ModelChild.sujects_child on one admin page (this happens with inlines of course). One form doesn't know that the other has selected the same subject so they both get saved. This is because validation of both forms happens before saving in the DB, so none of them sees the changes the other did.
blazt
I see now. This complicates things a bit. This level of protection must implement in the layer, that both forms use. I do not know the direct solution to your problem, but this kind of protection must be implemented in either the model layer or in the DBMS directly. Look at django documentation or in the worst case the DBMS documentation.
Klop
A: 

I have inherited a new class named ModelAdminWithInline from admin.ModelAdmin and modified methods add_view(...) and change_view(...) to call function is_cross_valid(self, form, formsets), where you can validate all the forms together. Both functions had:

#...
if all_valid(formsets) and form_validated:
#...

changed to:

#...
formsets_validated = all_valid(formsets)
cross_validated = self.is_cross_valid(form, formsets)
if formsets_validated and form_validated and cross_validated:
#...

The new function is_cross_valid(...) is defined like this:

def is_cross_valid(self, form, formsets):
  return True

so the new class should work exactly the same as ModelAdmin if you don't change is_cross_valid(...) function.

Now my admin.py looks like this:

###admin.py###
class ModelAdminWithInline(admin.ModelAdmin):
  def is_cross_valid(self, form, formsets):
    return True

  def add_view(self, request, form_url='', extra_context=None):
    #modified code

  def change_view(self, request, object_id, extra_context=None):
    #modified code

class ModelChildInline(admin.TabularInline):
  model = ModelChild

class ModelParentAdmin(ModelAdminWithInline):
  inlines = [ModelChildInline]

  def is_cross_valid(self, form, formsets):
    #Do some cross validation on forms
    #For example, here is my particular validation:
    valid = True

    if hasattr(form, 'cleaned_data'):   

      subjects_parent = form.cleaned_data.get("subjects_parent")

      #You can access forms from formsets like this:
      for formset in formsets:
        for formset_form in formset.forms:
          if hasattr(formset_form, 'cleaned_data'):

            subjects_child = formset_form.cleaned_data.get("subjects_child")
            delete_form = formset_form.cleaned_data.get("DELETE")

            if subjects_child and (delete_form == False):
              for subject in subjects_child:
                if subject in subjects_parent:
                  valid = False
                  #From here you can still report errors like in regular forms:
                  if "subjects_child" in formset_form.cleaned_data.keys():
                    formset_form._errors["subjects_child"] = ErrorList([u"Subject %s is already selected in parent ModelParent" % subject])
                    del formset_form.cleaned_data["subjects_child"]
                  else:
                    formset_form._errors["subjects_child"] += ErrorList(u"Subject %s is already selected in parent ModelParent" % subject])

      #return True on success or False otherwise.
      return valid

admin.site.register(ModelParent, ModelParentAdmin)

The solution is a little bit hackish but it works :). The errors show up the same as with regular ModelForm and ModelAdmin classes. Django 1.2 (which should be released shortly) should have model validation, so I hope that then this problem could be solved more nicely.

blazt