Dremet
Dremet

Reputation: 1038

How to replace Choices field in Django ModelForm with TextInput

I am working with Python 3.7.3 and Django 2.0.13.

Basically I want to show a form on my website, on which a user (=participant in model definition below) can be entered. The Django ModelForm makes this to a Choice Field automatically and shows a Dropdown with all users. I don't want to show a list of all users in the dropdown menu and want a TextInput field instead.

Code:

First, relevant part from models.py:

class Invite(models.Model):
    game        = models.ForeignKey(Game, on_delete=models.CASCADE)

    host        = models.ForeignKey(User, on_delete=models.CASCADE, related_name = "invites_as_host")
    participant = models.ForeignKey(User, on_delete=models.CASCADE, related_name = "invites_as_participant")

    accepted = models.BooleanField(blank = True, default = False)
    declined = models.BooleanField(blank = True, default = False)

    date_created = models.DateTimeField(auto_now_add=True)
    date_edited  = models.DateTimeField(auto_now=True)

    class Meta:
        unique_together = ["game", "host", "participant"]

forms.py:

class GameInviteNewForm(forms.ModelForm):
    class Meta:
        model = Invite
        fields = ["participant"]

What I tried is to overwrite the participant input field like this:

class GameInviteNewForm(forms.ModelForm):

    participant = forms.CharField(
                label=_("User to invite"), 
                max_length=100,
                widget = forms.TextInput
            )

    class Meta:
        model = Invite
        fields = ["participant"]

views.py (if relevant; I do not think it even gets to "form_valid", does it?)

class GameInviteNewView(LoginRequiredMixin, UserIsLeaderMixin, FormView):
    form_class = GameInviteNewForm 

    template_name = "app/game/new_invite.html"

    pk_url_kwarg  = "game_id"


    def get_success_url(self):
        return reverse_lazy("app:game_invite", kwargs={
            "game_id": self.kwargs['game_id']
        })


    def form_valid(self, form):
        participant = form.save(commit=False)

        participant = User.objects.get(username=participant.name)
        host = User.objects.get(username=self.request.user.username)
        game = Game.objects.get(id=self.kwargs['game_id'])

        invite.participant_id = participant.id
        invite.host_id = host.id
        invite.game_id = game.id

        invite.save()

        return redirect(self.get_success_url())

This does indeed show an TextInput Field on the website, but if I enter a username ("test") I get an error:

Internal Server Error: /app/game/invite/7
Traceback (most recent call last):
  File "/home/dremet/anaconda3/envs/django/lib/python3.7/site-packages/django/core/handlers/exception.py", line 35, in inner
    response = get_response(request)
  File "/home/dremet/anaconda3/envs/django/lib/python3.7/site-packages/django/core/handlers/base.py", line 128, in _get_response
    response = self.process_exception_by_middleware(e, request)
  File "/home/dremet/anaconda3/envs/django/lib/python3.7/site-packages/django/core/handlers/base.py", line 126, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/home/dremet/anaconda3/envs/django/lib/python3.7/contextlib.py", line 74, in inner
    return func(*args, **kwds)
  File "/home/dremet/anaconda3/envs/django/lib/python3.7/site-packages/django/views/generic/base.py", line 69, in view
    return self.dispatch(request, *args, **kwargs)
  File "/home/dremet/anaconda3/envs/django/lib/python3.7/site-packages/django/contrib/auth/mixins.py", line 52, in dispatch
    return super().dispatch(request, *args, **kwargs)
  File "/home/dremet/anaconda3/envs/django/lib/python3.7/site-packages/django/contrib/auth/mixins.py", line 109, in dispatch
    return super().dispatch(request, *args, **kwargs)
  File "/home/dremet/anaconda3/envs/django/lib/python3.7/site-packages/django/views/generic/base.py", line 89, in dispatch
    return handler(request, *args, **kwargs)
  File "/home/dremet/anaconda3/envs/django/lib/python3.7/site-packages/django/views/generic/edit.py", line 141, in post
    if form.is_valid():
  File "/home/dremet/anaconda3/envs/django/lib/python3.7/site-packages/django/forms/forms.py", line 179, in is_valid
    return self.is_bound and not self.errors
  File "/home/dremet/anaconda3/envs/django/lib/python3.7/site-packages/django/forms/forms.py", line 174, in errors
    self.full_clean()
  File "/home/dremet/anaconda3/envs/django/lib/python3.7/site-packages/django/forms/forms.py", line 378, in full_clean
    self._post_clean()
  File "/home/dremet/anaconda3/envs/django/lib/python3.7/site-packages/django/forms/models.py", line 396, in _post_clean
    self.instance = construct_instance(self, self.instance, opts.fields, opts.exclude)
  File "/home/dremet/anaconda3/envs/django/lib/python3.7/site-packages/django/forms/models.py", line 60, in construct_instance
    f.save_form_data(instance, cleaned_data[f.name])
  File "/home/dremet/anaconda3/envs/django/lib/python3.7/site-packages/django/db/models/fields/__init__.py", line 838, in save_form_data
    setattr(instance, self.name, data)
  File "/home/dremet/anaconda3/envs/django/lib/python3.7/site-packages/django/db/models/fields/related_descriptors.py", line 197, in __set__
    self.field.remote_field.model._meta.object_name,
ValueError: Cannot assign "'test'": "Invite.participant" must be a "User" instance.

The dropdown had the user id's in the "value" attribute of each option. Now, a string is entered. So I am not surprised that it does not work, but I am surprised that the error message says that it must be a "User" (not a user id). I tried to overwrite the "clean()" method and to use a regular form, but both without success. How should this be handled properly?


Solution:

As pointed out in the answers, I did need a "clean_participant" method, but I wrapped a try-except structure around it (and also I sticked with the forms.py change overwriting the participant field):

    def clean_participant(self):

        participant_string = self.cleaned_data['participant']

        try:
            participant = User.objects.get(username=participant_string)
        except User.DoesNotExist:
            raise forms.ValidationError("User does not exist.")

        return participant

Upvotes: 0

Views: 1315

Answers (2)

Ben Boyer
Ben Boyer

Reputation: 1234

You can add a function clean_participant that will get this input and then query the database to find the associated user. It it works then you can return the participant instance. Otherwise you need to return an error 'this participant doesn't exist'

Here is an exemple:

def clean_participant(self):
    participant = self.cleaned_data.get('participant')
    q = User.objects.get(username= participant)
    if q:
        return q
    raise forms.ValidationError("this participant doesn't exist")

But you need to use the default modelchoicefield and change the widget of that field to be a textinput.

Upvotes: 1

Nafees Anwar
Nafees Anwar

Reputation: 6598

You can override clean method for participant field and return a user instance from it. Model form will automatically do that if you are using a choice field because form will have id to find instance. Because you are overriding field you have to somehow find related instance yourself in cleaning process. You can do it by defining clean method for participant field like this.

class GameInviteNewForm(forms.ModelForm):
    class Meta:
        model = Invite
        fields = ["participant"]

    def clean_participant(self):
        # you have to return a user instance from here some how use filtering logic you want
        participant = self.cleaned_data['participant']
        # just an example handle exceptions and other stuff
        return User.objects.get(username=participant)

Upvotes: 1

Related Questions