Search code examples
python-3.xdjangodjango-modelsdjango-templates

How to display multiple self-related foreign key data in Django/HTML?


I am trying to create a django web app for displaying various data (a question and some additional data).My data consists of a question body and industry guidance, which contains several bullet points. Each bullet point may contain additional sub options itself as well, etc.

Example data:

Question body: Was everyone familiar with the location, purpose, testing and oepration of fire doors? Industry Guidance:

  1. Operational readiness
  2. Maintenance and testing
    • Regular inspections
    • Regular maintenance
      • Weekly
      • Monthly
    • ...

Below are my models:

class BaseGuidance(models.model):
    text = models.CharField(max_length=2000)
    parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, related_name="sub_options")

    class Meta:
        abstract = True

    def __str__(self):
        return self.text

class IndustryGuidance(BaseGuidance):
    pass

class Question(models.model):
    body = models.CharField(max_length=400)
    industry_guidance = models.ForeignKey(IndustryGuidance, on_delete=models.CASCADE, null=True, blank=True)

Here is my view, which is rendering the HTML:

def index(request):
    questions = Question.objects.all()
    industry_guidance = IndustryGuidance.objects.prefetch_related(
        'sub_options',
    ).all()
    return render(request, "sire/index.html",{
        "questions": questions,
        "industry_guidance": industry_guidance
    })

Below is my HTML:

 <td scope="col"> 
            <ul class="list-group">
                {% for guidance in industry_guidance %}
                    <li>{{ guidance.text }} </li>
                    <ul>
                        {% for sub_option in guidance.sub_options.all %}
                            <li>{{ sub_option.text }}</li>
                        {% endfor %}
                    </ul>
                {% endfor %}
            </ul>
        </td>

I am really confused on to how exactly I should approach this problem in order to fetch all data and display accordingly.

I tried prefetch_related, but my web app would not load, apparently stuck into some for loop cycle. I am also thinking of rearranging my models but I could not find any guidance or best practices on the matter.

Ultimately, I would like to display each question in a table with its industry guidance related to it a list. Each sub option should be indented into another list. Each bullet of sub option should be in another list, and so on... All data regarding the question should be in the same row.


Solution

  • I'd recommend you using django-mptt when working with trees.Here's an example of usage:

    pip install django-mptt

    add "mptt" to your INSTALLED_APPS list in settings.py

    INSTALLED APPS = [
        ...,
        "mptt",
    ]
    

    Modify the parent field of your BaseGuidance model in models.py:

    
    from mptt.fields import TreeForeignKey
    from mptt.models import MPTTModel
    
    
    class BaseGuidance(MPTTModel):
        text = models.CharField(max_length=2000)
        parent = TreeForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, related_name="sub_options")
    
        class Meta:
            abstract = True
    
        class MPTTMeta:
            order_insertion_by = ['-id']
            parent_attr = 'parent'
    
        def __str__(self):
            return self.text
    

    After you modify the model, migrate the changes:

    python ./manage.py makemigrations
    python ./manage.py migrate
    

    Then modify your views:

    def index(request):
        questions = (
            Question.objects.select_related('industry_guidance')
            .prefetch_related('industry_guidance__sub_options')  # Fetch sub-options for the MPTT structure
        )
    
        context = []
        for question in questions:
            guidance_tree = (
                question.industry_guidance.get_descendants(include_self=True)
                if question.industry_guidance else None
            )
            context.append({
                "question": question,
                "guidance_tree": guidance_tree,
            })
    
        return render(request, "index.html", {"context": context})
    

    And your sire/index.html file:

    {% for item in context %}
        <tr>
            <td>{{ item.question.body }}</td>
            <td>
                <ul>
                    {% if item.guidance_tree %}
                        {% recursetree item.guidance_tree %}
                            <li>{{ node.text }}</li>
                            <ul>
                                {% for child in node.get_children %}
                                    <li>{{ child.text }}</li>
                                {% endfor %}
                            </ul>
                        {% endrecursetree %}
                    {% else %}
                        <li>No guidance available for this question.</li>
                    {% endif %}
                </ul>
            </td>
        </tr>
    {% endfor %}
    

    Hope this works for you!