7

I have a project foreign key in by Phase model. I'm having hard time Create a dependent drop-down list inside my Django admin page.

I want to when user select a project from (project drop-down) phase of that project show in second dop-down

What would be the best way to achieve this?

It would be great if the dropdowns filter items based on the value of its parent.

enter image description here

class Project(models.Model):
    name                    = models.CharFieldmax_length = 100, unique= True)
    short_name              = models.CharField(max_length= 4, unique= True)
    slug                    = models.SlugField(max_length= 100, allow_unicode=True, null=True, editable= False)
    location                = models.OneToOneField(Location, on_delete = models.SET_NULL, null= True, blank= False, verbose_name= 'موقعیت')
    start_date              = models.DateField(default= timezone.now, null= True, blank= True)      
    end_date                = models.DateField(default= timezone.now, null= True, blank= True)
    duration                = models.IntegerField(default= 0, editable= False)

class Phase(models.Model):
    title                = models.CharField(max_length= 20)

class ProjectPhase(models.Model):
    project                 = models.ForeignKey(Project, on_delete= models.CASCADE, related_name= 'phase')
    phase                   = models.ForeignKey(Phase, on_delete=models.CASCADE, related_name= 'project')
    start_date              = models.DateField(default= timezone.now)      
    end_date                = models.DateField(default= timezone.now)
    duration                = models.IntegerField(default= 0, editable= True)
BABZI
  • 119
  • 3
  • 9

2 Answers2

5

1. import a js media file in ModelAdmin for Generaldata:

class YourModelAdmin(admin.ModelAdmin):    
    form = YourModelForm
    #list_display = ['your fields',]
    class Media:
        js = ("yourapp/selectajax.js",)

admin.site.register(YourModel, YourModelAdmin)

2. create a new js file which saved yourproject/yourapp/static/yourapp/ directory or another proper directory.

jQuery(function($){
    $(document).ready(function(){
        $("#id_project_select").change(function(){
            // console.log(obj.currentTarget.value);
            $.ajax({
                url:"/get_phases/",
                type:"POST",
                data:{project: $(this).val(),},
                success: function(result) {
                    console.log(result);
                    cols = document.getElementById("id_phase_select");
                    cols.options.length = 0;
                    for(var k in result){
                        cols.options.add(new Option(k, result[k]));
                    }
                },
                error: function(e){
                    console.error(JSON.stringify(e));
                },
            });
        });
    }); 
});

3. create a view to process ajax

@login_required
def get_phases(request):
    project = request.POST.get('project')
    phases = {}
    try:
        if project:
            prophases = Project.objects.get(pk=int(project)).phase
            phases = {pp.phase.title:pp.pk for pp in prophases}
    except:
        pass
    return JsonResponse(data=phases, safe=False)

4. add 'get_phases/ to urlpatterns.

Notice that you should modify some codes as your need.

Blackdoor
  • 922
  • 1
  • 6
  • 12
  • Hi! I just posted a solution with a proposal for improving your code. Would you like to modify you answer adapting my version of the code? If so, let me know and I'll remove the answer. – Pere Picornell Apr 13 '22 at 10:42
0

The answer by Blackdoor is a good approach and it's the one we just implemented, but it has a couple of problems:

  1. It's only executed when you change the main select, and I wanted the dependant select to be filtered also on page load.
  2. Does not keep que selected item in the dependant select.

In his solution, in step 2, replace his code with this one and adapt the names (I'm using service and sub_service instead of project / phase):

jQuery(function($){
    $(document).ready(function(){
        var clone = document.getElementById("id_sub_service").cloneNode(true);
        $("#id_service").change(function(){
            update_sub_services($(this).val(), clone)
        });
        update_sub_services($("#id_service").val(), clone)
    });

    function update_sub_services(service, clone) {
        $.ajax({
            url:"/chained_dropdowns/get_sub_services/",
            type:"GET",
            data:{service: service,},
            success: function(result) {
                var cols = document.getElementById("id_sub_service");
                cols.innerHTML = clone.innerHTML
                Array.from(cols.options).forEach(function(option_element) { 
                    var existing = false;
                    for (var k in result) {
                        if (option_element.value == k) {
                            existing = true
                        }
                    }
                    if (existing == false) {
                        $("#id_sub_service option[value='"+option_element.value+"']").remove();
                    }
                })
            },
            error: function(e){
                console.error(JSON.stringify(e));
            },
        });
    }
});

As you can see, now instead of removing all the items from the dependant select and then refilling it (which leaves you without the selected property and any other custom property), it removes the options that should not be there.

I'm not a JS developer and I don't know jQuery so my modifications are in native JS, please feel free to improve it :)

Pere Picornell
  • 874
  • 1
  • 8
  • 15