Dowi
Dowi

Reputation: 11

Validate count() constraint in many to many relation before saving an instance in django

I would like to prevent a save in a django model when a certain constraint is not met and give a validation error so that a django staff user knows what went wrong.

The constraint is the count() from an intermediate table specified using the through parameter.

models.py:

class Goal(models.Model):
  name = models.CharField(max_length=128)

class UserProfile(models.Model):
  goals = models.ManyToManyField(Goal, through=UserProfileGoals, blank=True)

class UserProfileGoal(models.Model):
  goal = models.ForeignKey(Goals)
  user_profile = models.ForeignKey(UserProfile)

class UserGoalConstraint(models.Model):
  user_profile = models.OneToOneField(UserProfile)
  max_goals = models.PositiveIntegerField()

So the UserGoalConstraint.max_goals gives me the number of the maximum definable UserProfile.goal which are stored in the UserProfileGoal model (same UserGoal can be stored more often to the UserProfile)

I have read and tried solutions from several posts, which are using ModelForm's clean(), Model's clean() and pre_save signal events,

but the actual problem I have is, how do I know if it is just an update or a new database entry, because

class UserProfileGoal(models.Model):
  goal = models.ForeignKey(Goals)
  user_profile = models.ForeignKey(UserProfile)

  def clean(self):
    goal_counter = self.user_profile.goals.count() + 1

    try:
      qs = UserGoalConstraint.objects.get(user_profile=self.user_profile)
    except UserGoalConstraint.DoesNotExist:
      raise ObjectDoesNotExist('Goal Constraint does not exist')

    if goal_counter > qs.max_goals:
      raise ValidationError('There are more goals than allowed goals')

does not really work, as clean() can also be an update and the +1 gives me a wrong result which leads to the ValidationError.

My client should use the django-admin interface to add goals to the user profile directly via an Inline:

admin.py:

class UserProfileGoalInline(admin.TabularInline):
  model=UserProfileGoal

class UserProfileAdmin(admin.ModelAdmin)
  ...
  inlines = [UserProfileGoalInline, ]

So he needs to be nicely informed when he adds to many goals to a user profile.

Maybe I am missing something obvious on how to solve this problem...? I am looking for a working and somehow user friendly solution (= get informed in admin interface).

[UPDATE]: I tried know to check wether it is created or not with the self.pk is None trick at the beginning of the clean()

if self.pk is not None:
  return  # it is not a create
...

I thought that would deal with the issue... However, in the admin inline, when the staff user adds more than one goal at the same time, the clean() does not recognize these. Debug output shows for 2 goals added, that the goal counter holds the same number even the second entry should have one more and should give an validation error

Upvotes: 0

Views: 1624

Answers (2)

Dowi
Dowi

Reputation: 11

Thanks to @zaidfazil for a starting solution:

class UserProfileGoalForm(forms.ModelForm):
  class Meta:
    model = UserProfileGoal
    ...

  def clean(self):
    cleaned_data = super(UserProfileGoalForm, self).clean()
    if self.instance.pk is not None:
      return cleaned_data
    user_profile = self.cleaned_data.get('user_profile')
    goal_count = user_profile.goals.count()
    goal_limit = UserGoalConstraint.objects.get(user_profile=user_profile).max_goals # removed try catch for get for easier reading
    if goal_count >= goal_limit:
      raise ValidationError('Maximum limit reached for goals')
    return cleaned_data

However, this does not handle the inline in the UserProfile admin interface: clean() won't handle correctly if you add more than one Goal at the same time and press save.

So I applied the UserProfileGoalForm to the inline and defined max_num :

class UserProfileGoalInline(admin.TabularInline):
  model=UserProfileGoal
  form = UserProfileGoalForm

  def get_max_num(self, request, obj=None, **kwargs):
    if obj is None:
      return
    goal_limit = UserGoalConstraint.objects.get(training_profile=obj).max_goals
    return goal_limit # which will overwrite the inline's max_num attribute

Now my client can only add at maximum the max_goals value from the UserGoalConstraint, and also a possible admin form for UserProfileGoal will handle the constraint:

class UserProfileGoalAdmin(admin.ModelAdmin):
  form = UserProfileGoalForm

Upvotes: 1

zaidfazil
zaidfazil

Reputation: 9235

You could handle it in ModelForm clean method,

class GoalForm(forms.ModelForm):
    class Meta:
        model = Goal
        .....

    def clean(self):
        cleaned_data = super(GoalForm, self).clean()
        if self.instance.pk is not None:
            return cleaned_data
        goal_limit = self.user_profile.usergoalconstraint.max_goals
        goal_count = self.user_profile.goals.count()
        if goal_count >= goal_limit:
            raise ValidationError("Maximum limit reached for goals")
        return cleaned_data

Upvotes: 0

Related Questions