summaryrefslogtreecommitdiff
path: root/yaksh
diff options
context:
space:
mode:
Diffstat (limited to 'yaksh')
-rw-r--r--yaksh/models.py87
-rw-r--r--yaksh/templates/yaksh/question.html36
-rw-r--r--yaksh/templates/yaksh/upload_file.html78
-rw-r--r--yaksh/test_models.py163
-rw-r--r--yaksh/urls.py5
-rw-r--r--yaksh/views.py157
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):