Django: how to filter form field based off foreignkey AND many to many relationship?

441 views Asked by At

Currently, when a user creates a task, they can assign it to all users. I only want them to be able to assign a task based on the members of the project. I feel like the concept I have right now works but I need to replace the ????. Task's assignee has a foreignkey relationship with the user_model. The user_model is also connected with members on a many to many relationship.

projects/models.py

class Project(models.Model):
    name = models.CharField(max_length=200)
    description = models.TextField()
    members = models.ManyToManyField(USER_MODEL, related_name="projects")

tasks/models.py

class Task(models.Model):
    name = models.CharField(max_length=200)
    start_date = models.DateTimeField()
    due_date = models.DateTimeField()
    is_completed = models.BooleanField(default=False)
    project = models.ForeignKey(
        "projects.Project", related_name="tasks", on_delete=models.CASCADE
    )
    assignee = models.ForeignKey(
        USER_MODEL, null=True, related_name="tasks", on_delete=models.SET_NULL
    )

tasks/views.py

class TaskCreateView(LoginRequiredMixin, CreateView):
        model = Task
        template_name = "tasks/create.html"
        # fields = ["name", "start_date", "due_date", "project", "assignee"]
    
        form_class = TaskForm
    
        def get_form_kwargs(self):
            kwargs = super(TaskCreateView, self).get_form_kwargs()
            kwargs["user"] = self.request.user
            kwargs["project_members"] = ??????????
            return kwargs

tasks/forms.py

class TaskForm(ModelForm):
    class Meta:
        model = Task
        fields = ["name", "start_date", "due_date", "project", "assignee"]

    def __init__(self, *args, **kwargs):
        user = kwargs.pop("user")
        project_members = kwargs.pop("project_members")
        super(TaskForm, self).__init__(*args, **kwargs)
        self.fields["project"].queryset = Project.objects.filter(members=user)
        self.fields["assignee"].queryset = Project.objects.filter(
            members=?????????
        )

Update: I followed SamSparx's suggestions and changed the URL paths so now TaskCreateView knows which project id. I updated my tasks/views to the following but I get a TypeError: "super(type, obj): obj must be an instance or subtype of type" and it points to the line: form = super(TaskForm, self).get_form(*args, **kwargs) Maybe it has something to do with having a get_form_kwargs and get_form function? I kept my existing features for the custom form such as when a user creates a task, they can only select projects they are associated with.

Views.py updated class TaskCreateView(LoginRequiredMixin, CreateView): model = Task template_name = "tasks/create.html"

    form_class = TaskForm

    def get_form_kwargs(self):
        kwargs = super(TaskCreateView, self).get_form_kwargs()
        kwargs["user"] = self.request.user
        return kwargs

    def get_form(self, *args, **kwargs):
        form = super(TaskForm, self).get_form(*args, **kwargs)
        form.fields["assignee"].queryset = Project.members.filter(
            project_id=self.kwargs["project_id"]
        )

    def form_valid(self, form):
        form.instance.project_id = Project.self.kwargs["project_id"]
        return super(TaskCreateView, self).form_valid(form)

    def get_success_url(self):
        return reverse_lazy("list_projects")

I have also tried to update the forms.py with the following but get an error that .filter cannot be used on Many to Many relationships.

Updated forms.py

class TaskForm(ModelForm):
    class Meta:
        model = Task
        fields = ["name", "start_date", "due_date", "project", "assignee"]

    def __init__(self, *args, **kwargs):
        user = kwargs.pop("user")
        super(TaskForm, self).__init__(*args, **kwargs)
        self.fields["project"].queryset = Project.objects.filter(members=user)
        self.fields["assignee"].queryset = Project.members.filter(
            project_id=self.kwargs["project_id"]
        )

Another thing I have tried is to go back to my first approach now that I have the url paths: tasks/create/(project_id)

Views.py

class TaskCreateView(LoginRequiredMixin, CreateView):
    model = Task
    template_name = "tasks/create.html"

    form_class = TaskForm

    def get_form_kwargs(self):
        kwargs = super(TaskCreateView, self).get_form_kwargs()
        kwargs["user"] = self.request.user
        kwargs["project_id"] = Project.objects.all()[0].members.name
        # prints to auth.User.none
        return kwargs

I feel like if the kwargs["project_id"] line can be changed to getting list of members of whatever project with the ID in the URL, then this should solve it

Forms.py

class TaskForm(ModelForm):
    class Meta:
        model = Task
        fields = ["name", "start_date", "due_date", "project", "assignee"]

    def __init__(self, *args, **kwargs):
        user = kwargs.pop("user")
        project_id = kwargs.pop("project_id")
        super(TaskForm, self).__init__(*args, **kwargs)
        self.fields["project"].queryset = Project.objects.filter(members=user)
        self.fields["assignee"].queryset = Project.objects.filter(
            members=project_id
        )
1

There are 1 answers

5
SamSparx On

The problem here is that your task doesn't know what members are relevant to include as assignees until you have chosen the project the task belongs to, and both project and assignee are chosen in the same form, so Django doeesn't know who is relevant yet.

The easiest way to handle this is to ensure the call to create a task is associated with the project it is going to be for - eg,

Update your URLs to handle the project ID

Path('create-task/<int:project_id>', TaskCreateView.as_view(), name='create_task')

Update your view

class TaskCreateView(LoginRequiredMixin, CreateView):
    model = Task
    template_name = "tasks/create.html"
    # fields = ["name", "start_date", "due_date", "assignee"]
    #NB: I have remove project from the field list, you may need to do the same in your form as it is handled elsewhere
    form_class = TaskForm

    def get_form(self, *args, **kwargs):
        form = super(TaskCreateView, self).get_form(*args, **kwargs)
        form.fields['assignee'].queryset = Project.members.filter(project_id = self.kwargs['project_id'])

Return form

    def form_valid(self, form):
        form.instance.project_id = project.self.kwargs['project_id']
        return super(TaskCreateView, self).form_valid(form)

Add links

<a href="{% url create_task project_id %}">Create Task for this project</a>

This will create a link on the project details page, or underneath the project in a listview to 'create task for this project', carrying the project informaton for the view via the URL. Otherwise you will have to get into some rather more complex ajax calls that populate the potential assignees list based on the selection within the project dropdown in a dynamic fashion