summaryrefslogtreecommitdiff
path: root/yaksh
diff options
context:
space:
mode:
Diffstat (limited to 'yaksh')
-rw-r--r--yaksh/fixtures/marks_correct.csv4
-rw-r--r--yaksh/models.py10
-rw-r--r--yaksh/templates/yaksh/monitor.html54
-rw-r--r--yaksh/test_views.py184
-rw-r--r--yaksh/urls.py2
-rw-r--r--yaksh/views.py104
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 da2327c..6f7af53 100644
--- a/yaksh/models.py
+++ b/yaksh/models.py
@@ -1710,12 +1710,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)
@@ -2359,6 +2364,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>&nbsp;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>&nbsp;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>&nbsp;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 e7bbd91..e87686c 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 82785ca..e716404 100644
--- a/yaksh/urls.py
+++ b/yaksh/urls.py
@@ -271,4 +271,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 69a7414..7a4810c 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(
@@ -4032,3 +4045,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()