Home > other >  django 3.2 how to loop a field with foreign-keys
django 3.2 how to loop a field with foreign-keys

Time:03-10

I'd like to loop a field based from the Question Model. Here are my models.## Heading ##

models.py

from django.db import models
from django.db.models.deletion import CASCADE
from django.conf import settings
class Question(models.Model):
    id = models.BigAutoField(primary_key=True)
    title = models.CharField(max_length=50, unique=True)
    question = models.CharField(max_length=255, unique=True)
class Declaration(models.Model):
    id = models.BigAutoField(primary_key=True)
    user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='declarations', on_delete=models.CASCADE)
class Checklist(models.Model):
    id = models.BigAutoField(primary_key=True)
    declaration = models.ForeignKey(Declaration, related_name='checklist_declaration', on_delete=models.CASCADE)
    question = models.ForeignKey(Question, related_name='checklist_question', on_delete=models.CASCADE, limit_choices_to={'is_active': 'True'})
    is_yes = models.BooleanField()

and my forms.py

from django.utils.safestring import mark_safe
from django import forms
from declaration.models import Checklist, Question

class HorizontalRadionRenderer(forms.RadioSelect):
    def render(self):
        return mark_safe(u'\n'.join([u'%s\n' % w for w in self]))

class ChecklistForm(forms.ModelForm):
    is_yes = forms.TypedChoiceField(
        coerce=lambda x: x == True,
        choices=((True, 'Yes'), (False, 'No')),
        widget=forms.RadioSelect()
    )

    class Meta:
        model = Checklist
        fields = ['is_yes']

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        questions = Question.objects.filter(group='symptom')
        for q in questions:
            self.fields['question_id'] = q.id

and here is my expected output enter image description here

I dunno how to do it in the forms.py since django's only way to validate fields is through forms.

CodePudding user response:

You are going to need two Forms, a DeclarationForm and a ChecklistForm (though I would probably rename your Checklist model to DeclarationQuestionAnswer). Remove the __init__ code from your ChecklistForm and add all the fields to fields.

You are going to use formsets to list all the questions in your form. You will have to check out the docs on how these work, but to set the formset up you will do this:

questions = []
for q in Question.objects.all():
    questions.append({'question': str(q.id), 'is_yes': None})
formset = inlineformset_factory(Declaration, Checklist,
    form=ChecklistForm, fk_name="declaration", extra=len(questions),
                can_delete=False)
question_formset = formset(prefix='questions_fs', initial=questions)

CodePudding user response:

Hi tenzai and welcome to the great stackoverflow community!

1. django model.ManyToManyField

One way to deal with such many to many architecture (Checklist) is the use django admin widget with the models.ManyToManyField declaration. Widget in forms.py from django.contrib.admin.widgets import FilteredSelectMultiple. But for that, you'll have to simplify your model.

  1. Checklist.is_yes field is useless since data presence in the checklist table attest that a user checked a question.
  2. If you don't have any other field in Declaration class it is also useless.

From there, the only thing you need is the declaration of a many2many relation to Question class from User class. But in that case, you'll have to use a custom user model or a one to one relation to Auth.models.User class of django. Cfr. How to Extend Django User Model

# Add this question field to the custom User class
question = models.ManyToManyField(Question, verbose_name="Question")

The form become

from django.contrib.auth import get_user_model
from django.contrib.admin.widgets import FilteredSelectMultiple

class ChecklistForm(forms.ModelForm):

    class Meta:
        model = get_user_model()
        widgets = {"question" : 
             FilteredSelectMultiple(Question._meta.verbose_name_plural, False)
        }

But this will result in a simple drag and drop of your questions from left to right.

2. Left join raw SQL

One other way to do it would be to build a function view with a left join raw SQL. Keep your model as it is

2.1. models.py

Remarks from point 1. remains and therefore Checklist class become

from django.contrib.auth import get_user_model

my_user_model = get_user_model()

class Checklist(models.Model):
    id = models.BigAutoField(primary_key=True)
    user = models.ForeignKey(my_user_model, related_name='checklist_declaration', on_delete=models.CASCADE)
    question = models.ForeignKey(Question, related_name='checklist_question', on_delete=models.CASCADE)

You must go for this architecture if you are planing to add specific fields into the Checklist (Question & User combinations)

2.2. views.py

Note for Checklist.is_deletable method called into the view:

If your selected fields of questions are referenced somewhere in your DB you need to ensure that the unselection of the choice will not delete all related field of the specified user quesion in your DB. That's where the is_deletable method intervenes.

import logging

from django.shortcuts import render, redirect
from django.utils.translation import gettext_lazy as _
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from .models import Checklist, Question

LOG = logging.getLogger(__name__)

@login_required
def my_question(request):
    """
    Function view based on raw select in order to manage outer join
    and display
    """
    myuser = request.user
    qchecks = Checklist.objects.raw('''
    select qq.id, qq.title, aa.qid from question_question as qq
    left join (select question_id as qid
                 from question_checklist
                where user_id = {0}) aa
    on aa.qid = qq.id
    order by qq.title ASC
    '''.format(myuser.id))

    if request.method == 'POST':
        # selected elements provided by the form
        selected = request.POST.getlist("qcheck")
        for ticked in selected:
            ticked_inst = Question.objects.get(id=ticked)
            Checklist.objects.for_user(myuser).get_or_create(profile=myuser,
                                                             question=ticked_inst)

        # Get unselected elements and remove them
        unselected_inst = Question.objects.exclude(id__in=selected)
        for unticked in unselected_inst:
            try:
                obj = Checklist.objects.filter(user=myuser).get(question=unticked)
                # check related before delete
                related_list = Checklist.is_deletable(obj)
                if related_list:
                    messages.error(request,
                        _(f"Checklist delete {obj}. Please first delete related records.")
                    )
                    for related in related_list:
                        related_model = related.model._meta.verbose_name
                        related_values = [i.__str__() for i in related.all()]
                        msg = f"{related_model}: {related_values}"
                        messages.warning(request, msg)
                    return redirect('my_question')
                else:
                    obj.delete()
            except Checklist.DoesNotExist:
                pass
        messages.success(request, _('Changes saved'),
                                fail_silently=True)
        
        return redirect('my_question')

    context = {'user': request.user,
               'qchecks': qchecks,
               'myuser': myuser,
               'upd_allowed': upd_allowed}

    return render(request, 'select_question_form.html', context)

2.3. html template (select_question_form.html)

{% extends 'base.html' %}
{% load i18n %}

{% block content %}

<h2>Do you have</h2>

{% if qchecks %}
    <table >
        <form action="" method="post" enctype="multipart/form-data">
        {% csrf_token %}
        {% for qcheck in qchecks %}
            <tr>
                <td>{{ qcheck.title }}</td>
                <td>
                    <input type="checkbox" name="qcheck" value="{{ qcheck.id }}" {% if qcheck.id == qcheck.qid %} checked {% endif %}
                    {% if not upd_allowed%} disabled{% endif %}/>
                </td>
            </tr>
        {% endfor %}
        {% if upd_allowed %}
        <tr><td colspan="2"><input  type="submit" value="{% trans "Save" %}" /></td></tr>
        {% endif %}
        
        </form>
    </table>
{% else %}
    <p>No question check.</p>
{% endif %}

{% endblock content %}

2.4 urls.py

from django.urls import path
from . import views

urlpatterns = [
    path('questions', views.my_question, name='my_question')
]
  • Related