diff options
author | Palaparthy Adityachandra | 2021-03-24 10:13:27 +0530 |
---|---|---|
committer | GitHub | 2021-03-24 10:13:27 +0530 |
commit | f44cbc461f4c23c08d655749ccb525d99f2e7dbc (patch) | |
tree | 7a7f0b29a23685734d45265773db916bedad380f /yaksh | |
parent | 6fda19daaa06482b8eb52eeb62f9b0a15d0a3da6 (diff) | |
parent | 8192bf7a17070f2202d9282a4873795127a17447 (diff) | |
download | online_test-f44cbc461f4c23c08d655749ccb525d99f2e7dbc.tar.gz online_test-f44cbc461f4c23c08d655749ccb525d99f2e7dbc.tar.bz2 online_test-f44cbc461f4c23c08d655749ccb525d99f2e7dbc.zip |
Merge pull request #823 from adityacp/s3_file_upload
Add AWS support for file upload
Diffstat (limited to 'yaksh')
-rw-r--r-- | yaksh/models.py | 65 | ||||
-rw-r--r-- | yaksh/static/yaksh/js/requesthandler.js | 43 | ||||
-rw-r--r-- | yaksh/storage_backends.py | 18 | ||||
-rw-r--r-- | yaksh/templates/yaksh/add_quiz.html | 4 | ||||
-rw-r--r-- | yaksh/templates/yaksh/complete.html | 100 | ||||
-rw-r--r-- | yaksh/templates/yaksh/question.html | 23 | ||||
-rw-r--r-- | yaksh/templates/yaksh/quit.html | 1 | ||||
-rw-r--r-- | yaksh/test_models.py | 29 | ||||
-rw-r--r-- | yaksh/views.py | 56 |
9 files changed, 226 insertions, 113 deletions
diff --git a/yaksh/models.py b/yaksh/models.py index 4798b23..51c2aa8 100644 --- a/yaksh/models.py +++ b/yaksh/models.py @@ -43,7 +43,8 @@ from django.template import Context, Template from django.conf import settings from django.forms.models import model_to_dict from django.db.models import Count - +from django.db.models.signals import pre_delete +from django.db.models.fields.files import FieldFile # Local Imports from yaksh.code_server import ( submit, get_result as get_result_from_code_server @@ -52,6 +53,7 @@ from yaksh.settings import SERVER_POOL_PORT, SERVER_HOST_NAME from .file_utils import extract_files, delete_files from grades.models import GradingSystem + languages = ( ("python", "Python"), ("bash", "Bash"), @@ -270,6 +272,17 @@ def is_valid_time_format(time): return status +def file_cleanup(sender, instance, *args, **kwargs): + ''' + Deletes the file(s) associated with a model instance. The model + is not saved after deletion of the file(s) since this is meant + to be used with the pre_delete signal. + ''' + for field_name, _ in instance.__dict__.items(): + field = getattr(instance, field_name) + if issubclass(field.__class__, FieldFile) and field.name: + field.delete(save=False) + ############################################################################### class CourseManager(models.Manager): @@ -1422,11 +1435,10 @@ class Question(models.Model): ] } - def consolidate_answer_data(self, user_answer, user=None): + def consolidate_answer_data(self, user_answer, user=None, regrade=False): question_data = {} metadata = {} test_case_data = [] - test_cases = self.get_test_cases() for test in test_cases: @@ -1439,19 +1451,34 @@ class Question(models.Model): metadata['partial_grading'] = self.partial_grading files = FileUpload.objects.filter(question=self) if files: - metadata['file_paths'] = [(file.file.path, file.extract) - for file in files] - if self.type == "upload": - assignment_files = AssignmentUpload.objects.filter( - assignmentQuestion=self - ) - if assignment_files: - metadata['assign_files'] = [(file.assignmentFile.path, False) - for file in assignment_files] + if settings.USE_AWS: + metadata['file_paths'] = [ + (file.file.url, file.extract) + for file in files + ] + else: + metadata['file_paths'] = [ + (self.get_file_url(file.file.url), file.extract) + for file in files + ] + if self.type == "upload" and regrade: + file = AssignmentUpload.objects.only( + "assignmentFile").filter( + assignmentQuestion_id=self.id, answer_paper__user_id=user.id + ).order_by("-id").first() + if file: + if settings.USE_AWS: + metadata['assign_files'] = [file.assignmentFile.url] + else: + metadata['assign_files'] = [ + self.get_file_url(file.assignmentFile.url) + ] question_data['metadata'] = metadata - return json.dumps(question_data) + def get_file_url(self, path): + return f'{settings.DOMAIN_HOST}{path}' + def dump_questions(self, question_ids, user): questions = Question.objects.filter(id__in=question_ids, user_id=user.id, active=True @@ -1690,7 +1717,7 @@ class FileUpload(models.Model): def get_filename(self): return os.path.basename(self.file.name) - +pre_delete.connect(file_cleanup, sender=FileUpload) ############################################################################### class Answer(models.Model): """Answers submitted by the users.""" @@ -2596,7 +2623,7 @@ class AnswerPaper(models.Model): return (False, f'{msg} {question.type} answer submission error') else: answer = user_answer.answer - json_data = question.consolidate_answer_data(answer) \ + json_data = question.consolidate_answer_data(answer, self.user, True) \ if question.type == 'code' else None result = self.validate_answer(answer, question, json_data, user_answer.id, @@ -2646,7 +2673,11 @@ class AssignmentUploadManager(models.Manager): answer_paper__question_paper=qp, answer_paper__course_id=course_id ) - file_name = User.objects.get(id=user_id).get_full_name() + user_name = assignment_files.values_list( + "answer_paper__user__first_name", + "answer_paper__user__last_name" + )[0] + file_name = "_".join(user_name) else: assignment_files = AssignmentUpload.objects.filter( answer_paper__question_paper=qp, @@ -2673,6 +2704,8 @@ class AssignmentUpload(models.Model): def __str__(self): return f'Assignment File of the user {self.answer_paper.user}' +pre_delete.connect(file_cleanup, sender=AssignmentUpload) + ############################################################################## class TestCase(models.Model): diff --git a/yaksh/static/yaksh/js/requesthandler.js b/yaksh/static/yaksh/js/requesthandler.js index 80b67fb..0a06c79 100644 --- a/yaksh/static/yaksh/js/requesthandler.js +++ b/yaksh/static/yaksh/js/requesthandler.js @@ -145,8 +145,49 @@ var global_editor = {}; var csrftoken = jQuery("[name=csrfmiddlewaretoken]").val(); var err_lineno; var marker; +Dropzone.autoDiscover = false; +var submitfiles; $(document).ready(function(){ - if(is_exercise == "True" && can_skip == "False"){ + 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) { + $("#upload_alert").modal("show"); + 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 + } + }); + if(is_exercise == "True" && can_skip == "False") { setTimeout(function() {show_solution();}, delay_time*1000); } // Codemirror object, language modes and initial content diff --git a/yaksh/storage_backends.py b/yaksh/storage_backends.py new file mode 100644 index 0000000..206e456 --- /dev/null +++ b/yaksh/storage_backends.py @@ -0,0 +1,18 @@ +from django.conf import settings +from storages.backends.s3boto3 import S3Boto3Storage + + +class StaticStorage(S3Boto3Storage): + if settings.USE_AWS: + location = settings.AWS_STATIC_LOCATION + else: + pass + + +class PublicMediaStorage(S3Boto3Storage): + if settings.USE_AWS: + location = settings.AWS_PUBLIC_MEDIA_LOCATION + file_overwrite = True + default_acl = 'public-read' + else: + pass diff --git a/yaksh/templates/yaksh/add_quiz.html b/yaksh/templates/yaksh/add_quiz.html index 1609639..d43c1c6 100644 --- a/yaksh/templates/yaksh/add_quiz.html +++ b/yaksh/templates/yaksh/add_quiz.html @@ -65,11 +65,11 @@ <br> <br> <h4>You can check the quiz by attempting it in the following modes:</h4> - <a class="btn btn-outline-info" name="button" href="{% url 'yaksh:test_quiz' 'usermode' quiz.id course_id %}" target="blank"> + <a class="btn btn-outline-info" name="button" href="{% url 'yaksh:test_quiz' 'usermode' quiz.id course_id %}" target="_blank"> Try as student </a> - <a class="btn btn-outline-primary" name="button" href="{% url 'yaksh:test_quiz' 'godmode' quiz.id course_id %}" target="blank"> + <a class="btn btn-outline-primary" name="button" href="{% url 'yaksh:test_quiz' 'godmode' quiz.id course_id %}" target="_blank"> Try as teacher </a> <a data-toggle="modal" data-target="#help"> diff --git a/yaksh/templates/yaksh/complete.html b/yaksh/templates/yaksh/complete.html index 4d921e1..79a1392 100644 --- a/yaksh/templates/yaksh/complete.html +++ b/yaksh/templates/yaksh/complete.html @@ -20,58 +20,58 @@ </div> </center> {% endif %} -{% csrf_token %} - {% if paper.questions_answered.all or paper.questions_unanswered.all %} - <center> - <div class="col-md-8"> - <h3>Submission Status</h3> - <table class="table table-dark table-responsive-sm" > - <thead class="thead-dark"> +{% if paper.questions_answered.all or paper.questions_unanswered.all %} + <center> + <div class="col-md-8"> + <h3>Submission Status</h3> + <table class="table table-dark table-responsive-sm" > + <thead class="thead-dark"> + <tr> + <th> Question</th> + <th> Status </th> + </tr> + </thead> + <tbody class="list"> + {% for question in paper.questions.all %} + {% if question in paper.questions_answered.all %} <tr> - <th> Question</th> - <th> Status </th> - </tr> - </thead> - <tbody class="list"> - {% for question in paper.questions.all %} - {% if question in paper.questions_answered.all %} - <tr> - <td> {{ question.summary }} </td> - <td> <span class="badge badge-success">Attempted</span> </td> - {% else %} - <tr> - <td> {{ question }} </td> - <td> <span class="badge badge-warning">Not Attempted</span> </td> - {% endif %} - </tr> - {% endfor %} - </tbody> - </table> - </div> - </center> - {% endif %} - <br><br> - <center class="container"> - <h5> - <span class="alert alert-success">{{message}}</span> - </h5> + <td> {{ question.summary }} </td> + <td> <span class="badge badge-success">Attempted</span> </td> + {% else %} + <tr> + <td> {{ question }} </td> + <td> <span class="badge badge-warning">Not Attempted</span> </td> + {% endif %} + </tr> + {% endfor %} + </tbody> + </table> + </div> </center> - <center> - <br> - {% if module_id and not paper.course.is_trial %} - {% if first_unit %} - <a href="{% url 'yaksh:next_unit' course_id module_id learning_unit.id '1' %}" class="btn btn-info btn-lg" id="Next"> Next - <span class="fa fa-step-forward"> - </span> - </a> - {% else %} - <a href="{% url 'yaksh:next_unit' course_id module_id learning_unit.id %}" class="btn btn-info btn-lg" id="Next"> Next - <span class="fa fa-step-forward"> - </span> - </a> - {% endif %} +{% endif %} +<br><br> +<center class="container"> + <h5> + <span class="alert alert-success">{{message}}</span> + </h5> +</center> +<center> +<br> +{% if module_id and not paper.course.is_trial %} + {% if first_unit %} + <a href="{% url 'yaksh:next_unit' course_id module_id learning_unit.id '1' %}" class="btn btn-info btn-lg" id="Next"> Next + <span class="fa fa-step-forward"> + </span> + </a> {% else %} - <a href="{% url 'yaksh:index' %}" id="home" class="btn btn-primary btn-lg"> Home </a> + <a href="{% url 'yaksh:next_unit' course_id module_id learning_unit.id %}" class="btn btn-info btn-lg" id="Next"> Next + <span class="fa fa-step-forward"> + </span> + </a> {% endif %} - </center> +{% else %} + <a href="{% url 'yaksh:index' %}" id="home" class="btn btn-primary btn-lg"> Home </a> +{% endif %} +</center> +<br><br> {% endblock content %} diff --git a/yaksh/templates/yaksh/question.html b/yaksh/templates/yaksh/question.html index 56edcf3..2779db2 100644 --- a/yaksh/templates/yaksh/question.html +++ b/yaksh/templates/yaksh/question.html @@ -7,6 +7,8 @@ <link rel="stylesheet" href="{% static 'yaksh/css/question.css' %}" type="text/css" /> <link rel="stylesheet" href="{% static 'yaksh/css/codemirror/lib/codemirror.css' %}" type="text/css" /> <link rel="stylesheet" href="{% static 'yaksh/css/exam.css' %}" type="text/css" /> +<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"> <style> .CodeMirror{ border-style: groove; @@ -21,6 +23,7 @@ {% block script %} +<script src="https://cdnjs.cloudflare.com/ajax/libs/dropzone/5.8.1/min/dropzone.min.js"></script> <script src="{% static 'yaksh/js/requesthandler.js' %}"></script> <script src="{% static 'yaksh/js/codemirror/lib/codemirror.js' %}"></script> <script src="{% static 'yaksh/js/codemirror/mode/python/python.js' %}"></script> @@ -29,6 +32,7 @@ <script src="{% static 'yaksh/js/codemirror/mode/r/r.js' %}"></script> <script type="text/javascript" src="{% static 'yaksh/js/mathjax/MathJax.js' %}?config=TeX-MML-AM_CHTML"></script> <script src="{% static 'yaksh/js/jquery-sortable.js' %}"></script> + <script> init_val = '{{ last_attempt|escape_quotes|safe }}'; lang = "{{ question.language }}" @@ -142,7 +146,6 @@ question_type = "{{ question.type }}"; </div> {% endif %} </center> - </div> <form id="code" action="{% url 'yaksh:check' question.id paper.attempt_number module.id paper.question_paper.id course.id %}" method="post" enctype="multipart/form-data"> {% csrf_token %} <input type=hidden name="question_id" id="question_id" value="{{ question.id }}"></input> @@ -174,6 +177,9 @@ question_type = "{{ question.type }}"; <small><strong>Marks: {{ question.points }}</strong></small> </span> </div> + <div class="badge badge-warning"> + Last submitted answer is considered for evaluation + </div> </div> </div> <div class="card-body"> @@ -181,10 +187,10 @@ question_type = "{{ question.type }}"; {{ question.description|safe }} </div> {% if files %} - <div class="col-md-5"> + <div class="col-md-8"> <div class="card"> <div class="card-header"> - <span> Files to download for this question </span> + <span class="badge badge-info"> Files to download for this question </span> </div> <div class="card-body"> {% for f_name in files %} @@ -265,8 +271,16 @@ question_type = "{{ question.type }}"; <!-- Upload type question --> {% if question.type == "upload" %} <p>Upload assignment file for the said question<p> - <input type=file id="assignment" name="assignment" multiple=""> + <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"> {% for as_file in assignment_files %} <li class="list-group-item"> @@ -274,6 +288,7 @@ question_type = "{{ question.type }}"; </li> {% endfor %} </ul> + </div> {% endif %} {% endif %} diff --git a/yaksh/templates/yaksh/quit.html b/yaksh/templates/yaksh/quit.html index 828ad60..e829b3c 100644 --- a/yaksh/templates/yaksh/quit.html +++ b/yaksh/templates/yaksh/quit.html @@ -57,4 +57,5 @@ {% endif %} </center> </form> + <br><br> {% endblock content %} diff --git a/yaksh/test_models.py b/yaksh/test_models.py index 2ee6e82..3b2fb4f 100644 --- a/yaksh/test_models.py +++ b/yaksh/test_models.py @@ -16,6 +16,7 @@ from datetime import datetime, timedelta from django.utils import timezone import pytz from django.db import IntegrityError +from django.conf import settings as dj_settings from django.core.files import File from textwrap import dedent import zipfile @@ -548,12 +549,12 @@ class QuestionTestCases(unittest.TestCase): # create a temp directory and add files for dumping questions test self.dump_tmp_path = tempfile.mkdtemp() shutil.copy(file_path, self.dump_tmp_path) - file2 = os.path.join(self.dump_tmp_path, "test.txt") - upload_file = open(file2, "r") - django_file = File(upload_file) - FileUpload.objects.create(file=django_file, - question=self.question2 - ) + file2 = os.path.join(dj_settings.MEDIA_ROOT, "test.txt") + with open(file2, "w") as upload_file: + django_file = File(upload_file) + FileUpload.objects.create(file=file2, + question=self.question2 + ) self.question1.tags.add('python', 'function') self.assertion_testcase = StandardTestCase( @@ -686,7 +687,7 @@ class QuestionTestCases(unittest.TestCase): tags = question_data.tags.all().values_list("name", flat=True) self.assertListEqual(list(tags), ['yaml_demo']) self.assertEqual(question_data.snippet, 'def fact()') - self.assertEqual(os.path.basename(file.file.path), "test.txt") + self.assertEqual(os.path.basename(file.file.url), "test.txt") self.assertEqual([case.get_field_value() for case in test_case], self.test_case_upload_data ) @@ -1545,7 +1546,7 @@ class AnswerPaperTestCases(unittest.TestCase): # When json_data = self.question1.consolidate_answer_data( - user_answer, user + user_answer, user, regrade=True ) get_result = self.answerpaper.validate_answer(user_answer, self.question1, @@ -2208,12 +2209,14 @@ class AssignmentUploadTestCases(unittest.TestCase): self.user1 = User.objects.create_user( username='creator1', password='demo', - email='demo@test1.com' + email='demo@test1.com', + first_name="dummy1", last_name="dummy1" ) self.user2 = User.objects.create_user( username='creator2', password='demo', - email='demo@test2.com' + email='demo@test2.com', + first_name="dummy1", last_name="dummy1" ) self.quiz = Quiz.objects.create( start_date_time=datetime( @@ -2252,8 +2255,8 @@ class AssignmentUploadTestCases(unittest.TestCase): self.user1, ip, attempt, self.course.id ) - file_path1 = os.path.join(tempfile.gettempdir(), "upload1.txt") - file_path2 = os.path.join(tempfile.gettempdir(), "upload2.txt") + file_path1 = os.path.join(dj_settings.MEDIA_ROOT, "upload1.txt") + file_path2 = os.path.join(dj_settings.MEDIA_ROOT, "upload2.txt") self.assignment1 = AssignmentUpload.objects.create( assignmentQuestion=self.question, assignmentFile=file_path1, answer_paper=self.answerpaper1, @@ -2411,8 +2414,6 @@ class FileUploadTestCases(unittest.TestCase): self.assertEqual(self.file_upload.get_filename(), self.filename) def tearDown(self): - if os.path.isfile(self.file_upload.file.path): - os.remove(self.file_upload.file.path) self.file_upload.delete() diff --git a/yaksh/views.py b/yaksh/views.py index 7b1e038..12bc072 100644 --- a/yaksh/views.py +++ b/yaksh/views.py @@ -21,6 +21,7 @@ from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from django.contrib import messages from taggit.models import Tag from django.urls import reverse +from django.conf import settings import json from textwrap import dedent import zipfile @@ -251,9 +252,7 @@ def add_question(request, question_id=None): extract_files_id = request.POST.getlist('extract') hide_files_id = request.POST.getlist('hide') if remove_files_id: - files = FileUpload.objects.filter(id__in=remove_files_id) - for file in files: - file.remove() + files = FileUpload.objects.filter(id__in=remove_files_id).delete() if extract_files_id: files = FileUpload.objects.filter(id__in=extract_files_id) for file in files: @@ -847,8 +846,11 @@ def check(request, q_id, attempt_num=None, questionpaper_id=None, elif current_question.type == 'upload': # if time-up at upload question then the form is submitted without # validation - assignment_filename = request.FILES.getlist('assignment') - if not assignment_filename: + 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" return show_question( request, current_question, paper, notification=msg, @@ -856,26 +858,29 @@ def check(request, q_id, attempt_num=None, questionpaper_id=None, previous_question=current_question ) uploaded_files = [] - for fname in assignment_filename: + AssignmentUpload.objects.filter( + assignmentQuestion_id=current_question.id, + answer_paper_id=paper.id + ).delete() + for fname in assign_files: fname._name = fname._name.replace(" ", "_") uploaded_files.append(AssignmentUpload( - assignmentQuestion=current_question, + assignmentQuestion_id=current_question.id, assignmentFile=fname, answer_paper_id=paper.id )) AssignmentUpload.objects.bulk_create(uploaded_files) user_answer = 'ASSIGNMENT UPLOADED' - if not current_question.grade_assignment_upload: - new_answer = Answer( - question=current_question, answer=user_answer, - correct=False, error=json.dumps([]) - ) - new_answer.save() - paper.answers.add(new_answer) - next_q = paper.add_completed_question(current_question.id) - return show_question(request, next_q, paper, - course_id=course_id, module_id=module_id, - previous_question=current_question) + new_answer = Answer( + question=current_question, answer=user_answer, + correct=False, error=json.dumps([]) + ) + new_answer.save() + paper.answers.add(new_answer) + next_q = paper.add_completed_question(current_question.id) + return show_question(request, next_q, paper, + course_id=course_id, module_id=module_id, + previous_question=current_question) else: user_answer = request.POST.get('answer') if not is_valid_answer(user_answer): @@ -902,12 +907,11 @@ def check(request, q_id, attempt_num=None, questionpaper_id=None, # questions, we obtain the results via XML-RPC with the code executed # safely in a separate process (the code_server.py) running as nobody. json_data = current_question.consolidate_answer_data( - user_answer, user) if current_question.type == 'code' or \ - current_question.type == 'upload' else None + user_answer, user) if current_question.type == 'code' else None result = paper.validate_answer( user_answer, current_question, json_data, uid ) - if current_question.type in ['code', 'upload']: + if current_question.type == 'code': if (paper.time_left() <= 0 and not paper.question_paper.quiz.is_exercise): url = '{0}:{1}'.format(SERVER_HOST_NAME, SERVER_POOL_PORT) @@ -2388,11 +2392,11 @@ def download_assignment_file(request, quiz_id, course_id, for f_name in assignment_files: folder = f_name.answer_paper.user.get_full_name().replace(" ", "_") sub_folder = f_name.assignmentQuestion.summary.replace(" ", "_") - folder_name = os.sep.join((folder, sub_folder, os.path.basename( - f_name.assignmentFile.name)) - ) - zip_file.write( - f_name.assignmentFile.path, folder_name + folder_name = os.sep.join((folder, sub_folder)) + download_url = f_name.assignmentFile.url + zip_file.writestr( + os.path.join(folder_name, os.path.basename(download_url)), + f_name.assignmentFile.read() ) zip_file.close() zipfile_name.seek(0) |