Django validate answer for specific question - django

Overview:
I want to build a Question-Answer website, where the user has to enter the correct answer for each question. I have made 3 models for this:
class ProblemSet(models.Model):
id = models.IntegerField(primary_key=True)
class Problem(models.Model):
id = models.IntegerField(primary_key=True)
problem_set = models.ForeignKey(ProblemSet, on_delete=models.CASCADE)
question = models.TextField()
solution = models.TextField()
class Solve(models.Model):
username = models.ForeignKey(User, on_delete=models.CASCADE)
problem_set = models.ForeignKey(ProblemSet, on_delete=models.CASCADE)
problem_id = models.ForeignKey(Problem, on_delete= models.CASCADE)
In solve model, if there is any entry that means that particular user has solved that problem_id.
So, I have utilized the generic Form View:
class IndexView(FormView):
form_class = ProblemForm
template_name = 'home/index.html'
success_url = reverse_lazy('index')
def get_context_data(self, **kwargs):
context = super(IndexView, self).get_context_data(**kwargs)
if self.request.user.is_authenticated:
inner_qs = "fetch ids that are solved from Solve model"
problem_obj = Problem.objects\
.exclude(id__in=inner_qs)\
.order_by('id').first()
else:
#do something
context['question'] = problem_obj.question
return context
The problem form is:
from django import forms
class ProblemForm(forms.Form):
solution = forms.CharField(widget=forms.TextInput())
How do I validate that the user is inputting the correct answer? I do get the value of solution field in def form_valid(self, form) function but how should I deal with it? Should i pass question_id in context and query the database in form_valid, or should i pass solution itself to context and access context data in form_valid() method to prevent double query but in this method I am not sure if this is secure as I don't want solution to be passed to client.
Is there any elegant way of doing this?
P.S. After user entered solution is compared to the one in database for that question, I add an entry in the Solve table denoting that this particular user has solved the question id.

The FormView is processing two separate requests: First the GET request when the student fetches the form with a question to answer. Then the POST request when the student submits her answer to the question.
Now HTTP is stateless so somehow you need to keep track of the question that was presented in the first request so you know which question was answered when receiving the POST request.
The easiest way I would say is to actually include the question_id in the form itself, as a hidden input field. There's not real security issue here: The question_id can be manipulated by the student even though it's hidden, but what's the point?
So this is what I would do:
Add problem as a ModelChoiceField with a HiddenInput widget to your ProblemForm.
problem = forms.ModelChoiceField(queryset=Problem.objects.all(), widget=forms.HiddenInput())
Set an initial value for the problem in the get_inital() method of your IndexView:
def get_problem(self): # use also in get_context_data() to add the question
if hasattr(self, 'problem'):
return self.problem
if self.request.user.is_authenticated:
inner_qs = "fetch ids that are solved from Solve model"
self.problem = Problem.objects\
.exclude(id__in=inner_qs)\
.order_by('id').first()
return self.problem
def get_initial(self):
initial = super().get_initial()
initial['problem'] = self.get_problem()}
return initial
When the form gets submitted and is valid, you'll see that form.cleaned_data['problem'] is the submitted problem. So you can use that in the form_valid() method:
def form_valid(self, form):
problem = form.cleaned_data['problem']
# check that it hasn't been solved by the user already
if problem.answer == form.cleaned_data['solution']:
# create solve object for the user
return redirect(...)
The alternative would be to not include it in the form but refetch the problem in form_valid (note that the problem is fetched in the above method as well, when the form maps the submitted problem_id to the actual problem instance to populate its cleaned_data).

Related

Add data to a form in `CreateView`

I have a model which the user submits on a form, and I would like to handle that form with a CreateView. However, there is one field in the model which the user doesn't provide, which is their IP address.
The problem is that the CreateView fails with an IntegrityError since the field is empty. I tried modifying request.POST to add the relevant field, but that's not allowed (and a bad idea).
I figured I could use a hidden input on the form and put the IP there but that means the user can blank it or modify it if they like, I want the exact IP that did the POST request.
If I understand correctly both the form_valid and form_invalid methods are too early in the process, since the object hasn't been created yet? Is there any other way of doing this?
Here is what the code looks like:
class Answer(models.Model):
ip_address = models.GenericIPAddressField()
text = models.TextField()
and the view:
class AnswerForm(CreateView):
template_name = "answer.html"
model = Answer
success_url = reverse_lazy('answer')
fields = ['text']
The request data is stored in the object itself, therefore you can access it as such:
class AnswerForm(CreateView):
template_name = "answer.html"
model = Answer
success_url = reverse_lazy('answer')
fields = ['text']
def form_valid(self, form):
form.instance.ip_address = get_ip_address(self.request)
form.save()
return super().form_valid(form)

Making a DRF POST operation work like a Django Admin create object

I am sorry if this question exists elsewhere, but in my search, I did not find anything on StackOverflow or the internet answering my query.
I am new to Django Rest Framework. I am working with a MCQ Question Submission model, which records the submission of a quiz as a collection of answers of the examinee to various questions(stored as MCQ Answer model) of a MCQ Quiz( MCQ Quiz model and Choice model).
I want to POST the student's name, the quiz name and the Answer Model instances and want to automatically calculate the marks of the student before saving the serializer.
Currently, my position is this - If I go the Django Admin and manually create some Answer instances and a Submission instance combining them, then the marks get automatically calculated. But the same doesn't happen while submitting a POST request.
Could anyone tell that what is the difference between a POST and an instance creation from Admin?
What I do now is this.
In serializers.py
class MCQSubmissionSerializer(serializers.ModelSerializer):
marks = serializers.IntegerField(source='marks_obtained')
emailId = serializers.EmailField(source='fetch_email')
class Meta:
model = MCQSubmission
fields = '__all__'
In models.py
class MCQSubmission(models.Model):
student = models.ForeignKey(Student, null=True, on_delete=models.SET_NULL)
quiz = models.ForeignKey(MCQQuiz, null=True, on_delete=models.CASCADE)
submitted = models.DateTimeField(auto_now_add=True, null=True, blank=True)
def marks_obtained(self):
marks = 0
for a in self.mcqanswer_set.all():
marks += a.marks()
return marks
def fetch_email(self):
return self.student.email
This creates a JSON object containing the marks and email-id when made from the Django Admin. But it asks to provide for marks, email-id when doing a post request.
I recently also found out that without a mcqanswer_Set object also given in the post, marks can anyways not be calculated.
EDIT: My views.py
class MCQSubmissionList(APIView):
def get(self, request, format=None):
submission = MCQSubmission.objects.all()
serializer = MCQSubmissionSerializer(submission, many=True)
return Response(serializer.data)
def post(self, request, format=None):
serializer = MCQSubmissionSerializer(data=request.data)
if serializer.is_valid():
submission = serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
As you can see, I have not written anything special in either the get or post views. Similarly, I first make a get request which gives me some already created JSONs and I POST one of them by removing email and marks.

Fill a form field with initial values from another model (which is a foreign key)

I have 2 models, Company and Post.
class Post(Meta):
company = models.ForeignKey(Company, related_name='company', on_delete=models.CASCADE)
company_description = models.TextField(max_length=800)
What I need:
1) When a user create a post, the company FK will be set thru code
2) Also at form initialization the field company_description, will be prepopulated from the Company model, field description(not the same name)
3) On save if the user doesn't modify anything to the field, the initial value will be save
1,2,3 only on creation.
I checked Django documentation regarding initialization
but their example, is more simple they just get some data/text, in my case I first need to set FK, than get description from the FK Model, so not just text.
The pk/slug of the company I can get from url or from request thru multiple calls request.user.acc.company
path('<int:pk>/post/add/', CompanyPostCreateView.as_view(), name='create')
and in the view:
company_pk = kwargs.get('pk')
and overwrite the form_valid, but here is the issue, form_valid is called on validation, and I want to show this info before validation on form initialization, and I don't know how.
There isn't anything particularly complicated about this. You can get the FK at the start of the view and use it in the initial dictionary:
company = Company.objects.get(pk=company_pk)
form = PostForm(intial={'company_description': company.description})
Edit With a CreateView, you could just override get_initial:
def get_initial(self):
company = Company.objects.get(pk=self.kwargs['pk'])
return {'company_description': company.description}
I found a solution by overwriting get_form_kwargs
def get_form_kwargs(self):
self.company = Company.objects.get(pk=self.kwargs['pk'])
kwargs = super().get_form_kwargs()
kwargs['initial']['company_description'] = self.company.description
return kwargs
I use self.company to pass later to a different method, not redo the query.

can I use duck-typing with class-based views

I may be completely off the reservation here. (Feel free to tell me if I am.)
My use case is that I have a list of schools. The school model is pretty simple:
class School(models.Model):
name = models.CharField(max_length=100)
mascot = models.CharField(max_length=100, null=True, blank=True)
When my user wants to edit one of these schools, I don't want them editing the master copy. Instead, I want to give them their own copy which they can play with. When they are done editing their copy, they can submit their change, and someone else will approve it. So I have another class for the user's copy of the school:
class UserSchool(models.Model):
name = models.CharField(max_length=100)
mascot = models.CharField(max_length=100, null=True, blank=True)
master_school = models.ForeignKey(School)
user = models.ForeignKey(settings.AUTH_USER_MODEL)
So I set up a form to handle the editing of the UserSchool:
class UserSchoolForm(forms.ModelForm):
class Meta:
model = UserSchool
fields = ['name','mascot']
And now I have my EditSchool form:
class EditSchool(UpdateView):
model = School
success_url = reverse_lazy('list_schools')
form_class = UserSchoolForm
def get(self, request, *args, **kwargs):
school = self.get_object()
# make a copy of the school for this user
user_school, created = UserSchool.objects.get_or_create(
master_school=school, user=request.user,
defaults={'name' : school.name, 'mascot' : school.mascot})
self.object = user_school
form = UserSchoolForm()
context = self.get_context_data(form=form)
return self.render_to_response(context)
I know that get() is making the copy correctly, but when the form displays, there are no values listed in the "name" or "default" fields. My suspicion is that the problem is with the fact that cls.model = School, but self.object is an instance of UserSchool.
Am I close but missing something? Am I completely on the wrong path? Is there a better model for this (like having a single School instance with a special user for "master")?
(And one small complication -- since I'm an old hand at Django, but new a class-based views, I'm trying to use Vanilla Views because I find it easier to figure out what's going on.)
Just to rule out the obvious - you're not passing anything to the form constructor. Have you tried it with instance=user_school? There might be more that needs work but I'd start there.
To expand on this a bit - in your view, you're completely overriding the built in get method. That's fine, but it means that you're bypassing some of the automated behavior of your view superclass. Specifically, the get method of ProcessFormView (one of your ancestor classes) instantiates the form using the get_form method of the view class. FormMixin, another ancestor, defines get_form:
return form_class(**self.get_form_kwargs())
And get_form_kwargs on ModelFormMixin adds self.object to the form's kwargs:
kwargs.update({'instance': self.object})
Because your overridden get method does not call get_form, it also doesn't call get_form_kwargs and therefore doesn't go through the whole path that provides an initial binding for the form.
I personally would try to handle this by modifying the get_object method of your custom view and leaving the rest alone:
class EditSchool(UpdateView):
model = School
success_url = reverse_lazy('list_schools')
form_class = UserSchoolForm
def get_object(self, queryset=None):
school = super(EditSchool, self).get_object(queryset=queryset)
user_school, created = UserSchool.objects.get_or_create(
master_school=school, user=self.request.user,
defaults={'name' : school.name, 'mascot' : school.mascot})
return user_school
There may be more changes needed - I haven't tested this - but both the get and set methods use get_object, and bind it to the form as appropriate.

CreateOrUpdateView for Django Models

In my models I have a ForeignKey relationship like this:
class Question(models.Model):
question = models.TextField(null=False)
class Answer(models.Model):
question = models.ForeignKey(Question, related_name='answer')
user = models.ForeignKey(User)
class Meta:
unique_together = (("question", "user"),)
the corresponding URL to submit an answer contains the id of the question, like this:
url(r'^a/(?P<pk>\d+)/$', AnswerQuestion.as_view(), name='answer-question'),
With user coming from self.request.user, I am trying to get something like a CreateOrUpdateView, to allow some convenient navigation for the user and URL scheme.
Until now I tried this with:
class AnswerQuestion(LoginRequiredMixin, CreateView):
and add initial value, but that isn't clean because of pk. With an UpdateView I run into problems because I have to set default values for the form.
Has anybody done something like this? I'd rather avoid having a Create and Update view for the same Answer.
The UpdateView and CreateView are really not that different, the only difference is that UpdateView sets self.object to self.get_object() and CreateView sets it to None.
The easiest way would be to subclass UpdateView and override get_object():
AnswerQuestionView(LoginRequiredMixin, UpdateView):
def get_object(queryset=None):
if queryset is None:
queryset = self.get_queryset()
# easy way to get the right question from the url parameters:
question = super(AnswerQuestionView, self).get_object(Question.objects.all())
try:
obj = queryset.get(user=self.request.user, question=question)
except ObjectDoesNotExist:
obj = None
return obj
Returns the right answer if it exists, None if it does not. Of course add any attributes you need to the class, like model, form_class etc.