I have a Model that needs to be associated with different objects depending on its type, so I'm validating this in the Model's .clean()
method. If it's a "Dialogue" type, then it should only have "dialogue
" objects (ForeignKeyField
) associated with it. If it's a "Mixed" type, then it should only have "mixed_chunk
" (ManyToManyField
with a through
model) objects associated with it.
However, I'm running into issues validating the ManyToManyField
("mixed_chunks
") because the relationship cannot be accessed before the object is saved and receives an id.
With the code below, when I test in the Admin and try to save a "D"
type Evaluation
object without attaching any mixed_chunks
objects (this is done through an inline in the Admin), the conditional if self.mixed_chunks
in my custom .clean()
method for the Evaluation
model evaluates as true even though I haven't selected any mixed_chunks
objects, therefore raising my validation error raise ValidationError(_("Only dialogues allowed to be attached for a 'Dialogue'-type eval"))
.
I've tried to print some things out to see what is going on:
when I have selected some EvalMixedEvalChunkCombo
objects
print(self.mixed_chunks)
= "evaluations.MixedEvalChunk.None"print(self.e_eval_mixed_chunk_combos)
= "evaluations.EvalMixedEvalChunkCombo.None"when I have not selected some EvalMixedEvalChunkCombo objects
print(self.mixed_chunks)
= "evaluations.MixedEvalChunk.None"print(self.e_eval_mixed_chunk_combos)
= "evaluations.EvalMixedEvalChunkCombo.None"when I have selected a dialogue
object
print(self.dialogue)
= the name of the dialogue objectwhen I have not selected a dialogue
object
print(self.dialogue)
= "None"This tells me that the object is not yet associated with the ManyToManyField
objects that I'm assigning to it in the admin at the time I hit "submit" on the form. How can I perform validation if they aren't accessible when the .clean()
method is called? If the answer to this question is correct, then it looks like there isn't a way.
I tried calling self.save()
in my .clean()
method but the print results above were exactly the same
I also don't think it has something to do with the admin itself because this happens no matter how I try to create a new Evaluation
object.
from scenario.models import Dialogue
class MixedEvalChunk(models.Model):
difficulty = models.CharField(max_length=15)
# bunch of other fields...
class EvalMixedEvalChunkCombo(models.Model):
eval = models.ForeignKey(
'Evaluation',
on_delete=models.CASCADE,
related_name="e_eval_mixed_chunk_combos"
)
mixed_eval_chunk = models.ForeignKey(
MixedEvalChunk,
on_delete=models.CASCADE,
related_name="m_eval_mixed_chunk_combos"
)
class Evaluation(models.Model):
class EVAL_TYPES(models.TextChoices):
D = 'D', _('Dialogue')
S = 'S', _('Simultaneous')
T = 'T', _('Translation')
M = 'M', _('Mixed')
type = models.CharField(max_length=1, choices=EVAL_TYPES.choices, default='D')
dialogue = models.ForeignKey(
Dialogue,
blank=True,
null=True,
on_delete=models.SET_NULL
)
mixed_chunks = models.ManyToManyField(
MixedEvalChunk,
blank=True,
through=EvalMixedEvalChunkCombo
)
def clean(self):
if self.type == 'D':
if self.mixed_chunks:
raise ValidationError(_("Only dialogues allowed to be attached for a 'Dialogue'-type eval"))
elif self.type == 'M':
if self.dialogue:
raise ValidationError(
{"dialogue": _("Only MIXED EVAL CHUNKS allowed to be attached for a 'Mixed'-type eval"),}
)
class MixedEvalChunkInlineAdmin(admin.TabularInline):
model = Evaluation.mixed_chunks.through
extra = 0
@admin.register(MixedEvalChunk)
class MixedEvalChunkAdmin(admin.ModelAdmin):
pass
@admin.register(EvalMixedEvalChunkCombo)
class EvalMixedEvalChunkComboAdmin(admin.ModelAdmin):
list_display = ["""bunch of stuff.."""]
fieldsets = ["""bunch of stuff.."""]
@admin.register(Evaluation)
class EvaluationAdmin(admin.ModelAdmin):
fieldsets = ["""bunch of stuff.."""]
inlines = (MixedEvalChunkInlineAdmin,)
First: if self.mixed_chunks:
doesn't evaluate to False
, because mixed_chunks
returns a related manager, not a queryset. You still have to ask it to query something, like .exists()
or .all()
, for your conditional check to work against the data as intended.
Try if self.mixed_chunks.exists():
and see if that resolves your other issues with validation.
Update:
Given your error message:
<object> needs to have a value for field “id” before this many-to-many relationship can be used
...the issue you're running into is a bit clearer. Reverse relationships emit additional queries when accessed (or when prefetched explicitly). They do need a primary key to exist before any foreign keys can reference them - including those in many-to-many relationships.
By definition, your Evaluation
records are allowed to exist independently of their mixed_chunk
relationships. It's not the Evaluation
model that needs to be responsible for the validation of the many-to-many data - it's the EvalMixedEvalChunkCombo
model.
Try putting the clean()
method on the EvalMixedEvalChunkCombo
instead. Note that I've renamed the field to evaluation
, because eval
is a Python built-in and should be avoided as a variable name:
class EvalMixedEvalChunkCombo(models.Model):
def clean(self):
if self.evaluation.type == 'D':
raise ValidationError
...
Other comments:
type
and eval
. If there's not a more appropriate name, append an underscore to avoid clobbering (e.g., type_
).TextChoices
in if
conditions, you can reference the class directly: if x == EVAL_TYPES.D:
.