diff options
author | Palaparthy Adityachandra | 2021-03-24 16:12:33 +0530 |
---|---|---|
committer | GitHub | 2021-03-24 16:12:33 +0530 |
commit | f8504f1077a532d0978fa1069c6eeb3c1912e939 (patch) | |
tree | 3b15ae27d9c6e6599fe8025ebc95075fb253c58a /yaksh | |
parent | f44cbc461f4c23c08d655749ccb525d99f2e7dbc (diff) | |
parent | 04684f6c636ed827a8ff029a8c0d9b7755096599 (diff) | |
download | online_test-f8504f1077a532d0978fa1069c6eeb3c1912e939.tar.gz online_test-f8504f1077a532d0978fa1069c6eeb3c1912e939.tar.bz2 online_test-f8504f1077a532d0978fa1069c6eeb3c1912e939.zip |
Merge pull request #807 from prathamesh920/add-test-cases-for-QRcode-based-upload
Add test cases for qrcode based upload
Diffstat (limited to 'yaksh')
-rw-r--r-- | yaksh/models.py | 87 | ||||
-rw-r--r-- | yaksh/templates/yaksh/question.html | 36 | ||||
-rw-r--r-- | yaksh/templates/yaksh/upload_file.html | 78 | ||||
-rw-r--r-- | yaksh/test_models.py | 163 | ||||
-rw-r--r-- | yaksh/urls.py | 5 | ||||
-rw-r--r-- | yaksh/views.py | 157 |
6 files changed, 510 insertions, 16 deletions
diff --git a/yaksh/models.py b/yaksh/models.py index 51c2aa8..8f9f051 100644 --- a/yaksh/models.py +++ b/yaksh/models.py @@ -26,9 +26,11 @@ import tempfile from textwrap import dedent from ast import literal_eval import pandas as pd +import qrcode +import hashlib # Django Imports -from django.db import models +from django.db import models, IntegrityError from django.contrib.auth.models import User, Group, Permission from django.core.exceptions import ValidationError from django.contrib.contenttypes.models import ContentType @@ -2267,7 +2269,7 @@ class AnswerPaper(models.Model): percent = models.FloatField(null=True, default=0.0) # Result of the quiz, True if student passes the exam. - passed = models.NullBooleanField() + passed = models.BooleanField(null=True) # Status of the quiz attempt status = models.CharField( @@ -3312,3 +3314,84 @@ class MicroManager(models.Model): def __str__(self): return 'MicroManager for {0} - {1}'.format(self.student.username, self.course.name) + + +class QRcode(models.Model): + random_key = models.CharField(max_length=128, blank=True) + short_key = models.CharField(max_length=128, null=True, unique=True) + image = models.ImageField(upload_to='qrcode', blank=True) + used = models.BooleanField(default=False) + active = models.BooleanField(default=False) + handler = models.ForeignKey('QRcodeHandler', on_delete=models.CASCADE) + + def __str__(self): + return 'QRcode {0}'.format(self.short_key) + + def is_active(self): + return self.active + + def is_used(self): + return self.used + + def deactivate(self): + self.active = False + + def activate(self): + self.active = True + + def set_used(self): + self.used = True + + def set_random_key(self): + key = hashlib.sha1('{0}'.format(self.id).encode()).hexdigest() + self.random_key = key + + def set_short_key(self): + key = self.random_key + if key: + num = 5 + for i in range(40): + try: + self.short_key = key[0:num] + self.save() + break + except IntegrityError: + num = num + 1 + + def is_qrcode_available(self): + return self.active and not self.used and self.image is not None + + def generate_image(self, content): + img = qrcode.make(content) + qr_dir = os.path.join(settings.MEDIA_ROOT, 'qrcode') + if not os.path.exists(qr_dir): + os.makedirs(qr_dir) + path = os.path.join(qr_dir, f'{self.short_key}.png') + img.save(path) + self.image = os.path.join('qrcode', '{0}.png'.format(self.short_key)) + self.activate() + + +class QRcodeHandler(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE) + answerpaper = models.ForeignKey(AnswerPaper, on_delete=models.CASCADE) + question = models.ForeignKey(Question, on_delete=models.CASCADE) + + def __str__(self): + return 'QRcode Handler for {0}'.format(self.user.username) + + def get_qrcode(self): + qrcodes = self.qrcode_set.filter(active=True, used=False) + if qrcodes.exists(): + return qrcodes.last() + else: + return self._create_qrcode() + + def _create_qrcode(self): + qrcode = QRcode.objects.create(handler=self) + qrcode.set_random_key() + qrcode.set_short_key() + return qrcode + + def can_use(self): + return self.answerpaper.is_attempt_inprogress() diff --git a/yaksh/templates/yaksh/question.html b/yaksh/templates/yaksh/question.html index 2779db2..f8f0c46 100644 --- a/yaksh/templates/yaksh/question.html +++ b/yaksh/templates/yaksh/question.html @@ -270,15 +270,6 @@ question_type = "{{ question.type }}"; <!-- Upload type question --> {% if question.type == "upload" %} - <p>Upload assignment file for the said question<p> - <div class="dropzone needsclick dz-clickable" id="dropzone_file"> - <div class="dz-message needsclick"> - <button type="button" class="dz-button"> - Drop files here or click to upload. - </button> - </div> - </div> - <br> {% if assignment_files %} <div> <ul class="list-group"> @@ -290,6 +281,32 @@ question_type = "{{ question.type }}"; </ul> </div> {% endif %} + <br /> + <div class="row"> + <div class="col-md-8"> + <p>Upload assignment file for the said question. + <br> + <span class="badge badge-primary"> + </u> <strong>You can upload using the file browser below or via the QR code.</strong></u> + </span> + </p> + <div class="dropzone needsclick dz-clickable" id="dropzone_file"> + <div class="dz-message needsclick"> + <button type="button" class="dz-button"> + Drop files here or click to upload. + </button> + </div> + </div> + </div> + <div class="col-md-4"> + {% if qrcode %} + <img src="{{ qrcode.image.url }}" width="200" height="200"> + {% else %} + <a class="active btn btn-outline-primary " href="{% url 'yaksh:generate_qrcode' paper.id question.id module.id %}" data-toggle="tooltip" + title="Upload from any device using the QR Code">Generate QR Code</a> + {% endif %} + </div> + </div> {% endif %} <!-- Arrange type question --> @@ -370,7 +387,6 @@ question_type = "{{ question.type }}"; {% if question.type == 'code' or question.type == 'upload' %} <div id="error_panel"></div> {% endif %} - <!-- Modal --> <div class="modal" id="upload_alert" > <div class="modal-dialog"> diff --git a/yaksh/templates/yaksh/upload_file.html b/yaksh/templates/yaksh/upload_file.html new file mode 100644 index 0000000..e25e8e7 --- /dev/null +++ b/yaksh/templates/yaksh/upload_file.html @@ -0,0 +1,78 @@ +{% load static %} +<html> +<title> Upload File </title> +<script language="JavaScript" type="text/javascript" src="{% static 'yaksh/js/jquery-3.3.1.min.js' %}"></script> +<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/dropzone/5.8.1/dropzone.min.css"> +<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/dropzone/5.8.1/basic.min.css"> +<script src="https://cdnjs.cloudflare.com/ajax/libs/dropzone/5.8.1/min/dropzone.min.js"></script> +<style> +div, input, button { + font-size: x-large; + text-align: center; +} +</style> +<div> +{% if success %} + <p> {{ msg }}</p> +{% else %} + <form id="code" action="{% url 'yaksh:upload_file' key %}" method="post" enctype="multipart/form-data"> + {% csrf_token %} + <h3>Upload assignment file for {{ question.summary }}</h3> + <div class="dropzone needsclick dz-clickable" id="dropzone_file"> + <div class="dz-message needsclick"> + <button type="button" class="dz-button"> + Drop files here or click to upload. + </button> + </div> + </div> + <br> + <button class="btn btn-success" type="submit" name="check" id="check">Upload</button> + </form> +{% endif %} +</div> +</html> +<script> + Dropzone.autoDiscover = false; + var submitfiles; + $(document).ready(function(){ + var filezone = $("div#dropzone_file").dropzone({ + url: $("#code").attr("action"), + parallelUploads: 10, + uploadMultiple: true, + maxFiles:20, + paramName: "assignment", + autoProcessQueue: false, + init: function() { + var submitButton = document.querySelector("#check"); + myDropzone = this; + submitButton.addEventListener("click", function(e) { + if (myDropzone.getQueuedFiles().length === 0) { + alert("Please select a file and upload"); + e.preventDefault(); + return; + } + if (myDropzone.getAcceptedFiles().length > 0) { + if (submitfiles === true) { + submitfiles = false; + return; + } + e.preventDefault(); + myDropzone.processQueue(); + myDropzone.on("complete", function () { + submitfiles = true; + $('#check').trigger('click'); + }); + } + }); + }, + success: function (file, response) { + document.open(); + document.write(response); + document.close(); + }, + headers: { + "X-CSRFToken": document.getElementById("code").elements[0].value + } + }); + }); +</script> diff --git a/yaksh/test_models.py b/yaksh/test_models.py index 3b2fb4f..c805c52 100644 --- a/yaksh/test_models.py +++ b/yaksh/test_models.py @@ -6,7 +6,8 @@ from yaksh.models import User, Profile, Question, Quiz, QuestionPaper,\ QuestionSet, AnswerPaper, Answer, Course, StandardTestCase,\ StdIOBasedTestCase, FileUpload, McqTestCase, AssignmentUpload,\ LearningModule, LearningUnit, Lesson, LessonFile, CourseStatus, \ - create_group, legend_display_types, Post, Comment, MicroManager + create_group, legend_display_types, Post, Comment, MicroManager, QRcode, \ + QRcodeHandler from yaksh.code_server import ( ServerPool, get_result as get_result_from_code_server ) @@ -23,6 +24,7 @@ import zipfile import os import shutil import tempfile +import hashlib from threading import Thread from collections import defaultdict from yaksh import settings @@ -123,7 +125,6 @@ def tearDownModule(): MicroManager.objects.all().delete() Group.objects.all().delete() - ############################################################################### class GlobalMethodsTestCases(unittest.TestCase): def test_create_group_when_group_exists(self): @@ -2495,3 +2496,161 @@ class PostModelTestCases(unittest.TestCase): self.user3.delete() self.course.delete() self.post1.delete() + + +class QRcodeTestCase(unittest.TestCase): + @classmethod + def setUpClass(cls): + quiz = Quiz.objects.get(description='demo quiz 1') + cls.questionpaper = QuestionPaper.objects.create(quiz=quiz) + question = Question.objects.get(summary='Q1') + question.type = 'upload' + question.save() + cls.questionpaper.fixed_questions.add(question) + cls.questionpaper.update_total_marks() + student = User.objects.get(username='course_user') + course = Course.objects.get(name='Python Course') + attempt = 1 + ip = '127.0.0.1' + answerpaper = cls.questionpaper.make_answerpaper( + student, ip, attempt, course.id) + cls.qrcode_handler = QRcodeHandler.objects.create( + user=student, answerpaper=answerpaper, question=question) + cls.old_qrcode = cls.qrcode_handler._create_qrcode() + cls.old_qrcode.set_used() + cls.old_qrcode.save() + cls.qrcode = cls.qrcode_handler.get_qrcode() + cls.answerpaper = answerpaper + + @classmethod + def tearDownClass(cls): + cls.qrcode.image.delete() + cls.qrcode.delete() + cls.qrcode_handler.delete() + cls.answerpaper.delete() + cls.questionpaper.delete() + QRcode.objects.all().delete() + QRcodeHandler.objects.all().delete() + + def test_active(self): + # Given + qrcode = self.qrcode + + # Then + self.assertFalse(qrcode.is_active()) + + # When + qrcode.activate() + qrcode.save() + + # Then + self.assertTrue(qrcode.is_active()) + + # When + qrcode.deactivate() + qrcode.save() + + # Then + self.assertFalse(qrcode.is_active()) + + def test_used(self): + # Given + qrcode = self.qrcode + + # Then + self.assertFalse(qrcode.is_used()) + + # When + qrcode.set_used() + qrcode.save() + + # Then + self.assertTrue(qrcode.is_used()) + + # When + qrcode.used = False + qrcode.save() + + # Then + self.assertFalse(qrcode.is_used()) + + def test_random_key(self): + # Given + qrcode = self.qrcode + + # When + expect_key = hashlib.sha1('{0}'.format(qrcode.id).encode()).hexdigest() + + # Then + self.assertEqual(qrcode.random_key, expect_key) + self.assertEqual(len(qrcode.random_key), 40) + + def test_short_key(self): + # Given + qrcode = self.qrcode + + # When + expect_key = hashlib.sha1('{0}'.format(qrcode.id).encode()).hexdigest() + + # Then + self.assertEqual(qrcode.short_key, expect_key[0:5]) + self.assertEqual(len(qrcode.short_key), 5) + + # Given + old_qrcode = self.old_qrcode + old_qrcode.random_key = qrcode.random_key + old_qrcode.save() + + # When + old_qrcode.set_short_key() + + # Then + self.assertEqual(old_qrcode.short_key, expect_key[0:6]) + self.assertEqual(len(old_qrcode.short_key), 6) + + def test_generate_image(self): + # Given + qrcode = self.qrcode + image_name = 'qrcode/{0}.png'.format(qrcode.short_key) + + # When + qrcode.generate_image('test') + + # Then + self.assertTrue(qrcode.is_active()) + self.assertEqual(qrcode.image.name, image_name) + + def test_get_qrcode(self): + # Given + handler = self.qrcode_handler + self.qrcode.activate() + self.qrcode.save() + expected_qrcode = self.qrcode + + # When + qrcode = handler.get_qrcode() + + # Then + self.assertEqual(qrcode, expected_qrcode) + self.qrcode.deactivate() + self.qrcode.save() + + def test_can_use(self): + # Given + handler = self.qrcode_handler + + # When + can_use = handler.can_use() + + # Then + self.assertTrue(can_use) + + # Given + answerpaper = self.answerpaper + + # When + answerpaper.update_marks(state='complete') + can_use = handler.can_use() + + # Then + self.assertFalse(can_use) diff --git a/yaksh/urls.py b/yaksh/urls.py index ba10265..a1aa607 100644 --- a/yaksh/urls.py +++ b/yaksh/urls.py @@ -158,7 +158,10 @@ urlpatterns = [ name="questions_filter"), url(r'^editprofile/$', views.edit_profile, name='edit_profile'), url(r'^viewprofile/$', views.view_profile, name='view_profile'), - + url(r'^generate_qrcode/(?P<answerpaper_id>\d+)/(?P<question_id>\d+)/' + r'(?P<module_id>\d+)/$', + views.generate_qrcode, name='generate_qrcode'), + url(r'^upload_file/(?P<key>.+)/$', views.upload_file, name='upload_file'), url(r'^manage/searchteacher/(?P<course_id>\d+)/$', views.search_teacher, name="search_teacher"), url(r'^manage/addteacher/(?P<course_id>\d+)/$', views.add_teacher, diff --git a/yaksh/views.py b/yaksh/views.py index 12bc072..d2aa319 100644 --- a/yaksh/views.py +++ b/yaksh/views.py @@ -43,7 +43,8 @@ from yaksh.models import ( StdIOBasedTestCase, StringTestCase, TestCase, User, get_model_class, FIXTURES_DIR_PATH, MOD_GROUP_NAME, Lesson, LessonFile, LearningUnit, LearningModule, CourseStatus, question_types, Post, Comment, - Topic, TableOfContents, LessonQuizAnswer, MicroManager, dict_to_yaml + Topic, TableOfContents, LessonQuizAnswer, MicroManager, QRcode, + QRcodeHandler, dict_to_yaml ) from stats.models import TrackLesson from yaksh.forms import ( @@ -61,6 +62,7 @@ from .send_emails import (send_user_mail, from .decorators import email_verified, has_profile from .tasks import regrade_papers, update_user_marks from notifications_plugin.models import Notification +import hashlib def my_redirect(url): @@ -675,6 +677,7 @@ def show_question(request, question, paper, error_message=None, quiz_type = 'Exam' can_skip = False assignment_files = [] + qrcode = [] if previous_question: delay_time = paper.time_left_on_question(previous_question) else: @@ -722,6 +725,13 @@ def show_question(request, question, paper, error_message=None, assignmentQuestion_id=question.id, answer_paper=paper ) + handlers = QRcodeHandler.objects.filter(user=request.user, + question=question, + answerpaper=paper) + qrcode = None + if handlers.exists(): + handler = handlers.last() + qrcode = handler.qrcode_set.filter(active=True, used=False).last() files = FileUpload.objects.filter(question_id=question.id, hide=False) course = Course.objects.get(id=course_id) module = course.learning_module.get(id=module_id) @@ -742,6 +752,7 @@ def show_question(request, question, paper, error_message=None, 'quiz_type': quiz_type, 'all_modules': all_modules, 'assignment_files': assignment_files, + 'qrcode': qrcode, } answers = paper.get_previous_answers(question) if answers: @@ -4072,6 +4083,150 @@ def upload_marks(request, course_id, questionpaper_id): return redirect('yaksh:monitor', quiz.id, course_id) +def _get_header_info(reader): + user_ids, question_ids = [], [] + fields = reader.fieldnames + for field in fields: + if field.startswith('Q') and field.count('-') > 0: + qid = int(field.split('-')[1]) + if qid not in question_ids: + question_ids.append(qid) + return question_ids + + +def _read_marks_csv(request, reader, course, question_paper, question_ids): + messages.info(request, 'Marks Uploaded!') + for row in reader: + username = row['username'] + user = User.objects.filter(username=username).first() + if user: + answerpapers = question_paper.answerpaper_set.filter( + course=course, user_id=user.id) + else: + messages.info(request, '{0} user not found!'.format(username)) + continue + answerpaper = answerpapers.last() + if not answerpaper: + messages.info(request, '{0} has no answerpaper!'.format(username)) + continue + answers = answerpaper.answers.all() + questions = answerpaper.questions.all().values_list('id', flat=True) + for qid in question_ids: + question = Question.objects.filter(id=qid).first() + if not question: + messages.info(request, + '{0} is an invalid question id!'.format(qid)) + continue + if qid in questions: + answer = answers.filter(question_id=qid).last() + if not answer: + answer = Answer(question_id=qid, marks=0, correct=False, + answer='Created During Marks Update!', + error=json.dumps([])) + answer.save() + answerpaper.answers.add(answer) + key1 = 'Q-{0}-{1}-{2}-marks'.format(qid, question.summary, + question.points) + key2 = 'Q-{0}-{1}-comments'.format(qid, question.summary, + question.points) + if key1 in reader.fieldnames: + try: + answer.set_marks(float(row[key1])) + except ValueError: + messages.info(request, + '{0} invalid marks!'.format(row[key1])) + if key2 in reader.fieldnames: + answer.set_comment(row[key2]) + answer.save() + answerpaper.update_marks(state='completed') + answerpaper.save() + messages.info( + request, + 'Updated successfully for user: {0}, question: {1}'.format( + username, question.summary)) + + +@login_required +@email_verified +def generate_qrcode(request, answerpaper_id, question_id, module_id): + user = request.user + answerpaper = get_object_or_404(AnswerPaper, pk=answerpaper_id) + question = get_object_or_404(Question, pk=question_id) + + if not answerpaper.is_attempt_inprogress(): + pass + handler = QRcodeHandler.objects.get_or_create(user=user, question=question, + answerpaper=answerpaper)[0] + qrcode = handler.get_qrcode() + if not qrcode.is_qrcode_available(): + content = request.build_absolute_uri( + reverse("yaksh:upload_file", args=[qrcode.short_key]) + ) + qrcode.generate_image(content) + qrcode.save() + return redirect( + reverse( + 'yaksh:skip_question', + kwargs={'q_id': question_id, 'next_q': question_id, + 'attempt_num': answerpaper.attempt_number, + 'module_id': module_id, + 'questionpaper_id': answerpaper.question_paper.id, + 'course_id': answerpaper.course_id} + ) + ) + + +def upload_file(request, key): + qrcode = get_object_or_404(QRcode, short_key=key, active=True, used=False) + handler = qrcode.handler + context = {'question': handler.question, 'key': qrcode.short_key} + if not handler.can_use(): + context['success'] = True + context['msg'] = 'Sorry, test time up!' + return render(request, 'yaksh/upload_file.html', context) + if request.method == 'POST': + assign_files = [] + assignments = request.FILES + for i in range(len(assignments)): + assign_files.append(assignments[f"assignment[{i}]"]) + if not assign_files: + msg = 'Please upload assignment file' + context['msg'] = msg + return render(request, 'yaksh/upload_file.html', context) + AssignmentUpload.objects.filter( + assignmentQuestion_id=handler.question_id, + answer_paper_id=handler.answerpaper_id + ).delete() + uploaded_files = [] + for fname in assign_files: + fname._name = fname._name.replace(" ", "_") + uploaded_files.append( + AssignmentUpload( + assignmentQuestion_id=handler.question_id, + assignmentFile=fname, + answer_paper_id=handler.answerpaper_id) + ) + AssignmentUpload.objects.bulk_create(uploaded_files) + user_answer = 'ASSIGNMENT UPLOADED' + new_answer = Answer( + question=handler.question, answer=user_answer, + correct=False, error=json.dumps([]) + ) + new_answer.save() + paper = handler.answerpaper + paper.answers.add(new_answer) + next_q = paper.add_completed_question(handler.question_id) + qrcode.set_used() + qrcode.deactivate() + qrcode.save() + context['success'] = True + msg = "File Uploaded Successfully! Reload the (test)question "\ + "page to see the uploaded file" + context['msg'] = msg + return render(request, 'yaksh/upload_file.html', context) + return render(request, 'yaksh/upload_file.html', context) + + @login_required @email_verified def upload_download_course_md(request, course_id): |