I'm trying to create a Quiz in Django. There are multiple questions and QuestionForm. In a view, I get set of Questions and create a list of QuestionForms for each object. Then this list is sent into the template and render text and form for each question separately.
This way is very uncomfortable and I think that there must be a more simple and straightforward way to do that.
The main flow:
When user takes a test, the Sitting object is created. This object holds information about user, quiz and preloaded questions. When user answeres the quiz, there is created SittingQuestion objects which holds information about question and user answer.
As you can see, I've added a parameter name = question-id for each form and the result of each form is answer-id. I have to parse id's and create object's using this id's.
I would appreciate any help to avoid parsing especially.
I'm attaching a QuestionForm and a view:
QuestionForm
class QuestionForm(forms.Form):
def __init__(self, question, *args, **kwargs):
super(QuestionForm, self).__init__(*args, **kwargs)
choice_list = [(x.pk, x.text) for x in question.get_answers_list()]
self.fields["question-{}".format(question.id)] = forms.ChoiceField(choices=choice_list,
widget=forms.RadioSelect)
View
def take_quiz(request, id):
if request.method == 'GET':
sitting = models.Sitting.objects.create(quiz=quiz, user=request.user)
sitting.load_questions()
formset = []
for q in sitting.sitting_questions.all():
formset.append((q.question, forms.QuestionForm(q.question)))
return render(request, 'quiz/quiz.html', context={'formset': formset})
quiz = get_object_or_404(models.LanguageQuiz, pk=id)
sitting = get_object_or_404(Sitting,user=request.user,quiz=quiz)
if request.method == 'POST':
for question in request.POST:
question_id = question.split('-')[1]
question_object = get_object_or_404(Question,id=question_id)
answer_id = request.POST[question_id][0]
answer_object = get_object_or_404(Answer,id=answer_id)
SittingQuestion.objects.create(sitting=sitting,question=question_object,answer=answer_object)
MODELS
class LanguageQuiz(models.Model):
name = models.CharField(max_length=40)
language = models.OneToOneField(sfl_models.Language)
max_questions = models.IntegerField()
time_to_repeat_in_days = models.PositiveIntegerField(default=0)
def __str__(self):
return '{} test'.format(self.name)
def __unicode__(self):
return self.__str__()
class Question(models.Model):
language_quiz = models.ForeignKey(LanguageQuiz,related_name='questions')
text = models.TextField()
def get_answers_list(self):
return self.answers.all()
class Answer(models.Model):
question = models.ForeignKey(Question,related_name='answers',on_delete=models.CASCADE)
text = models.TextField()
correct = models.BooleanField()
class Sitting(models.Model):
user = models.ForeignKey(sfl_models.User, related_name='test_sitting')
quiz = models.ForeignKey(LanguageQuiz)
date_opened = models.DateTimeField(auto_now_add=True)
date_closed = models.DateTimeField(null=True)
closed = models.BooleanField(default=0)
class Meta:
unique_together = ('user','quiz')
def __str__(self):
return 'Sitting - user: {}; quiz: {}'.format(self.user, self.quiz)
def load_questions(self):
questions = random.sample(self.quiz.questions.all(),min(self.quiz.max_questions,len(self.quiz.questions.all())))
for question in questions:
SittingQuestion.objects.create(question=question,sitting=self)
class SittingQuestion(models.Model):
sitting = models.ForeignKey(Sitting, related_name='sitting_questions', on_delete=models.CASCADE)
question = models.ForeignKey(Question, related_name='sitting_questions')
answer = models.ForeignKey(Answer,null=True, blank=True)
Here is one possible improvement to the design:
Instead of QuestionForm, make a QuizForm. Pass sitting.sitting_questions.all() to your form, and make each one its own ChoiceField. This will make it much easier to process your form. Once you initialize whatever variables you need, handling in the view is usually as simple as this:
if request.method == 'POST':
form = QuizForm(request.POST)
if form.is_valid():
# whatever you'd like to do
else: # GET
form = QuizForm(list_of_questions)
There is no need to parse to get the question id, you can just call question.id or question.pk.
some elaboration:
class QuizForm(forms.Form):
def __init__(self, questions, *args, **kwargs):
super(QuizForm, self).__init__(*args, **kwargs)
for question in questions:
choice_list = [("QUESTION TEXT", question.text)]
choice_list.append([(x.pk, x.text) for x in question.get_answers_list()])
self.fields["question-{}".format(question.id) = forms.ChoiceField(
choices=choice_list,
widget=forms.RadioSelect
)
Update: How to add Question text before options.
If your QuizForm has fields which are all questions, then when you iterate over your field you will get these question fields: {% for question in form %}. Since question is a field and not the actual Question object, I admit you can't simply access question.text in the loop. However, you can decide to add question.text to the choices field (a little hack-ish, but functional), I've included this possibility above. Then try something like this:
{% for question in form %}
{% for id, text in question.field.choices %}
{% if id == 'QUESTION TEXT' %}
{{ text }}
{% else %}
<!-- render however you like -->
{% endif %}
{% endfor %}
{% endfor %}
For rendering buttons: https://docs.djangoproject.com/es/1.9/ref/forms/widgets/#radioselect
I think there are a lot of similar questions on SO already for how to render the choices.
Related
I got the images working on my template, but the image being displayed is the current logged in user's image.
How do I filter it so that Profile.objects.get(user=[the owner of the post's user])?
class PostListView(ListView):
model = Post
def get_context_data(self, **kwargs):
context = super(PostListView, self).get_context_data(**kwargs)
context['user_post'] = Post.objects.filter(user_id=self.request.user.id)
# context['user_profile'] = Profile.objects.get(user=self.request.user.id)
context['user_profile'] = Profile.objects.get(user=1)
return context
The error code says: 'PostListView' object has no attribute 'user'. I don't understand this error code because from my understand PostListView's model is set to Post which has the following models:
class Post(models.Model):
user = models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
title = models.CharField(max_length=200)
text = models.TextField()
...
So in other words every post made to the blog has a user.
def get_context_data(self, **kwargs):
context = super(PostListView, self).get_context_data(**kwargs)
context['user_post'] = Post.objects.filter(user_id=self.request.user.id)
# context['user_profile'] = Profile.objects.get(user=self.request.user.id)
context['user_profile'] = Profile.objects.get(user=1)
return context
What I believe self is doing here is represents the current object in PostListView right?
Can you please ELI5? I am having some trouble understanding the documentation since the language used in there is difficult for me.
Try something like this,
class PostListView(ListView):
template_name = 'add_template_name.html'
model = Post
context_object_name = 'all_posts'
def get_queryset(self):
return Post.objects.all()
Now in your template add this,
{% for post in all_posts %}
<img src="{{ post.user.profile_picture }}" alt="image">
<p> {{ post.title }} </p>
{% endfor %}
Whenever you want to use foreign key element, in this case user, you can choose post.user. Assuming your user model has profile_picture and username, you can display them as post.user.profile_picture and post.user.username respectively. For more info, you can go through Django docs.
I'm following a tutorial from to build a simple blog app with Django.
I have noticed that in the new_entry() view, we need to pass topic_id in agrs when using the reverse function:
def new_entry(request, topic_id):
"""Add a new entry for a particular topic"""
topic = Topic.objects.get(id=topic_id)
if request.method != 'POST':
#No data submitted, create a blank form
form = EntryForm()
else:
#POST data submitted; process data
form = EntryForm(data=request.POST)
if form.is_valid():
new_entry = form.save(commit=False)
new_entry.topic = topic
new_entry.save()
return HttpResponseRedirect(reverse('learning_logs:topic', args=[topic_id]))
context = {'topic': topic, 'form': form}
return render(request, 'learning_logs/new_entry.html', context)
However, when creating the edit_entry() view (that allows users to edit existing entries), we need to pass topic.id
def edit_entry(request, entry_id):
"""Edit an existing entry"""
entry = Entry.objects.get(id=entry_id)
topic = entry.topic
if request.method != 'POST':
#Initial request, render the form with current entry
form = EntryForm(instance=entry)
else:
#Submit changes, process data
form = EntryForm(instance=entry, data=request.POST)
if form.is_valid():
form.save()
return HttpResponseRedirect(reverse('learning_logs:topic', args=[topic.id]))
context = {'topic':topic, 'entry':entry, 'form':form}
return render(request,'learning_logs/edit_entry.html', context)
Initially I thought this was a mistake so I used args=[topic_id] in both reverse functions and it worked fine
Later, I decided I wanted to add a title to each entry so I made some minor changes to models.py, migrated those changes to the database and then changed the templates to include {{entry.title}} in them.
Basically, all I did was add this code to models.py
title = models.CharField(max_length=200, default='Add a title')
models.py:
class Topic(models.Model):
"""A topic the user is learning about"""
text = models.CharField(max_length = 200)
date_added = models.DateTimeField(auto_now_add = True)
def __str__(self):
"""Return a string representation of the model"""
return self.text
class Entry(models.Model):
"""A blog post about a particular topic"""
topic = models.ForeignKey(Topic)
title = models.CharField(max_length=200, default='Add a title')
text = models.TextField()
date_added = models.DateTimeField(auto_now_add = True)
class Meta:
verbose_name_plural = 'entries'
def __str__(self):
"""Return a string representation of the model"""
char_numb = len(self.text)
if char_numb > 50:
return self.text[:50] + "..."
else:
return self.text
forms.py:
from django import forms
from .models import Topic, Entry
class TopicForm(forms.ModelForm):
class Meta:
model = Topic
fields = ['text']
labels = {'text': ''}
class EntryForm(forms.ModelForm):
class Meta:
model = Entry
fields = ['text','title']
labels = {'text':'', 'title': ''}
widgets = {'text': forms.Textarea(attrs={'cols': 80})}
After adding these changes, I got the following error when I tried to edit an entry's default title:
NameError at /edit_entry/4/
global name 'topic_id' is not defined
I changed args=[topic_id] to args=[topic.id] in the views.py file edit_entry() view and now it works fine, any idea why this is the case? What difference is there between topic_id and topic.id in this context?
This is the edit_entry.html template in case it makes any difference:
{% extends "learning_logs/base.html" %}
{% block content %}
<h1>{{topic}}
</h1>
<p>Edit your entry</p>
<form action = "{% url 'learning_logs:edit_entry' entry.id %}" method
= 'post'>
{% csrf_token %}
{{ form.as_p }}
<button name = "submit">save changes</button>
</form>
{% endblock content %}
Thanks in advance for any advice
In your first view, you have topic_id from the url and you fetch topic from the database on the first line, so you can use either topic_id or topic in the view.
def new_entry(request, topic_id):
"""Add a new entry for a particular topic"""
topic = Topic.objects.get(id=topic_id)
In the template context for they view, you set topic but not topic_id. Therefore you can only use topic.id in the template.
context = {'topic': topic, 'form': form}
In your second view, you get entry_id from the url and get topic via the entry. You don’t set topic_id anywhere so you must use topic.
def edit_entry(request, entry_id):
"""Edit an existing entry"""
entry = Entry.objects.get(id=entry_id)
topic = entry.topic
Forgive the logic of the table structure below example. It only meant as a simple example of the situation that I am.
So the situation is that I want to make an employee form page, where the department and line manager might or might not exist already. So I replace the drop-down box with the form field for the foreign key, so they can be created if need all in one step for the user. However, with this kind of dependency, I am not doing the right thing in view to make it work.
If you need more detail please do ask.
If you can make the Title more precise please do.
Thank you for your time.
Model
class Employee(models.Model):
name = models.CharField()
age = models.CharField()
line_manager = models.ForeignKey(LineManager)
department = models.ForeignKey(Department)
class LineManager(models.Model):
manager_name = models.CharField()
department = models.ForeignKey(Department)
class Department(models.Model):
department_name = models.CharField()
Form
class EmployeeForm(ModelForm):
class Meta:
model = Employee
fields = ['name',
'age'
#'line_manager' Not needed
#'department' Not needed]
exclude = ('line_manager', 'department')
class LineManagerForm(ModelForm):
class Meta:
model = LineManager
fields = ['manager_name']
exclude = ('department')
# There is a basic form for Department, as well.
View
class AddEmployeeView(View):
forms = {'department': DepartmentForm(self.request.POST or None),
'line_manager': LineManagerForm(self.request.POST or None),
'employee': EmployeeForm(self.request.POST or None)]}
def get(self, request, *args, **kwargs):
form_list = [form for _,form in forms]
return render (request, 'app/temp.html', {'forms': form_list}
def post(self, request, *args, **kwargs):
if all([form.is_valid() for _,form in forms]):
department_data = forms['department'].cleaned_data
department_obj, _ = Department.objects.get_or_create(department_data)
line_manager_instance = forms['line_manager'].instance
line_manager_instance.department = department_obj
line_manager_data = forms['line_manager'].cleaned_data
line_manager_obj, _ = LineManager.objects.get_or_create(line_manager_data)
employee_instance = forms['employee'].save(commit=False)
employee_instance.department = department_obj
employee_instance.line_manager = line_manager_obj
employee_instance.save()
html
<form method="POST">
{% csrf_token %}
{% form in forms %}
{{ form.as_p }}
{% endform %}
<input type='submit'/>
</form>
Currently I have the following structure.
I have Users, which can be Teachers or Students. Students can leave reviews for teachers. I've set up a detailed view for teachers and added a 'reviews' attribute to get_context_data to loop through reviews to display them.
Aim: Each user who is a student can submit a review of a teacher. I want to display a form at the bottom of my detailed view. If user already had a review, then we call update. If user doesn't have a review, we create it.
Models.py
class Teacher(models.Model):
user = models.OneToOneField(User, on_delete=models.PROTECT, related_name='Teacher')
availability = models.BooleanField(default=False)
def __str__(self):
return self.user.username
class Student(models.Model):
user = models.OneToOneField(User, on_delete=models.PROTECT, related_name='Student')
teacher = models.ForeignKey(Teacher)
reviewed = models.BooleanField(default=False)
def __str__(self):
return self.user.username
class Review(models.Model):
teacher = models.ForeignKey(Teacher)
student = models.OneToOneField(Student, on_delete=models.PROTECT, related_name='Student')
star = models.IntegerField(default=5)
body = models.TextField()
Views.py
class ReviewSubmitForm(forms.Form):
star = forms.IntegerField()
body = forms.CharField()
class TeacherView(generic.DetailView):
model = Teacher
def get_context_data(self, **kwargs):
# Call the base implementation first to get a context
context = super(TeacherView, self).get_context_data(**kwargs)
# Add extra context from another model
context['reviews'] = Review.objects.filter(teacher_id=self.kwargs['pk'])
context['rating'] = Review.objects.filter(teacher_id=self.kwargs['pk']).aggregate(Avg('star'))
context['form'] = ReviewSubmitForm()
return context
class ReviewSubmit(SingleObjectMixin, FormView):
template_name = 'users/teacher_detail.html'
form_class = ReviewSubmitForm
model = Review
def post(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return HttpResponseForbidden()
self.object = self.get_object()
return super(ReviewSubmit, self).post(request, *args, **kwargs)
def get_success_url(self):
return reverse('users:detail', kwargs={'pk': self.object.pk})
I'm now able to see the form and use it in my template.
My teacher view is located at /users/<pk>
The logic that I'd like is: look for , check current student id.
If there is an entry with pk=pk and student_id = student_id then load update form. Otherwise if it's a student load create form.
This example is somewhat similar, but not quite the same.
Edit: form_template.html
{% for field in form %}
<div class="form-group">
<span color="red">{{ field.errors }}</span>
<label>
{{ field.label_tag }}
</label>
<div>{{ field }}</div>
</div>
{% endfor %}
I have a form in my website, which is the same for three tables (Homework, Class, Random)
So basically I want to make a ChoiceField on the top of the form, to let user choose where to upload file.
I was thinking, because these tables have common abstract class, may be I can choose it from there somehow. But can not figure out how.
Or may be there is much better solution for this.
just in case this is my code:
#models.py
class FileDescription(models.Model):
class Meta:
abstract = True;
ordering = ['file_creation_time']
subject = models.ForeignKey('Subjects', null=True, blank=True, primary_key=True)
subject_name = models.CharField(max_length=100)
file_uploaded_by = models.CharField(max_length=100)
file_name = models.CharField(max_length=100)
file_description = models.TextField()
file_creation_time = models.DateTimeField(auto_now_add=True)
def __unicode__(self):
return u'%s' % (self.file_name)
#template
<ul id="tabs">
<li>Homework</li>
<li>Class Papers</li>
<li>Random Papers</li>
</ul>
<div id="homework" class="tab-section">
<h2>Homework</h2>
<p>This section contains Homework</p>
{% if homework_files %}
<ul>
{% for file in homework_files %}
<li>{{ file.file_name }}
{% endfor %}
</ul>
{% endif %}
</div>
#forms.py
class Homework_Content_Form(forms.ModelForm):
class Meta:
model=Homework_Content
exclude=('subject',
'subject_name',
'file_creation_time',
'file_uploaded_by',
)
Method 1: Separate Forms
class GenericContentForm(forms.ModelForm):
class Meta:
exclude = ('subject', 'subject_name', 'file_creation_time', 'file_uploaded_by')
TYPE_CHOICES = (
('homework', 'Homework'),
('class', 'Class Paper'),
('random', 'Random Paper'),
)
type = forms.ChoiceField(choices=TYPE_CHOICES)
class HomeworkForm(GenericContentForm):
class Meta(GenericContentForm.Meta):
model = Homework
class ClassPaperForm(GenericContentForm):
class Meta(GenericContentForm.Meta):
model = ClassPaper
class RandomPaperForm(GenericContentForm):
class Meta(GenericContentForm.Meta):
model = RandomPaper
Then in your view you just pick one to start with, and when you have the POST data, you can instantiate a different one instead:
def my_view(request):
if request.method == 'POST':
type = request.POST.get('type')
if type == 'homework':
form = HomeworkForm(request.POST)
elif type == 'class':
form = ClassPaperForm(request.POST)
elif type == 'random':
form = RandomPaperForm(request.POST)
else:
form = HomeworkForm()
...
Method 2: Use Proxy Models
Since these three models all share the same data, having three separate tables is redundant. Instead of FileDescription being abstract, make it just a normal standard model, and add a field to it for type, with choices of "Homework", "Class Paper" and "Random Paper". Then create proxy models for each:
class HomeworkManager(models.Manager):
def get_query_set(self, *args, **kwargs):
qs = super(HomeworkManager, self).get_query_set(*args, **kwargs)
return qs.filter(type='homework')
class Homework(FileDescription):
class Meta:
proxy = True
objects = HomeworkManager()
def save(self, *args, **kwargs):
self.type = 'homework'
super(Homework, self).save(*args, **kwargs)
Then, you just need one form for FileDescription and when the user's choice for the "type" will be saved. You can then access anything set as type "homework" with the standard Homework.objects.all().