Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 32 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,37 @@
## Online Course — Assessment Feature (My Contribution)

**General Notes**
This repository contains an online course application built with Django. I was responsible for designing and implementing the course assessment feature end-to-end: modeling exam data, rendering questions in the UI, capturing submissions, and evaluating results.

An `onlinecourse` app has already been provided in this repo upon which you will be adding a new assesement feature.
## What I built

- If you want to develop the final project on Theia hosted by [IBM Developer Skills Network](https://labs.cognitiveclass.ai/), you will need to create the same project structure on Theia workspace and save it everytime you close the browser
- Or you could develop the final project locally by setting up your own Python runtime and IDE
- Hints for the final project are left on source code files
- You may choose any cloud platform for deployment (default is IBM Cloud Foundry)
- Depends on your deployment, you may choose any SQL database Django supported such as SQLite3, PostgreSQL, and MySQL (default is SQLite3)
- Data model

**ER Diagram**
For your reference, we have prepared the ER diagram design for the new assesement feature.
- Question with a configurable grade

![Onlinecourse ER Diagram](https://github.com/ibm-developer-skills-network/final-cloud-app-with-database/blob/master/static/media/course_images/onlinecourse_app_er.png)
- Choice with is_correct flag (supports single and multiple correct answers)

- Submission that stores selected choices per attempt (M2M → Choice)

- Admin authoring

- Created/registered admin forms so instructors can build exams: Course → Questions → Choices

- Views & URLs

- Course detail view that renders the exam form

- Submission view that validates and persists selected choices

- Result view that evaluates the attempt and displays per-question feedback + total score

- Scoring logic

## Tech stack

Python 3.10+

Django 4.x/5.x

SQLite

Django Templates + Bootstrap for styling
16 changes: 15 additions & 1 deletion onlinecourse/admin.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from django.contrib import admin
# <HINT> Import any new Models here
from .models import Course, Lesson, Instructor, Learner
from .models import Course, Lesson, Instructor, Learner, Question, Submission, Choice

# <HINT> Register QuestionInline and ChoiceInline classes here

Expand All @@ -21,10 +21,24 @@ class CourseAdmin(admin.ModelAdmin):
class LessonAdmin(admin.ModelAdmin):
list_display = ['title']

class ChoiceInline(admin.StackedInline):
model = Choice
extra = 2

class QuestionInline(admin.StackedInline):
model = Question
extra = 2

class QuestionAdmin(admin.ModelAdmin):
inlines = [ChoiceInline]
list_display = ['content']

# <HINT> Register Question and Choice models here

admin.site.register(Course, CourseAdmin)
admin.site.register(Lesson, LessonAdmin)
admin.site.register(Instructor)
admin.site.register(Learner)
admin.site.register(Question, QuestionAdmin)
admin.site.register(Choice)
admin.site.register(Submission)
78 changes: 78 additions & 0 deletions onlinecourse/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Generated by Django 4.2.3 on 2025-08-27 21:50

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone


class Migration(migrations.Migration):

initial = True

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name='Course',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(default='online course', max_length=30)),
('image', models.ImageField(upload_to='course_images/')),
('description', models.CharField(max_length=1000)),
('pub_date', models.DateField(null=True)),
('total_enrollment', models.IntegerField(default=0)),
],
),
migrations.CreateModel(
name='Lesson',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(default='title', max_length=200)),
('order', models.IntegerField(default=0)),
('content', models.TextField()),
('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='onlinecourse.course')),
],
),
migrations.CreateModel(
name='Learner',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('occupation', models.CharField(choices=[('student', 'Student'), ('developer', 'Developer'), ('data_scientist', 'Data Scientist'), ('dba', 'Database Admin')], default='student', max_length=20)),
('social_link', models.URLField()),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='Instructor',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('full_time', models.BooleanField(default=True)),
('total_learners', models.IntegerField()),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='Enrollment',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date_enrolled', models.DateField(default=django.utils.timezone.now)),
('mode', models.CharField(choices=[('audit', 'Audit'), ('honor', 'Honor'), ('BETA', 'BETA')], default='audit', max_length=5)),
('rating', models.FloatField(default=5.0)),
('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='onlinecourse.course')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.AddField(
model_name='course',
name='instructors',
field=models.ManyToManyField(to='onlinecourse.instructor'),
),
migrations.AddField(
model_name='course',
name='users',
field=models.ManyToManyField(through='onlinecourse.Enrollment', to=settings.AUTH_USER_MODEL),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Generated by Django 4.2.3 on 2025-08-27 21:59

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('onlinecourse', '0001_initial'),
]

operations = [
migrations.CreateModel(
name='Choice',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('content', models.CharField(max_length=50)),
('is_correct', models.BooleanField(default=False)),
],
),
migrations.CreateModel(
name='Submission',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('choices', models.ManyToManyField(to='onlinecourse.choice')),
('enrollment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='onlinecourse.enrollment')),
],
),
migrations.CreateModel(
name='Question',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('content', models.CharField(max_length=100)),
('grade', models.FloatField(default=50)),
('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='onlinecourse.course')),
],
),
migrations.AddField(
model_name='choice',
name='question',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='onlinecourse.question'),
),
]
26 changes: 23 additions & 3 deletions onlinecourse/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,26 @@ class Lesson(models.Model):
content = models.TextField()


class Question(models.Model):
course = models.ForeignKey(Course, on_delete=models.CASCADE)
content = models.CharField(max_length=100)
grade = models.FloatField(default=50)

def __str__(self):
return "Question: " + self.content
# method to calculate if the learner gets the score of the question
def is_get_score(self, selected_ids):
all_answers = self.choice_set.filter(is_correct=True).count()
selected_correct = self.choice_set.filter(is_correct=True, id__in=selected_ids).count()
if all_answers == selected_correct:
return True
else:
return False

class Choice(models.Model):
question = models.ForeignKey(Question, on_delete=models.CASCADE)
content = models.CharField(max_length=50)
is_correct = models.BooleanField(default=False)
# Enrollment model
# <HINT> Once a user enrolled a class, an enrollment entry should be created between the user and course
# And we could use the enrollment to track information such as exam submissions
Expand All @@ -98,6 +118,6 @@ class Enrollment(models.Model):
# One enrollment could have multiple submission
# One submission could have multiple choices
# One choice could belong to multiple submissions
#class Submission(models.Model):
# enrollment = models.ForeignKey(Enrollment, on_delete=models.CASCADE)
# choices = models.ManyToManyField(Choice)
class Submission(models.Model):
enrollment = models.ForeignKey(Enrollment, on_delete=models.CASCADE)
choices = models.ManyToManyField(Choice)
27 changes: 27 additions & 0 deletions onlinecourse/templates/onlinecourse/course_detail_bootstrap.html
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,33 @@ <h2>{{ course.name }}</h2>
{% endfor %}
</div>
<!-- Course detail template changes go here -->
{% if user.is_authenticated %}
</br>
<button class="btn btn-primary btn-block" data-toggle="collapse" data-target="#exam">Start Exam</button>
<div id="exam" class="collapse">
<form id="questionform" action="{% url 'onlinecourse:submit' course.id %}" method="POST">
{% for question in course.question_set.all %}
<div class="card mt-1">
<div class="card-header">
<h5>{{ question.content }}</h5>
</div>
{% csrf_token %}
<div class="form-group">
{% for choice in question.choice_set.all %}
<div class="form-check">
<label class="form-check-label">
<input type="checkbox" name="choice_{{choice.id}}" class="form-check-input"
id="{{choice.id}}" value="{{choice.id}}">{{ choice.content }}
</label>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
<input class="btn btn-success btn-block" type="submit" value="Submit">
</form>
</div>
{% endif %}
</div>
</body>
</html>
23 changes: 22 additions & 1 deletion onlinecourse/templates/onlinecourse/exam_result_bootstrap.html
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,37 @@
{% if grade > 80 %}
<div class="alert alert-success">
<!--HINT Display passed info -->
<b>Congratulations, {{ user.first_name }}!</b> You have passed the exam and completed the course with score {{ grade }}/100
</div>
{% else %}
<div class="alert alert-danger">
<!--HINT Display failed info -->
<b>Failed</b> Sorry, {{ user.first_name }}! You have failed the exam with score {{ grade }}/100
</div>
<a class="btn btn-link text-danger" href="{% url 'onlinecourse:course_details' course.id %}">Re-test</a>
{% endif %}
<div class="card-columns-vertical mt-1">
<h5 class="">Exam results</h5>
<!--HINT Display exam results-->
{% for question in course.question_set.all %}
<div class="card mt-1">
<div class="card-header"><h5>{{ question.content }}</h5></div>
<div class="form-group">
{% for choice in question.choice_set.all %}
<div class="form-check">
{% if choice.is_correct and choice in choices %}
<div class="text-success">Correct answer: {{ choice.content }}</div>
{% else %}{% if choice.is_correct and not choice in choices %}
<div class="text-warning">Not selected: {{ choice.content }}</div>
{% else %}{% if not choice.is_correct and choice in choices %}
<div class="text-danger">Wrong answer: {{ choice.content }}</div>
{% else %}
<div>{{ choice.content }}</div>
{% endif %}{% endif %}{% endif %}
</div>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
</div>
</body>
Expand Down
2 changes: 2 additions & 0 deletions onlinecourse/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
path('<int:course_id>/enroll/', views.enroll, name='enroll'),

# <HINT> Create a route for submit view
path('<int:course_id>/submit/', views.submit, name="submit"),

# <HINT> Create a route for show_exam_result view
path('course/<int:course_id>/submission/<int:submission_id>/result/', views.show_exam_result, name="exam_result"),

] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
34 changes: 33 additions & 1 deletion onlinecourse/views.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,50 @@
from django.shortcuts import render
from django.http import HttpResponseRedirect
# <HINT> Import any new Models here
from .models import Course, Enrollment
from django.contrib.auth.models import User
from django.shortcuts import get_object_or_404, render, redirect
from django.urls import reverse
from django.views import generic
from django.contrib.auth import login, logout, authenticate
import logging
from .models import Course, Enrollment, Question, Choice, Submission
# Get an instance of a logger
logger = logging.getLogger(__name__)

# Create your views here.
def submit(request, course_id):
course = get_object_or_404(Course, pk=course_id)
user = request.user
enrollment = Enrollment.objects.get(user=user, course=course)
submission = Submission.objects.create(enrollment=enrollment)
choices = extract_answers(request)
submission.choices.set(choices)
submission_id = submission.id
return HttpResponseRedirect(reverse(viewname='onlinecourse:exam_result', args=(course_id, submission_id,)))

def show_exam_result(request, course_id, submission_id):
context = {}
course = get_object_or_404(Course, pk=course_id)
submission = Submission.objects.get(id=submission_id)
choices = submission.choices.all()

total_score = 0
questions = course.question_set.all() # Assuming course has related questions

for question in questions:
correct_choices = question.choice_set.filter(is_correct=True) # Get all correct choices for the question
selected_choices = choices.filter(question=question) # Get the user's selected choices for the question

# Check if the selected choices are the same as the correct choices
if set(correct_choices) == set(selected_choices):
total_score += question.grade # Add the question's grade only if all correct answers are selected

context['course'] = course
context['grade'] = total_score
context['choices'] = choices

return render(request, 'onlinecourse/exam_result_bootstrap.html', context)

def registration_request(request):
context = {}
if request.method == 'GET':
Expand Down