pythondjangoformsselectinput

How to use choices of a Select field of a form, when defined in the view in Django?


I have an issue with a form with a Select input. This input is used to select a user in a list. The list should only contain user from the group currently used.

I found a solution, however I am not completely sure of what am I doing in the form definition (I do not fully understand how works the def __init__ part). And of course, my code is not working : I obtain the right form, with the choices I need, however if a submit data, it's not saved in the database.

I have been able to check if the form is valid (it is not), the error is the following :

Category - Select a valid choice. That choice is not one of the available choices.

(I have the same error for the user field). I can't find my way in this, so if you can help, would be very appreciated!

My models:

class Group(models.Model):
    name = models.CharField(max_length=100)
    def __str__(self):
        return self.name   
    
class User(AbstractUser):
    groups = models.ManyToManyField(Group)
    current_group = models.ForeignKey(Group, on_delete=models.SET_NULL,blank = True , null = True, related_name="current_group")

class Category(models.Model):
    name = models.CharField(max_length=100)
    groups = models.ManyToManyField(Group)

    def __str__(self):
        return self.name

class Expanses(models.Model):
    date = models.DateTimeField()
    amount = models.DecimalField(decimal_places=2, max_digits=12)
    category = models.ForeignKey(Category, on_delete=models.CASCADE)
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    group = models.ForeignKey(Group, on_delete=models.CASCADE)
    comment = models.CharField(max_length=500)
    
    def __str__(self):
        return self.amount

My form:

class CreateExpanseForm(forms.ModelForm):
    class Meta:
        model = Expanses
        fields = ['date', 'amount', 'category', 'user','comment']
        widgets={'date':DateInput(attrs={'placeholder':'Date', 'class':'form-input', 'type':'date'}),
                 'amount':TextInput(attrs={'placeholder':'Amount', 'class':'form-input', 'type':'text'}),
                 'category':Select(attrs={'placeholder':'Category', 'class':'form-select', 'type':'text'}),
                 'user':Select(attrs={'placeholder':'user', 'class':'form-select', 'type':'text'}),
                 'comment':TextInput(attrs={'placeholder':'comment', 'class':'form-input', 'type':'text'}),}
    
    def __init__(self, *args, **kwargs):
        user_choices = kwargs.pop('user_choices', None)
        category_choices = kwargs.pop('category_choices', None)
        
        super().__init__(*args, **kwargs)
        if user_choices:
            self.fields['user'].choices = user_choices
        if category_choices:
            self.fields['category'].choices = category_choices   

My view:

def SummaryView(request):
    createExpanseForm = CreateExpanseForm(user_choices = [(user.username, user.username) for user in request.user.current_group.user_set.all()],
                                          category_choices = [(category.name, category.name) for category in request.user.current_group.category_set.all()])
    if request.method == "POST":
        if 'createExpanse' in request.POST:
            createExpanseForm = CreateExpanseForm(user_choices = [(user.username, user.username) for user in request.user.current_group.user_set.all()],
                                                  category_choices = [(category.name, category.name) for category in request.user.current_group.category_set.all()],
                                                  data=request.POST)
            if createExpanseForm.is_valid():
                expanse = createExpanseForm.save()
                if expanse is not None:
                    expanse.group = request.user.current_group
                    expanse.save()
                else:
                    messages.success(request, "Error!")
            
    context = {'createExpanseForm':createExpanseForm}
    return render(request, 'app/summary.html', context)

Solution

  • The form field of a ForeignKey is a ModelChoiceField [Django-doc], this neecds the primary key of the item to work with, not the name of a category. You thus try to do too much yourself, and thus invalidated the data of the form field.

    I suggest that you let the form handle it and specify the queryset when necessary:

    class CreateExpanseForm(forms.ModelForm):
        def __init__(self, *args, user=None, **kwargs):
            super().__init__(*args, **kwargs)
            if user is not None:
                self.fields['user'].queryset = user.current_group.user_set.all()
                self.fields[
                    'category'
                ].queryset = user.current_group.category_set.all()
    
        class Meta:
            model = Expanses
            fields = ['date', 'amount', 'category', 'user', 'comment']
            widgets = {
                'date': DateInput(
                    attrs={
                        'placeholder': 'Date',
                        'class': 'form-input',
                        'type': 'date',
                    }
                ),
                'amount': TextInput(
                    attrs={
                        'placeholder': 'Amount',
                        'class': 'form-input',
                        'type': 'text',
                    }
                ),
                'category': Select(
                    attrs={
                        'placeholder': 'Category',
                        'class': 'form-select',
                        'type': 'text',
                    }
                ),
                'user': Select(
                    attrs={
                        'placeholder': 'user',
                        'class': 'form-select',
                        'type': 'text',
                    }
                ),
                'comment': TextInput(
                    attrs={
                        'placeholder': 'comment',
                        'class': 'form-input',
                        'type': 'text',
                    }
                ),
            }

    In the view you can then just pass the logged in user:

    from django.contrib.auth.decorators import login_required
    from django.shortcuts import redirect
    
    
    @login_required
    def summary_view(request):
        form = CreateExpanseForm(user=request.user)
        if request.method == 'POST':
            if 'createExpanse' in request.POST:
                form = CreateExpanseForm(
                    request.POST, request.FILES, user=request.user
                )
                if form.is_valid():
                    form.instance.group = request.user.current_group
                    expanse = createExpanseForm.save()
                    return redirect('name-of-some-view')
    
        context = {'createExpanseForm': form}
        return render(request, 'app/summary.html', context)

    Note: Functions are normally written in snake_case, not PascalCase, therefore it is advisable to rename your function to summary_view, not SummaryView.


    Note: You can limit views to a view to authenticated users with the @login_required decorator [Django-doc].


    Note: In case of a successful POST request, you should make a redirect [Django-doc] to implement the Post/Redirect/Get pattern [wiki]. This avoids that you make the same POST request when the user refreshes the browser.