Skip to content

Commit

Permalink
Merge pull request #502 from ubc/master-#499
Browse files Browse the repository at this point in the history
Add peer feedback report
  • Loading branch information
xcompass authored Feb 20, 2017
2 parents 0d3c7c0 + 582036d commit 6ca2494
Show file tree
Hide file tree
Showing 4 changed files with 361 additions and 13 deletions.
97 changes: 95 additions & 2 deletions compair/api/report.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import csv
import os
import time
import re

from bouncer.constants import MANAGE
from flask import Blueprint, current_app, abort
from flask_login import login_required, current_user

from flask_restful import Resource, reqparse

from sqlalchemy import func
from sqlalchemy import func, and_, or_
from sqlalchemy.orm import joinedload

from compair.authorization import require
from compair.core import event
from compair.models import CourseRole, Assignment, UserCourse, Course, Answer, \
from compair.models import User, CourseRole, Assignment, UserCourse, Course, Answer, \
AnswerComment, AssignmentCriterion, Comparison, AnswerCommentType
from .util import new_restful_api

Expand Down Expand Up @@ -108,6 +110,21 @@ def post(self, course_uuid):
title_row2.append("Self Evaluation Submitted")
titles = [title_row1, title_row2]

elif report_type == "peer_feedback":
titles1 = [
"",
"Sender", "", "",
"Receiver", "", "",
"", ""
]
titles2 = [
"Assignment",
"Last Name", "First Name", "Student No",
"Last Name", "First Name", "Student No",
"Type", "Feedback"
]
data = peer_feedback_report(course, assignments, group_name)
titles = [titles1, titles2]
else:
return {'error': 'The requested report type cannot be found'}, 400

Expand Down Expand Up @@ -329,3 +346,79 @@ def participation_report(course, assignments, group_name):
report.append(temp)

return report

def peer_feedback_report(course, assignments, group_name):
report = []

senders = User.query \
.join("user_courses") \
.filter(and_(
UserCourse.course_id == course.id,
UserCourse.course_role == CourseRole.student
)) \
.order_by(User.lastname, User.firstname, User.id)
if group_name:
senders = senders.filter(UserCourse.group_name == group_name)
senders = senders.all()
sender_user_ids = [u.id for u in senders]

assignment_ids = [assignment.id for assignment in assignments]

answer_comments = AnswerComment.query \
.join(Answer, AnswerComment.answer_id == Answer.id) \
.join(User, User.id == Answer.user_id) \
.with_entities(
Answer.user_id.label("receiver_user_id"),
AnswerComment.user_id.label("sender_user_id"),
Answer.assignment_id.label("assignment_id"),
AnswerComment.comment_type,
AnswerComment.content,
User.firstname.label("receiver_firstname"),
User.lastname.label("receiver_lastname"),
User.student_number.label("receiver_student_number"),
) \
.filter(Answer.assignment_id.in_(assignment_ids)) \
.filter(AnswerComment.user_id.in_(sender_user_ids)) \
.filter(AnswerComment.comment_type != AnswerCommentType.self_evaluation) \
.filter(Answer.draft == False) \
.filter(Answer.practice == False) \
.filter(AnswerComment.draft == False) \
.order_by(AnswerComment.created) \
.all()

for assignment in assignments:
for user in senders:
user_sent_feedback = [ac for ac in answer_comments \
if ac.sender_user_id == user.id and ac.assignment_id == assignment.id]

if len(user_sent_feedback) > 0:
for feedback in user_sent_feedback:
temp = [
assignment.name,
user.lastname, user.firstname, user.student_number,
feedback.receiver_lastname, feedback.receiver_firstname, feedback.receiver_student_number,
feedback.comment_type.value, strip_html(feedback.content)
]
report.append(temp)

else:
# enter blank row
temp = [
assignment.name,
user.lastname, user.firstname, user.student_number,
"---", "---", "---",
"", ""
]
report.append(temp)

return report

def strip_html(text):
text = re.sub('<[^>]+>', '', text)
text = text.replace('&nbsp;', ' ')
text = text.replace('&amp;', '&')
text = text.replace('&lt;', '<')
text = text.replace('&gt;', '>')
text = text.replace('&quot;', '"')
text = text.replace('&#39;', '\'')
return text
3 changes: 2 additions & 1 deletion compair/static/modules/report/report-module.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ module.controller(
var allGroups = {'name': 'All Groups', 'value': 'all'};
$scope.types = [
{'id': 'participation', 'name': 'Participation Report (Regular)'},
{'id': 'participation_stat', 'name': 'Participation Report (Research)'}
{'id': 'participation_stat', 'name': 'Participation Report (Research)'},
{'id': 'peer_feedback', 'name': 'Peer Feedback Report'}
];

UserResource.getTeachingUserCourses().$promise.then(
Expand Down
219 changes: 216 additions & 3 deletions compair/tests/api/test_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import json
import io
import os
import re

from data.fixtures.test_data import TestFixture
from compair.tests.test_compair import ComPAIRAPITestCase
Expand All @@ -13,7 +14,7 @@ class ReportAPITest(ComPAIRAPITestCase):
def setUp(self):
super(ReportAPITest, self).setUp()
self.fixtures = TestFixture().add_course(num_students=30, num_assignments=2, num_additional_criteria=1, num_groups=2, num_answers=25,
with_draft_student=True)
with_draft_student=True, with_comments=True)
self.url = "/api/courses/" + self.fixtures.course.uuid + "/report"
self.files_to_cleanup = []

Expand All @@ -28,8 +29,6 @@ def tearDown(self):
except Exception as e:
print(e)



def test_generate_report(self):
# test login required
rv = self.client.post(self.url)
Expand Down Expand Up @@ -326,7 +325,163 @@ def test_generate_report(self):
next_row = next(reader)
user_stats = self._check_participation_stat_report_user_row(self.fixtures.assignments[0], student, next_row, overall_stats)

# peer_feedback with valid instructor
with self.login(self.fixtures.instructor.username):
input = {
'group_name': None,
'type': "peer_feedback",
'assignment': None
}

# test authorized user entire course
rv = self.client.post(self.url, data=json.dumps(input), content_type='application/json')
self.assert200(rv)
self.assertIsNotNone(rv.json['file'])
file_name = rv.json['file'].split("/")[-1]
self.files_to_cleanup.append(file_name)

tmp_name = os.path.join(current_app.config['REPORT_FOLDER'], file_name)
with open(tmp_name, 'rt') as csvfile:
reader = csv.reader(csvfile, delimiter=',')

heading1 = next(reader)
heading2 = next(reader)
self._check_peer_feedback_report_heading_rows(heading1, heading2)

sorted_students = sorted(self.fixtures.students,
key=lambda student: (student.lastname, student.firstname, student.id)
)

for assignment in self.fixtures.assignments:
for student in sorted_students:
self._check_peer_feedback_report_user_rows(assignment, student, reader)

# test authorized user one assignment
single_assignment_input = input.copy()
single_assignment_input['assignment'] = self.fixtures.assignments[0].uuid
rv = self.client.post(self.url, data=json.dumps(single_assignment_input), content_type='application/json')
self.assert200(rv)
self.assertIsNotNone(rv.json['file'])
file_name = rv.json['file'].split("/")[-1]
self.files_to_cleanup.append(file_name)

tmp_name = os.path.join(current_app.config['REPORT_FOLDER'], file_name)
with open(tmp_name, 'rt') as csvfile:
reader = csv.reader(csvfile, delimiter=',')

heading1 = next(reader)
heading2 = next(reader)
self._check_peer_feedback_report_heading_rows(heading1, heading2)

sorted_students = sorted(self.fixtures.students,
key=lambda student: (student.lastname, student.firstname, student.id)
)

for student in sorted_students:
self._check_peer_feedback_report_user_rows(self.fixtures.assignments[0], student, reader)

# test authorized user entire course with group_name filter
group_name_input = input.copy()
group_name_input['group_name'] = self.fixtures.groups[0]
rv = self.client.post(self.url, data=json.dumps(group_name_input), content_type='application/json')
self.assert200(rv)
self.assertIsNotNone(rv.json['file'])
file_name = rv.json['file'].split("/")[-1]
self.files_to_cleanup.append(file_name)

tmp_name = os.path.join(current_app.config['REPORT_FOLDER'], file_name)
with open(tmp_name, 'rt') as csvfile:
reader = csv.reader(csvfile, delimiter=',')

heading1 = next(reader)
heading2 = next(reader)
self._check_peer_feedback_report_heading_rows(heading1, heading2)

sorted_students = sorted(self.fixtures.students,
key=lambda student: (student.lastname, student.firstname, student.id)
)

for assignment in self.fixtures.assignments:
for student in sorted_students:
if student.user_courses[0].group_name != self.fixtures.groups[0]:
continue
self._check_peer_feedback_report_user_rows(assignment, student, reader)

# test authorized user one assignment
group_name_input = input.copy()
group_name_input['group_name'] = self.fixtures.groups[0]
group_name_input['assignment'] = self.fixtures.assignments[0].uuid
rv = self.client.post(self.url, data=json.dumps(group_name_input), content_type='application/json')
self.assert200(rv)
self.assertIsNotNone(rv.json['file'])
file_name = rv.json['file'].split("/")[-1]
self.files_to_cleanup.append(file_name)

tmp_name = os.path.join(current_app.config['REPORT_FOLDER'], file_name)
with open(tmp_name, 'rt') as csvfile:
reader = csv.reader(csvfile, delimiter=',')

heading1 = next(reader)
heading2 = next(reader)
self._check_peer_feedback_report_heading_rows(heading1, heading2)

sorted_students = sorted(self.fixtures.students,
key=lambda student: (student.lastname, student.firstname, student.id)
)

for student in sorted_students:
if student.user_courses[0].group_name != self.fixtures.groups[0]:
continue
self._check_peer_feedback_report_user_rows(self.fixtures.assignments[0], student, reader)

# test authorized user one assignment (content's html parsed)
original_content = {}
for answer_comment in self.fixtures.answer_comments:
if answer_comment.user.user_courses[0].group_name != self.fixtures.groups[0] or \
answer_comment.assignment_id != self.fixtures.assignments[0].id:
continue
original_content[answer_comment.id] = answer_comment.content
answer_comment.content = '<p id="some_id">&#39;&quot;&gt;&lt;&amp;&nbsp;<\/p>'+answer_comment.content
db.session.commit()

group_name_input = input.copy()
group_name_input['group_name'] = self.fixtures.groups[0]
group_name_input['assignment'] = self.fixtures.assignments[0].uuid
rv = self.client.post(self.url, data=json.dumps(group_name_input), content_type='application/json')
self.assert200(rv)
self.assertIsNotNone(rv.json['file'])
file_name = rv.json['file'].split("/")[-1]
self.files_to_cleanup.append(file_name)

tmp_name = os.path.join(current_app.config['REPORT_FOLDER'], file_name)
with open(tmp_name, 'rt') as csvfile:
reader = csv.reader(csvfile, delimiter=',')

heading1 = next(reader)
heading2 = next(reader)
self._check_peer_feedback_report_heading_rows(heading1, heading2)

sorted_students = sorted(self.fixtures.students,
key=lambda student: (student.lastname, student.firstname, student.id)
)

for student in sorted_students:
if student.user_courses[0].group_name != self.fixtures.groups[0]:
continue

answer_comments = sorted(
[ac for ac in self.fixtures.answer_comments if ac.user_id == student.id and
ac.assignment_id == self.fixtures.assignments[0].id],
key=lambda ac: (ac.created)
)

if len(answer_comments) > 0:
for answer_comment in answer_comments:
row = next(reader)
self.assertEqual(row[8], '\'"><& '+original_content[answer_comment.id])
else:
# skip user with no comments
row = next(reader)

def _check_participation_stat_report_heading_rows(self, heading):
expected_heading = ['Assignment', 'User UUID', 'Last Name', 'First Name',
Expand Down Expand Up @@ -488,3 +643,61 @@ def _check_participation_report_user_row(self, assignments, student, row):

self.assertEqual(row[index], str(evaluations_submitted))
index += 1

def _check_peer_feedback_report_heading_rows(self, heading1, heading2):
expected_heading1 = [
"",
"Sender", "", "",
"Receiver", "", "",
"", ""
]
self.assertEqual(expected_heading1, heading1)
expected_heading2 = [
"Assignment",
"Last Name", "First Name", "Student No",
"Last Name", "First Name", "Student No",
"Type", "Feedback"
]
self.assertEqual(expected_heading2, heading2)

def _check_peer_feedback_report_user_rows(self, assignment, student, reader):

answer_comments = sorted(
[ac for ac in self.fixtures.answer_comments if ac.user_id == student.id and ac.assignment_id == assignment.id],
key=lambda ac: (ac.created)
)

if len(answer_comments) > 0:
for answer_comment in answer_comments:
row = next(reader)
answer_user = answer_comment.answer.user

excepted_row = [
assignment.name,
student.lastname, student.firstname, student.student_number,
answer_user.lastname, answer_user.firstname, answer_user.student_number,
answer_comment.comment_type.value, self._strip_html(answer_comment.content)
]

self.assertEqual(row, excepted_row)
else:
row = next(reader)

excepted_row = [
assignment.name,
student.lastname, student.firstname, student.student_number,
"---", "---", "---",
"", ""
]

self.assertEqual(row, excepted_row)

def _strip_html(self, text):
text = re.sub('<[^>]+>', '', text)
text = text.replace('&nbsp;', ' ')
text = text.replace('&amp;', '&')
text = text.replace('&lt;', '<')
text = text.replace('&gt;', '>')
text = text.replace('&quot;', '"')
text = text.replace('&#39;', '\'')
return text
Loading

0 comments on commit 6ca2494

Please sign in to comment.