diff options
Diffstat (limited to 'yaksh')
-rw-r--r-- | yaksh/fixtures/marks_correct.csv | 4 | ||||
-rw-r--r-- | yaksh/models.py | 10 | ||||
-rw-r--r-- | yaksh/templates/yaksh/monitor.html | 54 | ||||
-rw-r--r-- | yaksh/test_views.py | 184 | ||||
-rw-r--r-- | yaksh/urls.py | 2 | ||||
-rw-r--r-- | yaksh/views.py | 104 |
6 files changed, 348 insertions, 10 deletions
diff --git a/yaksh/fixtures/marks_correct.csv b/yaksh/fixtures/marks_correct.csv new file mode 100644 index 0000000..9134da5 --- /dev/null +++ b/yaksh/fixtures/marks_correct.csv @@ -0,0 +1,4 @@ +username,Q-1212-Dummy1-1.0-marks,Q-1212-Dummy1-comments,Q-1213-Dummy2-1.0-marks,Q-1213-Dummy2-comments +student1,1,good work,1,nice +student2,1,good work,0,bad + diff --git a/yaksh/models.py b/yaksh/models.py index 2a06cc8..e2b9952 100644 --- a/yaksh/models.py +++ b/yaksh/models.py @@ -1711,12 +1711,17 @@ class Answer(models.Model): # Whether skipped or not. skipped = models.BooleanField(default=False) + comment = models.TextField(null=True, blank=True) + def set_marks(self, marks): if marks > self.question.points: self.marks = self.question.points else: self.marks = marks + def set_comment(self, comments): + self.comment = comments + def __str__(self): return "Answer for question {0}".format(self.question.summary) @@ -2360,6 +2365,11 @@ class AnswerPaper(models.Model): self.end_time = datetime self.save() + def get_answer_comment(self, question_id): + answer = self.answers.filter(question_id=question_id).last() + if answer: + return answer.comment + def get_question_answers(self): """ Return a dictionary with keys as questions and a list of the diff --git a/yaksh/templates/yaksh/monitor.html b/yaksh/templates/yaksh/monitor.html index 0a8e3e9..2b43ec1 100644 --- a/yaksh/templates/yaksh/monitor.html +++ b/yaksh/templates/yaksh/monitor.html @@ -4,7 +4,7 @@ {% block title %} Monitor {% endblock %} {% block pagetitle %} Monitor {% endblock pagetitle %} -{% block meta %} <meta http-equiv="refresh" content="30"/> {% endblock meta %} +{% block meta %} <meta http-equiv="refresh" content="300"/> {% endblock meta %} {% block script %} {% if papers %} @@ -88,21 +88,57 @@ $(document).ready(function() <br> <div class="row"> <div class="col-md-4"> - <a href="{% url 'yaksh:show_statistics' papers.0.question_paper.id course.id %}" class="btn btn-primary"> - <i class="fa fa-line-chart"></i> Question Statistics - </a> - </div> - <div class="col-md-4"> <button type="button" class="btn btn-info" data-toggle="modal" data-target="#csvModal"> <i class="fa fa-download"></i> Download CSV </button> </div> <div class="col-md-4"> + <a href="{% url 'yaksh:show_statistics' papers.0.question_paper.id course.id %}" class="btn btn-primary"> + <i class="fa fa-line-chart"></i> Question Statistics + </a> + </div> + <div class="col-md-4"> <div class="badge badge-info"> - Auto-Refreshes every 30 seconds + Auto-Refreshes every 5 minutes </div> </div> </div> + <hr> + <div class="row"> + <div class="col-md-6"> + <p> + <b> + - Download the CSV file from the button above<br /> + - Edit and upload the same <br /> + </b> + </p> + </div> + <div class="col-md-6"> + <form id="upload_users" action="{% url 'yaksh:upload_marks' course.id papers.0.question_paper.id %}" method="POST" enctype="multipart/form-data"> + {% csrf_token %} + <div class="input-group mb-3"> + <div class="custom-file"> + <input type="file" class="custom-file-input" name="csv_file" id="upload"/> + <label class="custom-file-label" for="upload">Select</label> + </div> + <div class="input-group-append"> + <button class="btn btn-outline-primary" type="submit"> + <i class="fa fa-upload"></i> + Upload + </button> + </div> + </div> + <script> + $('#upload').on('change',function(){ + //get the file name + var fileName = $(this).val(); + //replace the "Choose a file" label + $(this).next('.custom-file-label').html(fileName); + }) + </script> + </form> + </div> + </div> <br> <table id="result-table" class="tablesorter table table-striped table-responsive-sm"> <thead> @@ -172,7 +208,11 @@ $(document).ready(function() {% for field in csv_fields %} <div class="form-check form-check-inline"> <label class="form-check-label"> + {% if field == 'username' or field == 'questions' %} + <input class="form-check-input" name="csv_fields" type="checkbox" value="{{ field }}" checked onclick="return false"> {{ field }} + {% else %} <input class="form-check-input" name="csv_fields" type="checkbox" value="{{ field }}" checked> {{ field }} + {% endif %} </label> </div> <br> diff --git a/yaksh/test_views.py b/yaksh/test_views.py index 4351a66..3443c36 100644 --- a/yaksh/test_views.py +++ b/yaksh/test_views.py @@ -2566,6 +2566,190 @@ class TestAddCourse(TestCase): target_status_code=301) +class TestUploadMarks(TestCase): + def setUp(self): + self.client = Client() + + self.mod_group = Group.objects.create(name='moderator') + + # Create Moderator with profile + self.teacher = User.objects.create_user( + username='teacher', + password='teacher', + first_name='teacher', + last_name='teaacher', + email='teacher@test.com' + ) + + Profile.objects.create( + user=self.teacher, + roll_number=101, + institute='IIT', + department='Chemical', + position='Moderator', + timezone='UTC', + is_moderator=True + ) + + self.TA = User.objects.create_user( + username='TA', + password='TA', + first_name='TA', + last_name='TA', + email='TA@test.com' + ) + + Profile.objects.create( + user=self.TA, + roll_number=102, + institute='IIT', + department='Aeronautical', + position='Moderator', + timezone='UTC', + is_moderator=True + ) + + # Create Student + self.student1 = User.objects.create_user( + username='student1', + password='student1', + first_name='student_first_name', + last_name='student_last_name', + email='demo_student1@test.com' + ) + self.student2 = User.objects.create_user( + username='student2', + password='student2', + first_name='student_first_name', + last_name='student_last_name', + email='demo_student2@test.com' + ) + + # Add to moderator group + self.mod_group.user_set.add(self.teacher) + self.mod_group.user_set.add(self.TA) + + self.course = Course.objects.create( + name="Python Course", + enrollment="Enroll Request", creator=self.teacher + ) + + self.question1 = Question.objects.create( + id=1212, summary='Dummy1', points=1, + type='code', user=self.teacher + ) + self.question2 = Question.objects.create( + id=1213, summary='Dummy2', points=1, + type='code', user=self.teacher + ) + + self.quiz = Quiz.objects.create(time_between_attempts=0, + description='demo quiz') + self.question_paper = QuestionPaper.objects.create( + quiz=self.quiz, total_marks=2.0 + ) + self.question_paper.fixed_questions.add(self.question1) + self.question_paper.fixed_questions.add(self.question2) + self.question_paper.save() + + self.ans_paper1 = AnswerPaper.objects.create( + user=self.student1, attempt_number=1, + question_paper=self.question_paper, start_time=timezone.now(), + user_ip='101.0.0.1', course=self.course, + end_time=timezone.now()+timezone.timedelta(minutes=20) + ) + self.ans_paper2 = AnswerPaper.objects.create( + user=self.student2, attempt_number=1, + question_paper=self.question_paper, start_time=timezone.now(), + user_ip='101.0.0.1', course=self.course, + end_time=timezone.now()+timezone.timedelta(minutes=20) + ) + self.answer1 = Answer( + question=self.question1, answer="answer1", + correct=False, error=json.dumps([]), marks=0 + ) + self.answer2 = Answer( + question=self.question2, answer="answer2", + correct=False, error=json.dumps([]), marks=0 + ) + self.answer1.save() + self.answer2.save() + self.ans_paper1.answers.add(self.answer1) + self.ans_paper1.answers.add(self.answer2) + self.ans_paper1.questions_answered.add(self.question1) + self.ans_paper1.questions_answered.add(self.question2) + self.ans_paper1.questions.add(self.question1) + self.ans_paper1.questions.add(self.question2) + + def tearDown(self): + self.client.logout() + self.student1.delete() + self.student2.delete() + self.TA.delete() + self.teacher.delete() + self.course.delete() + self.ans_paper1.delete() + self.ans_paper2.delete() + self.question_paper.delete() + self.quiz.delete() + self.question1.delete() + self.question2.delete() + self.mod_group.delete() + + def test_upload_users_with_correct_csv(self): + # Given + self.client.login( + username=self.teacher.username, + password='teacher' + ) + csv_file_path = os.path.join(FIXTURES_DIR_PATH, "marks_correct.csv") + csv_file = open(csv_file_path, 'rb') + upload_file = SimpleUploadedFile(csv_file_path, csv_file.read()) + previous_total = self.ans_paper1.marks_obtained + + # When + response = self.client.post( + reverse('yaksh:upload_marks', + kwargs={'course_id': self.course.id, + 'questionpaper_id': self.question_paper.id}), + data={'csv_file': upload_file}) + csv_file.close() + + # Then + self.assertEqual(response.status_code, 302) + self.assertEqual(previous_total, 0) + ans_paper = AnswerPaper.objects.get(user=self.student1, + question_paper=self.question_paper, + course=self.course) + self.assertEqual(ans_paper.marks_obtained, 2) + answer = Answer.objects.get(answer='answer1') + self.assertEqual(answer.comment.strip(), 'good work') + + def test_upload_users_with_wrong_csv(self): + # Given + self.client.login( + username='teacher', + password='teacher' + ) + csv_file_path = os.path.join(FIXTURES_DIR_PATH, "demo_questions.zip") + csv_file = open(csv_file_path, 'rb') + upload_file = SimpleUploadedFile(csv_file_path, csv_file.read()) + message = "The file uploaded is not a CSV file." + + # When + response = self.client.post( + reverse('yaksh:upload_marks', + kwargs={'course_id': self.course.id, + 'questionpaper_id': self.question_paper.id}), + data={'csv_file': upload_file}) + csv_file.close() + + # Then + self.assertEqual(response.status_code, 302) + messages = [m.message for m in get_messages(response.wsgi_request)] + self.assertEqual('The file uploaded is not a CSV file.', messages[0]) + + class TestCourseDetail(TestCase): def setUp(self): self.client = Client() diff --git a/yaksh/urls.py b/yaksh/urls.py index f15d91a..e93d80a 100644 --- a/yaksh/urls.py +++ b/yaksh/urls.py @@ -269,4 +269,6 @@ urlpatterns = [ views.lesson_statistics, name='lesson_statistics'), path('manage/download/sample/toc', views.download_sample_toc, name='download_sample_toc'), + path('manage/upload_marks/<int:course_id>/<int:questionpaper_id>/', + views.upload_marks, name='upload_marks'), ] diff --git a/yaksh/views.py b/yaksh/views.py index bd8ca5d..dd9090d 100644 --- a/yaksh/views.py +++ b/yaksh/views.py @@ -1818,7 +1818,10 @@ def _expand_questions(questions, field_list): field_list.remove('questions') for question in questions: field_list.insert( - i, '{0}-{1}'.format(question.summary, question.points)) + i, 'Q-{0}-{1}-{2}-marks'.format(question.id, question.summary, + question.points)) + field_list.insert( + i+1, 'Q-{0}-{1}-comments'.format(question.id, question.summary)) return field_list @@ -1878,8 +1881,18 @@ def download_quiz_csv(request, course_id, quiz_id): '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) + questions_scores['Q-{0}-{1}-{2}-marks'.format( + question.id, question.summary, question.points)] \ + = 'answerpaper.get_per_question_score({0})'.format(question.id) + answer = question.answer_set.last() + comment = None + if answer: + comment = answer.comment + else: + comment = '' + questions_scores['Q-{0}-{1}-comments'.format( + question.id, question.summary)] \ + = 'answerpaper.get_answer_comment({0})'.format(question.id) csv_fields_values.update(questions_scores) users = users.exclude(id=course.creator.id).exclude( @@ -4030,3 +4043,88 @@ def lesson_statistics(request, course_id, lesson_id, toc_id=None): context['is_que_data'] = True context['objects'] = per_que_data return render(request, 'yaksh/show_lesson_statistics.html', context) + + +@login_required +@email_verified +def upload_marks(request, course_id, questionpaper_id): + user = request.user + course = get_object_or_404(Course, pk=course_id) + question_paper = get_object_or_404(QuestionPaper, pk=questionpaper_id) + quiz = question_paper.quiz + + if not (course.is_teacher(user) or course.is_creator(user)): + raise Http404('You are not allowed to view this page!') + if request.method == 'POST': + if 'csv_file' not in request.FILES: + messages.warning(request, "Please upload a CSV file.") + return redirect('yaksh:monitor', quiz.id, course_id) + csv_file = request.FILES['csv_file'] + is_csv_file, dialect = is_csv(csv_file) + if not is_csv_file: + messages.warning(request, "The file uploaded is not a CSV file.") + return redirect('yaksh:monitor', quiz.id, course_id) + try: + reader = csv.DictReader( + csv_file.read().decode('utf-8').splitlines(), + dialect=dialect) + except TypeError: + messages.warning(request, "Bad CSV file") + return redirect('yaksh:monitor', quiz.id, course_id) + user_ids, question_ids = _get_header_info(reader) + csv_file.seek(0) + reader = csv.DictReader(csv_file.read().decode('utf-8').splitlines(), + dialect=dialect) + _read_marks_csv(reader, course, question_paper, user_ids, question_ids) + messages.warning(request, "Marks uploaded!") + 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) + for row in reader: + username = row['username'] + user = User.objects.filter(username=username).first() + if not user: + pass + user_ids.append(user.id) + return user_ids, question_ids + + +def _read_marks_csv(reader, course, question_paper, user_ids, question_ids): + answerpapers = question_paper.answerpaper_set.filter(course=course, + user_id__in=user_ids) + for row in reader: + username = row['username'] + user = User.objects.filter(username=username).first() + if not user: + pass + answerpaper = answerpapers.get(user=user) + answers = answerpaper.answers.all() + answered = answerpaper.questions_answered.all().values_list('id', + flat=True) + for qid in question_ids: + question = Question.objects.filter(id=qid).first() + if not question: + pass + if qid in answered: + answer = answers.filter(question_id=qid).last() + if not answer: + pass + answer.set_marks( + float(row['Q-{0}-{1}-{2}-marks'.format( + qid, question.summary, question.points)]) + ) + answer.set_comment( + row['Q-{0}-{1}-comments'.format( + qid, question.summary, question.points)] + ) + answer.save() + answerpaper.update_marks(state='completed') + answerpaper.save() |