'django 3.2 how to loop a field with foreign-keys
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 DeclarationForm(forms.ModelForm):
class Meta:
model = Declaration
fields = '__all__'
class ChecklistForm(forms.ModelForm):
is_yes = forms.BooleanField(required=False)
class Meta:
model = Checklist
fields = ['question', 'is_yes']
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.
*Additional question this is my views.
def submit(request):
if request.method == 'POST':
checklist = ChecklistForm(request.POST)
declaration = DeclarationForm(request.POST)
if declaration.is_valid() and checklist.is_valid():
user = request.user
declaration = Declaration.objects.create(user=user)
declaration.save()
checklist.save(commit=False)
checklist.declaration = declaration
checklist.save()
return redirect('student-dashboard')
do i need to loop to check every checklist to work? it only saves declaration object but no checklists objects and shows no error.
Updated***
Following the approach of TaipanRex it works but my problem is it looks like this enter image description here
Is there a way to instead of show dropdown menu of the question just the quest text instead?
Solution 1:[1]
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)
UPDATED: Your view for saving the forms is not correct, should be something like this:
if request.method == 'POST':
formset = inlineformset_factory(Declaration, Checklist,
form=ChecklistForm, fk_name="declaration")
declaration_form = DeclarationForm(request.POST)
if declaration_form.is_valid():
declaration_form.user = request.user
declaration = declaration_form.save(commit=False)
checklist = formset(request.POST, prefix='question_fs', instance=declaration)
if checklist.is_valid():
with transaction.atomic():
declaration.save()
checklist.save()
Solution 2:[2]
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.
Checklist.is_yesfield is useless since data presence in the checklist table attest that a user checked a question.- If you don't have any other field in
Declarationclass 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 class="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 class="btn btn-danger2" 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')
]
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | |
| Solution 2 | openHBP |
