Django 3.2 как зациклить поле с иностранными ключами
Я хочу зациклить поле, основанное на модели вопроса. Вот мои модели.## Заголовок ##
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()
и мой 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
и вот мой ожидаемый результат введите здесь описание изображения
Я не знаю, как сделать это в forms.py, поскольку единственный способ валидации полей в django - через формы.
Вам понадобятся две формы, DeclarationForm и ChecklistForm (хотя я бы, вероятно, переименовал вашу модель Checklist в DeclarationQuestionAnswer). Удалите код __init__ из вашей формы ChecklistForm и добавьте все поля в fields.
Вы собираетесь использовать formsets для перечисления всех вопросов в вашей форме. Вам придется изучить документацию о том, как это работает, но для установки набора форм вы сделаете следующее:
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)
Привет, Тензай, и добро пожаловать в великое сообщество stackoverflow!
1. django model.ManyToManyField
1. Одним из способов справиться с такой архитектурой "многие ко многим" (Checklist) является использование виджета администратора django с объявлением models.ManyToManyField. Виджет в forms.py from django.contrib.admin.widgets import FilteredSelectMultiple. Но для этого вам придется упростить вашу модель.
Checklist.is_yesполе бесполезно, так как наличие данных в таблице контрольного списка свидетельствует о том, что пользователь отметил вопрос.- Если у вас нет другого поля в классе
Declaration, то оно также бесполезно.
Отсюда единственное, что вам нужно, это объявление отношения many2many к классу Question из класса User. Но в этом случае вам придется использовать custom user model или one to one relation to Auth.models.User class of django. Cfr. Как расширить пользовательскую модель Django
# Add this question field to the custom User class
question = models.ManyToManyField(Question, verbose_name="Question")
Форма стала
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)
}
Но это приведет к простому перетаскиванию ваших вопросов слева направо.
2. Left join raw SQL
2.Другой способ сделать это - построить представление функции с левым соединением необработанного SQL. Сохраните свою модель как есть
2.1. models.py
Заметки из пункта 1. остается и поэтому Checklist класс становится
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)
Вы должны использовать эту архитектуру, если планируете добавить определенные поля в контрольный список (комбинации вопросов и пользователей)
2.2. views.py
Примечание для метода Checklist.is_deletable, вызываемого в представлении:
Если выбранные поля вопросов ссылаются где-то в вашей БД, вам нужно убедиться, что отмена выбора не будет удалить все связанные поля указанного пользовательского вопроса в вашей БД. Для этого необходимо is_deletable вмешивается метод
.
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-шаблон (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')
]