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.
Checklist.is_yes
field 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
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')
]