diff options
-rw-r--r-- | CHANGELOG.txt | 2 | ||||
-rw-r--r-- | yaksh/models.py | 16 | ||||
-rw-r--r-- | yaksh/templates/yaksh/monitor.html | 47 | ||||
-rw-r--r-- | yaksh/test_models.py | 74 | ||||
-rw-r--r-- | yaksh/test_views.py | 20 | ||||
-rw-r--r-- | yaksh/urls.py | 4 | ||||
-rw-r--r-- | yaksh/views.py | 119 |
7 files changed, 232 insertions, 50 deletions
diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 352eb56..56c8910 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -12,6 +12,8 @@ * Fixed a bug that did not allow expected input in Standard I/O type question to be none. * UI changes in grade user, view answerpaper and monitor pages. * Fixed a bug that would require shebang to be put in for Bash assertion based questions. +* Bug fixed that did not allow to edit a profile. +* CSV download for quiz attempts enhanced. * Updated Courses Page to show Active Courses on top. * Added a Datetime Picker to Add Course Page diff --git a/yaksh/models.py b/yaksh/models.py index d02b6d6..cb9b481 100644 --- a/yaksh/models.py +++ b/yaksh/models.py @@ -840,6 +840,13 @@ class QuestionPaper(models.Model): objects = QuestionPaperManager() + def get_question_bank(self): + ''' Gets all the questions in the question paper''' + questions = list(self.fixed_questions.all()) + for random_set in self.random_questions.all(): + questions += list(random_set.questions.all()) + return questions + def _create_duplicate_questionpaper(self, quiz): new_questionpaper = QuestionPaper.objects.create(quiz=quiz, shuffle_questions=self.shuffle_questions, @@ -1201,6 +1208,15 @@ class AnswerPaper(models.Model): objects = AnswerPaperManager() + def get_per_question_score(self, question_id): + if question_id not in self.get_questions().values_list('id', flat=True): + return 'NA' + answer = self.get_latest_answer(question_id) + if answer: + return answer.marks + else: + return 0 + def current_question(self): """Returns the current active question to display.""" unanswered_questions = self.questions_unanswered.all() diff --git a/yaksh/templates/yaksh/monitor.html b/yaksh/templates/yaksh/monitor.html index 9ce0dc4..8df2e7d 100644 --- a/yaksh/templates/yaksh/monitor.html +++ b/yaksh/templates/yaksh/monitor.html @@ -76,7 +76,10 @@ $(document).ready(function() <p>Papers in progress:<b> {{ inprogress_papers }} </b></p> <p><a href="{{URL_ROOT}}/exam/manage/statistics/question/{{papers.0.question_paper.id}}">Question Statisitics</a></p> -<p><a href="{{URL_ROOT}}/exam/manage/monitor/download_csv/{{papers.0.question_paper.id}}">Download CSV</a></p> +<p> + <button type="button" class="btn btn-info btn-lg" data-toggle="modal" data-target="#csvModal"> + Download CSV <span class="glyphicon glyphicon-save"></span> + </button></p> <table id="result-table" class="tablesorter table table table-striped"> <thead> <tr> @@ -115,4 +118,46 @@ $(document).ready(function() <h4>No Quiz Found</h4> {% endif %} {% endif %} + +<!-- CSV Modal --> +<div class="modal fade" id="csvModal" role="dialog"> + <div class="modal-dialog"> + + <!-- Modal content--> + <div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal">×</button> + <h3 class="modal-title">Uncheck unwanted columns</h3> + </div> + <form action="{{URL_ROOT}}/exam/manage/download_quiz_csv/{{ quiz.course.id }}/{{ quiz.id }}/" method="post"> + {% csrf_token %} + <div class="modal-body"> + {% for field in csv_fields %} + <div class="form-check form-check-inline"> + <label class="form-check-label"> + <input class="form-check-input" name="csv_fields" type="checkbox" value="{{ field }}" checked> {{ field }} + </label> + </div> + {% endfor %} + <b>Select Attempt Number: Default latest attempt</b> + <select class="form-control" name = "attempt_number"> + {%for attempt_number in attempt_numbers %} + {% if forloop.last %} + <option value="{{ attempt_number }}" selected>{{ attempt_number }} (Latest)</option> + {% else %} + <option value = "{{ attempt_number }}"> {{ attempt_number }}</option> + {% endif %} + {% endfor %} + </select> + </div> + <div class="modal-footer"> + <button type="submit" class="btn btn-primary"> Download <span class="glyphicon glyphicon-save"></span></button> + <button type="button" class="btn btn-default" data-dismiss="modal">Close</button> + </div> + </form> + </div> + </div> +</div> + + {% endblock %} diff --git a/yaksh/test_models.py b/yaksh/test_models.py index 71969fc..ddacb2a 100644 --- a/yaksh/test_models.py +++ b/yaksh/test_models.py @@ -384,6 +384,26 @@ class QuestionPaperTestCases(unittest.TestCase): self.questions = Question.objects.filter(active=True) self.quiz = Quiz.objects.get(description="demo quiz 1") + # create question paper with only fixed questions + self.question_paper_fixed_questions = QuestionPaper.objects.create( + quiz=self.quiz) + self.question_paper_fixed_questions.fixed_questions.add( + self.questions.get(summary='Q11'), self.questions.get(summary='Q10')) + + # create question paper with only random questions + self.question_paper_random_questions = QuestionPaper.objects.create( + quiz=self.quiz) + self.question_set_random = QuestionSet.objects.create(marks=2, + num_questions=2) + self.question_set_random.questions.add(self.questions.get(summary='Q13'), + self.questions.get(summary='Q5'), self.questions.get(summary='Q7')) + self.question_paper_random_questions.random_questions.add( + self.question_set_random) + + # create question paper with no questions + self.question_paper_no_questions = QuestionPaper.objects.create( + quiz=self.quiz) + # create question paper self.question_paper = QuestionPaper.objects.create(quiz=self.quiz, total_marks=0.0, @@ -441,6 +461,31 @@ class QuestionPaperTestCases(unittest.TestCase): self.trial_course = Course.objects.create_trial_course(self.user) self.trial_quiz = Quiz.objects.create_trial_quiz(self.trial_course, self.user) + + def test_get_question_bank(self): + # Given + summaries = ['Q11', 'Q10'] + questions = list(Question.objects.filter(summary__in=summaries)) + # When + question_bank = self.question_paper_fixed_questions.get_question_bank() + # Then + self.assertSequenceEqual(questions, question_bank) + + # Given + summaries = ['Q13','Q5','Q7'] + questions = list(Question.objects.filter(summary__in=summaries)) + # When + question_bank = self.question_paper_random_questions.get_question_bank() + # Then + self.assertSequenceEqual(questions, question_bank) + + # Given + questions = [] + # When + question_bank = self.question_paper_no_questions.get_question_bank() + # Then + self.assertSequenceEqual(questions, question_bank) + def test_questionpaper(self): """ Test question paper""" self.assertEqual(self.question_paper.quiz.description, 'demo quiz 1') @@ -630,7 +675,7 @@ class AnswerPaperTestCases(unittest.TestCase): error=json.dumps([]) ) self.single_answer.save() - self.answerpaper.answers.add(self.single_answer) + self.answerpaper_single_question.answers.add(self.single_answer) self.question1.language = 'python' self.question1.test_case_type = 'standardtestcase' @@ -691,6 +736,31 @@ class AnswerPaperTestCases(unittest.TestCase): self.user2, self.ip, 1 ) + def test_get_per_question_score(self): + # Given + question_id = self.question4.id + expected_score = 1 + # When + score = self.answerpaper_single_question.get_per_question_score(question_id) + # Then + self.assertEqual(score, expected_score) + + # Given + question_id = self.question2.id + expected_score = 0 + # When + score = self.answerpaper.get_per_question_score(question_id) + # Then + self.assertEqual(score, expected_score) + + # Given + question_id = 131 + expected_score = 'NA' + # When + score = self.answerpaper.get_per_question_score(question_id) + # Then + self.assertEqual(score, expected_score) + def test_returned_question_is_not_none(self): # Test add_completed_question and next_question # When all questions are answered @@ -976,7 +1046,7 @@ class AnswerPaperTestCases(unittest.TestCase): first_answer_obj = first_answer['answer'] self.assertEqual(first_answer_obj.answer, 'Demo answer') self.assertTrue(first_answer_obj.correct) - self.assertEqual(len(answered), 3) + self.assertEqual(len(answered), 2) def test_is_answer_correct(self): self.assertTrue(self.answerpaper.is_answer_correct(self.questions[0])) diff --git a/yaksh/test_views.py b/yaksh/test_views.py index 0950785..e3b0168 100644 --- a/yaksh/test_views.py +++ b/yaksh/test_views.py @@ -2936,6 +2936,7 @@ class TestDownloadcsv(TestCase): self.mod_group.user_set.add(self.user) self.course = Course.objects.create(name="Python Course", enrollment="Enroll Request", creator=self.user) + self.course.students.add(self.student) self.quiz = Quiz.objects.create( start_date_time=datetime(2014, 10, 9, 10, 8, 15, 0, tzone), @@ -2988,8 +2989,9 @@ class TestDownloadcsv(TestCase): username=self.student.username, password=self.student_plaintext_pass ) - response = self.client.get(reverse('yaksh:download_csv', - kwargs={"questionpaper_id": self.question_paper.id}), + response = self.client.get(reverse('yaksh:download_quiz_csv', + kwargs={"course_id": self.course.id, + "quiz_id": self.quiz.id}), follow=True ) self.assertEqual(response.status_code, 404) @@ -3017,8 +3019,9 @@ class TestDownloadcsv(TestCase): username=self.student.username, password=self.student_plaintext_pass ) - response = self.client.get(reverse('yaksh:download_csv', - kwargs={"questionpaper_id": self.question_paper.id}), + response = self.client.get(reverse('yaksh:download_quiz_csv', + kwargs={"course_id": self.course.id, + "quiz_id": self.quiz.id}), follow=True ) self.assertEqual(response.status_code, 404) @@ -3063,11 +3066,14 @@ class TestDownloadcsv(TestCase): username=self.user.username, password=self.user_plaintext_pass ) - response = self.client.get(reverse('yaksh:download_csv', - kwargs={'questionpaper_id': self.question_paper.id}), + response = self.client.get(reverse('yaksh:download_quiz_csv', + kwargs={"course_id": self.course.id, + "quiz_id": self.quiz.id}), + follow=True ) - file_name = "{0}.csv".format(self.quiz.description) + file_name = "{0}-{1}-attempt{2}.csv".format(self.course.name.replace('.', ''), + self.quiz.description.replace('.', ''), 1) self.assertEqual(response.status_code, 200) self.assertEqual(response.get('Content-Disposition'), 'attachment; filename="{0}"'.format(file_name)) diff --git a/yaksh/urls.py b/yaksh/urls.py index 4c5593a..9288956 100644 --- a/yaksh/urls.py +++ b/yaksh/urls.py @@ -56,8 +56,8 @@ urlpatterns = [ views.show_statistics, name="show_statistics"), url(r'^manage/statistics/question/(?P<questionpaper_id>\d+)/(?P<attempt_number>\d+)/$', views.show_statistics, name="show_statistics"), - url(r'^manage/monitor/download_csv/(?P<questionpaper_id>\d+)/$', - views.download_csv, name="download_csv"), + url(r'^manage/download_quiz_csv/(?P<course_id>\d+)/(?P<quiz_id>\d+)/$', + views.download_quiz_csv, name="download_quiz_csv"), url(r'^manage/duplicate_course/(?P<course_id>\d+)/$', views.duplicate_course, name='duplicate_course'), url(r'manage/courses/$', views.courses, name='courses'), diff --git a/yaksh/views.py b/yaksh/views.py index 63da956..0ba3270 100644 --- a/yaksh/views.py +++ b/yaksh/views.py @@ -80,6 +80,10 @@ def add_to_group(users): user.groups.add(group) +CSV_FIELDS = ['name', 'username', 'roll_number', 'institute', 'department', + 'questions', 'marks_obtained', 'out_of', 'percentage', 'status'] + + @email_verified def index(request, next_url=None): """The start page. @@ -993,7 +997,12 @@ def monitor(request, quiz_id=None): papers = [] q_paper = None latest_attempts = [] + attempt_numbers = [] else: + if q_paper: + attempt_numbers = AnswerPaper.objects.get_attempt_numbers(q_paper.last().id) + else: + attempt_numbers = [] latest_attempts = [] papers = AnswerPaper.objects.filter(question_paper=q_paper).order_by( 'user__profile__roll_number' @@ -1009,11 +1018,14 @@ def monitor(request, quiz_id=None): attempt_number=last_attempt['last_attempt_num'] ) ) + csv_fields = CSV_FIELDS context = { "papers": papers, "quiz": quiz, "msg": "Quiz Results", - "latest_attempts": latest_attempts + "latest_attempts": latest_attempts, + "csv_fields": csv_fields, + "attempt_numbers": attempt_numbers } return my_render_to_response('yaksh/monitor.html', context, context_instance=ci) @@ -1266,49 +1278,80 @@ def user_data(request, user_id, questionpaper_id=None): context_instance=RequestContext(request)) +def _expand_questions(questions, field_list): + i = field_list.index('questions') + field_list.remove('questions') + for question in questions: + field_list.insert(i, '{0}-{1}'.format(question.summary, question.points)) + return field_list + + @login_required @email_verified -def download_csv(request, questionpaper_id): - user = request.user - if not is_moderator(user): +def download_quiz_csv(request, course_id, quiz_id): + current_user = request.user + if not is_moderator(current_user): raise Http404('You are not allowed to view this page!') - quiz = Quiz.objects.get(questionpaper=questionpaper_id) + course = get_object_or_404(Course, id=course_id) + quiz = get_object_or_404(Quiz, id=quiz_id) + if not course.is_creator(current_user) and not course.is_teacher(current_user): + raise Http404('The quiz does not belong to your course') + users = course.get_enrolled().order_by('first_name') + if not users: + return monitor(request, quiz_id) + csv_fields = [] + attempt_number = None + question_paper = quiz.questionpaper_set.last() + last_attempt_number =AnswerPaper.objects.get_attempt_numbers(question_paper.id).last() + if request.method == 'POST': + csv_fields = request.POST.getlist('csv_fields') + attempt_number = request.POST.get('attempt_number', last_attempt_number) + if not csv_fields: + csv_fields = CSV_FIELDS + if not attempt_number: + attempt_number = last_attempt_number + + questions = question_paper.get_question_bank() + answerpapers = AnswerPaper.objects.filter(question_paper=question_paper, + attempt_number=attempt_number) + if not answerpapers: + return monitor(request, quiz_id) - if not quiz.course.is_creator(user) and not quiz.course.is_teacher(user): - raise Http404('The question paper does not belong to your course') - papers = AnswerPaper.objects.get_latest_attempts(questionpaper_id) - if not papers: - return monitor(request, questionpaper_id) response = HttpResponse(content_type='text/csv') - response['Content-Disposition'] = 'attachment; filename="{0}.csv"'.format( - (quiz.description).replace('.', '')) + response['Content-Disposition'] = 'attachment; filename="{0}-{1}-attempt{2}.csv"'.format( + course.name.replace('.', ''), quiz.description.replace('.', ''), + attempt_number) writer = csv.writer(response) - header = [ - 'name', - 'username', - 'roll_number', - 'institute', - 'marks_obtained', - 'total_marks', - 'percentage', - 'questions', - 'questions_answered', - 'status' - ] - writer.writerow(header) - for paper in papers: - row = [ - '{0} {1}'.format(paper.user.first_name, paper.user.last_name), - paper.user.username, - paper.user.profile.roll_number, - paper.user.profile.institute, - paper.marks_obtained, - paper.question_paper.total_marks, - paper.percent, - paper.questions.all(), - paper.questions_answered.all(), - paper.status - ] + if 'questions' in csv_fields: + csv_fields = _expand_questions(questions, csv_fields) + writer.writerow(csv_fields) + + csv_fields_values = {'name': 'user.get_full_name().title()', + 'roll_number': 'user.profile.roll_number', + 'institute': 'user.profile.institute', + 'department': 'user.profile.department', + 'username': 'user.username', + 'marks_obtained': 'answerpaper.marks_obtained', + 'out_of': 'question_paper.total_marks', + 'percentage': 'answerpaper.percent', 'status': 'answerpaper.status'} + questions_scores = {} + for question in questions: + questions_scores['{0}-{1}'.format(question.summary, question.points)] \ + = 'answerpaper.get_per_question_score({0})'.format(question.id) + csv_fields_values.update(questions_scores) + + users = users.exclude(id=course.creator.id).exclude(id__in=course.teachers.all()) + for user in users: + row = [] + answerpaper = None + papers = answerpapers.filter(user=user) + if papers: + answerpaper = papers.first() + for field in csv_fields: + try: + row.append(eval(csv_fields_values[field])) + except AttributeError: + row.append('-') writer.writerow(row) return response |