├── mcq ├── __init__.py ├── migrations │ ├── __init__.py │ ├── 0002_auto_20180727_2247.py │ └── 0001_initial.py ├── admin.py ├── tests.py ├── views.py ├── apps.py └── models.py ├── quiz ├── __init__.py ├── migrations │ ├── __init__.py │ ├── 0004_csvupload_title.py │ ├── 0002_auto_20180728_0028.py │ ├── 0003_csvupload.py │ └── 0001_initial.py ├── templatetags │ ├── __init__.py │ └── quiz_tags.py ├── tests.py ├── apps.py ├── signals.py ├── templates │ ├── index.html │ ├── quiz │ │ ├── category_list.html │ │ ├── quiz_detail.html │ │ ├── quiz_list.html │ │ ├── sitting_list.html │ │ └── sitting_detail.html │ ├── single_complete.html │ ├── login.html │ ├── view_quiz_category.html │ ├── correct_answer.html │ ├── progress.html │ ├── question.html │ ├── result.html │ └── base.html ├── forms.py ├── validators.py ├── urls.py ├── admin.py ├── views.py └── models.py ├── online_test ├── __init__.py ├── wsgi.py ├── urls.py └── settings.py ├── .gitignore ├── requirements.txt ├── screenshots ├── login.png ├── results.png └── quiz_page.png ├── manage.py └── README.md /mcq/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /quiz/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /online_test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mcq/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /quiz/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /quiz/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | csv/ 2 | *sqlite3 3 | *.idea 4 | *.code 5 | *__pycache__ -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | django==2.2.7 2 | django-model-utils==3.1.2 3 | -------------------------------------------------------------------------------- /mcq/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /mcq/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /mcq/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /quiz/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /screenshots/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sswapnil2/django-quiz-app/HEAD/screenshots/login.png -------------------------------------------------------------------------------- /screenshots/results.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sswapnil2/django-quiz-app/HEAD/screenshots/results.png -------------------------------------------------------------------------------- /mcq/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class McqConfig(AppConfig): 5 | name = 'mcq' 6 | -------------------------------------------------------------------------------- /screenshots/quiz_page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sswapnil2/django-quiz-app/HEAD/screenshots/quiz_page.png -------------------------------------------------------------------------------- /quiz/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class QuizConfig(AppConfig): 5 | name = 'quiz' 6 | -------------------------------------------------------------------------------- /quiz/signals.py: -------------------------------------------------------------------------------- 1 | import django.dispatch 2 | 3 | csv_uploaded = django.dispatch.Signal(providing_args=["user", "csv_file_list"]) -------------------------------------------------------------------------------- /quiz/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |

This is my index page

5 | {% if user.is_authenticated %} 6 |

Username: {{user.username}}

7 |

Name: {{user.first_name}} {{user.last_name}}

8 |

Email: {{user.email}}

9 | {% endif %} 10 | 11 | {% endblock %} -------------------------------------------------------------------------------- /quiz/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.forms.widgets import RadioSelect 3 | 4 | 5 | class QuestionForm(forms.Form): 6 | def __init__(self, question, *args, **kwargs): 7 | super(QuestionForm, self).__init__(*args, **kwargs) 8 | choice_list = [x for x in question.get_answers_list()] 9 | self.fields["answers"] = forms.ChoiceField(choices=choice_list, widget=RadioSelect) 10 | 11 | 12 | -------------------------------------------------------------------------------- /quiz/templates/quiz/category_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load i18n %} 3 | {% block title %}{% trans "All Quizzes" %}{% endblock %} 4 | 5 | {% block content %} 6 |

{% trans "Category list" %}

7 | 8 | 17 | 18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /online_test/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for online_test project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "online_test.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /quiz/templates/single_complete.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | 4 | {% load quiz_tags %} 5 | 6 | {% block title %} {{ quiz.title }} {% endblock %} 7 | {% block description %} {{ quiz.title }} - {{ quiz.description }} {% endblock %} 8 | 9 | {% block content %} 10 | 11 | 12 | {% if user.is_authenticated %} 13 |

{% trans "You have already sat this exam and only one sitting is permitted" %}.

14 | {% else %} 15 |

{% trans "This exam is only accessible to signed in users" %}.

16 | {% endif %} 17 | 18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /quiz/templates/quiz/quiz_detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load i18n %} 3 | {% block title %} 4 | {{ quiz.title }} 5 | {% endblock %} 6 | 7 | {% block content %} 8 |

{{ quiz.title }}

9 |

{% trans "Category" %}: {{ quiz.category }}

10 | {% if quiz.single_attempt %} 11 |

{% trans "You will only get one attempt at this quiz" %}.

12 | {% endif %} 13 |

{{ quiz.description }}

14 |

15 | 16 | {% trans "Start quiz" %} 17 | 18 |

19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "online_test.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError as exc: 10 | raise ImportError( 11 | "Couldn't import Django. Are you sure it's installed and " 12 | "available on your PYTHONPATH environment variable? Did you " 13 | "forget to activate a virtual environment?" 14 | ) from exc 15 | execute_from_command_line(sys.argv) 16 | -------------------------------------------------------------------------------- /quiz/migrations/0004_csvupload_title.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.1 on 2018-07-27 20:52 2 | 3 | from django.db import migrations, models 4 | import django.utils.timezone 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('quiz', '0003_csvupload'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='csvupload', 16 | name='title', 17 | field=models.CharField(default=django.utils.timezone.now, max_length=100, verbose_name='Title'), 18 | preserve_default=False, 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /quiz/templates/login.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} {% block content %} 2 | 3 |
4 |

Login

5 |
6 | {% csrf_token %} 7 |
8 | 9 |
10 |
11 | 12 |
13 | 14 |
15 |
16 | 17 | {% endblock %} -------------------------------------------------------------------------------- /mcq/migrations/0002_auto_20180727_2247.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.1 on 2018-07-27 17:17 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('mcq', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='mcqquestion', 15 | name='answer_order', 16 | field=models.CharField(blank=True, choices=[('content', 'Content'), ('none', 'None')], help_text='The order in which multichoice answer options are displayed to the user', max_length=30, null=True, verbose_name='Answer Order'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /quiz/templates/view_quiz_category.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load i18n %} 3 | {% block title %}{% trans "Quizzes related to" %} {{ category.category }}{% endblock %} 4 | 5 | {% block content %} 6 |

{% trans "Quizzes in the" %} {{ category.category }} {% trans "category" %}

7 | 8 | {% with object_list as quizzes %} 9 | {% if quizzes %} 10 | 19 | {% else %} 20 |

{% trans "There are no quizzes" %}

21 | {% endif %} 22 | {% endwith %} 23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /quiz/validators.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import os 3 | import io 4 | 5 | from django.core.exceptions import ValidationError 6 | 7 | REQUIRED_HEADER = ['username' ,'email','password','first name','last name'] 8 | 9 | def csv_file_validator(value): 10 | filename, ext = os.path.splitext(value.name) 11 | if str(ext) != '.csv': 12 | raise ValidationError("Must be a csv file") 13 | decoded_file = value.read().decode('utf-8') 14 | io_string = io.StringIO(decoded_file) 15 | reader = csv.reader(io_string, delimiter=';', quotechar='|') 16 | header_ = next(reader)[0].split(',') 17 | if header_[-1] == '': 18 | header_.pop() 19 | required_header = REQUIRED_HEADER 20 | if required_header != header_: 21 | raise ValidationError("Invalid File. Please use valid CSV Header and/or Staff Upload Template.") 22 | return True -------------------------------------------------------------------------------- /quiz/templatetags/quiz_tags.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | register = template.Library() 4 | 5 | 6 | @register.inclusion_tag('correct_answer.html', takes_context=True) 7 | def correct_answer_for_all(context, question): 8 | """ 9 | processes the correct answer based on a given question object 10 | if the answer is incorrect, informs the user 11 | """ 12 | answers = question.get_answers() 13 | incorrect_list = context.get('incorrect_questions', []) 14 | if question.id in incorrect_list: 15 | user_was_incorrect = True 16 | else: 17 | user_was_incorrect = False 18 | 19 | return {'previous': {'answers': answers}, 20 | 'user_was_incorrect': user_was_incorrect} 21 | 22 | 23 | @register.filter 24 | def answer_choice_to_string(question, answer): 25 | return question.answer_choice_to_string(answer) 26 | -------------------------------------------------------------------------------- /online_test/urls.py: -------------------------------------------------------------------------------- 1 | """online_test URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/2.0/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import path, include 18 | 19 | 20 | urlpatterns = [ 21 | path('admin/', admin.site.urls), 22 | path('', include('quiz.urls')) 23 | ] 24 | -------------------------------------------------------------------------------- /quiz/migrations/0002_auto_20180728_0028.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.1 on 2018-07-27 18:58 2 | 3 | from django.db import migrations, models 4 | import django.utils.timezone 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('quiz', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='progress', 16 | name='correct_answer', 17 | field=models.CharField(default=django.utils.timezone.now, max_length=10, verbose_name='Correct Answers'), 18 | preserve_default=False, 19 | ), 20 | migrations.AddField( 21 | model_name='progress', 22 | name='wrong_answer', 23 | field=models.CharField(default=django.utils.timezone.now, max_length=10, verbose_name='Wrong Answers'), 24 | preserve_default=False, 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /quiz/templates/correct_answer.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% if previous.answers %} 3 | 4 | {% if user_was_incorrect %} 5 |
6 | {% trans "You answered the above question incorrectly" %} 7 |
8 | {% endif %} 9 | 10 | 11 | 12 | {% for answer in previous.answers %} 13 | {% if answer.correct %} 14 | 15 | 16 | 17 | {% else %} 18 | 19 | 20 | 27 | {% endif %} 28 | 29 | {% endfor %} 30 | 31 |
{{ answer.content }}{% trans "This is the correct answer" %}
{{ answer.content }} 21 | {% if previous.question_type.MCQuestion %} 22 | {% if answer.id|add:"0" == previous.previous_answer|add:"0" %} 23 | {% trans "This was your answer." %} 24 | {% endif %} 25 | {% endif %} 26 |
32 | {% endif %} 33 | -------------------------------------------------------------------------------- /quiz/migrations/0003_csvupload.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.1 on 2018-07-27 19:40 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | import quiz.models 7 | import quiz.validators 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | dependencies = [ 13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ('quiz', '0002_auto_20180728_0028'), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='CSVUpload', 20 | fields=[ 21 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 22 | ('file', models.FileField(upload_to=quiz.models.upload_csv_file, validators=[quiz.validators.csv_file_validator])), 23 | ('completed', models.BooleanField(default=False)), 24 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 25 | ], 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /quiz/templates/quiz/quiz_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load i18n %} 3 | {% load quiz_tags %} 4 | {% block title %}{% trans "All Quizzes" %}{% endblock %} 5 | 6 | {% block content %} 7 |

{% trans "List of quizzes" %}

8 | {% if quiz_list %} 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | {% for quiz in quiz_list %} 24 | 25 | 26 | 27 | 28 | 29 | 30 | 35 | 36 | {% endfor %} 37 | 38 | 39 |
{% trans "Title" %}{% trans "Category" %}{% trans "Exam" %}{% trans "Single attempt" %}
{{ quiz.title }}{{ quiz.category }}{{ quiz.exam_paper }}{{ quiz.single_attempt }} 31 | 32 | {% trans "View details" %} 33 | 34 |
40 | 41 | {% else %} 42 |

{% trans "There are no available quizzes" %}.

43 | {% endif %} 44 | {% endblock %} 45 | -------------------------------------------------------------------------------- /quiz/templates/quiz/sitting_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load i18n %} 3 | {% block title %}{% trans "All Quizzes" %}{% endblock %} 4 | 5 | {% block content %} 6 |

{% trans "List of complete exams" %}

7 | {% if sitting_list %} 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | {% for sitting in sitting_list %} 34 | 35 | 36 | 37 | 38 | 39 | 40 | 45 | 46 | {% endfor %} 47 | 48 | 49 | 50 |
{% trans "User" %}{% trans "Quiz" %}{% trans "Completed" %}{% trans "Score" %}(%)
{{ sitting.user }}{{ sitting.quiz }}{{ sitting.end|date }}{{ sitting.get_percent_correct }} 41 | 42 | {% trans "View details" %} 43 | 44 |
51 | {% else %} 52 |

{% trans "There are no matching quizzes" %}.

53 | {% endif %} 54 | {% endblock %} 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # django-quiz-app 2 | It is django based quiz app for multiple choice questions. 3 | 4 | 5 | ### Snaps of project 6 | Login: 7 | ![alt text](https://github.com/sswapnil2/django-quiz-app/blob/master/screenshots/login.png "login page") 8 |
9 | 10 | quiz: 11 | ![alt text](https://github.com/sswapnil2/django-quiz-app/blob/master/screenshots/quiz_page.png "quiz page") 12 |
13 | 14 | results: 15 | ![alt text](https://github.com/sswapnil2/django-quiz-app/blob/master/screenshots/results.png "results") 16 |
17 | 18 | # Instructions 19 | 20 | 1) ### Installations 21 | Make sure to have python version 3 install on you pc or laptop. 22 | If not install it from [here](https://www.python.org)
23 | **Clone repository**
24 | `https://github.com/sswapnil2/django-quiz-app.git`
25 | `cd django-quiz-app` 26 | 27 | 2) ### Installing dependencies 28 | It will install all required dependies in the project.
29 | `pip install -r requirements.txt` 30 | 31 | 3) ### Migrations 32 | To run migrations.
33 | `python manage.py makemigrations`
34 | `python manage.py migrate` 35 | 36 | 4) ### Create superuser 37 | To create super user run.
38 | `python manage.py createsuperuser`
39 | After running this command it will ask for username, password. 40 | You can access admin panel from `localhost:8000/admin/` 41 | 42 | 4) ### Running locally 43 | To run at localhost. It will run on port 8000 by default.
44 | `python manage.py runserver` 45 | 46 | 5) ### Reference 47 | I have refernced this quizz app from [tomwalker's](https://github.com/tomwalker) original repo. 48 | Reference link to the quiz app repo is [here](https://github.com/tomwalker/django_quiz) 49 | 50 | -------------------------------------------------------------------------------- /quiz/templates/progress.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | 4 | {% load quiz_tags %} 5 | 6 | {% block title %} {% trans "Progress Page" %} {% endblock %} 7 | {% block description %} {% trans "User Progress Page" %} {% endblock %} 8 | 9 | {% block content %} 10 | 11 | {% if cat_scores %} 12 | 13 |

{% trans "Question Category Scores" %}

14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | {% for cat, value in cat_scores.items %} 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | {% endfor %} 38 | 39 | 40 | 41 |
{% trans "Category" %}{% trans "Correctly answererd" %}{% trans "Incorrect" %}%
{{ cat }}{{ value.0 }}{{ value.1 }}{{ value.2 }}
42 | 43 | 44 | {% endif %} 45 | 46 | {% if exams %} 47 | 48 |
49 | 50 |

{% trans "Previous exam papers" %}

51 |

52 | {% trans "Below are the results of exams that you have sat." %} 53 |

54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | {% for exam in exams %} 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | {% endfor %} 78 | 79 | 80 | 81 |
{% trans "Quiz Title" %}{% trans "Score" %}{% trans "Possible Score" %}%
{{ exam.quiz.title }}{{ exam.current_score }}{{ exam.get_max_score }}{{ exam.get_percent_correct }}
82 | 83 | {% endif %} 84 | 85 | 86 | {% endblock %} 87 | -------------------------------------------------------------------------------- /quiz/templates/quiz/sitting_detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load i18n %} 3 | {% load quiz_tags %} 4 | {% block title %} 5 | {% trans "Result of" %} {{ sitting.quiz.title }} {% trans "for" %} {{ sitting.user }} 6 | {% endblock %} 7 | 8 | {% block content %} 9 |

{% trans "Quiz title" %}: {{ sitting.quiz.title }}

10 |

{% trans "Category" %}: {{ sitting.quiz.category }}

11 |

{{ sitting.quiz.description }}

12 |
13 |

{% trans "User" %}: {{ sitting.user }}

14 |

{% trans "Completed" %}: {{ sitting.end|date }}

15 |

{% trans "Score" %}: {{ sitting.get_percent_correct }}%

16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | {% for question in questions %} 30 | 31 | 32 | 38 | 39 | 46 | 52 | 53 | 54 | {% endfor %} 55 | 56 | 57 | 58 |
{% trans "Question" %}{% trans "User answer" %}
33 | {{ question.content }} 34 | {% if question.figure %} 35 |
{{ question.figure }}
36 | {% endif %} 37 |
{{ question|answer_choice_to_string:question.user_answer }} 40 | {% if question.id in sitting.get_incorrect_questions %} 41 |

{% trans "incorrect" %}

42 | {% else %} 43 |

{% trans "Correct" %}

44 | {% endif %} 45 |
47 |
{% csrf_token %} 48 | 49 | 50 |
51 |
59 | {% endblock %} 60 | -------------------------------------------------------------------------------- /quiz/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | from .views import QuizListView, CategoriesListView,\ 3 | ViewQuizListByCategory, QuizUserProgressView, QuizMarkingList,\ 4 | QuizMarkingDetail, QuizDetailView, QuizTake, index, login_user, logout_user 5 | from django.urls import path 6 | 7 | 8 | urlpatterns = [ url(regex=r'^$', view=index, name='index'), 9 | url(regex=r'^login/$', view=login_user, name='login'), 10 | url(regex=r'^logout/$', view=logout_user, name='logout'), 11 | url(regex=r'^quizzes/$', 12 | view=QuizListView.as_view(), 13 | name='quiz_index'), 14 | 15 | url(regex=r'^category/$', 16 | view=CategoriesListView.as_view(), 17 | name='quiz_category_list_all'), 18 | 19 | url(regex=r'^category/(?P[\w|\W-]+)/$', 20 | view=ViewQuizListByCategory.as_view(), 21 | name='quiz_category_list_matching'), 22 | 23 | url(regex=r'^progress/$', 24 | view=QuizUserProgressView.as_view(), 25 | name='quiz_progress'), 26 | 27 | url(regex=r'^marking/$', 28 | view=QuizMarkingList.as_view(), 29 | name='quiz_marking'), 30 | 31 | url(regex=r'^marking/(?P[\d.]+)/$', 32 | view=QuizMarkingDetail.as_view(), 33 | name='quiz_marking_detail'), 34 | 35 | # passes variable 'quiz_name' to quiz_take view 36 | url(regex=r'^(?P[\w-]+)/$', 37 | view=QuizDetailView.as_view(), 38 | name='quiz_start_page'), 39 | 40 | url(regex=r'^(?P[\w-]+)/take/$', 41 | view=QuizTake.as_view(), 42 | name='quiz_question'), 43 | ] -------------------------------------------------------------------------------- /mcq/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.7 on 2018-07-23 19:35 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ('quiz', '0001_initial'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Answer', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('content', models.CharField(help_text='Enter the answer text that you want displayed', max_length=1000, verbose_name='Content')), 21 | ('correct', models.BooleanField(default=False, help_text='Is this a correct answer?', verbose_name='Correct')), 22 | ], 23 | options={ 24 | 'verbose_name': 'Answer', 25 | 'verbose_name_plural': 'Answers', 26 | }, 27 | ), 28 | migrations.CreateModel( 29 | name='MCQQuestion', 30 | fields=[ 31 | ('question_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='quiz.Question')), 32 | ('answer_order', models.CharField(blank=True, choices=[('content', 'Content'), ('random', 'Random'), ('none', 'None')], help_text='The order in which multichoice answer options are displayed to the user', max_length=30, null=True, verbose_name='Answer Order')), 33 | ], 34 | options={ 35 | 'verbose_name': 'Multiple Choice Question', 36 | 'verbose_name_plural': 'Multiple Choice Questions', 37 | }, 38 | bases=('quiz.question',), 39 | ), 40 | migrations.AddField( 41 | model_name='answer', 42 | name='question', 43 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='mcq.MCQQuestion', verbose_name='Question'), 44 | ), 45 | ] 46 | -------------------------------------------------------------------------------- /quiz/templates/question.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n%} 3 | 4 | 5 | 6 | {% block title %} {{ quiz.title }} {% endblock %} 7 | {% block description %} {{ quiz.title }} - {{ quiz.description }} {% endblock %} 8 | 9 | {% block content %} 10 | 11 | {% if previous.answers %} 12 | 13 |

{% trans "The previous question" %}:

14 |

{{ previous.previous_question }}

15 | 16 | {% if previous.previous_outcome %} 17 |
18 | {% else %} 19 |
20 | {% endif %} 21 |

22 | {% trans "Your answer was" %} 23 | 24 | {{ previous.previous_outcome|yesno:"correct,incorrect" }} 25 | 26 |

27 | 28 |
29 | 30 | {% include 'correct_answer.html' %} 31 | 32 |

{% trans "Explanation" %}:

33 |
34 |

{{ previous.previous_question.explanation }}

35 |
36 | 37 |
38 | 39 | {% endif %} 40 | 41 |
42 | 43 | {% if question %} 44 | 45 | {% if progress %} 46 |
47 | {% trans "Question" %} {{ progress.0|add:1 }} {% trans "of" %} {{ progress.1 }} 48 |
49 | {% endif %} 50 | 51 |

52 | {% trans "Question category" %}: 53 | {{ question.category }} 54 |

55 | 56 |

{{ question.content }}

57 | 58 | {% if question.figure %} 59 | {{ question.content }} 60 | {% endif %} 61 | 62 |
{% csrf_token %} 63 | 64 | 65 |
    66 | 67 | {% for answer in form.answers %} 68 |
  • 69 | {{ answer }} 70 |
  • 71 | {% endfor %} 72 | 73 |
74 | {% if progress.0|add:1 == progress.1 %} 75 | 76 | {% else %} 77 | 78 | {% endif %} 79 |
80 | 81 | {% endif %} 82 | 83 |
84 | 85 | 86 | {% endblock %} 87 | -------------------------------------------------------------------------------- /mcq/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from quiz.models import Question 3 | 4 | ANSWER_ORDER_OPTIONS = ( 5 | ('content', 'Content'), 6 | ('none', 'None'), 7 | # ('random', 'Random') 8 | ) 9 | 10 | 11 | class MCQQuestion(Question): 12 | 13 | answer_order = models.CharField( 14 | max_length=30, null=True, blank=True, 15 | choices=ANSWER_ORDER_OPTIONS, 16 | help_text="The order in which multichoice \ 17 | answer options are displayed \ 18 | to the user", 19 | verbose_name="Answer Order") 20 | 21 | def check_if_correct(self, guess): 22 | answer = Answer.objects.get(id=guess) 23 | 24 | if answer.correct is True: 25 | return True 26 | else: 27 | return False 28 | 29 | def order_answers(self, queryset): 30 | if self.answer_order == 'content': 31 | return queryset.order_by('content') 32 | # if self.answer_order == 'random': 33 | # return queryset.order_by('Random') 34 | if self.answer_order == 'none': 35 | return queryset.order_by('None') 36 | 37 | def get_answers(self): 38 | return self.order_answers(Answer.objects.filter(question=self)) 39 | 40 | def get_answers_list(self): 41 | return [(answer.id, answer.content) for answer in self.order_answers(Answer.objects.filter(question=self))] 42 | 43 | def answer_choice_to_string(self, guess): 44 | return Answer.objects.get(id=guess).content 45 | 46 | class Meta: 47 | verbose_name = "Multiple Choice Question" 48 | verbose_name_plural = "Multiple Choice Questions" 49 | 50 | 51 | class Answer(models.Model): 52 | question = models.ForeignKey(MCQQuestion, verbose_name='Question', on_delete=models.CASCADE) 53 | 54 | content = models.CharField(max_length=1000, 55 | blank=False, 56 | help_text="Enter the answer text that \ 57 | you want displayed", 58 | verbose_name="Content") 59 | 60 | correct = models.BooleanField(blank=False, 61 | default=False, 62 | help_text="Is this a correct answer?", 63 | verbose_name="Correct") 64 | 65 | def __str__(self): 66 | return self.content 67 | 68 | 69 | class Meta: 70 | verbose_name = "Answer" 71 | verbose_name_plural = "Answers" 72 | 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /quiz/templates/result.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | 4 | {% load quiz_tags %} 5 | 6 | {% block title %} {{ quiz.title}} {% endblock %} 7 | {% block description %} {% trans "Exam Results for" %} {{ quiz.title }} {% endblock %} 8 | 9 | {% block content %} 10 | 11 | {% if previous.answers %} 12 | 13 |

{% trans "The previous question" %}:

14 |

{{ previous.previous_question }}

15 |

Your answer was 16 | 17 | {{ previous.previous_outcome|yesno:"correct,incorrect" }} 18 | 19 |

20 | {% include 'correct_answer.html' %} 21 |

{% trans "Explanation" %}:

22 |
23 |

{{ previous.previous_question.explanation }}

24 |
25 |
26 | 27 | {% endif %} 28 | 29 | {% if max_score %} 30 | 31 |
32 |

{% trans "Exam results" %}

33 |

34 | {% trans "Exam title" %}: 35 | {{ quiz.title }}

36 | 37 |

38 | {% trans "You answered" %} {{ score }} {% trans "questions correctly out of" %} {{ max_score }}, {% trans "giving you" %} {{ percent }} {% trans "percent correct" %} 39 |

40 | 41 | {% if quiz.pass_mark %} 42 |
43 |

{{ sitting.result_message }}

44 |
45 | 46 | {% endif %} 47 | 48 |

{% trans "Review the questions below and try the exam again in the future"%}.

49 | 50 | {% if user.is_authenticated %} 51 | 52 |

{% trans "The result of this exam will be stored in your progress section so you can review and monitor your progression" %}.

53 | 54 | {% endif %} 55 |
56 | 57 | 58 | {% endif %} 59 | 60 | 61 |
62 | 63 | {% if possible %} 64 | 65 |

66 | {% trans "Your session score is" %} {{ session }} {% trans "out of a possible" %} {{ possible }} 67 |

68 | 69 |
70 | 71 | {% endif %} 72 | 73 | {% if questions %} 74 | 75 | {% for question in questions %} 76 | 77 |

78 | {{ question.content }} 79 |

80 | 81 | {% correct_answer_for_all question %} 82 | 83 | {% if question.user_answer %} 84 |

{% trans "Your answer" %}: {{ question|answer_choice_to_string:question.user_answer }}

85 | {% endif %} 86 | 87 |

{% trans "Explanation" %}:

88 |
89 |

{{ question.explanation|safe }}

90 |
91 | 92 |
93 | 94 | {% endfor %} 95 | 96 | {% endif %} 97 | 98 | 99 | {% endblock %} 100 | -------------------------------------------------------------------------------- /quiz/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django import forms 3 | from django.contrib.admin.widgets import FilteredSelectMultiple 4 | # Register your models here. 5 | from .models import Quiz, Category, Question, Progress 6 | from mcq.models import MCQQuestion, Answer 7 | from django.utils.translation import ugettext_lazy as _ 8 | from .models import CSVUpload 9 | 10 | 11 | class CSVUploadsAdmin(admin.ModelAdmin): 12 | model = CSVUpload 13 | list_display= ('title',) 14 | 15 | class AnswerInline(admin.TabularInline): 16 | model = Answer 17 | 18 | 19 | class QuizAdminForm(forms.ModelForm): 20 | """ 21 | below is from 22 | http://stackoverflow.com/questions/11657682/ 23 | django-admin-interface-using-horizontal-filter-with- 24 | inline-manytomany-field 25 | """ 26 | 27 | class Meta: 28 | model = Quiz 29 | exclude = [] 30 | 31 | questions = forms.ModelMultipleChoiceField( 32 | queryset=Question.objects.all().select_subclasses(), 33 | required=False, 34 | label=_("Questions"), 35 | widget=FilteredSelectMultiple( 36 | verbose_name=_("Questions"), 37 | is_stacked=False)) 38 | 39 | def __init__(self, *args, **kwargs): 40 | super(QuizAdminForm, self).__init__(*args, **kwargs) 41 | if self.instance.pk: 42 | self.fields['questions'].initial = \ 43 | self.instance.question_set.all().select_subclasses() 44 | 45 | def save(self, commit=True): 46 | quiz = super(QuizAdminForm, self).save(commit=False) 47 | quiz.save() 48 | quiz.question_set.set(self.cleaned_data['questions']) 49 | self.save_m2m() 50 | return quiz 51 | 52 | 53 | class QuizAdmin(admin.ModelAdmin): 54 | form = QuizAdminForm 55 | 56 | list_display = ('title', 'category', ) 57 | list_filter = ('category',) 58 | search_fields = ('description', 'category', ) 59 | 60 | 61 | class CategoryAdmin(admin.ModelAdmin): 62 | search_fields = ('category', ) 63 | 64 | 65 | class MCQuestionAdmin(admin.ModelAdmin): 66 | list_display = ('content', 'category', ) 67 | list_filter = ('category',) 68 | fields = ('content', 'category', 69 | 'figure', 'quiz', 'explanation', 'answer_order') 70 | 71 | search_fields = ('content', 'explanation') 72 | filter_horizontal = ('quiz',) 73 | 74 | inlines = [AnswerInline] 75 | 76 | 77 | class ProgressAdmin(admin.ModelAdmin): 78 | """ 79 | to do: 80 | create a user section 81 | """ 82 | search_fields = ('user', 'score', ) 83 | 84 | 85 | admin.site.register(Quiz, QuizAdmin) 86 | admin.site.register(Category, CategoryAdmin) 87 | admin.site.register(MCQQuestion, MCQuestionAdmin) 88 | admin.site.register(Progress, ProgressAdmin) 89 | admin.site.register(CSVUpload, CSVUploadsAdmin) 90 | -------------------------------------------------------------------------------- /quiz/templates/base.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | {% trans "Example Quiz Website" %} | {% block title %}{% endblock %} 9 | 10 | 11 | 12 | 13 | 15 | 16 | 19 | 20 | 21 | 22 | 23 | 52 | 53 | 54 | 55 | 56 |
57 | {% if messages %} {% for message in messages %} 58 | 64 | {% endfor %} {% endif %} {% block content %} {% endblock %} 65 | 66 |
67 | 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /online_test/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for online_test project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.0.7. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.0/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'q^n4^0*8v2f9%qs$+hg7l0g!-461fja26bzq=cwp)y3u&k6i8&' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'mcq', 35 | 'quiz', 36 | 'django.contrib.admin', 37 | 'django.contrib.auth', 38 | 'django.contrib.contenttypes', 39 | 'django.contrib.sessions', 40 | 'django.contrib.messages', 41 | 'django.contrib.staticfiles', 42 | ] 43 | 44 | MIDDLEWARE = [ 45 | 'django.middleware.security.SecurityMiddleware', 46 | 'django.contrib.sessions.middleware.SessionMiddleware', 47 | 'django.middleware.common.CommonMiddleware', 48 | 'django.middleware.csrf.CsrfViewMiddleware', 49 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 50 | 'django.contrib.messages.middleware.MessageMiddleware', 51 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 52 | ] 53 | 54 | ROOT_URLCONF = 'online_test.urls' 55 | 56 | TEMPLATES = [ 57 | { 58 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 59 | 'DIRS': [], 60 | 'APP_DIRS': True, 61 | 'OPTIONS': { 62 | 'context_processors': [ 63 | 'django.template.context_processors.debug', 64 | 'django.template.context_processors.request', 65 | 'django.contrib.auth.context_processors.auth', 66 | 'django.contrib.messages.context_processors.messages', 67 | ], 68 | }, 69 | }, 70 | ] 71 | 72 | WSGI_APPLICATION = 'online_test.wsgi.application' 73 | 74 | 75 | # Database 76 | # https://docs.djangoproject.com/en/2.0/ref/settings/#databases 77 | 78 | DATABASES = { 79 | 'default': { 80 | 'ENGINE': 'django.db.backends.sqlite3', 81 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 82 | } 83 | } 84 | 85 | 86 | # Password validation 87 | # https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators 88 | 89 | AUTH_PASSWORD_VALIDATORS = [ 90 | { 91 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 92 | }, 93 | { 94 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 95 | }, 96 | { 97 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 98 | }, 99 | { 100 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 101 | }, 102 | ] 103 | 104 | 105 | # Internationalization 106 | # https://docs.djangoproject.com/en/2.0/topics/i18n/ 107 | 108 | LANGUAGE_CODE = 'en-us' 109 | 110 | TIME_ZONE = 'Asia/Kolkata' 111 | 112 | USE_I18N = True 113 | 114 | USE_L10N = True 115 | 116 | USE_TZ = True 117 | 118 | 119 | # Static files (CSS, JavaScript, Images) 120 | # https://docs.djangoproject.com/en/2.0/howto/static-files/ 121 | 122 | STATIC_URL = '/static/' 123 | 124 | -------------------------------------------------------------------------------- /quiz/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.7 on 2018-07-23 19:35 2 | 3 | from django.conf import settings 4 | import django.core.validators 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | import re 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | initial = True 13 | 14 | dependencies = [ 15 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 16 | ] 17 | 18 | operations = [ 19 | migrations.CreateModel( 20 | name='Category', 21 | fields=[ 22 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 23 | ('category', models.CharField(blank=True, max_length=250, null=True, unique=True, verbose_name='Category')), 24 | ], 25 | options={ 26 | 'verbose_name': 'Category', 27 | 'verbose_name_plural': 'Categories', 28 | }, 29 | ), 30 | migrations.CreateModel( 31 | name='Progress', 32 | fields=[ 33 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 34 | ('score', models.CharField(max_length=1024, validators=[django.core.validators.RegexValidator(re.compile('^\\d+(?:\\,\\d+)*\\Z'), code='invalid', message='Enter only digits separated by commas.')], verbose_name='Score')), 35 | ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User')), 36 | ], 37 | options={ 38 | 'verbose_name': 'User Progress', 39 | 'verbose_name_plural': 'User progress records', 40 | }, 41 | ), 42 | migrations.CreateModel( 43 | name='Question', 44 | fields=[ 45 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 46 | ('figure', models.ImageField(blank=True, null=True, upload_to='uploads/%Y/%m/%d', verbose_name='Figure')), 47 | ('content', models.CharField(help_text='Enter the question text that you want displayed', max_length=1000, verbose_name='Question')), 48 | ('explanation', models.TextField(blank=True, help_text='Explanation to be shown after the question has been answered.', max_length=2000, verbose_name='Explanation')), 49 | ('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='quiz.Category', verbose_name='Category')), 50 | ], 51 | options={ 52 | 'verbose_name': 'Question', 53 | 'verbose_name_plural': 'Questions', 54 | 'ordering': ['category'], 55 | }, 56 | ), 57 | migrations.CreateModel( 58 | name='Quiz', 59 | fields=[ 60 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 61 | ('title', models.CharField(max_length=60, verbose_name='Title')), 62 | ('description', models.TextField(blank=True, help_text='a description of the quiz', verbose_name='Description')), 63 | ('url', models.SlugField(help_text='a user friendly url', max_length=60, verbose_name='user friendly url')), 64 | ('random_order', models.BooleanField(default=False, help_text='Display the questions in a random order or as they are set?', verbose_name='Random Order')), 65 | ('max_questions', models.PositiveIntegerField(blank=True, help_text='Number of questions to be answered on each attempt.', null=True, verbose_name='Max Questions')), 66 | ('answers_at_end', models.BooleanField(default=False, help_text='Correct answer is NOT shown after question. Answers displayed at the end.', verbose_name='Answers at end')), 67 | ('exam_paper', models.BooleanField(default=False, help_text='If yes, the result of each attempt by a user will be stored. Necessary for marking.', verbose_name='Exam Paper')), 68 | ('single_attempt', models.BooleanField(default=False, help_text='If yes, only one attempt by a user will be permitted. Non users cannot sit this exam.', verbose_name='Single Attempt')), 69 | ('pass_mark', models.SmallIntegerField(blank=True, default=0, help_text='Percentage required to pass exam.', validators=[django.core.validators.MaxValueValidator(100)], verbose_name='Pass Mark')), 70 | ('success_text', models.TextField(blank=True, help_text='Displayed if user passes.', verbose_name='Success Text')), 71 | ('fail_text', models.TextField(blank=True, help_text='Displayed if user fails.', verbose_name='Fail Text')), 72 | ('draft', models.BooleanField(default=False, help_text='If yes, the quiz is not displayed in the quiz list and can only be taken by users who can edit quizzes.', verbose_name='Draft')), 73 | ('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='quiz.Category', verbose_name='Category')), 74 | ], 75 | options={ 76 | 'verbose_name': 'Quiz', 77 | 'verbose_name_plural': 'Quizzes', 78 | }, 79 | ), 80 | migrations.CreateModel( 81 | name='Sitting', 82 | fields=[ 83 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 84 | ('question_order', models.CharField(max_length=1024, validators=[django.core.validators.RegexValidator(re.compile('^\\d+(?:\\,\\d+)*\\Z'), code='invalid', message='Enter only digits separated by commas.')], verbose_name='Question Order')), 85 | ('question_list', models.CharField(max_length=1024, validators=[django.core.validators.RegexValidator(re.compile('^\\d+(?:\\,\\d+)*\\Z'), code='invalid', message='Enter only digits separated by commas.')], verbose_name='Question List')), 86 | ('incorrect_questions', models.CharField(blank=True, max_length=1024, validators=[django.core.validators.RegexValidator(re.compile('^\\d+(?:\\,\\d+)*\\Z'), code='invalid', message='Enter only digits separated by commas.')], verbose_name='Incorrect questions')), 87 | ('current_score', models.IntegerField(verbose_name='Current Score')), 88 | ('complete', models.BooleanField(default=False, verbose_name='Complete')), 89 | ('user_answers', models.TextField(blank=True, default='{}', verbose_name='User Answers')), 90 | ('start', models.DateTimeField(auto_now_add=True, verbose_name='Start')), 91 | ('end', models.DateTimeField(blank=True, null=True, verbose_name='End')), 92 | ('quiz', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='quiz.Quiz', verbose_name='Quiz')), 93 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User')), 94 | ], 95 | options={ 96 | 'permissions': (('view_sittings', 'Can see completed exams.'),), 97 | }, 98 | ), 99 | migrations.AddField( 100 | model_name='question', 101 | name='quiz', 102 | field=models.ManyToManyField(blank=True, to='quiz.Quiz', verbose_name='Quiz'), 103 | ), 104 | ] 105 | -------------------------------------------------------------------------------- /quiz/views.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from django.contrib.auth.decorators import login_required, permission_required 4 | from django.core.exceptions import PermissionDenied 5 | from django.shortcuts import get_object_or_404, render 6 | from django.utils.decorators import method_decorator 7 | from django.views.generic import DetailView, ListView, TemplateView 8 | from django.views.generic.edit import FormView 9 | from .forms import QuestionForm 10 | from .models import Quiz, Category, Progress, Sitting, Question 11 | from django.shortcuts import render, redirect 12 | from django.contrib.auth import authenticate, login, logout 13 | from django.contrib import messages 14 | 15 | 16 | class QuizMarkerMixin(object): 17 | @method_decorator(login_required) 18 | @method_decorator(permission_required('quiz.view_sittings')) 19 | def dispatch(self, *args, **kwargs): 20 | return super(QuizMarkerMixin, self).dispatch(*args, **kwargs) 21 | 22 | 23 | class SittingFilterTitleMixin(object): 24 | def get_queryset(self): 25 | queryset = super(SittingFilterTitleMixin, self).get_queryset() 26 | quiz_filter = self.request.GET.get('quiz_filter') 27 | if quiz_filter: 28 | queryset = queryset.filter(quiz__title__icontains=quiz_filter) 29 | 30 | return queryset 31 | 32 | 33 | class QuizListView(ListView): 34 | model = Quiz 35 | # @login_required 36 | def get_queryset(self): 37 | queryset = super(QuizListView, self).get_queryset() 38 | return queryset.filter(draft=False) 39 | 40 | 41 | class QuizDetailView(DetailView): 42 | model = Quiz 43 | slug_field = 'url' 44 | 45 | def get(self, request, *args, **kwargs): 46 | self.object = self.get_object() 47 | 48 | if self.object.draft and not request.user.has_perm('quiz.change_quiz'): 49 | raise PermissionDenied 50 | 51 | context = self.get_context_data(object=self.object) 52 | return self.render_to_response(context) 53 | 54 | 55 | class CategoriesListView(ListView): 56 | model = Category 57 | 58 | 59 | class ViewQuizListByCategory(ListView): 60 | model = Quiz 61 | template_name = 'view_quiz_category.html' 62 | 63 | def dispatch(self, request, *args, **kwargs): 64 | self.category = get_object_or_404( 65 | Category, 66 | category=self.kwargs['category_name'] 67 | ) 68 | 69 | return super(ViewQuizListByCategory, self).\ 70 | dispatch(request, *args, **kwargs) 71 | 72 | def get_context_data(self, **kwargs): 73 | context = super(ViewQuizListByCategory, self)\ 74 | .get_context_data(**kwargs) 75 | 76 | context['category'] = self.category 77 | return context 78 | 79 | def get_queryset(self): 80 | queryset = super(ViewQuizListByCategory, self).get_queryset() 81 | return queryset.filter(category=self.category, draft=False) 82 | 83 | 84 | class QuizUserProgressView(TemplateView): 85 | template_name = 'progress.html' 86 | 87 | @method_decorator(login_required) 88 | def dispatch(self, request, *args, **kwargs): 89 | return super(QuizUserProgressView, self)\ 90 | .dispatch(request, *args, **kwargs) 91 | 92 | def get_context_data(self, **kwargs): 93 | context = super(QuizUserProgressView, self).get_context_data(**kwargs) 94 | progress, c = Progress.objects.get_or_create(user=self.request.user) 95 | context['cat_scores'] = progress.list_all_cat_scores 96 | context['exams'] = progress.show_exams() 97 | return context 98 | 99 | 100 | class QuizMarkingList(QuizMarkerMixin, SittingFilterTitleMixin, ListView): 101 | model = Sitting 102 | 103 | def get_queryset(self): 104 | queryset = super(QuizMarkingList, self).get_queryset()\ 105 | .filter(complete=True) 106 | 107 | user_filter = self.request.GET.get('user_filter') 108 | if user_filter: 109 | queryset = queryset.filter(user__username__icontains=user_filter) 110 | 111 | return queryset 112 | 113 | class Meta: 114 | pass 115 | 116 | 117 | class QuizMarkingDetail(QuizMarkerMixin, DetailView): 118 | model = Sitting 119 | 120 | def post(self, request, *args, **kwargs): 121 | sitting = self.get_object() 122 | 123 | q_to_toggle = request.POST.get('qid', None) 124 | if q_to_toggle: 125 | q = Question.objects.get_subclass(id=int(q_to_toggle)) 126 | if int(q_to_toggle) in sitting.get_incorrect_questions: 127 | sitting.remove_incorrect_question(q) 128 | else: 129 | sitting.add_incorrect_question(q) 130 | 131 | return self.get(request) 132 | 133 | def get_context_data(self, **kwargs): 134 | context = super(QuizMarkingDetail, self).get_context_data(**kwargs) 135 | context['questions'] =\ 136 | context['sitting'].get_questions(with_answers=True) 137 | return context 138 | 139 | 140 | class QuizTake(FormView): 141 | form_class = QuestionForm 142 | template_name = 'question.html' 143 | 144 | def dispatch(self, request, *args, **kwargs): 145 | self.quiz = get_object_or_404(Quiz, url=self.kwargs['quiz_name']) 146 | if self.quiz.draft and not request.user.has_perm('quiz.change_quiz'): 147 | raise PermissionDenied 148 | 149 | self.logged_in_user = self.request.user.is_authenticated 150 | 151 | if self.logged_in_user: 152 | self.sitting = Sitting.objects.user_sitting(request.user, 153 | self.quiz) 154 | if self.sitting is False: 155 | return render(request, 'single_complete.html') 156 | 157 | return super(QuizTake, self).dispatch(request, *args, **kwargs) 158 | 159 | def get_form(self, form_class=QuestionForm): 160 | if self.logged_in_user: 161 | self.question = self.sitting.get_first_question() 162 | self.progress = self.sitting.progress() 163 | return form_class(**self.get_form_kwargs()) 164 | 165 | def get_form_kwargs(self): 166 | kwargs = super(QuizTake, self).get_form_kwargs() 167 | 168 | return dict(kwargs, question=self.question) 169 | 170 | def form_valid(self, form): 171 | if self.logged_in_user: 172 | self.form_valid_user(form) 173 | if self.sitting.get_first_question() is False: 174 | return self.final_result_user() 175 | self.request.POST = {} 176 | 177 | return super(QuizTake, self).get(self, self.request) 178 | 179 | def get_context_data(self, **kwargs): 180 | context = super(QuizTake, self).get_context_data(**kwargs) 181 | context['question'] = self.question 182 | context['quiz'] = self.quiz 183 | if hasattr(self, 'previous'): 184 | context['previous'] = self.previous 185 | if hasattr(self, 'progress'): 186 | context['progress'] = self.progress 187 | return context 188 | 189 | def form_valid_user(self, form): 190 | progress, c = Progress.objects.get_or_create(user=self.request.user) 191 | guess = form.cleaned_data['answers'] 192 | is_correct = self.question.check_if_correct(guess) 193 | 194 | if is_correct is True: 195 | self.sitting.add_to_score(1) 196 | progress.update_score(self.question, 1, 1) 197 | else: 198 | self.sitting.add_incorrect_question(self.question) 199 | progress.update_score(self.question, 0, 1) 200 | 201 | if self.quiz.answers_at_end is not True: 202 | self.previous = {'previous_answer': guess, 203 | 'previous_outcome': is_correct, 204 | 'previous_question': self.question, 205 | 'answers': self.question.get_answers(), 206 | 'question_type': {self.question 207 | .__class__.__name__: True}} 208 | else: 209 | self.previous = {} 210 | 211 | self.sitting.add_user_answer(self.question, guess) 212 | self.sitting.remove_first_question() 213 | 214 | def final_result_user(self): 215 | results = { 216 | 'quiz': self.quiz, 217 | 'score': self.sitting.get_current_score, 218 | 'max_score': self.sitting.get_max_score, 219 | 'percent': self.sitting.get_percent_correct, 220 | 'sitting': self.sitting, 221 | 'previous': self.previous, 222 | } 223 | 224 | self.sitting.mark_quiz_complete() 225 | 226 | if self.quiz.answers_at_end: 227 | results['questions'] =\ 228 | self.sitting.get_questions(with_answers=True) 229 | results['incorrect_questions'] =\ 230 | self.sitting.get_incorrect_questions 231 | 232 | if self.quiz.exam_paper is False: 233 | self.sitting.delete() 234 | 235 | return render(self.request, 'result.html', results) 236 | 237 | 238 | 239 | 240 | def index(request): 241 | return render(request, 'index.html', {}) 242 | 243 | 244 | def login_user(request): 245 | 246 | if request.method == 'POST': 247 | username = request.POST['username'] 248 | password = request.POST['password'] 249 | user = authenticate(request, username=username, password=password) 250 | if user is not None: 251 | login(request, user) 252 | messages.success(request, 'You have successfully logged in') 253 | return redirect("index") 254 | else: 255 | messages.success(request, 'Error logging in') 256 | return redirect('login') 257 | else: 258 | return render(request, 'login.html', {}) 259 | 260 | 261 | def logout_user(request): 262 | logout(request) 263 | messages.success(request, 'You have been logged out!') 264 | print('logout function working') 265 | return redirect('login') 266 | 267 | -------------------------------------------------------------------------------- /quiz/models.py: -------------------------------------------------------------------------------- 1 | import re 2 | import json 3 | import csv 4 | from django.db import models 5 | from django.core.exceptions import ValidationError, ImproperlyConfigured 6 | from django.core.validators import MaxValueValidator, validate_comma_separated_integer_list 7 | from django.utils.timezone import now 8 | from django.conf import settings 9 | from django.utils.translation import ugettext as _ 10 | from model_utils.managers import InheritanceManager 11 | from django.db.models.signals import pre_save, post_save 12 | import io 13 | from .signals import csv_uploaded 14 | from .validators import csv_file_validator 15 | from django.contrib.auth.models import User 16 | from django.contrib import messages 17 | 18 | 19 | class CategoryManager(models.Manager): 20 | 21 | def new_category(self, category): 22 | new_category = self.create(category=re.sub('\s+', '-', category) 23 | .lower()) 24 | 25 | new_category.save() 26 | return new_category 27 | 28 | 29 | class Category(models.Model): 30 | 31 | category = models.CharField( 32 | verbose_name=_("Category"), 33 | max_length=250, blank=True, 34 | unique=True, null=True) 35 | 36 | objects = CategoryManager() 37 | 38 | class Meta: 39 | verbose_name = _("Category") 40 | verbose_name_plural = _("Categories") 41 | 42 | def __str__(self): 43 | return self.category 44 | 45 | 46 | class Quiz(models.Model): 47 | 48 | title = models.CharField( 49 | verbose_name=_("Title"), 50 | max_length=60, blank=False) 51 | 52 | description = models.TextField( 53 | verbose_name=_("Description"), 54 | blank=True, help_text=_("a description of the quiz")) 55 | 56 | url = models.SlugField( 57 | max_length=60, blank=False, 58 | help_text=_("a user friendly url"), 59 | verbose_name=_("user friendly url")) 60 | 61 | category = models.ForeignKey( 62 | Category, null=True, blank=True, 63 | verbose_name=_("Category"), on_delete=models.CASCADE) 64 | 65 | random_order = models.BooleanField( 66 | blank=False, default=False, 67 | verbose_name=_("Random Order"), 68 | help_text=_("Display the questions in " 69 | "a random order or as they " 70 | "are set?")) 71 | 72 | max_questions = models.PositiveIntegerField( 73 | blank=True, null=True, verbose_name=_("Max Questions"), 74 | help_text=_("Number of questions to be answered on each attempt.")) 75 | 76 | answers_at_end = models.BooleanField( 77 | blank=False, default=False, 78 | help_text=_("Correct answer is NOT shown after question." 79 | " Answers displayed at the end."), 80 | verbose_name=_("Answers at end")) 81 | 82 | exam_paper = models.BooleanField( 83 | blank=False, default=False, 84 | help_text=_("If yes, the result of each" 85 | " attempt by a user will be" 86 | " stored. Necessary for marking."), 87 | verbose_name=_("Exam Paper")) 88 | 89 | single_attempt = models.BooleanField( 90 | blank=False, default=False, 91 | help_text=_("If yes, only one attempt by" 92 | " a user will be permitted." 93 | " Non users cannot sit this exam."), 94 | verbose_name=_("Single Attempt")) 95 | 96 | pass_mark = models.SmallIntegerField( 97 | blank=True, default=0, 98 | verbose_name=_("Pass Mark"), 99 | help_text=_("Percentage required to pass exam."), 100 | validators=[MaxValueValidator(100)]) 101 | 102 | success_text = models.TextField( 103 | blank=True, help_text=_("Displayed if user passes."), 104 | verbose_name=_("Success Text")) 105 | 106 | fail_text = models.TextField( 107 | verbose_name=_("Fail Text"), 108 | blank=True, help_text=_("Displayed if user fails.")) 109 | 110 | draft = models.BooleanField( 111 | blank=True, default=False, 112 | verbose_name=_("Draft"), 113 | help_text=_("If yes, the quiz is not displayed" 114 | " in the quiz list and can only be" 115 | " taken by users who can edit" 116 | " quizzes.")) 117 | 118 | def save(self, force_insert=False, force_update=False, *args, **kwargs): 119 | self.url = re.sub('\s+', '-', self.url).lower() 120 | 121 | self.url = ''.join(letter for letter in self.url if 122 | letter.isalnum() or letter == '-') 123 | 124 | if self.single_attempt is True: 125 | self.exam_paper = True 126 | 127 | if self.pass_mark > 100: 128 | raise ValidationError('%s is above 100' % self.pass_mark) 129 | 130 | super(Quiz, self).save(force_insert, force_update, *args, **kwargs) 131 | 132 | class Meta: 133 | verbose_name = _("Quiz") 134 | verbose_name_plural = _("Quizzes") 135 | 136 | def __str__(self): 137 | return self.title 138 | 139 | def get_questions(self): 140 | return self.question_set.all().select_subclasses() 141 | 142 | @property 143 | def get_max_score(self): 144 | return self.get_questions().count() 145 | 146 | def anon_score_id(self): 147 | return str(self.id) + "_score" 148 | 149 | def anon_q_list(self): 150 | return str(self.id) + "_q_list" 151 | 152 | def anon_q_data(self): 153 | return str(self.id) + "_data" 154 | 155 | 156 | # progress manager 157 | class ProgressManager(models.Manager): 158 | 159 | def new_progress(self, user): 160 | new_progress = self.create(user=user, 161 | score="") 162 | new_progress.save() 163 | return new_progress 164 | 165 | 166 | class Progress(models.Model): 167 | """ 168 | Progress is used to track an individual signed in users score on different 169 | quiz's and categories 170 | Data stored in csv using the format: 171 | category, score, possible, category, score, possible, ... 172 | """ 173 | user = models.OneToOneField(settings.AUTH_USER_MODEL, verbose_name=_("User"), on_delete=models.CASCADE) 174 | 175 | score = models.CharField(validators=[validate_comma_separated_integer_list], max_length=1024, 176 | verbose_name=_("Score")) 177 | 178 | correct_answer = models.CharField(max_length=10, verbose_name=_('Correct Answers')) 179 | 180 | wrong_answer = models.CharField(max_length=10, verbose_name=_('Wrong Answers')) 181 | 182 | objects = ProgressManager() 183 | 184 | class Meta: 185 | verbose_name = _("User Progress") 186 | verbose_name_plural = _("User progress records") 187 | 188 | @property 189 | def list_all_cat_scores(self): 190 | """ 191 | Returns a dict in which the key is the category name and the item is 192 | a list of three integers. 193 | The first is the number of questions correct, 194 | the second is the possible best score, 195 | the third is the percentage correct. 196 | The dict will have one key for every category that you have defined 197 | """ 198 | score_before = self.score 199 | output = {} 200 | 201 | for cat in Category.objects.all(): 202 | to_find = re.escape(cat.category) + r",(\d+),(\d+)," 203 | # group 1 is score, group 2 is highest possible 204 | 205 | match = re.search(to_find, self.score, re.IGNORECASE) 206 | 207 | if match: 208 | score = int(match.group(1)) 209 | possible = int(match.group(2)) 210 | 211 | try: 212 | percent = int(round((float(score) / float(possible)) 213 | * 100)) 214 | except: 215 | percent = 0 216 | 217 | output[cat.category] = [score, possible, percent] 218 | 219 | else: # if category has not been added yet, add it. 220 | self.score += cat.category + ",0,0," 221 | output[cat.category] = [0, 0] 222 | 223 | if len(self.score) > len(score_before): 224 | # If a new category has been added, save changes. 225 | self.save() 226 | 227 | return output 228 | 229 | def update_score(self, question, score_to_add=0, possible_to_add=0): 230 | """ 231 | Pass in question object, amount to increase score 232 | and max possible. 233 | Does not return anything. 234 | """ 235 | category_test = Category.objects.filter(category=question.category)\ 236 | .exists() 237 | 238 | if any([item is False for item in [category_test, 239 | score_to_add, 240 | possible_to_add, 241 | isinstance(score_to_add, int), 242 | isinstance(possible_to_add, int)]]): 243 | return _("error"), _("category does not exist or invalid score") 244 | 245 | to_find = re.escape(str(question.category)) +\ 246 | r",(?P\d+),(?P\d+)," 247 | 248 | match = re.search(to_find, self.score, re.IGNORECASE) 249 | 250 | if match: 251 | updated_score = int(match.group('score')) + abs(score_to_add) 252 | updated_possible = int(match.group('possible')) +\ 253 | abs(possible_to_add) 254 | 255 | new_score = ",".join( 256 | [ 257 | str(question.category), 258 | str(updated_score), 259 | str(updated_possible), "" 260 | ]) 261 | 262 | # swap old score for the new one 263 | self.score = self.score.replace(match.group(), new_score) 264 | self.save() 265 | 266 | else: 267 | # if not present but existing, add with the points passed in 268 | self.score += ",".join( 269 | [ 270 | str(question.category), 271 | str(score_to_add), 272 | str(possible_to_add), 273 | "" 274 | ]) 275 | self.save() 276 | 277 | def show_exams(self): 278 | """ 279 | Finds the previous quizzes marked as 'exam papers'. 280 | Returns a queryset of complete exams. 281 | """ 282 | return Sitting.objects.filter(user=self.user, complete=True) 283 | 284 | def __str__(self): 285 | return self.user.username + ' - ' + self.score 286 | 287 | 288 | class SittingManager(models.Manager): 289 | 290 | def new_sitting(self, user, quiz): 291 | if quiz.random_order is True: 292 | question_set = quiz.question_set.all() \ 293 | .select_subclasses() \ 294 | .order_by('?') 295 | else: 296 | question_set = quiz.question_set.all() \ 297 | .select_subclasses() 298 | 299 | question_set = [item.id for item in question_set] 300 | 301 | if len(question_set) == 0: 302 | raise ImproperlyConfigured('Question set of the quiz is empty. ' 303 | 'Please configure questions properly') 304 | 305 | if quiz.max_questions and quiz.max_questions < len(question_set): 306 | question_set = question_set[:quiz.max_questions] 307 | 308 | questions = ",".join(map(str, question_set)) + "," 309 | 310 | new_sitting = self.create(user=user, 311 | quiz=quiz, 312 | question_order=questions, 313 | question_list=questions, 314 | incorrect_questions="", 315 | current_score=0, 316 | complete=False, 317 | user_answers='{}') 318 | return new_sitting 319 | 320 | def user_sitting(self, user, quiz): 321 | if quiz.single_attempt is True and self.filter(user=user, 322 | quiz=quiz, 323 | complete=True)\ 324 | .exists(): 325 | return False 326 | 327 | try: 328 | sitting = self.get(user=user, quiz=quiz, complete=False) 329 | except Sitting.DoesNotExist: 330 | sitting = self.new_sitting(user, quiz) 331 | except Sitting.MultipleObjectsReturned: 332 | sitting = self.filter(user=user, quiz=quiz, complete=False)[0] 333 | return sitting 334 | 335 | 336 | class Sitting(models.Model): 337 | """ 338 | Used to store the progress of logged in users sitting a quiz. 339 | Replaces the session system used by anon users. 340 | Question_order is a list of integer pks of all the questions in the 341 | quiz, in order. 342 | Question_list is a list of integers which represent id's of 343 | the unanswered questions in csv format. 344 | Incorrect_questions is a list in the same format. 345 | Sitting deleted when quiz finished unless quiz.exam_paper is true. 346 | User_answers is a json object in which the question PK is stored 347 | with the answer the user gave. 348 | """ 349 | 350 | user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_("User"), on_delete=models.CASCADE) 351 | 352 | quiz = models.ForeignKey(Quiz, verbose_name=_("Quiz"), on_delete=models.CASCADE) 353 | 354 | question_order = models.CharField(validators=[validate_comma_separated_integer_list], 355 | max_length=1024, verbose_name=_("Question Order")) 356 | 357 | question_list = models.CharField(validators=[validate_comma_separated_integer_list], 358 | max_length=1024, verbose_name=_("Question List")) 359 | 360 | incorrect_questions = models.CharField(validators=[validate_comma_separated_integer_list], 361 | max_length=1024, blank=True, verbose_name=_("Incorrect questions")) 362 | 363 | current_score = models.IntegerField(verbose_name=_("Current Score")) 364 | 365 | complete = models.BooleanField(default=False, blank=False, 366 | verbose_name=_("Complete")) 367 | 368 | user_answers = models.TextField(blank=True, default='{}', 369 | verbose_name=_("User Answers")) 370 | 371 | start = models.DateTimeField(auto_now_add=True, 372 | verbose_name=_("Start")) 373 | 374 | end = models.DateTimeField(null=True, blank=True, verbose_name=_("End")) 375 | 376 | objects = SittingManager() 377 | 378 | class Meta: 379 | permissions = (("view_sittings", _("Can see completed exams.")),) 380 | 381 | def get_first_question(self): 382 | """ 383 | Returns the next question. 384 | If no question is found, returns False 385 | Does NOT remove the question from the front of the list. 386 | """ 387 | if not self.question_list: 388 | return False 389 | 390 | first, _ = self.question_list.split(',', 1) 391 | question_id = int(first) 392 | return Question.objects.get_subclass(id=question_id) 393 | 394 | def remove_first_question(self): 395 | if not self.question_list: 396 | return 397 | 398 | _, others = self.question_list.split(',', 1) 399 | self.question_list = others 400 | self.save() 401 | 402 | def add_to_score(self, points): 403 | self.current_score += int(points) 404 | self.save() 405 | 406 | @property 407 | def get_current_score(self): 408 | return self.current_score 409 | 410 | def _question_ids(self): 411 | return [int(n) for n in self.question_order.split(',') if n] 412 | 413 | @property 414 | def get_percent_correct(self): 415 | dividend = float(self.current_score) 416 | divisor = len(self._question_ids()) 417 | if divisor < 1: 418 | return 0 # prevent divide by zero error 419 | 420 | if dividend > divisor: 421 | return 100 422 | 423 | correct = int(round((dividend / divisor) * 100)) 424 | 425 | if correct >= 1: 426 | return correct 427 | else: 428 | return 0 429 | 430 | def mark_quiz_complete(self): 431 | self.complete = True 432 | self.end = now() 433 | self.save() 434 | 435 | def add_incorrect_question(self, question): 436 | """ 437 | Adds uid of incorrect question to the list. 438 | The question object must be passed in. 439 | """ 440 | if len(self.incorrect_questions) > 0: 441 | self.incorrect_questions += ',' 442 | self.incorrect_questions += str(question.id) + "," 443 | if self.complete: 444 | self.add_to_score(-1) 445 | self.save() 446 | 447 | @property 448 | def get_incorrect_questions(self): 449 | """ 450 | Returns a list of non empty integers, representing the pk of 451 | questions 452 | """ 453 | return [int(q) for q in self.incorrect_questions.split(',') if q] 454 | 455 | def remove_incorrect_question(self, question): 456 | current = self.get_incorrect_questions 457 | current.remove(question.id) 458 | self.incorrect_questions = ','.join(map(str, current)) 459 | self.add_to_score(1) 460 | self.save() 461 | 462 | @property 463 | def check_if_passed(self): 464 | return self.get_percent_correct >= self.quiz.pass_mark 465 | 466 | @property 467 | def result_message(self): 468 | if self.check_if_passed: 469 | return self.quiz.success_text 470 | else: 471 | return self.quiz.fail_text 472 | 473 | def add_user_answer(self, question, guess): 474 | current = json.loads(self.user_answers) 475 | current[question.id] = guess 476 | self.user_answers = json.dumps(current) 477 | self.save() 478 | 479 | def get_questions(self, with_answers=False): 480 | question_ids = self._question_ids() 481 | questions = sorted( 482 | self.quiz.question_set.filter(id__in=question_ids) 483 | .select_subclasses(), 484 | key=lambda q: question_ids.index(q.id)) 485 | 486 | if with_answers: 487 | user_answers = json.loads(self.user_answers) 488 | for question in questions: 489 | question.user_answer = user_answers[str(question.id)] 490 | 491 | return questions 492 | 493 | @property 494 | def questions_with_user_answers(self): 495 | return { 496 | q: q.user_answer for q in self.get_questions(with_answers=True) 497 | } 498 | 499 | @property 500 | def get_max_score(self): 501 | return len(self._question_ids()) 502 | 503 | def progress(self): 504 | """ 505 | Returns the number of questions answered so far and the total number of 506 | questions. 507 | """ 508 | answered = len(json.loads(self.user_answers)) 509 | total = self.get_max_score 510 | return answered, total 511 | 512 | 513 | class Question(models.Model): 514 | """ 515 | Base class for all question types. 516 | Shared properties placed here. 517 | """ 518 | 519 | quiz = models.ManyToManyField(Quiz, 520 | verbose_name=_("Quiz"), 521 | blank=True) 522 | 523 | category = models.ForeignKey(Category, 524 | verbose_name=_("Category"), 525 | blank=True, 526 | null=True, on_delete=models.CASCADE) 527 | 528 | figure = models.ImageField(upload_to='uploads/%Y/%m/%d', 529 | blank=True, 530 | null=True, 531 | verbose_name=_("Figure")) 532 | 533 | content = models.CharField(max_length=1000, 534 | blank=False, 535 | help_text=_("Enter the question text that " 536 | "you want displayed"), 537 | verbose_name=_('Question')) 538 | 539 | explanation = models.TextField(max_length=2000, 540 | blank=True, 541 | help_text=_("Explanation to be shown " 542 | "after the question has " 543 | "been answered."), 544 | verbose_name=_('Explanation')) 545 | 546 | objects = InheritanceManager() 547 | 548 | class Meta: 549 | verbose_name = _("Question") 550 | verbose_name_plural = _("Questions") 551 | ordering = ['category'] 552 | 553 | def __str__(self): 554 | return self.content 555 | 556 | 557 | def upload_csv_file(instance, filename): 558 | qs = instance.__class__.objects.filter(user=instance.user) 559 | if qs.exists(): 560 | num_ = qs.last().id + 1 561 | else: 562 | num_ = 1 563 | return f'csv/{num_}/{instance.user.username}/{filename}' 564 | 565 | 566 | class CSVUpload(models.Model): 567 | title = models.CharField(max_length=100, verbose_name=_('Title'), blank=False) 568 | user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) 569 | file = models.FileField(upload_to=upload_csv_file, validators=[csv_file_validator]) 570 | completed = models.BooleanField(default=False) 571 | # questions = models.BooleanField(default=True) 572 | # students = models.BooleanField(default=False) 573 | 574 | def __str__(self): 575 | return self.user.username 576 | 577 | def create_user(data): 578 | user = User.objects.create_user(username=data['username'], 579 | email=data['email'], 580 | password=data['password'], 581 | first_name=data['first_name'], 582 | last_name=data['last_name'] 583 | ) 584 | user.is_admin=False 585 | user.is_staff=False 586 | user.save() 587 | 588 | 589 | def convert_header(csvHeader): 590 | header_ = csvHeader[0] 591 | cols = [x.replace(' ', '_').lower() for x in header_.split(",")] 592 | return cols 593 | 594 | 595 | def csv_upload_post_save(sender, instance, created, *args, **kwargs): 596 | if not instance.completed: 597 | csv_file = instance.file 598 | decoded_file = csv_file.read().decode('utf-8') 599 | io_string = io.StringIO(decoded_file) 600 | reader = csv.reader(io_string, delimiter=';', quotechar='|') 601 | header_ = next(reader) 602 | header_cols = convert_header(header_) 603 | print(header_cols, str(len(header_cols))) 604 | parsed_items = [] 605 | 606 | ''' 607 | if using a custom signal 608 | ''' 609 | for line in reader: 610 | # print(line) 611 | parsed_row_data = {} 612 | i = 0 613 | print(line[0].split(','), len(line[0].split(','))) 614 | row_item = line[0].split(',') 615 | for item in row_item: 616 | key = header_cols[i] 617 | parsed_row_data[key] = item 618 | i+=1 619 | create_user(parsed_row_data) # create user 620 | parsed_items.append(parsed_row_data) 621 | # messages.success(parsed_items) 622 | print(parsed_items) 623 | csv_uploaded.send(sender=instance, user=instance.user, csv_file_list=parsed_items) 624 | ''' 625 | if using a model directly 626 | for line in reader: 627 | new_obj = YourModelKlass() 628 | i = 0 629 | row_item = line[0].split(',') 630 | for item in row_item: 631 | key = header_cols[i] 632 | setattr(new_obj, key) = item 633 | i+=1 634 | new_obj.save() 635 | ''' 636 | instance.completed = True 637 | instance.save() 638 | 639 | 640 | post_save.connect(csv_upload_post_save, sender=CSVUpload) 641 | 642 | 643 | --------------------------------------------------------------------------------