-
Notifications
You must be signed in to change notification settings - Fork 38
Review summary backend #1317
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
base: develop
Are you sure you want to change the base?
Review summary backend #1317
Changes from 38 commits
922abc9
561ef22
c4851df
25ac768
5531c1a
503b569
5dc41a2
fb33cb1
965f85c
e0a9e81
6b2587b
60242b1
a6b4e60
9012c5f
2c360cf
33bd3d1
32002b3
c5e3e89
e894f1c
be6dc9d
841d8d9
dba4490
9517bc0
b0b05d6
043490e
613a150
c707680
d9a713b
8c91e74
1d21025
d413d55
111704a
abdeccc
10cc03f
c699cf3
81b0461
f2d8483
50a8050
a04301e
6ae24b3
1ef444e
18e3eae
dc2f977
1621c77
6625ff2
321d707
f7c13a8
51e3783
b1dbd7d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,17 +4,24 @@ | |
| from flask import g, request | ||
| from sqlalchemy.exc import SQLAlchemyError | ||
|
|
||
|
|
||
| from app.outcome.models import Outcome, Status | ||
| from app.outcome.repository import OutcomeRepository as outcome_repository | ||
| from app.events.repository import EventRepository as event_repository | ||
| from app.users.repository import UserRepository as user_repository | ||
| from app.utils.emailer import email_user | ||
|
|
||
| from app.responses.repository import ResponseRepository as response_repository | ||
| from app.utils import errors, strings | ||
| from app.reviews.repository import ReviewRepository as review_repository | ||
| from app.events.models import EventType | ||
|
|
||
| from app.utils.auth import auth_required, event_admin_required | ||
| from app import LOGGER | ||
| from app import db | ||
| from app.utils import errors | ||
| from app.utils import misc | ||
| from app.reviews.api import ReviewResponseDetailListAPI | ||
|
|
||
|
|
||
| def _extract_status(outcome): | ||
|
|
@@ -26,6 +33,7 @@ def _extract_status(outcome): | |
| 'id': fields.Integer, | ||
| 'status': fields.String(attribute=_extract_status), | ||
| 'timestamp': fields.DateTime(dt_format='iso8601'), | ||
| 'review_summary': fields.String, | ||
| } | ||
|
|
||
| user_fields = { | ||
|
|
@@ -36,27 +44,50 @@ def _extract_status(outcome): | |
| 'user_title': fields.String | ||
| } | ||
|
|
||
|
|
||
| answer_fields = { | ||
| 'id': fields.Integer, | ||
| 'question_id': fields.Integer, | ||
| 'question': fields.String(attribute='question.headline'), | ||
| 'value': fields.String(attribute='value_display'), | ||
| 'question_type': fields.String(attribute='question.type') | ||
| } | ||
|
|
||
| response_fields = { | ||
| 'id': fields.Integer, | ||
| 'application_form_id': fields.Integer, | ||
| 'user_id': fields.Integer, | ||
| 'is_submitted': fields.Boolean, | ||
| 'submitted_timestamp': fields.DateTime(dt_format='iso8601'), | ||
| 'is_withdrawn': fields.Boolean, | ||
| 'withdrawn_timestamp': fields.DateTime(dt_format='iso8601'), | ||
| 'started_timestamp': fields.DateTime(dt_format='iso8601'), | ||
| 'answers': fields.List(fields.Nested(answer_fields)) | ||
| } | ||
|
|
||
| outcome_list_fields = { | ||
| 'id': fields.Integer, | ||
| 'status': fields.String(attribute=_extract_status), | ||
| 'timestamp': fields.DateTime(dt_format='iso8601'), | ||
| 'user': fields.Nested(user_fields), | ||
| 'updated_by_user': fields.Nested(user_fields) | ||
| 'updated_by_user': fields.Nested(user_fields), | ||
| 'response': fields.Nested(response_fields) | ||
| } | ||
|
|
||
|
|
||
| class OutcomeAPI(restful.Resource): | ||
| @event_admin_required | ||
| @marshal_with(outcome_fields) | ||
| def get(self, event_id): | ||
| req_parser = reqparse.RequestParser() | ||
| req_parser.add_argument('user_id', type=int, required=True) | ||
| req_parser.add_argument('response_id', type=int, required=True) | ||
| args = req_parser.parse_args() | ||
|
|
||
| user_id = args['user_id'] | ||
| response_id=args['response_id'] | ||
|
|
||
| try: | ||
| outcome = outcome_repository.get_latest_by_user_for_event(user_id, event_id) | ||
| outcome = outcome_repository.get_latest_by_user_for_event(user_id, event_id,response_id) | ||
| if not outcome: | ||
| return errors.OUTCOME_NOT_FOUND | ||
|
|
||
|
|
@@ -68,22 +99,28 @@ def get(self, event_id): | |
| except: | ||
| LOGGER.error("Encountered unknown error: {}".format(traceback.format_exc())) | ||
| return errors.DB_NOT_AVAILABLE | ||
|
|
||
|
|
||
|
|
||
| @event_admin_required | ||
| @marshal_with(outcome_fields) | ||
| def post(self, event_id): | ||
| req_parser = reqparse.RequestParser() | ||
| req_parser.add_argument('user_id', type=int, required=True) | ||
| req_parser.add_argument('outcome', type=str, required=True) | ||
| req_parser.add_argument('response_id', type=int, required=True) | ||
| req_parser.add_argument('review_summary', type=str, required=True) | ||
|
||
| args = req_parser.parse_args() | ||
|
|
||
| event = event_repository.get_by_id(event_id) | ||
|
|
||
| if not event: | ||
| return errors.EVENT_NOT_FOUND | ||
|
|
||
| user = user_repository.get_by_id(args['user_id']) | ||
| if not user: | ||
| return errors.USER_NOT_FOUND | ||
|
|
||
|
|
||
| try: | ||
| status = Status[args['outcome']] | ||
|
|
@@ -92,7 +129,7 @@ def post(self, event_id): | |
|
|
||
| try: | ||
| # Set existing outcomes to no longer be the latest outcome | ||
| existing_outcomes = outcome_repository.get_all_by_user_for_event(args['user_id'], event_id) | ||
| existing_outcomes = outcome_repository.get_all_by_user_for_event(args['user_id'], event_id, args['response_id']) | ||
| for existing_outcome in existing_outcomes: | ||
| existing_outcome.reset_latest() | ||
|
|
||
|
|
@@ -101,21 +138,54 @@ def post(self, event_id): | |
| event_id, | ||
| args['user_id'], | ||
| status, | ||
| g.current_user['id']) | ||
| g.current_user['id'], | ||
| args['response_id'], | ||
| args['review_summary']) | ||
|
|
||
| outcome_repository.add(outcome) | ||
| db.session.commit() | ||
|
|
||
| if (status == Status.REJECTED or status == Status.WAITLIST): # Email will be sent with offer for accepted candidates | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the list of code changes, it looks like this code is deleted, but it is shifted down. It might be cleaner to swap the "if" statements? |
||
| email_user( | ||
| 'outcome-rejected' if status == Status.REJECTED else 'outcome-waitlist', | ||
| template_parameters=dict( | ||
| host=misc.get_baobab_host() | ||
| ), | ||
| event=event, | ||
| user=user, | ||
| ) | ||
| if (event.event_type==EventType.JOURNAL): | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. space around == ? |
||
| response = response_repository.get_by_id_and_user_id(outcome.response_id, outcome.user_id) | ||
| submission_title=strings.answer_by_question_key('submission_title', response.application_form, response.answers) | ||
|
||
| review_form = review_repository.get_review_form(outcome.event_id) | ||
|
|
||
| if review_form is not None: | ||
| response_reviews = review_repository.get_all_review_responses_by_response(review_form.id, outcome.response_id) | ||
| else: | ||
|
|
||
| raise errors.REVIEW_FORM_NOT_FOUND | ||
|
|
||
| serialized_reviews = [ReviewResponseDetailListAPI._serialise_review_response(response, user.user_primaryLanguage) for response in response_reviews] | ||
|
|
||
| question_answer_summary = strings.build_review_email_body(serialized_reviews, user.user_primaryLanguage, review_form) | ||
| email_user( | ||
| 'response-journal', | ||
| template_parameters=dict( | ||
| summary=outcome.review_summary, | ||
| outcome=outcome.status.value, | ||
| submission_title=submission_title, | ||
| reviewers_contents=question_answer_summary, | ||
| ), | ||
| subject_parameters=dict( | ||
| submission_title=submission_title, | ||
| ), | ||
| event=event, | ||
| user=user, | ||
| ) | ||
|
|
||
| else: | ||
| if (status == Status.REJECTED or status == Status.WAITLIST): # Email will be sent with offer for accepted candidates | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. see earlier comment |
||
| email_user( | ||
| 'outcome-rejected' if status == Status.REJECTED else 'outcome-waitlist', | ||
| template_parameters=dict( | ||
| host=misc.get_baobab_host() | ||
| ), | ||
| event=event, | ||
| user=user, | ||
| ) | ||
|
|
||
|
|
||
| return outcome, 201 | ||
|
|
||
| except SQLAlchemyError as e: | ||
|
|
@@ -124,6 +194,7 @@ def post(self, event_id): | |
| except: | ||
| LOGGER.error("Encountered unknown error: {}".format(traceback.format_exc())) | ||
| return errors.DB_NOT_AVAILABLE | ||
|
|
||
|
|
||
|
|
||
| class OutcomeListAPI(restful.Resource): | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,12 +3,12 @@ | |
| from enum import Enum | ||
|
|
||
| class Status(Enum): | ||
| ACCEPTED = "accepted" | ||
| REJECTED = "rejected" | ||
| WAITLIST = "waitlist" | ||
| REVIEW = "in review" | ||
| ACCEPT_W_REVISION = "accept with minor revision" | ||
| REJECT_W_ENCOURAGEMENT = "reject with encouragement to resubmit" | ||
| ACCEPTED = "Accepted" | ||
|
||
| REJECTED = "Rejected" | ||
| WAITLIST = "Waitlist" | ||
| REVIEW = "In review" | ||
| ACCEPT_W_REVISION = "Accept with minor revision" | ||
| REJECT_W_ENCOURAGEMENT = "Reject with encouragement to resubmit" | ||
|
|
||
| class Outcome(db.Model): | ||
| id = db.Column(db.Integer(), primary_key = True, nullable = False) | ||
|
|
@@ -23,18 +23,28 @@ class Outcome(db.Model): | |
| user = db.relationship('AppUser', foreign_keys=[user_id]) | ||
| updated_by_user = db.relationship('AppUser', foreign_keys=[updated_by_user_id]) | ||
|
|
||
| response_id = db.Column(db.Integer(), db.ForeignKey('response.id'), nullable=True) | ||
| response = db.relationship('Response', foreign_keys=[response_id]) | ||
|
|
||
| review_summary = db.Column(db.String(), nullable=True) | ||
|
|
||
|
|
||
| def __init__(self, | ||
| event_id, | ||
| user_id, | ||
| status, | ||
| updated_by_user_id | ||
| updated_by_user_id, | ||
| response_id, | ||
| review_summary | ||
| ): | ||
| self.event_id = event_id | ||
| self.user_id = user_id | ||
| self.status = status | ||
| self.timestamp = datetime.now() | ||
| self.latest = True | ||
| self.updated_by_user_id = updated_by_user_id | ||
| self.response_id = response_id | ||
| self.review_summary = review_summary | ||
|
|
||
| def reset_latest(self): | ||
| self.latest = False | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,16 +5,16 @@ | |
| class OutcomeRepository(): | ||
|
|
||
| @staticmethod | ||
| def get_latest_by_user_for_event(user_id, event_id): | ||
| def get_latest_by_user_for_event(user_id, event_id,response_id=None): | ||
| outcome = (db.session.query(Outcome) | ||
| .filter_by(user_id=user_id, event_id=event_id, latest=True) | ||
| .filter_by(user_id=user_id, event_id=event_id, latest=True,response_id=response_id) | ||
|
||
| .first()) | ||
| return outcome | ||
|
|
||
| @staticmethod | ||
| def get_all_by_user_for_event(user_id, event_id): | ||
| def get_all_by_user_for_event(user_id, event_id, response_id=None): | ||
| outcomes = (db.session.query(Outcome) | ||
| .filter_by(user_id=user_id, event_id=event_id) | ||
| .filter_by(user_id=user_id, event_id=event_id, response_id=response_id) | ||
| .all()) | ||
| return outcomes | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -29,6 +29,11 @@ def _extract_outcome_status(response): | |
| return None | ||
| return response.outcome.status.name | ||
|
|
||
| def _extract_review_summary(response): | ||
| if not hasattr(response, "review_summary") or not isinstance(response.outcome, Outcome): | ||
| return None | ||
| return response.outcome.review_summary | ||
|
|
||
|
|
||
| class ResponseAPI(ResponseMixin, restful.Resource): | ||
|
|
||
|
|
@@ -50,7 +55,8 @@ class ResponseAPI(ResponseMixin, restful.Resource): | |
| 'answers': fields.List(fields.Nested(answer_fields)), | ||
| 'language': fields.String, | ||
| 'parent_id': fields.Integer(default=None), | ||
| 'outcome': fields.String(attribute=_extract_outcome_status) | ||
| 'outcome': fields.String(attribute=_extract_outcome_status), | ||
| 'review_summary': fields.String(attribute=_extract_review_summary) | ||
| } | ||
|
|
||
| def find_answer(self, question_id: int, answers: Sequence[Answer]) -> Optional[Answer]: | ||
|
|
@@ -123,9 +129,10 @@ def get(self): | |
| responses = response_repository.get_all_for_user_application(current_user_id, form.id) | ||
|
|
||
| # TODO: Link outcomes to responses rather than events to cater for multiple submissions. | ||
|
||
| outcome = outcome_repository.get_latest_by_user_for_event(current_user_id, event_id) | ||
| for response in responses: | ||
| outcome = outcome_repository.get_latest_by_user_for_event(current_user_id, event_id, response.id) | ||
| response.outcome = outcome | ||
| response.review_summary = outcome | ||
|
|
||
upaq marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| return marshal(responses, ResponseAPI.response_fields), 200 | ||
|
|
||
|
|
@@ -136,6 +143,10 @@ def post(self): | |
| is_submitted = args['is_submitted'] | ||
| application_form_id = args['application_form_id'] | ||
| language = args['language'] | ||
| parent_id = args.get('parent_id', None) | ||
| allow_multiple_submissions = args['multiple_submission'] | ||
|
||
|
|
||
|
|
||
| if len(language) != 2: | ||
| language = 'en' # Fallback to English if language doesn't look like an ISO 639-1 code | ||
|
|
||
|
|
@@ -146,10 +157,10 @@ def post(self): | |
| user = user_repository.get_by_id(user_id) | ||
| responses = response_repository.get_all_for_user_application(user_id, application_form_id) | ||
|
|
||
| if not application_form.nominations and len(responses) > 0: | ||
| if not allow_multiple_submissions and len(responses) > 0: | ||
|
||
| return errors.RESPONSE_ALREADY_SUBMITTED | ||
|
|
||
| response = Response(application_form_id, user_id, language) | ||
| response = Response(application_form_id, user_id, language, parent_id) | ||
| response_repository.save(response) | ||
|
|
||
| answers = [] | ||
|
|
@@ -296,16 +307,34 @@ def send_confirmation(self, user, response): | |
| event_description = event.get_description(user.user_primaryLanguage) | ||
| else: | ||
| event_description = event.get_description('en') | ||
|
|
||
| if (event.event_type==EventType.JOURNAL): | ||
| submission_title=strings.answer_by_question_key('submission_title', response.application_form, response.answers) | ||
| emailer.email_user( | ||
| 'submitting-article-journal', | ||
| template_parameters=dict( | ||
| event_description=event_description, | ||
| question_answer_summary=question_answer_summary, | ||
| ), | ||
| subject_parameters=dict( | ||
| submission_title=submission_title, | ||
| ), | ||
| event=event, | ||
| user=user | ||
| ) | ||
|
|
||
|
|
||
| emailer.email_user( | ||
| 'confirmation-response-call' if event.event_type == EventType.CALL else 'confirmation-response', | ||
| template_parameters=dict( | ||
| event_description=event_description, | ||
| question_answer_summary=question_answer_summary, | ||
| ), | ||
| event=event, | ||
| user=user | ||
| ) | ||
| else: | ||
|
|
||
| emailer.email_user( | ||
| 'confirmation-response-call' if event.event_type == EventType.CALL else 'confirmation-response', | ||
| template_parameters=dict( | ||
| event_description=event_description, | ||
| question_answer_summary=question_answer_summary, | ||
| ), | ||
| event=event, | ||
| user=user | ||
| ) | ||
|
|
||
| except Exception as e: | ||
| LOGGER.error('Could not send confirmation email for response with id : {response_id} due to: {e}'.format(response_id=response.id, e=e)) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
General style comment (please fix everywhere): please ensure there is a space after a comma in an argument list.