Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Render reviews #1111

Merged
merged 6 commits into from
Apr 6, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 64 additions & 4 deletions api/app/reviews/api.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from flask import g
import flask_restful as restful
from flask_restful import reqparse, fields, marshal_with
from flask_restful import reqparse, fields, marshal_with, marshal
from math import ceil
import random
from sqlalchemy.sql import func, exists
Expand Down Expand Up @@ -79,7 +79,26 @@
'response_id': fields.Integer,
'reviewer_user_id': fields.Integer,
'scores': fields.List(fields.Nested(review_scores_fields), attribute='review_scores'),
'language': fields.String
'language': fields.String,
'is_submitted': fields.Boolean,
'submitted_timestamp': fields.DateTime(dt_format='iso8601')
}

extended_scores_fields = {
'review_question_id': fields.Integer,
'headline': fields.String,
'description': fields.String,
'type': fields.String,
'value': fields.String
}

extended_review_fields = {
'review_response_id': fields.Integer,
'response_id': fields.Integer,
'reviewer_user_title': fields.String,
'reviewer_user_firstname': fields.String,
'reviewer_user_lastname': fields.String,
'scores': fields.List(fields.Nested(extended_scores_fields), attribute='scores'),
}

reference_fields = {
Expand All @@ -101,6 +120,14 @@
'submitted_timestamp': fields.DateTime(dt_format='iso8601')
}

reviewer_user_fields = {
'id': fields.Integer,
'email': fields.String,
'firstname': fields.String,
'lastname': fields.String,
'user_title': fields.String,
}

review_question_detail_fields = {
'id': fields.Integer,
'question_id': fields.Integer,
Expand Down Expand Up @@ -135,6 +162,11 @@
'sections': fields.List(fields.Nested(review_section_detail_fields), attribute='review_sections')
}

response_review_fields = {
'review_form': fields.Raw,
'review_responses': fields.List(fields.Nested(extended_review_fields)),
}

def _serialize_review_question(review_question, language):
translation = review_question.get_translation(language)
if translation is None:
Expand Down Expand Up @@ -191,13 +223,19 @@ def _add_reviewer_role(user_id, event_id):

class ReviewResponseUser():
def __init__(self, review_form, response, reviews_remaining_count, language, reference_responses=None,
review_response=None):
review_response=None, reviewer=None):
self.review_form = _serialize_review_form(review_form, language)
self.response = response
self.user = None if response is None else response.user
self.reviews_remaining_count = reviews_remaining_count
self.references = reference_responses
self.review_response = review_response
self.reviewer = reviewer

class ReviewResponses():
def __init__(self, review_form, review_responses, language):
self.review_form = _serialize_review_form(review_form, language)
self.review_responses = review_responses


class ReviewResponseReference():
Expand Down Expand Up @@ -294,6 +332,28 @@ def get(self):

return ReviewResponseUser(review_form, response, 0, args['language'], review_response=review_response)

class ResponseReviewEventAdminAPI(restful.Resource):
@event_admin_required
def get(self, event_id):
parser = reqparse.RequestParser()
parser.add_argument('response_id', type=int, required=True)
parser.add_argument('language', type=str, required=True)
args = parser.parse_args()

response_id = args['response_id']

review_form = review_repository.get_review_form(event_id)
if review_form is None:
return REVIEW_FORM_NOT_FOUND

response_reviews = (review_repository.get_all_review_responses_by_response(review_form.id, response_id))
serialized_reviews = [
ReviewResponseDetailListAPI._serialise_review_response(response, args['language'])
for response in response_reviews
]
return marshal(ReviewResponses(review_form, serialized_reviews, args['language']), response_review_fields), 201



class ReviewResponseAPI(GetReviewResponseMixin, PostReviewResponseMixin, restful.Resource):

Expand Down Expand Up @@ -711,7 +771,7 @@ def _serialise_score(review_score, language):
'headline': review_question_translation.headline,
'description': review_question_translation.description,
'type': review_score.review_question.type,
'score': review_score.value,
'value': review_score.value,
'weight': review_score.review_question.weight,
}

Expand Down
8 changes: 8 additions & 0 deletions api/app/reviews/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,14 @@ def get_all_review_responses_by_event(event_id):
.filter_by(event_id=event_id)
.all()
)

@staticmethod
def get_all_review_responses_by_response(review_form_id, response_id):
return (
db.session.query(ReviewResponse)
.filter_by(review_form_id=review_form_id, response_id=response_id, is_submitted=True)
.all()
)

@staticmethod
def get_average_score_for_review_question(response_id: int, review_question_id: int):
Expand Down
12 changes: 6 additions & 6 deletions api/app/reviews/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -1925,19 +1925,19 @@ def test_review_responses_for_event(self):
self.assertEqual(data[0]['scores'][0]['headline'], 'Ethical Considerations')
self.assertEqual(data[0]['scores'][0]['description'], "How ethical is the candidate's proposal from 1 to 5?")
self.assertEqual(data[0]['scores'][0]['type'], 'short-text')
self.assertEqual(data[0]['scores'][0]['score'], '4')
self.assertEqual(data[0]['scores'][0]['value'], '4')
self.assertEqual(data[0]['scores'][0]['weight'], 1)

self.assertEqual(data[0]['scores'][1]['headline'], None)
self.assertEqual(data[0]['scores'][1]['description'], 'Comments for the candidate')
self.assertEqual(data[0]['scores'][1]['type'], 'long-text')
self.assertEqual(data[0]['scores'][1]['score'], 'This is a very good proposal')
self.assertEqual(data[0]['scores'][1]['value'], 'This is a very good proposal')
self.assertEqual(data[0]['scores'][1]['weight'], 0)

self.assertEqual(data[0]['scores'][2]['headline'], None)
self.assertEqual(data[0]['scores'][2]['description'], 'What is your overall rating?')
self.assertEqual(data[0]['scores'][2]['type'], 'multi-choice')
self.assertEqual(data[0]['scores'][2]['score'], '5')
self.assertEqual(data[0]['scores'][2]['value'], '5')
self.assertEqual(data[0]['scores'][2]['weight'], 2)

self.assertEqual(data[0]['total'], 14)
Expand All @@ -1964,19 +1964,19 @@ def test_review_responses_for_event(self):
self.assertEqual(data[1]['scores'][0]['headline'], 'Ethical Considerations')
self.assertEqual(data[1]['scores'][0]['description'], "How ethical is the candidate's proposal from 1 to 5?")
self.assertEqual(data[1]['scores'][0]['type'], 'short-text')
self.assertEqual(data[1]['scores'][0]['score'], '3')
self.assertEqual(data[1]['scores'][0]['value'], '3')
self.assertEqual(data[1]['scores'][0]['weight'], 1)

self.assertEqual(data[1]['scores'][1]['headline'], None)
self.assertEqual(data[1]['scores'][1]['description'], 'Comments for the candidate')
self.assertEqual(data[1]['scores'][1]['type'], 'long-text')
self.assertEqual(data[1]['scores'][1]['score'], 'Not bad!')
self.assertEqual(data[1]['scores'][1]['value'], 'Not bad!')
self.assertEqual(data[1]['scores'][1]['weight'], 0)

self.assertEqual(data[1]['scores'][2]['headline'], None)
self.assertEqual(data[1]['scores'][2]['description'], 'What is your overall rating?')
self.assertEqual(data[1]['scores'][2]['type'], 'multi-choice')
self.assertEqual(data[1]['scores'][2]['score'], '3')
self.assertEqual(data[1]['scores'][2]['value'], '3')
self.assertEqual(data[1]['scores'][2]['weight'], 2)

self.assertEqual(data[1]['total'], 9)
Expand Down
1 change: 1 addition & 0 deletions api/app/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@
rest_api.add_resource(responses_api.ResponseDetailAPI, '/api/v1/responsedetail')
rest_api.add_resource(reviews_api.ReviewListAPI, '/api/v1/reviewlist')
rest_api.add_resource(reviews_api.ResponseReviewAPI, '/api/v1/responsereview')
rest_api.add_resource(reviews_api.ResponseReviewEventAdminAPI, '/api/v1/responsereview-admin')
rest_api.add_resource(reviews_api.ResponseReviewAssignmentAPI, '/api/v1/assignresponsereviewer')
rest_api.add_resource(reviews_api.ReviewResponseDetailListAPI, '/api/v1/reviewresponsedetaillist')
rest_api.add_resource(reviews_api.ReviewResponseSummaryListAPI, '/api/v1/reviewresponsesummarylist')
Expand Down
5 changes: 5 additions & 0 deletions webapp/src/pages/ResponsePage/ResponsePage.css
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,11 @@ html .headings-lower label {
font-size: 13px;
}

.reviewers-section h4 {
font-size: 17px;
font-weight: bold;
}

.reviewers-section h5 {
margin-top: 11px;
color: grey;
Expand Down
68 changes: 61 additions & 7 deletions webapp/src/pages/ResponsePage/ResponsePage.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class ResponsePage extends Component {
removeReviewerModalVisible: false,
tagToRemove: null,
reviewToRemove: null,
reviewResponses: [],
outcome: {'status':null,'timestamp':null},
}
};
Expand All @@ -41,7 +42,6 @@ class ResponsePage extends Component {
applicationFormService.getForEvent(this.props.event.id),
responsesService.getResponseDetail(this.props.match.params.id, this.props.event.id),
tagsService.getTagList(this.props.event.id),
reviewService.getReviewAssignments(this.props.event.id)
]).then(responses => {
console.log(responses);
this.setState({
Expand All @@ -50,15 +50,24 @@ class ResponsePage extends Component {
applicationForm: responses[1].formSpec,
applicationData: responses[2].detail,
tagList: responses[3].tags,
reviewers: responses[4].reviewers,
error: responses[0].error || responses[1].error || responses[2].error || responses[3].error || responses[4].error,
error: responses[0].error || responses[1].error || responses[2].error || responses[3].error,
}, () => {
this.handleData();
this.getOutcome();
this.getReviewResponses(responses[2].detail);
});
});
};

getReviewResponses(applicationData) {
reviewService.getResponseReviewAdmin(applicationData.id, this.props.event.id)
.then(resp => {
this.setState( {
reviewResponses: resp.form.review_responses,
reviewForm: resp.form.review_form,
isLoading: false
});
});
}

// Misc Functions

Expand Down Expand Up @@ -110,6 +119,42 @@ class ResponsePage extends Component {
};
};

renderReviewResponse(review_response, section) {
const questions = section.review_questions.map(q => {
const a = review_response.scores.find(a => a.review_question_id === q.id);

if (a) {
return <div>
<div key={q.question_id} className="question-answer-block">
<p><span className="question-headline">{q.headline}</span>
{q.description && a && <span className="question-description"><br/>{q.description}</span>}
</p>
<h6><AnswerValue answer={a} question={q} /></h6>
</div>
</div>
}


});
return questions
};

// Render Reviews
renderCompleteReviews(){
if (this.state.reviewResponses) {
const reviews = this.state.reviewResponses.map((val, index) => {
return <div className="section">
<h4
className="reviewer-section" >
{this.props.t(val.reviewer_user_firstname + " " + val.reviewer_user_lastname)}
</h4>
{this.renderReviewResponse(val, this.state.reviewForm.review_sections[0])}
</div>
});
return reviews
}
}

getOutcome() {
outcomeService.getOutcome(this.props.event.id, this.state.applicationData.user_id).then(response => {
if (response.status === 200) {
Expand Down Expand Up @@ -354,7 +399,6 @@ class ResponsePage extends Component {

};


// Tag Functions
// Post Response API
postResponseTag(tagId, responseId, eventId) {
Expand Down Expand Up @@ -591,7 +635,7 @@ class ResponsePage extends Component {
return < ReviewModal
handlePost={(data) => this.postReviewerService(data)}
response={this.state.applicationData}
reviewers={this.state.reviewers.filter(r => !this.state.applicationData.reviewers.some(rr => rr.reviewer_user_id === r.reviewer_user_id))}
reviewers={this.state.reviewResponses.filter(r => !this.state.applicationData.reviewers.some(rr => rr.reviewer_user_id === r.reviewer_user_id))}
event={this.props.event}
t={this.props.t}
/>
Expand Down Expand Up @@ -710,11 +754,21 @@ class ResponsePage extends Component {
</button>
</div>
</div>

<div className="divider"></div>
</div>

{this.renderSections()}
{
this.reviewResponses && (
<div>
<div className="divider"></div>
<div className="reviewers-section">
<h3>{t('Reviewer Feedback')}</h3>
{this.renderCompleteReviews()}
</div>
</div>

)}
</div>
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ class ReviewListComponent extends Component {
Header: headline,
accessor: r => {
const score = r.scores.find(s => s.review_question_id === i.review_question_id);
return score ? score.score : "";
return score ? score.value : "";
}
});
}
Expand Down
23 changes: 23 additions & 0 deletions webapp/src/services/reviews/review.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const reviewService = {
getReviewHistory,
getReviewList,
getResponseReview,
getResponseReviewAdmin,
assignResponsesToReviewer,
deleteResponseReviewer,
getReviewDetails,
Expand Down Expand Up @@ -89,6 +90,28 @@ function getResponseReview(responseId, eventId) {
});
}

function getResponseReviewAdmin(responseId, eventId) {
return axios
.get(baseUrl + `/api/v1/responsereview-admin?response_id=${responseId}&event_id=${eventId}`, {
headers: authHeader()
})
.then(function(response) {
return {
form: response.data,
error: ""
};
})
.catch(function(error) {
return {
form: null,
error:
error.response && error.response.data
? error.response.data.message
: error.message
};
});
}

function submit(responseId, reviewFormId, scores, shouldUpdate, isSubmitted) {
let review = {
response_id: responseId,
Expand Down