diff --git a/onlinecourse/admin.py b/onlinecourse/admin.py index ffd8a631..737dc02f 100644 --- a/onlinecourse/admin.py +++ b/onlinecourse/admin.py @@ -1,9 +1,19 @@ from django.contrib import admin # Import any new Models here -from .models import Course, Lesson, Instructor, Learner +from .models import Course, Lesson, Instructor, Learner, Question, Choice, Submission # Register QuestionInline and ChoiceInline classes here +class QuestionInline(admin.StackedInline): + model = Question + extra = 2 +class ChoiceInline(admin.StackedInline): + model = Choice + extra = 2 + +class QuestionAdmin(admin.ModelAdmin): + inlines = [ChoiceInline] + search_fields = ['content'] class LessonInline(admin.StackedInline): model = Lesson @@ -23,7 +33,9 @@ class LessonAdmin(admin.ModelAdmin): # Register Question and Choice models here - +admin.site.register(Question, QuestionAdmin) +admin.site.register(Choice) +admin.site.register(Submission) admin.site.register(Course, CourseAdmin) admin.site.register(Lesson, LessonAdmin) admin.site.register(Instructor) diff --git a/onlinecourse/migrations/0001_initial.py b/onlinecourse/migrations/0001_initial.py new file mode 100644 index 00000000..b45d381d --- /dev/null +++ b/onlinecourse/migrations/0001_initial.py @@ -0,0 +1,78 @@ +# Generated by Django 4.2.3 on 2024-09-12 05:06 + +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), + ), + ] diff --git a/onlinecourse/migrations/0002_choice_submission_question_choice_question.py b/onlinecourse/migrations/0002_choice_submission_question_choice_question.py new file mode 100644 index 00000000..2a8836d1 --- /dev/null +++ b/onlinecourse/migrations/0002_choice_submission_question_choice_question.py @@ -0,0 +1,44 @@ +# Generated by Django 4.2.3 on 2024-09-12 05:43 + +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=200)), + ('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=200)), + ('grade', models.IntegerField(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'), + ), + ] diff --git a/onlinecourse/models.py b/onlinecourse/models.py index f6fbcbb5..d01bc47d 100644 --- a/onlinecourse/models.py +++ b/onlinecourse/models.py @@ -94,10 +94,32 @@ class Enrollment(models.Model): mode = models.CharField(max_length=5, choices=COURSE_MODES, default=AUDIT) rating = models.FloatField(default=5.0) +class Question(models.Model): + course = models.ForeignKey(Course, on_delete=models.CASCADE) + content = models.CharField(max_length=200) + grade = models.IntegerField(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=200) + is_correct = models.BooleanField(default=False) # 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) diff --git a/onlinecourse/templates/onlinecourse/course_detail_bootstrap.html b/onlinecourse/templates/onlinecourse/course_detail_bootstrap.html index fb48e939..cd990c4d 100644 --- a/onlinecourse/templates/onlinecourse/course_detail_bootstrap.html +++ b/onlinecourse/templates/onlinecourse/course_detail_bootstrap.html @@ -28,7 +28,7 @@ {% csrf_token %}
- + Sign Up
@@ -51,6 +51,31 @@

{{ course.name }}

{% endfor %} + {% if user.is_authenticated %} +
+ +
+
+ {% for question in course.question_set.all %} +
+
{{ question.content}}
+ {% csrf_token %} +
+ {% for choice in question.choice_set.all %} +
+ +
+ {% endfor %} +
+
+ {% endfor %} + +
+
+ {% endif %} diff --git a/onlinecourse/templates/onlinecourse/course_list_bootstrap.html b/onlinecourse/templates/onlinecourse/course_list_bootstrap.html index 34793b31..08acd268 100644 --- a/onlinecourse/templates/onlinecourse/course_list_bootstrap.html +++ b/onlinecourse/templates/onlinecourse/course_list_bootstrap.html @@ -25,7 +25,7 @@ {% csrf_token %}
- + Sign Up
diff --git a/onlinecourse/templates/onlinecourse/exam_result_bootstrap.html b/onlinecourse/templates/onlinecourse/exam_result_bootstrap.html index 264728f5..df8db62c 100644 --- a/onlinecourse/templates/onlinecourse/exam_result_bootstrap.html +++ b/onlinecourse/templates/onlinecourse/exam_result_bootstrap.html @@ -39,17 +39,39 @@ {% if grade > 80 %}
+ Congratulations, {{ user.first_name }}! You have passed the exam and completed the course with score {{ grade }}/100
{% else %}
+ Failed Sorry, {{ user.first_name }}! You have failed the exam with score {{ grade }}/100
Re-test {% endif %}
-
Exam results
+
Exam results
+ {% for question in course.question_set.all %} +
+
{{ question.content }}
+
+ {% for choice in question.choice_set.all %} +
+ {% if choice.is_correct and choice in choices %} +
Correct answer: {{ choice.content }}
+ {% else %}{% if choice.is_correct and not choice in choices %} +
Not selected: {{ choice.content }}
+ {% else %}{% if not choice.is_correct and choice in choices %} +
Wrong answer: {{ choice.content }}
+ {% else %} +
{{ choice.content }}
+ {% endif %}{% endif %}{% endif %} +
+ {% endfor %} +
+
+ {% endfor %} +
- \ No newline at end of file diff --git a/onlinecourse/urls.py b/onlinecourse/urls.py index 2960e104..fa15fbee 100644 --- a/onlinecourse/urls.py +++ b/onlinecourse/urls.py @@ -18,7 +18,7 @@ path('/enroll/', views.enroll, name='enroll'), # Create a route for submit view - + path('/submit/', views.submit, name="submit"), # Create a route for show_exam_result view - + path('course//submission//result/', views.show_exam_result, name="exam_result"), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/onlinecourse/views.py b/onlinecourse/views.py index 53eb02f0..1e296bb6 100644 --- a/onlinecourse/views.py +++ b/onlinecourse/views.py @@ -1,7 +1,7 @@ from django.shortcuts import render from django.http import HttpResponseRedirect # Import any new Models here -from .models import Course, Enrollment +from .models import Course, Enrollment, Choice, Question, Submission from django.contrib.auth.models import User from django.shortcuts import get_object_or_404, render, redirect from django.urls import reverse @@ -111,7 +111,15 @@ def enroll(request, course_id): # Add each selected choice object to the submission object # Redirect to show_exam_result with the submission id #def submit(request, course_id): - +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,))) # An example method to collect the selected choices from the exam form from the request object def extract_answers(request): @@ -131,6 +139,51 @@ def extract_answers(request): # For each selected choice, check if it is a correct answer or not # Calculate the total score #def show_exam_result(request, 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() + choice_ids=[] + for choice in choices: + choice_ids.append(choice.id) + # created a list of choice_ids of a submission + # choices that were submitted,make a list + total_score = 0 + questions = course.question_set.all() + # questions in the course + # Handles even multiple choice questions with multi correct options + for question in questions: + # for each question there are few options + # each question have their own grade and all questions grades total to 100 + q_grade = question.grade + temp_score=0 + for q_choice in question.choice_set.all(): + if q_choice.is_correct: + # if this choice is not submitted + if q_choice.id not in choice_ids: + temp_score=0 + break + else: + # option is correct and marked by user + temp_score=q_grade + else: + # implies this option is incorrect, so if it is in submission-->it is incorrect + if q_choice.id in choice_ids: + temp_score=0 + break + if temp_score>0: + total_score+=q_grade + + context['course'] = course + context['grade'] = total_score + context['choices'] = choices + + return render(request, 'onlinecourse/exam_result_bootstrap.html', context) + # The below logic doesnt work in multi correct answer + # for choice in choices: + # if choice.is_correct: + # total_score += choice.question.grade diff --git a/static/media/course_images/faisal-BI465ksrlWs-unsplash.jpg b/static/media/course_images/faisal-BI465ksrlWs-unsplash.jpg new file mode 100644 index 00000000..e977f0e1 Binary files /dev/null and b/static/media/course_images/faisal-BI465ksrlWs-unsplash.jpg differ diff --git a/static/media/course_images/faisal-BI465ksrlWs-unsplash_cgZ9DGp.jpg b/static/media/course_images/faisal-BI465ksrlWs-unsplash_cgZ9DGp.jpg new file mode 100644 index 00000000..e977f0e1 Binary files /dev/null and b/static/media/course_images/faisal-BI465ksrlWs-unsplash_cgZ9DGp.jpg differ