diff options
-rw-r--r-- | yaksh/migrations/0005_auto_20170410_1024.py | 26 | ||||
-rw-r--r-- | yaksh/models.py | 31 | ||||
-rw-r--r-- | yaksh/static/yaksh/js/add_question.js | 3 | ||||
-rw-r--r-- | yaksh/templates/404.html | 4 | ||||
-rw-r--r-- | yaksh/templates/yaksh/grade_user.html | 70 | ||||
-rw-r--r-- | yaksh/templates/yaksh/view_answerpaper.html | 8 | ||||
-rw-r--r-- | yaksh/test_models.py | 73 | ||||
-rw-r--r-- | yaksh/urls.py | 6 | ||||
-rw-r--r-- | yaksh/views.py | 111 |
9 files changed, 270 insertions, 62 deletions
diff --git a/yaksh/migrations/0005_auto_20170410_1024.py b/yaksh/migrations/0005_auto_20170410_1024.py new file mode 100644 index 0000000..13b4cce --- /dev/null +++ b/yaksh/migrations/0005_auto_20170410_1024.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.5 on 2017-04-10 10:24 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('yaksh', '0004_auto_20170331_0632'), + ] + + operations = [ + migrations.AddField( + model_name='assignmentupload', + name='question_paper', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='yaksh.QuestionPaper'), + ), + migrations.AlterField( + model_name='hooktestcase', + name='hook_code', + field=models.TextField(default='def check_answer(user_answer):\n \'\'\' Evaluates user answer to return -\n success - Boolean, indicating if code was executed correctly\n mark_fraction - Float, indicating fraction of the\n weight to a test case\n error - String, error message if success is false\n\n In case of assignment upload there will be no user answer \'\'\'\n\n success = False\n err = "Incorrect Answer" # Please make this more specific\n mark_fraction = 0.0\n\n # write your code here\n\n return success, err, mark_fraction\n\n'), + ), + ] diff --git a/yaksh/models.py b/yaksh/models.py index 802a1fc..6646615 100644 --- a/yaksh/models.py +++ b/yaksh/models.py @@ -79,7 +79,8 @@ test_status = ( def get_assignment_dir(instance, filename): return os.sep.join(( - instance.user.username, str(instance.assignmentQuestion.id), filename + instance.question_paper.quiz.description, instance.user.username, + str(instance.assignmentQuestion.id), filename )) @@ -1305,11 +1306,35 @@ class AnswerPaper(models.Model): .format(u.first_name, u.last_name, q.description) -############################################################################### +################################################################################ +class AssignmentUploadManager(models.Manager): + + def get_assignments(self, qp, que_id=None, user_id=None): + if que_id and user_id: + assignment_files = AssignmentUpload.objects.filter( + assignmentQuestion_id=que_id, user_id=user_id, + question_paper=qp + ) + file_name = User.objects.get(id=user_id).get_full_name() + else: + assignment_files = AssignmentUpload.objects.filter( + question_paper=qp + ) + + file_name = "{0}_Assignment_files".format( + assignment_files[0].question_paper.quiz.description + ) + + return assignment_files, file_name + + +################################################################################ class AssignmentUpload(models.Model): user = models.ForeignKey(User) assignmentQuestion = models.ForeignKey(Question) assignmentFile = models.FileField(upload_to=get_assignment_dir) + question_paper = models.ForeignKey(QuestionPaper, blank=True, null=True) + objects = AssignmentUploadManager() ############################################################################### @@ -1372,7 +1397,9 @@ class HookTestCase(TestCase): mark_fraction - Float, indicating fraction of the weight to a test case error - String, error message if success is false + In case of assignment upload there will be no user answer ''' + success = False err = "Incorrect Answer" # Please make this more specific mark_fraction = 0.0 diff --git a/yaksh/static/yaksh/js/add_question.js b/yaksh/static/yaksh/js/add_question.js index 05752b4..5bec8c6 100644 --- a/yaksh/static/yaksh/js/add_question.js +++ b/yaksh/static/yaksh/js/add_question.js @@ -122,9 +122,8 @@ function textareaformat() }); document.getElementById('my').innerHTML = document.getElementById('id_description').value ; - if (document.getElementById('id_grade_assignment_upload').checked || - document.getElementById('id_type').val() == 'upload'){ + document.getElementById('id_type').value == 'upload'){ $("#id_grade_assignment_upload").prop("disabled", false); } else{ diff --git a/yaksh/templates/404.html b/yaksh/templates/404.html index 7d33dd3..e9d99de 100644 --- a/yaksh/templates/404.html +++ b/yaksh/templates/404.html @@ -1,5 +1,7 @@ {% extends "base.html" %} {% block content %} -The requested page does not exist. +It seems that you have encountered an error +Type of Error - {{ exception }} +Please contact your administrator {% endblock %} diff --git a/yaksh/templates/yaksh/grade_user.html b/yaksh/templates/yaksh/grade_user.html index 67f25a4..c93ec10 100644 --- a/yaksh/templates/yaksh/grade_user.html +++ b/yaksh/templates/yaksh/grade_user.html @@ -51,6 +51,12 @@ {% endif %} {% endif %} +{% if has_quiz_assignments %} + +<a href="{{URL_ROOT}}/exam/manage/download/quiz_assignments/{{quiz_id}}/"> + Download All Assignments</a> +{% endif %} + <div id = "paper" class="col-md-10"> {% if data %} @@ -84,7 +90,6 @@ Attempt Number: <b>{{paper.attempt_number}} </b> </option> {% endfor %} </select> - <br/>Questions correctly answered: {{ paper.get_answered_str }} <br/> Total attempts at questions: {{ paper.answers.count }} <br/> Marks obtained: {{ paper.marks_obtained }} <br/> @@ -99,7 +104,6 @@ Status : <b style="color: red;"> Failed </b><br/> Status : <b style="color: green;"> Passed </b><br/> {% endif %} </p> - {% if paper.answers.count %} <h4> Report </h4><br> @@ -126,8 +130,8 @@ Status : <b style="color: green;"> Passed </b><br/> {% endif %} method="post"> {% csrf_token %} -{% for question, answers in paper.get_question_answers.items %} +{% for question, answers in paper.get_question_answers.items %} <div class="panel panel-info"> <div class="panel-heading"> <strong> Details: {{forloop.counter}}. {{ question.summary }} @@ -157,7 +161,6 @@ Status : <b style="color: green;"> Passed </b><br/> <strong>{{ testcase.error_margin|safe }}</strong> {% endif %} {% endfor %} - {% else %} <h5> <u>Test cases: </u></h5> {% for testcase in question.get_test_cases %} @@ -167,29 +170,50 @@ Status : <b style="color: green;"> Passed </b><br/> </div> </div> <h5>Student answer: </h5> - {% for ans in answers %} - {% if ans.answer.correct %} - <div class="panel panel-success"> - <div class="panel-heading">Correct answer: + {% if question.type == "upload" %} + {% if has_user_assignments %} + <a href="{{URL_ROOT}}/exam/manage/download/user_assignment/{{question.id}}/{{data.user.id}}/{{paper.question_paper.quiz.id}}"> + <div class="panel"> + Assignment File for {{ data.user.get_full_name.title }} + </div> + </a> + {% with answers|last as answer%} + {% if answer.answer.correct %} + <div class="panel panel-success"> + <div class="panel-heading">Correct answer</div></div> + {% else %} + <div class="panel panel-danger"> + <div class="panel-heading">Incorrect Answer</div></div> + {% endif %} + {% endwith %} + {% else %} + <h5>No Assignment submitted by {{ data.user.get_full_name.title }}</h5> + {% endif %} + {% else %} + {% for ans in answers %} + {% if ans.answer.correct %} + <div class="panel panel-success"> + <div class="panel-heading">Correct answer: + {% else %} + <div class="panel panel-danger"> + <div class="panel-heading">Error: + {% endif %} + {% for err in ans.error_list %} + <div><pre>{{ err }}</pre></div> + {% endfor %} + </div> + <div class="panel-body"> + {% if question.type != "code" %} + <div class="well well-sm"> + {{ ans.answer.answer.strip|safe }} + </div> {% else %} - <div class="panel panel-danger"> - <div class="panel-heading">Error: + <pre><code>{{ ans.answer.answer.strip|safe }}</code></pre> {% endif %} - {% for err in ans.error_list %} - <div><pre>{{ err }}</pre></div> + </div> + </div> {% endfor %} - </div> - <div class="panel-body"> - {% if question.type != "code" %} - <div class="well well-sm"> - {{ ans.answer.answer.strip|safe }} - </div> - {% else %} - <pre><code>{{ ans.answer.answer.strip|safe }}</code></pre> {% endif %} - </div> - </div> - {% endfor %} {% with answers|last as answer %} Marks: <input id="q{{ question.id }}" type="text" name="q{{ question.id }}_marks" size="4" diff --git a/yaksh/templates/yaksh/view_answerpaper.html b/yaksh/templates/yaksh/view_answerpaper.html index 4520ac3..f4c8846 100644 --- a/yaksh/templates/yaksh/view_answerpaper.html +++ b/yaksh/templates/yaksh/view_answerpaper.html @@ -84,7 +84,13 @@ <h5><u>Student answer:</u></h5> <div class="well well-sm"> {{ answers.0.answer|safe }} - </div> + {% if question.type == "upload" and has_user_assignment %} + <a href="{{URL_ROOT}}/exam/download/user_assignment/{{question.id}}/{{data.user.id}}/{{paper.question_paper.quiz.id}}"> + <div class="panel"> + Assignment File for {{ data.user.get_full_name.title }} + </div></a> + {% endif %} + </div> </div> </div> {% else %} diff --git a/yaksh/test_models.py b/yaksh/test_models.py index dbd367b..9bd8492 100644 --- a/yaksh/test_models.py +++ b/yaksh/test_models.py @@ -1,7 +1,7 @@ import unittest from yaksh.models import User, Profile, Question, Quiz, QuestionPaper,\ QuestionSet, AnswerPaper, Answer, Course, StandardTestCase,\ - StdIOBasedTestCase, FileUpload, McqTestCase + StdIOBasedTestCase, FileUpload, McqTestCase, AssignmentUpload import json from datetime import datetime, timedelta from django.utils import timezone @@ -68,12 +68,7 @@ def tearDownModule(): Quiz.objects.all().delete() Course.objects.all().delete() QuestionPaper.objects.all().delete() - - que_id_list = ["25", "22", "24", "27"] - for que_id in que_id_list: - dir_path = os.path.join(os.getcwd(), "yaksh", "data","question_{0}".format(que_id)) - if os.path.exists(dir_path): - shutil.rmtree(dir_path) + ############################################################################### class ProfileTestCases(unittest.TestCase): @@ -1034,3 +1029,67 @@ class TestCaseTestCases(unittest.TestCase): exp_data = json.loads(self.answer_data_json) self.assertEqual(actual_data['metadata']['user_answer'], exp_data['metadata']['user_answer']) self.assertEqual(actual_data['test_case_data'], exp_data['test_case_data']) + + +class AssignmentUploadTestCases(unittest.TestCase): + def setUp(self): + self.user1 = User.objects.get(username="demo_user") + self.user1.first_name = "demo" + self.user1.last_name = "user" + self.user1.save() + self.user2 = User.objects.get(username="demo_user3") + self.user2.first_name = "demo" + self.user2.last_name = "user3" + self.user2.save() + self.quiz = Quiz.objects.get(description="demo quiz 1") + + self.questionpaper = QuestionPaper.objects.create(quiz=self.quiz, + total_marks=0.0, + shuffle_questions=True + ) + self.question = Question.objects.create(summary='Assignment', + language='Python', + type='upload', + active=True, + description='Upload a file', + points=1.0, + snippet='', + user=self.user1 + ) + self.questionpaper.fixed_question_order = "{0}".format(self.question.id) + self.questionpaper.fixed_questions.add(self.question) + file_path1 = os.path.join(tempfile.gettempdir(), "upload1.txt") + file_path2 = os.path.join(tempfile.gettempdir(), "upload2.txt") + self.assignment1 = AssignmentUpload.objects.create(user=self.user1, + assignmentQuestion=self.question, assignmentFile=file_path1, + question_paper=self.questionpaper + ) + self.assignment2 = AssignmentUpload.objects.create(user=self.user2, + assignmentQuestion=self.question, assignmentFile=file_path2, + question_paper=self.questionpaper + ) + + def test_get_assignments_for_user_files(self): + assignment_files, file_name = AssignmentUpload.objects.get_assignments( + self.questionpaper, self.question.id, + self.user1.id + ) + self.assertIn("upload1.txt", assignment_files[0].assignmentFile.name) + self.assertEqual(assignment_files[0].user, self.user1) + actual_file_name = self.user1.get_full_name().replace(" ", "_") + file_name = file_name.replace(" ", "_") + self.assertEqual(file_name, actual_file_name) + + def test_get_assignments_for_quiz_files(self): + assignment_files, file_name = AssignmentUpload.objects.get_assignments( + self.questionpaper + ) + files = [os.path.basename(file.assignmentFile.name) + for file in assignment_files] + question_papers = [file.question_paper for file in assignment_files] + self.assertIn("upload1.txt", files) + self.assertIn("upload2.txt", files) + self.assertEqual(question_papers[0].quiz, self.questionpaper.quiz) + actual_file_name = self.quiz.description.replace(" ", "_") + file_name = file_name.replace(" ", "_") + self.assertIn(actual_file_name, file_name) diff --git a/yaksh/urls.py b/yaksh/urls.py index 91f4137..8ddfe67 100644 --- a/yaksh/urls.py +++ b/yaksh/urls.py @@ -26,6 +26,8 @@ urlpatterns = [ url(r'^enroll_request/(?P<course_id>\d+)/$', views.enroll_request, name='enroll_request'), url(r'^self_enroll/(?P<course_id>\d+)/$', views.self_enroll, name='self_enroll'), url(r'^view_answerpaper/(?P<questionpaper_id>\d+)/$', views.view_answerpaper, name='view_answerpaper'), + url(r'^download/user_assignment/(?P<question_id>\d+)/(?P<user_id>\d+)/(?P<quiz_id>\d+)$', + views.download_assignment_file, name="download_user_assignment"), url(r'^manage/$', views.prof_manage, name='manage'), url(r'^manage/addquestion/$', views.add_question), url(r'^manage/addquestion/(?P<question_id>\d+)/$', views.add_question), @@ -91,4 +93,8 @@ urlpatterns = [ url(r'^manage/create_demo_course/$', views.create_demo_course), url(r'^manage/courses/download_course_csv/(?P<course_id>\d+)/$', views.download_course_csv), + url(r'^manage/download/user_assignment/(?P<question_id>\d+)/(?P<user_id>\d+)/(?P<quiz_id>\d+)/$', + views.download_assignment_file), + url(r'^manage/download/quiz_assignments/(?P<quiz_id>\d+)/$', + views.download_assignment_file) ] diff --git a/yaksh/views.py b/yaksh/views.py index 27276ae..44f06b1 100644 --- a/yaksh/views.py +++ b/yaksh/views.py @@ -22,6 +22,11 @@ from taggit.models import Tag from itertools import chain import json import six +import zipfile +try: + from StringIO import StringIO as string_io +except ImportError: + from io import BytesIO as string_io # Local imports. from yaksh.models import get_model_class, Quiz, Question, QuestionPaper, QuestionSet, Course from yaksh.models import Profile, Answer, AnswerPaper, User, TestCase, FileUpload,\ @@ -494,22 +499,33 @@ 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 - if 'assignment' in request.FILES: - assignment_filename = request.FILES.getlist('assignment') + assignment_filename = request.FILES.getlist('assignment') + if not assignment_filename: + msg = "Please upload assignment file" + return show_question(request, current_question, paper, notification=msg) + for fname in assignment_filename: - if AssignmentUpload.objects.filter( - assignmentQuestion=current_question, - assignmentFile__icontains=fname, user=user).exists(): - assign_file = AssignmentUpload.objects.get( - assignmentQuestion=current_question, - assignmentFile__icontains=fname, user=user) + assignment_files = AssignmentUpload.objects.filter( + assignmentQuestion=current_question, + assignmentFile__icontains=fname, user=user, + question_paper=questionpaper_id) + if assignment_files.exists(): + assign_file = assignment_files.get( + assignmentQuestion=current_question, + assignmentFile__icontains=fname, user=user, + question_paper=questionpaper_id) os.remove(assign_file.assignmentFile.path) assign_file.delete() AssignmentUpload.objects.create(user=user, - assignmentQuestion=current_question, assignmentFile=fname + assignmentQuestion=current_question, assignmentFile=fname, + question_paper_id=questionpaper_id ) 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) else: @@ -913,14 +929,15 @@ def design_questionpaper(request, quiz_id, questionpaper_id=None): if 'remove-fixed' in request.POST: question_ids = request.POST.getlist('added-questions', None) - que_order = question_paper.fixed_question_order.split(",") - for qid in question_ids: - que_order.remove(qid) - if que_order: - question_paper.fixed_question_order = ",".join(que_order) - else: - question_paper.fixed_question_order = "" - question_paper.save() + if question_paper.fixed_question_order: + que_order = question_paper.fixed_question_order.split(",") + for qid in question_ids: + que_order.remove(qid) + if que_order: + question_paper.fixed_question_order = ",".join(que_order) + else: + question_paper.fixed_question_order = "" + question_paper.save() question_paper.fixed_questions.remove(*question_ids) if 'add-random' in request.POST: @@ -1106,7 +1123,13 @@ def grade_user(request, quiz_id=None, user_id=None, attempt_number=None): if not quiz.course.is_creator(current_user) and not \ quiz.course.is_teacher(current_user): raise Http404('This course does not belong to you') - context = {"users": user_details, "quiz_id": quiz_id, "quiz": quiz} + + has_quiz_assignments = AssignmentUpload.objects.filter( + question_paper_id=questionpaper_id + ).exists() + context = {"users": user_details, "quiz_id": quiz_id, "quiz":quiz, + "has_quiz_assignments": has_quiz_assignments + } if user_id is not None: attempts = AnswerPaper.objects.get_user_all_attempts\ @@ -1116,23 +1139,27 @@ def grade_user(request, quiz_id=None, user_id=None, attempt_number=None): attempt_number = attempts[0].attempt_number except IndexError: raise Http404('No attempts for paper') - + has_user_assignments = AssignmentUpload.objects.filter( + question_paper_id=questionpaper_id, + user_id=user_id + ).exists() user = User.objects.get(id=user_id) data = AnswerPaper.objects.get_user_data(user, questionpaper_id, attempt_number ) - context = {'data': data, "quiz_id": quiz_id, "users": user_details, - "attempts": attempts, "user_id": user_id + "attempts": attempts, "user_id": user_id, + "has_user_assignments": has_user_assignments, + "has_quiz_assignments": has_quiz_assignments } if request.method == "POST": papers = data['papers'] for paper in papers: - for question, answers, errors in six.iteritems(paper.get_question_answers()): + for question, answers in six.iteritems(paper.get_question_answers()): marks = float(request.POST.get('q%d_marks' % question.id, 0)) - answers = answers[-1] - answers.set_marks(marks) - answers.save() + answer = answers[-1]['answer'] + answer.set_marks(marks) + answer.save() paper.update_marks() paper.comments = request.POST.get( 'comments_%d' % paper.question_paper.id, 'No comments') @@ -1313,7 +1340,10 @@ def view_answerpaper(request, questionpaper_id): quiz = get_object_or_404(QuestionPaper, pk=questionpaper_id).quiz if quiz.view_answerpaper and user in quiz.course.students.all(): data = AnswerPaper.objects.get_user_data(user, questionpaper_id) - context = {'data': data, 'quiz': quiz} + has_user_assignment = AssignmentUpload.objects.filter(user=user, + question_paper_id=questionpaper_id).exists() + context = {'data': data, 'quiz': quiz, + "has_user_assignment":has_user_assignment} return my_render_to_response('yaksh/view_answerpaper.html', context) else: return my_redirect('/exam/quizzes/') @@ -1410,3 +1440,32 @@ def download_course_csv(request, course_id): for student in students: writer.writerow(student) return response + + +@login_required +def download_assignment_file(request, quiz_id, question_id=None, user_id=None): + user = request.user + qp = QuestionPaper.objects.get(quiz_id=quiz_id) + assignment_files, file_name = AssignmentUpload.objects.get_assignments(qp, + question_id, + user_id + ) + zipfile_name = string_io() + zip_file = zipfile.ZipFile(zipfile_name, "w") + for f_name in assignment_files: + folder = f_name.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 + ) + zip_file.close() + zipfile_name.seek(0) + response = HttpResponse(content_type='application/zip') + response['Content-Disposition'] = '''attachment;\ + filename={0}.zip'''.format( + file_name.replace(" ", "_") + ) + response.write(zipfile_name.read()) + return response |