How can I convert django forms.py select items to a list in html?

Viewed 156

I have a form with which I intend to capture student marks. I have an issue like I don't know the best way to put it so that I have student name and markfield beside it for all the students. calling {{ form }} brings the form with select dropdown items(not what I want). Specifying form fields do not populate anything.i.e

{% for field in form %}
{{ field.student }}
{{ field.marks }}
{% endfor %}

This is the form

class AddMarksForm(forms.ModelForm):
    def __init__(self, school,klass,term,stream,year, *args, **kwargs):
        super(AddMarksForm, self).__init__(*args, **kwargs)
        self.fields['student'] = forms.ModelChoiceField(
            queryset=Students.objects.filter(school=school,klass__name__in=klass,stream__name=stream[0]))
    class Meta:
        model = Marks
        fields = ('student','marks')

This is how I tried the html rendering

<form method="post" enctype="multipart/form-data" action="{% url 'add_marks' %}">
    {% csrf_token %}
    {% if form %}
    <table>
      <tr>
        <td>Student</td>
        <td>Marks</td>
      </tr>
      {% for field in form %}
      <tr>
        <td>{{ field.student }}</td>
        <td>{{ field.marks }}</td>
      </tr>
    </table>
  {% endfor %}
<button class="btn btn-outline-primary">Save</button>
    </form>

My models.

class Year(models.Model):
    year = models.IntegerField(validators=[MinValueValidator(2018), MaxValueValidator(4000),])
    year_term = models.ForeignKey('exams.Term',on_delete=models.SET_NULL,related_name='year_term', null=True,blank=True)

class Subject(models.Model):
    name = models.CharField(max_length=50)
    teacher = models.ForeignKey(TeacherData,on_delete=models.CASCADE)
    students = models.ManyToManyField(Students)


class Term(models.Model):
    year = models.ForeignKey(Year,on_delete=models.CASCADE)
    name = models.CharField(max_length=30)
    exam_term = models.ForeignKey('exams.Exam',on_delete=models.SET_NULL,null=True,blank=True,related_name='exam_term')
    
class Exam(models.Model):
    school = models.ForeignKey(School,on_delete=models.CASCADE)
    year = models.ForeignKey(Year,on_delete=models.SET_NULL, null=True)
    term = models.ForeignKey(Term,on_delete=models.SET_NULL, null=True)
    name = models.CharField(max_length=20)
    klass = models.ManyToManyField("students.Klass", related_name='klass',null=True,blank=True)
    

class Marks(models.Model):
    exam = models.ForeignKey(Exam,on_delete=models.SET_NULL,null=True,blank=True)
    subject = models.ForeignKey(Subject,on_delete=models.SET_NULL,null=True,blank=True)
    student = models.ForeignKey(Students,on_delete=models.SET_NULL,null=True,blank=True)
    marks = models.IntegerField(validators=[MinValueValidator(0), MaxValueValidator(100),] ,null=True,blank=True)
    

More models

class Klass(models.Model):
    name = models.IntegerField(validators=[MinValueValidator(1), MaxValueValidator(4),], help_text='E.g 1,2,3, 4')
    school = models.ForeignKey(School,on_delete=models.CASCADE)
    exam = models.ForeignKey("exams.Exam",on_delete=models.CASCADE, related_name='exam',null=True,blank=True)
class Stream(models.Model):
    name = models.CharField(max_length=50,help_text='Name of the stream')
    klass = models.ForeignKey(Klass,on_delete=models.CASCADE,help_text='Choose a class the stream belongs to')
class Students(models.Model):
    id = models.AutoField(primary_key=True)
    #student_user = models.OneToOneField(User, on_delete=models.CASCADE,null=True,blank=True)
    school = models.ForeignKey(School,on_delete=models.CASCADE,help_text='A school must have a name')
    adm = models.IntegerField(unique=True,validators=[MinValueValidator(1), MaxValueValidator(1000000),],help_text="Student's admission number" )
    name = models.CharField(max_length=200,help_text="Student's name")
    kcpe = models.IntegerField(validators=[MinValueValidator(100), MaxValueValidator(500),], help_text='What the student scored in primary')
    klass = models.ForeignKey(Klass, on_delete=models.CASCADE,null=True,help_text="Student's Class/Form")
    stream = models.ForeignKey(Stream,on_delete=models.CASCADE,blank=True,null=True,help_text="Student's Stream")
    gender = models.ForeignKey(Gender,on_delete=models.CASCADE,null=True,help_text="Student's Gender")
    notes = models.TextField(blank=True,null=True,help_text='Anything to say about the student. Can always be edited later')
1 Answers

Here is my suggestion based on the discussion in question comments.

# models.py
class User(models.Model):
    is_teacher = models.BoolField(default=False)
    is_student = models.BoolField(default=False)


class Subject(models.Model):
    taught_by = models.ForeignKey(User, on_delete=models.PROTECT, limit_choices_to={"is_teacher": True})
    students = models.ManyToManyField(User, limit_choices_to={"is_student": True})


class Exam(models.Model):
    subject = models.ForeignKey(Subject, on_delete=models.CASCADE)


class Marks(models.Model):
    exam = models.ForeignKey(Exam, on_delete=models.CASCADE)
    student = models.ForeignKey(User, on_delete=models.CASCADE, limit_choices_to={"is_student": True})
    marks = models.IntegerField(validators=[MinValueValidator(0), MaxValueValidator(100),], null=True, blank=True)
    # no need for a subject field here, that is linked through the Exam object
    class Meta:
        unique_togetger = [('exam', 'student'),]

I have only written the important fields, you can add more fields that you need.

# views.py

class AddMarks(views.View):
    def get(self, request, exam_id):
        try:
            exam = Exam.objects.get(id=exam_id)
        except Exam.DoesNotExist:
            # some kind of handler that returns (important!)
        context = {
            "students": exam.subject.students.all()
            "current_marks": Marks.objects.filter(exam=exam)
        }
        return render("add_marks.html", context=context)
    def post(self, request, exam_id):
        try:
            exam = Exam.objects.get(id=exam_id)
        except Exam.DoesNotExist:
            # some kind of handler that returns (important!)
        for student in exam.subject.students.all():
            marks = int(request.POST.get(f"marks_{student.username}", None))
            if marks:
                # if that particular marks field exists in POST and is filled, let's update (if it exists) or create a new marks object
                marks_object = Marks.objects.filter(exam=exam, student=student).first() or Marks(exam=exam, student=student)
                marks_object.marks = marks
                marks_object.save()
        return self.get(request, exam_id)
<!--add_marks.html-->

<form method="POST">
    <table>
        {% for student in students %}
            <tr>
                <td>{{ student.get_full_name }}</td>
                <td><input type="number" name="marks_{{ student.username }}" /></td>
            </tr>
        {% endfor %}
        {% if students %}
            <tr><td colspan="2"><input type="submit" value="Save marks" /></td></tr>
        {% else %}
            <tr><td colspan="2">No student is taking this class</td></tr>
        {% endif %}
    </table>
</form>

{% if current_marks %}
    <h3>Currently saved marks</h3>
    <table>
        {% for marks in current_marks %}
            <tr>
                <td>{{ marks.student.get_full_name }}</td>
                <td>{{ marks.marks }}</td>
            </tr>
         {% endfor %}
    </table>
{% endfor %}

I did make a couple of changes. Most noticeable is the Student model, or rather the lack thereof. I have made the User flaggable as either student or teacher, allowing your students to log in and see their grades or perhaps sign up for classes.

Second, as mentioned in the code, there is no need for a subject field in the Marks model, as that data can be pulled from the Exam itself.

Lastly about what this does. It will create a form with a table in it, where each row will contain a field named marks_<username>. The field can then be retrieved in the post method handler. If the field is there and has a value, the view will either update existing, or create a new Marks model. If there are already any marks saved, they will be showed below in a separate table.

I have removed the form entirely as that is redundant in this case. If you really wanted to, somewhere else perhaps, this multiple-forms-on-one-page thing can be achieved by using Django's FormSet API. The documentation about that is [here][1]. That allows you to stack many forms of the same kind one after another, for editing many rows at once. But since we only really have one field, I find it easier to just handle that one field myself.

One last thing, I did not write any error checking and handling. That I will leave to you. Potential places that need treatment are the two except blocks, that need to return from the method so that the code below isn't executed. Some nice 404 page should suffice. Another place is the int() call around getting the POST contents. That might throw an exception if something non-integer is sent in. You should be able to handle that with a simple try-except block.

Related