3

This is my model in an app that describes a graph (a DAG to be precise):

class Node(models.Model):
    name = models.CharField(max_length=120, blank=True, null=True)
    parents = models.ManyToManyField('self', blank=True, symmetrical=False)

    def list_parents(self):
        return self.parents.all()

    def list_children(self):
        return Node.objects.filter(parents=self.id)

    def list_withindirect(self, arg):
        direct = getattr(self, arg)()
        withindirect = set(direct)
        for d in direct:
             withindirect |= set(d.list_withindirect(arg))
        return list(withindirect)

    def list_ancestors(self):
         return self.list_withindirect('list_parents')

    def list_descendants(self):
         return self.list_withindirect('list_children')

    def list_of_allowed_parents(self):
        return list(
            set(Node.objects.all()) - set(self.list_descendants()) - {self}
        )

    def __str__(self):
        return self.name

Each node can have many other nodes as a parent. The essential point here is, that a given node has a specific set of allowed parents. That is what the method list_of_allowed_parents is for. But how can I get a form to show only these nodes in the dropdown?

Currently this is the form:

class NodeForm(forms.ModelForm):
    class Meta:
        model = Node
        fields = ['name', 'parents']

Which I register with the admin:

class NodeAdmin(admin.ModelAdmin):
    list_display = ['id', 'name', 'list_parents', 'list_children']
    form = NodeForm

admin.site.register(Node, NodeAdmin)

I assume I will have to use something like:

parents = forms.ModelMultipleChoiceField(queryset=node.list_of_allowed_parents())

But how would I pass the specific node to the definition in forms.py?

Ideally this constraint would be added in the model. ManyToManyField offers the parameters choices and limit_choices_to, but there seems to be no way to add self.list_of_allowed_parents here.

Watchduck
  • 1,076
  • 1
  • 9
  • 29

3 Answers3

0

You can call the form __init__ method to update parent queryset. And node can be send from view by updating form kwargs or you can use instance if updating the same object.

forms.py

class NodeForm(forms.ModelForm):
    def __init__(self, *args, **kwargs):
        node = kwargs.pop('node', None)
        # node = kwargs.pop('instance', None)

        super(NodeForm, self).__init__(*args, **kwargs) 
        self.fields['parents'].queryset = node.list_of_allowed_parents()

    class Meta: 
        model = Node 
        fields = ['name', 'parents']

You can try this

views.py

If you are using class based view..

 def get_form_kwargs(self): 
    """Returns the keyword arguments for instantiating the form.""" 
    kwargs = super(YourViewName, self).get_form_kwargs() 
    node = get_object_or_404(Node, pk=self.kwargs['node_id'])
    kwargs['node'] = node
    return kwargs

If you are using funnction based view..

node = get_object_or_404(Node, pk=node_id)
form = NodeForm(node=node)
Ashish
  • 354
  • 2
  • 6
  • This is the 'old style'. Now `limit_choices_to` is available :) – dani herrera Jan 22 '17 at 09:17
  • What is the `//` supposed to do? When I leave the second line in `__init__`, I get the error `'list' object has no attribute 'iterator'`, and when I remove it the first error is `'NoneType' object has no attribute 'list_of_allowed_parents'`. – Watchduck Jan 22 '17 at 11:11
  • Actually it was not clear that how you pass the node object in the form.. so it show both way whether from node key or instance key so that you can use according to your need. – Ashish Jan 22 '17 at 14:45
  • I will use any method that works. Currently I have no idea how I am supposed to get a node key or instance to forms.py. – Watchduck Jan 22 '17 at 15:44
  • So can you please show me you view.py where you use that form..so that i can figure out your problem. – Ashish Jan 22 '17 at 16:02
  • Currently I only use the admin. So I add `form = NodeForm` to `NodeAdmin`, which I then register. – Watchduck Jan 22 '17 at 16:20
  • Do you have node object in your view and how did you get that object in your view. – Ashish Jan 22 '17 at 16:31
  • I have a detail view `detail(request, node_id)`, which gets `node_id` passed from the URL. The node object is `node = get_object_or_404(Node, pk=node_id)`. Later I would indeed like to create and pass a form here. But for now I just try to get it to work in the admin. – Watchduck Jan 22 '17 at 16:47
  • With the somewhat foolish modification `self.fields['parents'].queryset = Node.objects.filter(pk__in=[n.id for n in node.list_of_allowed_parents()])` I indeed got the list of allowed parents through. But the current parents are not preselected, just like the current name is not in the name field. That is what I wanted to have in the admin: The allowed parents with the current ones preselected. I am quite sure Django can do this, but it seems to be severely nontrivial. – Watchduck Jan 25 '17 at 02:09
0

You are looking for callable limit_choices_to many2manyField parameter.

Discussed here: https://stackoverflow.com/a/252087/842935

Community
  • 1
  • 1
dani herrera
  • 48,760
  • 8
  • 117
  • 177
0

You can try this

views.py

If you are using class based view..

 def get_form_kwargs(self): 
    """Returns the keyword arguments for instantiating the form.""" 
    kwargs = super(YourViewName, self).get_form_kwargs() 
    node = get_object_or_404(Node, pk=self.kwargs['node_id'])
    kwargs['node'] = node
    return kwargs

If you are using funnction based view..

node = get_object_or_404(Node, pk=node_id)
form = NodeForm(node=node)
Ashish
  • 354
  • 2
  • 6