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 |

60 | {% endif %}
61 |
62 |
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 |
59 |
62 | {{message}}
63 |
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 |
--------------------------------------------------------------------------------