From de835fd2205a2e8d8c2b15eeef6132ac788f9794 Mon Sep 17 00:00:00 2001 From: Amrit Purshotam Date: Thu, 17 Mar 2022 20:26:55 +0100 Subject: [PATCH 1/8] Add attribute to use only LF line endings --- .gitattributes | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..658066882 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +text=auto eol=lf + +*.{png,pdf,ico,enc,jpg,jpeg,gif,webp,woff,woff2} binary \ No newline at end of file From 7214133b7e8525b1e9302edfde88df99f053cf8e Mon Sep 17 00:00:00 2001 From: Amrit Purshotam Date: Thu, 17 Mar 2022 20:30:48 +0100 Subject: [PATCH 2/8] Convert all CRLF to LF --- api/app/attendance/api.py | 344 +- api/app/attendance/mixins.py | 10 +- api/app/attendance/models.py | 44 +- api/app/attendance/repository.py | 50 +- api/app/events/mixins.py | 84 +- api/app/files/api.py | 118 +- api/app/files/mixins.py | 10 +- api/app/references/mixins.py | 80 +- api/app/reviews/api.py | 2182 +++++------ api/app/reviews/mixins.py | 80 +- api/app/reviews/models.py | 514 +-- api/app/reviews/repository.py | 810 ++-- api/app/reviews/tests.py | 5896 +++++++++++++++--------------- api/app/users/repository.py | 160 +- 14 files changed, 5191 insertions(+), 5191 deletions(-) diff --git a/api/app/attendance/api.py b/api/app/attendance/api.py index 622abc4c3..eb7ed830a 100644 --- a/api/app/attendance/api.py +++ b/api/app/attendance/api.py @@ -1,172 +1,172 @@ -from flask import g -import flask_restful as restful -from flask_restful import reqparse, fields, marshal_with -from app import db -from app.attendance.mixins import AttendanceMixin -from app.attendance.models import Attendance -from app.attendance.repository import AttendanceRepository as attendance_repository -from app.events.repository import EventRepository as event_repository -from app.users.repository import UserRepository as user_repository -from app.utils.auth import auth_required -from app.utils.emailer import email_user -from app.utils.errors import ATTENDANCE_ALREADY_CONFIRMED, ATTENDANCE_NOT_FOUND, EVENT_NOT_FOUND, FORBIDDEN, USER_NOT_FOUND, OFFER_NOT_FOUND, REGISTRATION_NOT_FOUND -from app.registration.models import RegistrationQuestion -from app.registration.models import Offer -from app.invitedGuest.models import GuestRegistration -from app.registration.models import RegistrationForm -from app.registration.models import Registration -from app.registration.models import get_registration_answer_based_headline -from app.events.models import EventRole -from app.invitedGuest.models import InvitedGuest - -attendance_fields = { - 'id': fields.Integer, - 'event_id': fields.Integer, - 'user_id': fields.Integer, - 'timestamp': fields.DateTime('iso8601'), - 'updated_by_user_id': fields.Integer, - 'accommodation_award': fields.Boolean, - 'shirt_size': fields.String, - 'is_invitedguest': fields.Boolean, - 'bringing_poster': fields.Boolean, - 'message': fields.String, - 'invitedguest_role': fields.String, -} - - -class AttendanceUser(): - def __init__(self, attendance, accommodation_award, shirt_size, is_invitedguest, bringing_poster, invitedguest_role): - self.id = attendance.id - self.event_id = attendance.event_id - self.user_id = attendance.user_id - self.timestamp = attendance.timestamp - self.updated_by_user_id = attendance.updated_by_user_id - self.accommodation_award = accommodation_award - self.shirt_size = shirt_size - self.is_invitedguest = is_invitedguest - self.bringing_poster = bringing_poster - self.invitedguest_role = invitedguest_role - - -class AttendanceAPI(AttendanceMixin, restful.Resource): - - @auth_required - @marshal_with(attendance_fields) - def get(self): - args = self.req_parser.parse_args() - event_id = args['event_id'] - user_id = args['user_id'] - registration_user_id = g.current_user['id'] - - registration_user = user_repository.get_by_id(registration_user_id) - if not registration_user.is_registration_volunteer(event_id): - return FORBIDDEN - - event = event_repository.get_by_id(event_id) - if event is None: - return EVENT_NOT_FOUND - - user = user_repository.get_by_id(user_id) - if user is None: - return USER_NOT_FOUND - - attendance = attendance_repository.get(event_id, user_id) - if attendance is None: - return ATTENDANCE_NOT_FOUND - - return attendance, 200 - - @auth_required - @marshal_with(attendance_fields) - def post(self): - args = self.req_parser.parse_args() - event_id = args['event_id'] - user_id = args['user_id'] - registration_user_id = g.current_user['id'] - - registration_user = user_repository.get_by_id(registration_user_id) - if not registration_user.is_registration_volunteer(event_id): - return FORBIDDEN - - event = event_repository.get_by_id(event_id) - if event is None: - return EVENT_NOT_FOUND - - user = user_repository.get_by_id(user_id) - if user is None: - return USER_NOT_FOUND - - if attendance_repository.exists(event_id, user_id): - return ATTENDANCE_ALREADY_CONFIRMED - - attendance = Attendance(event_id, user_id, registration_user_id) - attendance_repository.create(attendance) - - email_user( - 'attendance-confirmation', - event=event, - user=user - ) - - # Other Fields - unavailable_response = None - invitedguest_role = unavailable_response - is_invited_guest = unavailable_response - has_accepted_accom_award = unavailable_response - - offer = db.session.query(Offer).filter( - Offer.user_id == user_id).filter(Offer.event_id == event_id).first() - if offer: - has_accepted_accom_award = ( - offer.accommodation_award and offer.accepted_accommodation_award) - - # Check if invited guest - invited_guest = db.session.query(InvitedGuest).filter( - InvitedGuest.event_id == event_id).filter(InvitedGuest.user_id == user.id).first() - if(invited_guest): - is_invited_guest = True - invitedguest_role = invited_guest.role - - - # Shirt Size - shirt_answer = (get_registration_answer_based_headline( - user_id, "T-Shirt Size") and get_registration_answer_based_headline( - user_id, "T-Shirt Size").value) or unavailable_response - - # Poster registration - bringing_poster = False - poster_answer = get_registration_answer_based_headline( - user_id, "Will you be bringing a poster?") - if (poster_answer is not None and poster_answer.value == 'yes'): - bringing_poster = True - - attendance_user = AttendanceUser( - attendance, has_accepted_accom_award, shirt_answer, is_invited_guest, bringing_poster, invitedguest_role) - return attendance_user, 201 - - @auth_required - def delete(self): - args = self.req_parser.parse_args() - event_id = args['event_id'] - user_id = args['user_id'] - registration_user_id = g.current_user['id'] - - registration_user = user_repository.get_by_id(registration_user_id) - if not registration_user.is_registration_volunteer(event_id): - return FORBIDDEN - - event = event_repository.get_by_id(event_id) - if event is None: - return EVENT_NOT_FOUND - - user = user_repository.get_by_id(user_id) - if user is None: - return USER_NOT_FOUND - - attendance = attendance_repository.get(event_id, user_id) - if attendance is None: - return ATTENDANCE_NOT_FOUND - - attendance_repository.delete(attendance) - - return 200 +from flask import g +import flask_restful as restful +from flask_restful import reqparse, fields, marshal_with +from app import db +from app.attendance.mixins import AttendanceMixin +from app.attendance.models import Attendance +from app.attendance.repository import AttendanceRepository as attendance_repository +from app.events.repository import EventRepository as event_repository +from app.users.repository import UserRepository as user_repository +from app.utils.auth import auth_required +from app.utils.emailer import email_user +from app.utils.errors import ATTENDANCE_ALREADY_CONFIRMED, ATTENDANCE_NOT_FOUND, EVENT_NOT_FOUND, FORBIDDEN, USER_NOT_FOUND, OFFER_NOT_FOUND, REGISTRATION_NOT_FOUND +from app.registration.models import RegistrationQuestion +from app.registration.models import Offer +from app.invitedGuest.models import GuestRegistration +from app.registration.models import RegistrationForm +from app.registration.models import Registration +from app.registration.models import get_registration_answer_based_headline +from app.events.models import EventRole +from app.invitedGuest.models import InvitedGuest + +attendance_fields = { + 'id': fields.Integer, + 'event_id': fields.Integer, + 'user_id': fields.Integer, + 'timestamp': fields.DateTime('iso8601'), + 'updated_by_user_id': fields.Integer, + 'accommodation_award': fields.Boolean, + 'shirt_size': fields.String, + 'is_invitedguest': fields.Boolean, + 'bringing_poster': fields.Boolean, + 'message': fields.String, + 'invitedguest_role': fields.String, +} + + +class AttendanceUser(): + def __init__(self, attendance, accommodation_award, shirt_size, is_invitedguest, bringing_poster, invitedguest_role): + self.id = attendance.id + self.event_id = attendance.event_id + self.user_id = attendance.user_id + self.timestamp = attendance.timestamp + self.updated_by_user_id = attendance.updated_by_user_id + self.accommodation_award = accommodation_award + self.shirt_size = shirt_size + self.is_invitedguest = is_invitedguest + self.bringing_poster = bringing_poster + self.invitedguest_role = invitedguest_role + + +class AttendanceAPI(AttendanceMixin, restful.Resource): + + @auth_required + @marshal_with(attendance_fields) + def get(self): + args = self.req_parser.parse_args() + event_id = args['event_id'] + user_id = args['user_id'] + registration_user_id = g.current_user['id'] + + registration_user = user_repository.get_by_id(registration_user_id) + if not registration_user.is_registration_volunteer(event_id): + return FORBIDDEN + + event = event_repository.get_by_id(event_id) + if event is None: + return EVENT_NOT_FOUND + + user = user_repository.get_by_id(user_id) + if user is None: + return USER_NOT_FOUND + + attendance = attendance_repository.get(event_id, user_id) + if attendance is None: + return ATTENDANCE_NOT_FOUND + + return attendance, 200 + + @auth_required + @marshal_with(attendance_fields) + def post(self): + args = self.req_parser.parse_args() + event_id = args['event_id'] + user_id = args['user_id'] + registration_user_id = g.current_user['id'] + + registration_user = user_repository.get_by_id(registration_user_id) + if not registration_user.is_registration_volunteer(event_id): + return FORBIDDEN + + event = event_repository.get_by_id(event_id) + if event is None: + return EVENT_NOT_FOUND + + user = user_repository.get_by_id(user_id) + if user is None: + return USER_NOT_FOUND + + if attendance_repository.exists(event_id, user_id): + return ATTENDANCE_ALREADY_CONFIRMED + + attendance = Attendance(event_id, user_id, registration_user_id) + attendance_repository.create(attendance) + + email_user( + 'attendance-confirmation', + event=event, + user=user + ) + + # Other Fields + unavailable_response = None + invitedguest_role = unavailable_response + is_invited_guest = unavailable_response + has_accepted_accom_award = unavailable_response + + offer = db.session.query(Offer).filter( + Offer.user_id == user_id).filter(Offer.event_id == event_id).first() + if offer: + has_accepted_accom_award = ( + offer.accommodation_award and offer.accepted_accommodation_award) + + # Check if invited guest + invited_guest = db.session.query(InvitedGuest).filter( + InvitedGuest.event_id == event_id).filter(InvitedGuest.user_id == user.id).first() + if(invited_guest): + is_invited_guest = True + invitedguest_role = invited_guest.role + + + # Shirt Size + shirt_answer = (get_registration_answer_based_headline( + user_id, "T-Shirt Size") and get_registration_answer_based_headline( + user_id, "T-Shirt Size").value) or unavailable_response + + # Poster registration + bringing_poster = False + poster_answer = get_registration_answer_based_headline( + user_id, "Will you be bringing a poster?") + if (poster_answer is not None and poster_answer.value == 'yes'): + bringing_poster = True + + attendance_user = AttendanceUser( + attendance, has_accepted_accom_award, shirt_answer, is_invited_guest, bringing_poster, invitedguest_role) + return attendance_user, 201 + + @auth_required + def delete(self): + args = self.req_parser.parse_args() + event_id = args['event_id'] + user_id = args['user_id'] + registration_user_id = g.current_user['id'] + + registration_user = user_repository.get_by_id(registration_user_id) + if not registration_user.is_registration_volunteer(event_id): + return FORBIDDEN + + event = event_repository.get_by_id(event_id) + if event is None: + return EVENT_NOT_FOUND + + user = user_repository.get_by_id(user_id) + if user is None: + return USER_NOT_FOUND + + attendance = attendance_repository.get(event_id, user_id) + if attendance is None: + return ATTENDANCE_NOT_FOUND + + attendance_repository.delete(attendance) + + return 200 diff --git a/api/app/attendance/mixins.py b/api/app/attendance/mixins.py index 4cd325346..274be156d 100644 --- a/api/app/attendance/mixins.py +++ b/api/app/attendance/mixins.py @@ -1,6 +1,6 @@ -from flask_restful import reqparse - -class AttendanceMixin(object): - req_parser = reqparse.RequestParser() - req_parser.add_argument('event_id', type=int, required=True) +from flask_restful import reqparse + +class AttendanceMixin(object): + req_parser = reqparse.RequestParser() + req_parser.add_argument('event_id', type=int, required=True) req_parser.add_argument('user_id', type=int, required=True) \ No newline at end of file diff --git a/api/app/attendance/models.py b/api/app/attendance/models.py index 714dd3cd3..4c29e5bad 100644 --- a/api/app/attendance/models.py +++ b/api/app/attendance/models.py @@ -1,23 +1,23 @@ -from datetime import datetime - -from app import db - -class Attendance(db.Model): - id = db.Column(db.Integer(), primary_key=True) - event_id = db.Column(db.Integer(), db.ForeignKey('event.id'), nullable=False) - user_id = db.Column(db.Integer(), db.ForeignKey('app_user.id'), nullable=False) - timestamp = db.Column(db.DateTime(), nullable=False) - updated_by_user_id = db.Column(db.Integer(), db.ForeignKey('app_user.id'), nullable=False) - - event = db.relationship('Event', foreign_keys=[event_id]) - user = db.relationship('AppUser', foreign_keys=[user_id]) - updated_by_user = db.relationship('AppUser', foreign_keys=[updated_by_user_id]) - - def __init__(self, - event_id, - user_id, - updated_by_user_id): - self.event_id = event_id - self.user_id = user_id - self.timestamp = datetime.now() +from datetime import datetime + +from app import db + +class Attendance(db.Model): + id = db.Column(db.Integer(), primary_key=True) + event_id = db.Column(db.Integer(), db.ForeignKey('event.id'), nullable=False) + user_id = db.Column(db.Integer(), db.ForeignKey('app_user.id'), nullable=False) + timestamp = db.Column(db.DateTime(), nullable=False) + updated_by_user_id = db.Column(db.Integer(), db.ForeignKey('app_user.id'), nullable=False) + + event = db.relationship('Event', foreign_keys=[event_id]) + user = db.relationship('AppUser', foreign_keys=[user_id]) + updated_by_user = db.relationship('AppUser', foreign_keys=[updated_by_user_id]) + + def __init__(self, + event_id, + user_id, + updated_by_user_id): + self.event_id = event_id + self.user_id = user_id + self.timestamp = datetime.now() self.updated_by_user_id = updated_by_user_id \ No newline at end of file diff --git a/api/app/attendance/repository.py b/api/app/attendance/repository.py index 8a008be29..5da2830ee 100644 --- a/api/app/attendance/repository.py +++ b/api/app/attendance/repository.py @@ -1,26 +1,26 @@ -from app import db -from app.attendance.models import Attendance - -class AttendanceRepository(): - - @staticmethod - def exists(event_id, user_id): - return db.session.query(Attendance.id)\ - .filter_by(event_id=event_id, user_id=user_id)\ - .first() is not None - - @staticmethod - def get(event_id, user_id): - return db.session.query(Attendance)\ - .filter_by(event_id=event_id, user_id=user_id)\ - .first() - - @staticmethod - def create(attendance): - db.session.add(attendance) - db.session.commit() - - @staticmethod - def delete(attendance): - db.session.delete(attendance) +from app import db +from app.attendance.models import Attendance + +class AttendanceRepository(): + + @staticmethod + def exists(event_id, user_id): + return db.session.query(Attendance.id)\ + .filter_by(event_id=event_id, user_id=user_id)\ + .first() is not None + + @staticmethod + def get(event_id, user_id): + return db.session.query(Attendance)\ + .filter_by(event_id=event_id, user_id=user_id)\ + .first() + + @staticmethod + def create(attendance): + db.session.add(attendance) + db.session.commit() + + @staticmethod + def delete(attendance): + db.session.delete(attendance) db.session.commit() \ No newline at end of file diff --git a/api/app/events/mixins.py b/api/app/events/mixins.py index c820dd4e9..260ec1cb6 100644 --- a/api/app/events/mixins.py +++ b/api/app/events/mixins.py @@ -1,42 +1,42 @@ -from datetime import datetime - -from flask_restful import reqparse - - -class EventMixin(object): - dt_format = lambda x: datetime.strptime(x, '%Y-%m-%dT%H:%M:%SZ') - - req_parser = reqparse.RequestParser() - req_parser.add_argument('id', type=int, required=False) - req_parser.add_argument('name', type=dict, required=True, location='json') - req_parser.add_argument('description', type=dict, required=True, location='json') - req_parser.add_argument('start_date', type=dt_format, required=True) - req_parser.add_argument('end_date', type=dt_format, required=True) - req_parser.add_argument('key', type=str, required=True) - req_parser.add_argument('organisation_id', type=int, required=True) - req_parser.add_argument('email_from', type=str, required=True) - req_parser.add_argument('url', type=str, required=True) - req_parser.add_argument('application_open', type=dt_format, required=True) - req_parser.add_argument('application_close', type=dt_format, required=True) - req_parser.add_argument('review_open', type=dt_format, required=True) - req_parser.add_argument('review_close', type=dt_format, required=True) - req_parser.add_argument('selection_open', type=dt_format, required=True) - req_parser.add_argument('selection_close', type=dt_format, required=True) - req_parser.add_argument('offer_open', type=dt_format, required=True) - req_parser.add_argument('offer_close', type=dt_format, required=True) - req_parser.add_argument('registration_open', type=dt_format, required=True) - req_parser.add_argument('registration_close', type=dt_format, required=True) - req_parser.add_argument('event_type', type=str, required=True) - req_parser.add_argument('travel_grant', type=bool, required=True) - req_parser.add_argument('miniconf_url', type=str, required=False) - - -class EventsMixin(object): - req_parser = reqparse.RequestParser() - req_parser.add_argument('event_id', type=int, required=True) - - -class EventsKeyMixin(object): - req_parser = reqparse.RequestParser() - req_parser.add_argument('event_key', type=str, required=True) - req_parser.add_argument('language', type=str, required=False) +from datetime import datetime + +from flask_restful import reqparse + + +class EventMixin(object): + dt_format = lambda x: datetime.strptime(x, '%Y-%m-%dT%H:%M:%SZ') + + req_parser = reqparse.RequestParser() + req_parser.add_argument('id', type=int, required=False) + req_parser.add_argument('name', type=dict, required=True, location='json') + req_parser.add_argument('description', type=dict, required=True, location='json') + req_parser.add_argument('start_date', type=dt_format, required=True) + req_parser.add_argument('end_date', type=dt_format, required=True) + req_parser.add_argument('key', type=str, required=True) + req_parser.add_argument('organisation_id', type=int, required=True) + req_parser.add_argument('email_from', type=str, required=True) + req_parser.add_argument('url', type=str, required=True) + req_parser.add_argument('application_open', type=dt_format, required=True) + req_parser.add_argument('application_close', type=dt_format, required=True) + req_parser.add_argument('review_open', type=dt_format, required=True) + req_parser.add_argument('review_close', type=dt_format, required=True) + req_parser.add_argument('selection_open', type=dt_format, required=True) + req_parser.add_argument('selection_close', type=dt_format, required=True) + req_parser.add_argument('offer_open', type=dt_format, required=True) + req_parser.add_argument('offer_close', type=dt_format, required=True) + req_parser.add_argument('registration_open', type=dt_format, required=True) + req_parser.add_argument('registration_close', type=dt_format, required=True) + req_parser.add_argument('event_type', type=str, required=True) + req_parser.add_argument('travel_grant', type=bool, required=True) + req_parser.add_argument('miniconf_url', type=str, required=False) + + +class EventsMixin(object): + req_parser = reqparse.RequestParser() + req_parser.add_argument('event_id', type=int, required=True) + + +class EventsKeyMixin(object): + req_parser = reqparse.RequestParser() + req_parser.add_argument('event_key', type=str, required=True) + req_parser.add_argument('language', type=str, required=False) diff --git a/api/app/files/api.py b/api/app/files/api.py index 8959f5c33..e8bc09f6f 100644 --- a/api/app/files/api.py +++ b/api/app/files/api.py @@ -1,59 +1,59 @@ -import uuid - -import flask_restful as restful -from flask_restful import reqparse -from flask import send_file -from config import FILE_SIZE_LIMIT -from app.utils.errors import FILE_SIZE_EXCEEDED -from app.files.mixins import FileUploadMixin -from app.utils.auth import auth_required - - -import tempfile - -from app import LOGGER -from app.utils import storage - - -class FileUploadAPI(FileUploadMixin, restful.Resource): - - def get(self): - req_parser = reqparse.RequestParser() - req_parser.add_argument('filename', type=str, required=True) - req_parser.add_argument('rename', type=str, required=False) - args = req_parser.parse_args() - - bucket = storage.get_storage_bucket() - - blob = bucket.blob(args['filename']) - with tempfile.NamedTemporaryFile() as temp: - blob.download_to_filename(temp.name) - renamed = args['rename'] or args['filename'] - return send_file( - temp.name, - as_attachment=True, - attachment_filename=args['rename'] or args['filename']) - - - def post(self): - args = self.req_parser.parse_args() - - bucket = storage.get_storage_bucket() - - unique_name = str(uuid.uuid4().hex) - blob = bucket.blob(unique_name) - - file = args['file'] - bytes_file = file.read() - content_type = file.content_type - file_size = len(bytes_file) - - if file_size > FILE_SIZE_LIMIT: - LOGGER.debug('File size of {} exceeds limit of {}'.format(file_size, FILE_SIZE_EXCEEDED)) - return FILE_SIZE_EXCEEDED - - blob.upload_from_string(bytes_file, content_type=content_type) - - return { - 'file_id': unique_name, - }, 201 +import uuid + +import flask_restful as restful +from flask_restful import reqparse +from flask import send_file +from config import FILE_SIZE_LIMIT +from app.utils.errors import FILE_SIZE_EXCEEDED +from app.files.mixins import FileUploadMixin +from app.utils.auth import auth_required + + +import tempfile + +from app import LOGGER +from app.utils import storage + + +class FileUploadAPI(FileUploadMixin, restful.Resource): + + def get(self): + req_parser = reqparse.RequestParser() + req_parser.add_argument('filename', type=str, required=True) + req_parser.add_argument('rename', type=str, required=False) + args = req_parser.parse_args() + + bucket = storage.get_storage_bucket() + + blob = bucket.blob(args['filename']) + with tempfile.NamedTemporaryFile() as temp: + blob.download_to_filename(temp.name) + renamed = args['rename'] or args['filename'] + return send_file( + temp.name, + as_attachment=True, + attachment_filename=args['rename'] or args['filename']) + + + def post(self): + args = self.req_parser.parse_args() + + bucket = storage.get_storage_bucket() + + unique_name = str(uuid.uuid4().hex) + blob = bucket.blob(unique_name) + + file = args['file'] + bytes_file = file.read() + content_type = file.content_type + file_size = len(bytes_file) + + if file_size > FILE_SIZE_LIMIT: + LOGGER.debug('File size of {} exceeds limit of {}'.format(file_size, FILE_SIZE_EXCEEDED)) + return FILE_SIZE_EXCEEDED + + blob.upload_from_string(bytes_file, content_type=content_type) + + return { + 'file_id': unique_name, + }, 201 diff --git a/api/app/files/mixins.py b/api/app/files/mixins.py index 641277984..596d0f4ea 100644 --- a/api/app/files/mixins.py +++ b/api/app/files/mixins.py @@ -1,6 +1,6 @@ -from flask_restful import reqparse -from werkzeug.datastructures import FileStorage - -class FileUploadMixin(object): - req_parser = reqparse.RequestParser() +from flask_restful import reqparse +from werkzeug.datastructures import FileStorage + +class FileUploadMixin(object): + req_parser = reqparse.RequestParser() req_parser.add_argument('file', location='files', type=FileStorage, required=True) \ No newline at end of file diff --git a/api/app/references/mixins.py b/api/app/references/mixins.py index ed233be78..d290b36d8 100644 --- a/api/app/references/mixins.py +++ b/api/app/references/mixins.py @@ -1,40 +1,40 @@ -from flask_restful import reqparse - - -class ReferenceRequestsMixin(object): - post_req_parser = reqparse.RequestParser() - post_req_parser.add_argument('response_id', type=int, required=True) - post_req_parser.add_argument('title', type=str, required=True) - post_req_parser.add_argument('firstname', type=str, required=True) - post_req_parser.add_argument('lastname', type=str, required=True) - post_req_parser.add_argument('relation', type=str, required=True) - post_req_parser.add_argument('email', type=str, required=True) - get_req_parser = reqparse.RequestParser() - get_req_parser.add_argument('id', type=int, required=False) - -class ReferenceRequestsListMixin(object): - - req_parser = reqparse.RequestParser() - req_parser.add_argument('response_id', type=int, required=True) - - -class ReferenceRequestDetailMixin(object): - get_req_parser = reqparse.RequestParser() - get_req_parser.add_argument('response_id', type=int, required=True) - - post_req_parser = reqparse.RequestParser() - post_req_parser.add_argument('token', type=str, required=True) - - -class ReferenceMixin(object): - - get_req_parser = reqparse.RequestParser() - get_req_parser.add_argument('response_id', type=int, required=True) - - post_req_parser = reqparse.RequestParser() - post_req_parser.add_argument('token', type=str, required=True) - post_req_parser.add_argument('uploaded_document', type=str, required=True) - - put_req_parser = reqparse.RequestParser() - put_req_parser.add_argument('token', type=str, required=True) - put_req_parser.add_argument('uploaded_document', type=str, required=True) +from flask_restful import reqparse + + +class ReferenceRequestsMixin(object): + post_req_parser = reqparse.RequestParser() + post_req_parser.add_argument('response_id', type=int, required=True) + post_req_parser.add_argument('title', type=str, required=True) + post_req_parser.add_argument('firstname', type=str, required=True) + post_req_parser.add_argument('lastname', type=str, required=True) + post_req_parser.add_argument('relation', type=str, required=True) + post_req_parser.add_argument('email', type=str, required=True) + get_req_parser = reqparse.RequestParser() + get_req_parser.add_argument('id', type=int, required=False) + +class ReferenceRequestsListMixin(object): + + req_parser = reqparse.RequestParser() + req_parser.add_argument('response_id', type=int, required=True) + + +class ReferenceRequestDetailMixin(object): + get_req_parser = reqparse.RequestParser() + get_req_parser.add_argument('response_id', type=int, required=True) + + post_req_parser = reqparse.RequestParser() + post_req_parser.add_argument('token', type=str, required=True) + + +class ReferenceMixin(object): + + get_req_parser = reqparse.RequestParser() + get_req_parser.add_argument('response_id', type=int, required=True) + + post_req_parser = reqparse.RequestParser() + post_req_parser.add_argument('token', type=str, required=True) + post_req_parser.add_argument('uploaded_document', type=str, required=True) + + put_req_parser = reqparse.RequestParser() + put_req_parser.add_argument('token', type=str, required=True) + put_req_parser.add_argument('uploaded_document', type=str, required=True) diff --git a/api/app/reviews/api.py b/api/app/reviews/api.py index 7258b622e..2a5917185 100644 --- a/api/app/reviews/api.py +++ b/api/app/reviews/api.py @@ -1,1091 +1,1091 @@ -from flask import g -import flask_restful as restful -from flask_restful import reqparse, fields, marshal_with -from math import ceil -import random -from sqlalchemy.sql import func, exists - -from typing import Any, Mapping -from datetime import datetime - -from app import db, LOGGER -from app.applicationModel.models import ApplicationForm -from app.events.models import Event, EventRole -from app.events.repository import EventRepository as event_repository -from app.responses.repository import ResponseRepository as response_repository -from app.responses.models import Response, ResponseReviewer -from app.reviews.mixins import ReviewMixin, GetReviewResponseMixin, PostReviewResponseMixin, PostReviewAssignmentMixin, \ - GetReviewAssignmentMixin, GetReviewHistoryMixin, GetReviewSummaryMixin -from app.reviews.models import ReviewForm, ReviewResponse, ReviewScore, ReviewQuestion, ReviewSection, ReviewSectionTranslation, ReviewQuestionTranslation -from app.reviews.repository import ReviewRepository as review_repository -from app.reviews.repository import ReviewConfigurationRepository as review_configuration_repository -from app.references.repository import ReferenceRequestRepository as reference_repository - -from app.users.models import AppUser, Country, UserCategory -from app.users.repository import UserRepository as user_repository - -from app.events.repository import EventRepository as event_repository -from app.utils.auth import auth_required - -from app.utils.auth import auth_required, event_admin_required -from app.utils.errors import EVENT_NOT_FOUND, REVIEW_RESPONSE_NOT_FOUND, FORBIDDEN, USER_NOT_FOUND, RESPONSE_NOT_FOUND, \ - REVIEW_FORM_NOT_FOUND, REVIEW_ALREADY_COMPLETED, NO_ACTIVE_REVIEW_FORM, REVIEW_FORM_FOR_STAGE_NOT_FOUND - -from app.utils import misc -from app.utils.emailer import email_user - -option_fields = { - 'value': fields.String, - 'label': 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)) -} - -user_fields = { - 'nationality_country': fields.String(attribute='nationality_country.name'), - 'residence_country': fields.String(attribute='residence_country.name'), - 'affiliation': fields.String, - 'department': fields.String, - 'user_category': fields.String(attribute='user_category.name'), - 'id': fields.Integer -} - -review_scores_fields = { - 'review_question_id': fields.Integer, - 'value': fields.String -} - -review_response_fields = { - 'id': fields.Integer, - 'review_form_id': fields.Integer, - 'response_id': fields.Integer, - 'reviewer_user_id': fields.Integer, - 'scores': fields.List(fields.Nested(review_scores_fields), attribute='review_scores'), - 'language': fields.String -} - -reference_fields = { - 'firstname': fields.String, - 'title': fields.String, - 'lastname': fields.String, - 'relation': fields.String, - 'uploaded_document': fields.String, -} - -review_fields = { - 'review_form': fields.Raw, - 'response': fields.Nested(response_fields), - 'user': fields.Nested(user_fields), - 'reviews_remaining_count': fields.Integer, - 'review_response': fields.Nested(review_response_fields), - 'references': fields.List(fields.Nested(reference_fields)), - 'is_submitted': fields.Boolean, - 'submitted_timestamp': fields.DateTime(dt_format='iso8601') -} - -review_question_detail_fields = { - 'id': fields.Integer, - 'question_id': fields.Integer, - 'type': fields.String, - 'is_required': fields.Boolean, - 'order': fields.Integer, - 'weight': fields.Float, - 'headline': fields.Raw(attribute=lambda s: s.headline_translations), - 'description': fields.Raw(attribute=lambda s: s.description_translations), - 'placeholder': fields.Raw(attribute=lambda s: s.placeholder_translations), - 'options': fields.Raw(attribute=lambda s: s.options_translations), - 'validation_regex': fields.Raw(attribute=lambda s: s.validation_regex_translations), - 'validation_text': fields.Raw(attribute=lambda s: s.validation_text_translations) -} - -review_section_detail_fields = { - 'id': fields.Integer, - 'order': fields.Integer, - 'name': fields.Raw(attribute=lambda s: s.headline_translations), - 'description': fields.Raw(attribute=lambda s: s.description_translations), - 'questions': fields.List(fields.Nested(review_question_detail_fields), attribute='review_questions') -} - -review_form_detail_fields = { - 'id': fields.Integer, - 'event_id': fields.Integer, - 'application_form_id': fields.Integer, - 'is_open': fields.Boolean, - 'deadline': fields.DateTime(dt_format='iso8601'), - 'active': fields.Boolean, - 'stage': fields.Integer, - 'sections': fields.List(fields.Nested(review_section_detail_fields), attribute='review_sections') -} - -def _serialize_review_question(review_question, language): - translation = review_question.get_translation(language) - if translation is None: - LOGGER.warn('Missing {} translation for review review_question id {}'.format(language, review_question.id)) - translation = review_question.get_translation('en') - return { - 'id': review_question.id, - 'question_id': review_question.question_id, - 'description': translation.description, - 'headline': translation.headline, - 'type': review_question.type, - 'placeholder': translation.placeholder, - 'options': translation.options, - 'is_required': review_question.is_required, - 'order': review_question.order, - 'validation_regex': translation.validation_regex, - 'validation_text': translation.validation_text, - 'weight': review_question.weight - } - -def _serialize_review_form(review_form: ReviewForm, language: str) -> Mapping[str, Any]: - review_sections = [] - - for section in review_form.review_sections: - translation = section.get_translation(language) - if translation is None: - LOGGER.warn('Missing {} translation for review section id {}'.format(language, section.id)) - translation = section.get_translation('en') - review_sections.append({ - 'id': section.id, - 'order': section.order, - 'headline': translation.headline, - 'description': translation.description, - 'review_questions': [_serialize_review_question(q, language) for q in section.review_questions] - }) - - form = { - 'id': review_form.id, - 'application_form_id': review_form.application_form_id, - 'is_open': review_form.is_open, - 'deadline': review_form.deadline.isoformat(), - 'stage': review_form.stage, - 'review_sections': review_sections - } - - return form - - -def _add_reviewer_role(user_id, event_id): - event_role = EventRole('reviewer', user_id, event_id) - db.session.add(event_role) - db.session.commit() - - -class ReviewResponseUser(): - def __init__(self, review_form, response, reviews_remaining_count, language, reference_responses=None, - review_response=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 - - -class ReviewResponseReference(): - def __init__(self, title, firstname, lastname, relation, uploaded_document): - self.title = title - self.firstname = firstname - self.lastname = lastname - self.relation = relation - self.uploaded_document = uploaded_document - - -class ReviewAPI(ReviewMixin, restful.Resource): - - @auth_required - @marshal_with(review_fields) - def get(self): - args = self.req_parser.parse_args() - event_id = args['event_id'] - - review_form = review_repository.get_review_form(event_id) - if review_form is None: - return EVENT_NOT_FOUND - - reviews_remaining_count = review_repository.get_remaining_reviews_count(g.current_user['id'], - review_form.application_form_id) - skip = self.sanitise_skip(args['skip'], reviews_remaining_count) - - response = review_repository.get_response_to_review(skip, g.current_user['id'], review_form.application_form_id) - - references = [] - if response is not None: - reference_requests = reference_repository.get_all_by_response_id(response.id) - - for r in reference_requests: - reference = reference_repository.get_reference_by_reference_request_id(r.id) - if reference is not None: - references.append( - ReviewResponseReference(r.title, r.firstname, r.lastname, r.relation, - reference.uploaded_document) - ) - - review_response = None if response is None else review_repository.get_review_response(review_form.id, - response.id, - g.current_user['id']) - references = None if len(references) == 0 else references - - return ReviewResponseUser( - review_form, - response, - reviews_remaining_count, - args['language'], - reference_responses=references, - review_response=review_response - ) - - def sanitise_skip(self, skip, reviews_remaining_count): - if skip is None: - skip = 0 - - if skip < 0: - skip = 0 - - if reviews_remaining_count == 0: - skip = 0 - elif skip >= reviews_remaining_count: - skip = reviews_remaining_count - 1 - - return skip - - -class ResponseReviewAPI(restful.Resource): - @auth_required - @marshal_with(review_fields) - def get(self): - parser = reqparse.RequestParser() - parser.add_argument('response_id', type=int, required=True) - parser.add_argument('event_id', type=int, required=True) - parser.add_argument('language', type=str, required=True) - args = parser.parse_args() - - response_id = args['response_id'] - event_id = args['event_id'] - - review_form = review_repository.get_review_form(event_id) - if review_form is None: - return REVIEW_FORM_NOT_FOUND - - response = review_repository.get_response_by_reviewer(response_id, g.current_user['id']) - - if response is None: - return RESPONSE_NOT_FOUND - - review_response = review_repository.get_review_response(review_form.id, response_id, g.current_user['id']) - - return ReviewResponseUser(review_form, response, 0, args['language'], review_response=review_response) - - -class ReviewResponseAPI(GetReviewResponseMixin, PostReviewResponseMixin, restful.Resource): - - @auth_required - @marshal_with(review_fields) - def get(self): - args = self.get_req_parser.parse_args() - id = args['id'] - reviewer_user_id = g.current_user['id'] - - review_form_response = review_repository.get_review_response_with_form(id, reviewer_user_id) - - if review_form_response is None: - return REVIEW_RESPONSE_NOT_FOUND - - review_form, review_response = review_form_response - - response = review_repository.get_response_by_review_response(id, reviewer_user_id, - review_form.application_form_id) - - return ReviewResponseUser(review_form, response, 0, args['language'], review_response=review_response) - - @auth_required - @marshal_with(review_response_fields) - def post(self): - args = self.post_req_parser.parse_args() - validation_result = self.validate_scores(args['scores']) - if validation_result is not None: - return validation_result - - response_id = args['response_id'] - review_form_id = args['review_form_id'] - reviewer_user_id = g.current_user['id'] - scores = args['scores'] - language = args['language'] - is_submitted = args['is_submitted'] - - response_reviewer = review_repository.get_response_reviewer(response_id, reviewer_user_id) - if response_reviewer is None: - return FORBIDDEN - - review_response = ReviewResponse(review_form_id, reviewer_user_id, response_id, language) - review_response.review_scores = self.get_review_scores(scores) - if is_submitted: - review_response.submit() - review_repository.add_model(review_response) - - return review_response, 201 - - @auth_required - @marshal_with(review_response_fields) - def put(self): - args = self.post_req_parser.parse_args() - validation_result = self.validate_scores(args['scores']) - if validation_result is not None: - return validation_result - - response_id = args['response_id'] - review_form_id = args['review_form_id'] - reviewer_user_id = g.current_user['id'] - scores = args['scores'] - is_submitted = args['is_submitted'] - - response_reviewer = review_repository.get_response_reviewer(response_id, reviewer_user_id) - if response_reviewer is None: - return FORBIDDEN - - review_response = review_repository.get_review_response(review_form_id, response_id, reviewer_user_id) - if review_response is None: - return REVIEW_RESPONSE_NOT_FOUND - - review_repository.deactivate_review(review_response) - review_response.review_scores = self.get_review_scores(scores) - if is_submitted: - review_response.submit() - db.session.commit() - - return review_response, 200 - - def get_review_scores(self, scores): - review_scores = [] - for score in scores: - review_score = ReviewScore(score['review_question_id'], score['value']) - review_scores.append(review_score) - return review_scores - - def validate_scores(self, scores): - for score in scores: - if 'review_question_id' not in score.keys(): - return self.get_error_message('review_question_id') - if 'value' not in score.keys(): - return self.get_error_message('value') - - def get_error_message(self, key): - return ( - {'message': {key: 'Missing required parameter in the JSON body or the post body or the query string'}}, 400) - - -class ReviewCountView(): - def __init__(self, count): - self.email = count.email - self.user_title = count.user_title - self.firstname = count.firstname - self.lastname = count.lastname - self.reviews_allocated = count.reviews_allocated - self.reviews_completed = count.reviews_completed - self.reviewer_user_id = count.reviewer_user_id - -class ReviewSummaryAPI(GetReviewSummaryMixin, restful.Resource): - @auth_required - def get(self): - args = self.get_req_parser.parse_args() - event_id = args['event_id'] - user_id = g.current_user['id'] - - current_user = user_repository.get_by_id(user_id) - if not current_user.is_event_admin(event_id): - return FORBIDDEN - - config = review_configuration_repository.get_configuration_for_event(event_id) - - return { - 'reviews_unallocated': review_repository.count_unassigned_reviews(event_id, config.num_reviews_required) - } - - -class ReviewAssignmentAPI(GetReviewAssignmentMixin, PostReviewAssignmentMixin, restful.Resource): - reviews_count_fields = { - 'reviewer_user_id': fields.Integer, - 'email': fields.String, - 'user_title': fields.String, - 'firstname': fields.String, - 'lastname': fields.String, - 'reviews_allocated': fields.Integer, - 'reviews_completed': fields.Integer - } - - @auth_required - @marshal_with(reviews_count_fields) - def get(self): - args = self.get_req_parser.parse_args() - event_id = args['event_id'] - user_id = g.current_user['id'] - - current_user = user_repository.get_by_id(user_id) - if not current_user.is_event_admin(event_id): - return FORBIDDEN - - counts = review_repository.count_reviews_allocated_and_completed_per_reviewer(event_id) - views = [ReviewCountView(count) for count in counts] - return views - - @auth_required - def post(self): - args = self.post_req_parser.parse_args() - user_id = g.current_user['id'] - event_id = args['event_id'] - reviewer_user_email = args['reviewer_user_email'] - num_reviews = args['num_reviews'] - - event = event_repository.get_by_id(event_id) - if not event: - return EVENT_NOT_FOUND - - current_user = user_repository.get_by_id(user_id) - if not current_user.is_event_admin(event_id): - return FORBIDDEN - - reviewer_user = user_repository.get_by_email(reviewer_user_email, g.organisation.id) - if reviewer_user is None: - return USER_NOT_FOUND - - if not reviewer_user.is_reviewer(event_id): - _add_reviewer_role(reviewer_user.id, event_id) - - config = review_configuration_repository.get_configuration_for_event(event_id) - - response_ids = self.get_eligible_response_ids(event_id, reviewer_user.id, num_reviews, - config.num_reviews_required) - response_reviewers = [ResponseReviewer(response_id, reviewer_user.id) for response_id in response_ids] - db.session.add_all(response_reviewers) - db.session.commit() - - if len(response_ids) > 0: - email_user( - 'reviews-assigned', - template_parameters=dict( - num_reviews=len(response_ids), - baobab_host=misc.get_baobab_host(), - system_name=g.organisation.system_name, - event_key=event.key - ), - event=event, - user=reviewer_user) - return {}, 201 - - def add_reviewer_role(self, user_id, event_id): - event_role = EventRole('reviewer', user_id, event_id) - db.session.add(event_role) - db.session.commit() - - def get_eligible_response_ids(self, event_id, reviewer_user_id, num_reviews, reviews_required): - candidate_responses = review_repository.get_candidate_response_ids(event_id, reviewer_user_id, reviews_required) - candidate_response_ids = set([r.id for r in candidate_responses]) - - # Now remove any responses that the reviewer is already assigned to - already_assigned = review_repository.get_already_assigned(reviewer_user_id) - already_assigned_ids = set([r.response_id for r in already_assigned]) - responses = list(candidate_response_ids - already_assigned_ids) - - return random.sample(responses, min(len(responses), num_reviews)) - - -_review_history_fields = { - 'response_id': fields.Integer, - 'review_response_id': fields.Integer, - 'submitted_timestamp': fields.DateTime(dt_format='iso8601'), - 'reviewed_user_id': fields.String -} - -review_history_fields = { - 'reviews': fields.List(fields.Nested(_review_history_fields)), - 'num_entries': fields.Integer, - 'current_pagenumber': fields.Integer, - 'total_pages': fields.Integer -} - - -class ReviewHistoryModel: - def __init__(self, review): - self.response_id = review.Response.id - self.review_response_id = review.id - self.submitted_timestamp = review.submitted_timestamp - self.reviewed_user_id = review.AppUser.id - - -class ReviewHistoryAPI(GetReviewHistoryMixin, restful.Resource): - - @auth_required - @marshal_with(review_history_fields) - def get(self): - args = self.get_req_parser.parse_args() - user_id = g.current_user['id'] - event_id = args['event_id'] - page_number = args['page_number'] - limit = args['limit'] - sort_column = args['sort_column'] - - reviewer = user_repository.get_by_id(user_id).is_reviewer(event_id) - if not reviewer: - return FORBIDDEN - - reviews = review_repository.get_review_history(user_id, event_id) - - if sort_column == 'review_response_id': - reviews = reviews.order_by(ReviewResponse.id) - - if sort_column == 'submitted_timestamp': - reviews = reviews.order_by(ReviewResponse.submitted_timestamp) - - reviews = reviews.slice(page_number * limit, page_number * limit + limit).all() - - num_entries = review_repository.get_review_history_count(user_id, event_id) - - total_pages = ceil(float(num_entries) / limit) - - reviews = [ReviewHistoryModel(review) for review in reviews] - return {'reviews': reviews, 'num_entries': num_entries, 'current_pagenumber': page_number, - 'total_pages': total_pages} - - -class ReviewListAPI(restful.Resource): - - @staticmethod - def _serialize_answer(answer, language): - translation = answer.question.get_translation(language) - if not translation: - translation = answer.question.get_translation('en') - LOGGER.warn('Could not find {} translation for question id {}'.format(language, answer.question.id)) - return { - 'headline': translation.headline, - 'value': answer.value_display - } - - @staticmethod - def _serialize_response(response, review_response, language): - info = [ - ReviewListAPI._serialize_answer(answer, language) - for answer in response.answers if answer.question.key == 'review-identifier'] - - submitted = None - if review_response and review_response.submitted_timestamp: - submitted = review_response.submitted_timestamp.isoformat() - - return { - 'response_id': response.id, - 'language': response.language, - 'information': info, - 'started': review_response is not None, - 'submitted': submitted, - 'total_score': review_response.calculate_score() if review_response is not None else 0.0 - } - - @auth_required - def get(self): - parser = reqparse.RequestParser() - parser.add_argument('event_id', type=int, required=True) - parser.add_argument('language', type=str, required=True) - args = parser.parse_args() - event_id = args['event_id'] - user_id = g.current_user['id'] - language = args['language'] - - if not user_repository.get_by_id(user_id).is_reviewer(event_id): - return FORBIDDEN - - responses_to_review = review_repository.get_review_list(user_id, event_id) - - return [ReviewListAPI._serialize_response(response, review_response, language) - for response, review_response in responses_to_review] - -class ResponseReviewAssignmentAPI(restful.Resource): - @event_admin_required - def post(self, event_id): - parser = reqparse.RequestParser() - parser.add_argument('response_ids', type=int, required=True, action='append') - parser.add_argument('reviewer_email', type=str, required=True) - args = parser.parse_args() - - response_ids = args['response_ids'] - reviewer_email = args['reviewer_email'] - - filtered_response_ids = response_repository.filter_ids_to_event(response_ids, event_id) - - if set(filtered_response_ids) != set(response_ids): - return FORBIDDEN - - event = event_repository.get_by_id(event_id) - - reviewer_user = user_repository.get_by_email(reviewer_email, g.organisation.id) - if reviewer_user is None: - return USER_NOT_FOUND - - if not reviewer_user.is_reviewer(event_id): - _add_reviewer_role(reviewer_user.id, event_id) - - response_reviewers = [ResponseReviewer(response_id, reviewer_user.id) for response_id in response_ids] - db.session.add_all(response_reviewers) - db.session.commit() - - if len(response_ids) > 0: - email_user( - 'reviews-assigned', - template_parameters=dict( - num_reviews=len(response_ids), - baobab_host=misc.get_baobab_host(), - system_name=g.organisation.system_name, - event_key=event.key - ), - event=event, - user=reviewer_user) - return {}, 201 - - @event_admin_required - def delete(self, event_id): - parser = reqparse.RequestParser() - parser.add_argument('response_id', type=int, required=True) - parser.add_argument('reviewer_user_id', type=int, required=True) - args = parser.parse_args() - - response_id = args['response_id'] - reviewer_user_id = args['reviewer_user_id'] - - review_form = review_repository.get_review_form(event_id) - if not review_form: - return REVIEW_FORM_NOT_FOUND - - # If the reviewer has already completed the review, action can't be completed - review_response = review_repository.get_review_response(review_form.id, response_id, reviewer_user_id) - if review_response: - return REVIEW_ALREADY_COMPLETED - - review_repository.delete_response_reviewer(response_id, reviewer_user_id) - - return {}, 200 - -class ReviewResponseDetailListAPI(restful.Resource): - @staticmethod - def _serialise_identifier(answer, language): - question_translation = answer.question.get_translation(language) - if question_translation is None: - question_translation = answer.question.get_translation('en') - LOGGER.warn('Could not find {} translation for question id {}'.format(language, answer.question.id)) - - return { - 'headline': question_translation.headline, - 'value': answer.value_display - } - - @staticmethod - def _serialise_score(review_score, language): - review_question_translation = review_score.review_question.get_translation(language) - if review_question_translation is None: - review_question_translation = review_score.review_question.get_translation('en') - LOGGER.warn('Could not find {} translation for review question id {}'.format(language, - review_score.review_question.id)) - - return { - 'review_question_id': review_score.review_question.id, - 'headline': review_question_translation.headline, - 'description': review_question_translation.description, - 'type': review_score.review_question.type, - 'score': review_score.value, - 'weight': review_score.review_question.weight, - } - - @staticmethod - def _serialise_review_response(review_response, language): - return { - 'review_response_id': review_response.id, - 'response_id': review_response.response_id, - - 'reviewer_user_title': review_response.reviewer_user.user_title, - 'reviewer_user_firstname': review_response.reviewer_user.firstname, - 'reviewer_user_lastname': review_response.reviewer_user.lastname, - - 'response_user_title': review_response.response.user.user_title, - 'response_user_firstname': review_response.response.user.firstname, - 'response_user_lastname': review_response.response.user.lastname, - - 'identifiers': [ - ReviewResponseDetailListAPI._serialise_identifier(answer, language) - for answer in review_response.response.answers - if answer.question.is_review_identifier() - ], - - 'scores': [ - ReviewResponseDetailListAPI._serialise_score(review_score, language) - for review_score in review_response.review_scores - if (review_score.review_question.type == 'multi-choice' - or review_score.review_question.type == 'long-text' - or review_score.review_question.type == 'short-text' - ) - ], - - 'total': review_response.calculate_score() - } - - @auth_required - @event_admin_required - def get(self, event_id): - parser = reqparse.RequestParser() - parser.add_argument('language', type=str, required=True) - args = parser.parse_args() - - review_responses = review_repository.get_all_review_responses_by_event(event_id) - return [ - ReviewResponseDetailListAPI._serialise_review_response(review_response, args['language']) - for review_response in review_responses - ], 200 - -class ReviewResponseSummaryListAPI(restful.Resource): - @staticmethod - def _serialise_response(response: Response, review_form: ReviewForm, language: str): - scores = [] - for review_section in review_form.review_sections: - for review_question in review_section.review_questions: - if review_question.weight > 0: - review_question_translation = review_question.get_translation(language) - if not review_question_translation: - review_question_translation = review_question.get_translation('en') - LOGGER.warn('Could not find {} translation for review question id {}'.format(language, review_score.review_question.id)) - - average_score = review_repository.get_average_score_for_review_question(response.id, review_question.id) - - score = { - "review_question_id": review_question.id, - "headline": review_question_translation.headline, - "description": review_question_translation.description, - "type": review_question.type, - "score": average_score, - "weight": review_question.weight - } - scores.append(score) - - response_summary = { - "response_id": response.id, - "response_user_title": response.user.user_title, - "response_user_firstname": response.user.firstname, - "response_user_lastname": response.user.lastname, - - "identifiers": [ - ReviewResponseDetailListAPI._serialise_identifier(answer, response.language) - for answer in response.answers - if answer.question.is_review_identifier() - ], - - "scores": scores, - "total": sum(score['score'] * score['weight'] for score in scores) - } - return response_summary - - @auth_required - @event_admin_required - def get(self, event_id): - parser = reqparse.RequestParser() - parser.add_argument('language', type=str, required=True) - args = parser.parse_args() - - responses = response_repository.get_all_for_event(event_id) - review_form = review_repository.get_review_form(event_id) - - return [ - ReviewResponseSummaryListAPI._serialise_response(response, review_form, args['language']) - for response in responses - ], 200 - - -class ReviewStageAPI(restful.Resource): - - @auth_required - def get(self): - parser = reqparse.RequestParser() - parser.add_argument('event_id', type=int, required=True) - args = parser.parse_args() - - event_id = args['event_id'] - - review_forms = review_repository.get_all_review_forms_for_event(event_id) - current_form = [r for r in review_forms if r.active] - - if not current_form: - return NO_ACTIVE_REVIEW_FORM - - current_form = current_form[0] - - return { - 'current_stage': current_form.stage, - 'total_stages': len(review_forms) - } - - @auth_required - @event_admin_required - def post(self, event_id): - parser = reqparse.RequestParser() - parser.add_argument('stage', type=int, required=True) - args = parser.parse_args() - - stage = args['stage'] - - review_forms = review_repository.get_all_review_forms_for_event(event_id) - selected_form = [r for r in review_forms if r.stage == stage] - - if not selected_form: - return REVIEW_FORM_FOR_STAGE_NOT_FOUND - - selected_form = selected_form[0] - - for form in review_forms: - form.deactivate() - - selected_form.activate() - - db.session.commit() - - return {}, 201 - - -class ReviewFormDetailAPI(restful.Resource): - - @event_admin_required - @marshal_with(review_form_detail_fields) - def get(self, event_id): - req_parser = reqparse.RequestParser() - req_parser.add_argument('stage', type=int, required=False) - args = req_parser.parse_args() - - stage = args['stage'] - - review_form = review_repository.get_review_form(event_id, stage=stage) - - if not review_form: - return REVIEW_FORM_FOR_STAGE_NOT_FOUND - - review_form.event_id = event_id - - return review_form - - @event_admin_required - @marshal_with(review_form_detail_fields) - def post(self, event_id): - dt_format = lambda x: datetime.strptime(x, '%Y-%m-%dT%H:%M:%S') - - req_parser = reqparse.RequestParser() - req_parser.add_argument('application_form_id', type=int, required=True) - req_parser.add_argument('stage', type=int, required=True) - req_parser.add_argument('is_open', type=bool, required=True) - req_parser.add_argument('deadline', type=dt_format, required=True) - req_parser.add_argument('active', type=bool, required=True) - req_parser.add_argument('sections', type=dict, required=True, action='append') - args = req_parser.parse_args() - - application_form_id = args['application_form_id'] - stage = args['stage'] - is_open = args['is_open'] - deadline = args['deadline'] - active = args['active'] - sections_data = args['sections'] - - review_form = review_repository.get_review_form(event_id, stage) - if review_form is not None: - return REVIEW_FORM_EXISTS - - other_review_forms = review_repository.get_review_forms(event_id) - - review_form = ReviewForm( - application_form_id, - deadline, - stage, - # Only make this stage active if there is no other active stage. - active=not any(o.active for o in other_review_forms)) - - review_repository.add_model(review_form) - - for section_data in sections_data: - section = ReviewSection(review_form.id, section_data['order']) - review_repository.add_model(section) - - languages = section_data['name'].keys() - for language in languages: - section_translation = ReviewSectionTranslation( - review_section_id=section.id, - language=language, - headline=section_data['name'][language], - description=section_data['description'][language]) - review_repository.add_model(section_translation) - - for question_data in section_data['questions']: - question = ReviewQuestion( - review_section_id=section.id, - question_id=question_data['question_id'] or None, - type=question_data['type'], - is_required=question_data['is_required'], - order=question_data['order'], - weight=question_data['weight']) - - review_repository.add_model(question) - - languages = question_data['headline'].keys() - for language in languages: - question_translation = ReviewQuestionTranslation( - review_question_id=question.id, - language=language, - description=question_data["description"][language], - headline=question_data["headline"][language], - placeholder=question_data["placeholder"][language], - options=question_data["options"][language], - validation_regex=question_data["validation_regex"][language], - validation_text=question_data["validation_text"][language]) - - review_repository.add_model(question_translation) - - new_review_form = review_repository.get_review_form_by_id(review_form.id) - new_review_form.event_id = event_id - - return new_review_form, 201 - - @event_admin_required - @marshal_with(review_form_detail_fields) - def put(self, event_id): - dt_format = lambda x: datetime.strptime(x, '%Y-%m-%dT%H:%M:%S') - - req_parser = reqparse.RequestParser() - req_parser.add_argument('id', type=int, required=True) - req_parser.add_argument('application_form_id', type=int, required=True) - req_parser.add_argument('stage', type=int, required=True) - req_parser.add_argument('is_open', type=bool, required=True) - req_parser.add_argument('deadline', type=dt_format, required=True) - req_parser.add_argument('sections', type=dict, required=True, action='append') - args = req_parser.parse_args() - - id = args['id'] - application_form_id = args['application_form_id'] - stage = args['stage'] - is_open = args['is_open'] - deadline = args['deadline'] - sections_data = args['sections'] - - review_form = review_repository.get_review_form_by_id(id) - if not review_form: - return REVIEW_FORM_NOT_FOUND - - if application_form_id != review_form.application_form_id: - return UPDATE_CONFLICT - - if event_id != review_form.application_form.event_id: - return UPDATE_CONFLICT - - review_form.stage = stage - review_form.is_open = is_open - review_form.deadline = deadline - - # Note: we don't update the active property which is controlled by - # the ReviewStageAPI - - # Delete questions in the review form that no longer exist - all_question_ids = [q["id"] for s in sections_data for q in s["questions"] if "id" in q] - for section in review_form.review_sections: - for question in section.review_questions: - if question.id not in all_question_ids: - review_repository.delete_review_question(question) - - all_section_ids = [s["id"] for s in sections_data if "id" in s] - for section in review_form.review_sections: - if section.id not in all_section_ids: - review_repository.delete_review_section(section) - - for section_data in sections_data: - if 'id' in section_data: - # If ID is populated, then update the existing section - section = next((s for s in review_form.review_sections if s.id == section_data['id']), None) # type: ReviewSection - if not section: - return SECTION_NOT_FOUND - - current_translations = section.translations # type: Sequence[ReviewSectionTranslation] - for current_translation in current_translations: - current_translation.description = section_data['description'][current_translation.language] - current_translation.headline = section_data['name'][current_translation.language] - - section.order = section_data["order"] - else: - # if not populated, then add new section - section = ReviewSection(review_form.id, section_data["order"]) - review_repository.add_model(section) - - languages = section_data['name'].keys() - for language in languages: - section_translation = ReviewSectionTranslation( - section.id, - language, - section_data['name'][language], - section_data['description'][language]) - review_repository.add_model(section_translation) - - for question_data in section_data["questions"]: - if "id" in question_data: - current_question = next((q for q in section.review_questions if q.id == question_data['id']), None) # type: ReviewQuestion - if not current_question: - return QUESTION_NOT_FOUND - - current_question.question_id = question_data["question_id"] or None - current_question.type = question_data["type"] - current_question.order = question_data["order"] - current_question.is_required = question_data["is_required"] - current_question.weight = question_data["weight"] - - current_translations = current_question.translations # type: Sequence[ReviewQuestionTranslation] - for current_translation in current_translations: - current_translation.description = question_data['description'][current_translation.language] - current_translation.headline = question_data['headline'][current_translation.language] - current_translation.options = question_data['options'][current_translation.language] - current_translation.placeholder = question_data['placeholder'][current_translation.language] - current_translation.validation_regex = question_data['validation_regex'][current_translation.language] - current_translation.validation_text = question_data['validation_text'][current_translation.language] - else: - question = ReviewQuestion( - review_section_id=section.id, - question_id=question_data["question_id"] or None, - type=question_data["type"], - is_required=question_data["is_required"], - order=question_data["order"], - weight=question_data["weight"]) - - review_repository.add_model(question) - - for language in question_data['headline'].keys(): - translation = ReviewQuestionTranslation( - review_question_id=question.id, - language=language, - description=question_data["description"][language], - headline=question_data["headline"][language], - placeholder=question_data["placeholder"][language], - options=question_data["options"][language], - validation_regex=question_data["validation_regex"][language], - validation_text=question_data["validation_text"][language]) - - review_repository.add_model(translation) - - - db.session.commit() - - review_form = review_repository.get_review_form_by_id(id) - review_form.event_id = event_id - - return review_form, 200 +from flask import g +import flask_restful as restful +from flask_restful import reqparse, fields, marshal_with +from math import ceil +import random +from sqlalchemy.sql import func, exists + +from typing import Any, Mapping +from datetime import datetime + +from app import db, LOGGER +from app.applicationModel.models import ApplicationForm +from app.events.models import Event, EventRole +from app.events.repository import EventRepository as event_repository +from app.responses.repository import ResponseRepository as response_repository +from app.responses.models import Response, ResponseReviewer +from app.reviews.mixins import ReviewMixin, GetReviewResponseMixin, PostReviewResponseMixin, PostReviewAssignmentMixin, \ + GetReviewAssignmentMixin, GetReviewHistoryMixin, GetReviewSummaryMixin +from app.reviews.models import ReviewForm, ReviewResponse, ReviewScore, ReviewQuestion, ReviewSection, ReviewSectionTranslation, ReviewQuestionTranslation +from app.reviews.repository import ReviewRepository as review_repository +from app.reviews.repository import ReviewConfigurationRepository as review_configuration_repository +from app.references.repository import ReferenceRequestRepository as reference_repository + +from app.users.models import AppUser, Country, UserCategory +from app.users.repository import UserRepository as user_repository + +from app.events.repository import EventRepository as event_repository +from app.utils.auth import auth_required + +from app.utils.auth import auth_required, event_admin_required +from app.utils.errors import EVENT_NOT_FOUND, REVIEW_RESPONSE_NOT_FOUND, FORBIDDEN, USER_NOT_FOUND, RESPONSE_NOT_FOUND, \ + REVIEW_FORM_NOT_FOUND, REVIEW_ALREADY_COMPLETED, NO_ACTIVE_REVIEW_FORM, REVIEW_FORM_FOR_STAGE_NOT_FOUND + +from app.utils import misc +from app.utils.emailer import email_user + +option_fields = { + 'value': fields.String, + 'label': 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)) +} + +user_fields = { + 'nationality_country': fields.String(attribute='nationality_country.name'), + 'residence_country': fields.String(attribute='residence_country.name'), + 'affiliation': fields.String, + 'department': fields.String, + 'user_category': fields.String(attribute='user_category.name'), + 'id': fields.Integer +} + +review_scores_fields = { + 'review_question_id': fields.Integer, + 'value': fields.String +} + +review_response_fields = { + 'id': fields.Integer, + 'review_form_id': fields.Integer, + 'response_id': fields.Integer, + 'reviewer_user_id': fields.Integer, + 'scores': fields.List(fields.Nested(review_scores_fields), attribute='review_scores'), + 'language': fields.String +} + +reference_fields = { + 'firstname': fields.String, + 'title': fields.String, + 'lastname': fields.String, + 'relation': fields.String, + 'uploaded_document': fields.String, +} + +review_fields = { + 'review_form': fields.Raw, + 'response': fields.Nested(response_fields), + 'user': fields.Nested(user_fields), + 'reviews_remaining_count': fields.Integer, + 'review_response': fields.Nested(review_response_fields), + 'references': fields.List(fields.Nested(reference_fields)), + 'is_submitted': fields.Boolean, + 'submitted_timestamp': fields.DateTime(dt_format='iso8601') +} + +review_question_detail_fields = { + 'id': fields.Integer, + 'question_id': fields.Integer, + 'type': fields.String, + 'is_required': fields.Boolean, + 'order': fields.Integer, + 'weight': fields.Float, + 'headline': fields.Raw(attribute=lambda s: s.headline_translations), + 'description': fields.Raw(attribute=lambda s: s.description_translations), + 'placeholder': fields.Raw(attribute=lambda s: s.placeholder_translations), + 'options': fields.Raw(attribute=lambda s: s.options_translations), + 'validation_regex': fields.Raw(attribute=lambda s: s.validation_regex_translations), + 'validation_text': fields.Raw(attribute=lambda s: s.validation_text_translations) +} + +review_section_detail_fields = { + 'id': fields.Integer, + 'order': fields.Integer, + 'name': fields.Raw(attribute=lambda s: s.headline_translations), + 'description': fields.Raw(attribute=lambda s: s.description_translations), + 'questions': fields.List(fields.Nested(review_question_detail_fields), attribute='review_questions') +} + +review_form_detail_fields = { + 'id': fields.Integer, + 'event_id': fields.Integer, + 'application_form_id': fields.Integer, + 'is_open': fields.Boolean, + 'deadline': fields.DateTime(dt_format='iso8601'), + 'active': fields.Boolean, + 'stage': fields.Integer, + 'sections': fields.List(fields.Nested(review_section_detail_fields), attribute='review_sections') +} + +def _serialize_review_question(review_question, language): + translation = review_question.get_translation(language) + if translation is None: + LOGGER.warn('Missing {} translation for review review_question id {}'.format(language, review_question.id)) + translation = review_question.get_translation('en') + return { + 'id': review_question.id, + 'question_id': review_question.question_id, + 'description': translation.description, + 'headline': translation.headline, + 'type': review_question.type, + 'placeholder': translation.placeholder, + 'options': translation.options, + 'is_required': review_question.is_required, + 'order': review_question.order, + 'validation_regex': translation.validation_regex, + 'validation_text': translation.validation_text, + 'weight': review_question.weight + } + +def _serialize_review_form(review_form: ReviewForm, language: str) -> Mapping[str, Any]: + review_sections = [] + + for section in review_form.review_sections: + translation = section.get_translation(language) + if translation is None: + LOGGER.warn('Missing {} translation for review section id {}'.format(language, section.id)) + translation = section.get_translation('en') + review_sections.append({ + 'id': section.id, + 'order': section.order, + 'headline': translation.headline, + 'description': translation.description, + 'review_questions': [_serialize_review_question(q, language) for q in section.review_questions] + }) + + form = { + 'id': review_form.id, + 'application_form_id': review_form.application_form_id, + 'is_open': review_form.is_open, + 'deadline': review_form.deadline.isoformat(), + 'stage': review_form.stage, + 'review_sections': review_sections + } + + return form + + +def _add_reviewer_role(user_id, event_id): + event_role = EventRole('reviewer', user_id, event_id) + db.session.add(event_role) + db.session.commit() + + +class ReviewResponseUser(): + def __init__(self, review_form, response, reviews_remaining_count, language, reference_responses=None, + review_response=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 + + +class ReviewResponseReference(): + def __init__(self, title, firstname, lastname, relation, uploaded_document): + self.title = title + self.firstname = firstname + self.lastname = lastname + self.relation = relation + self.uploaded_document = uploaded_document + + +class ReviewAPI(ReviewMixin, restful.Resource): + + @auth_required + @marshal_with(review_fields) + def get(self): + args = self.req_parser.parse_args() + event_id = args['event_id'] + + review_form = review_repository.get_review_form(event_id) + if review_form is None: + return EVENT_NOT_FOUND + + reviews_remaining_count = review_repository.get_remaining_reviews_count(g.current_user['id'], + review_form.application_form_id) + skip = self.sanitise_skip(args['skip'], reviews_remaining_count) + + response = review_repository.get_response_to_review(skip, g.current_user['id'], review_form.application_form_id) + + references = [] + if response is not None: + reference_requests = reference_repository.get_all_by_response_id(response.id) + + for r in reference_requests: + reference = reference_repository.get_reference_by_reference_request_id(r.id) + if reference is not None: + references.append( + ReviewResponseReference(r.title, r.firstname, r.lastname, r.relation, + reference.uploaded_document) + ) + + review_response = None if response is None else review_repository.get_review_response(review_form.id, + response.id, + g.current_user['id']) + references = None if len(references) == 0 else references + + return ReviewResponseUser( + review_form, + response, + reviews_remaining_count, + args['language'], + reference_responses=references, + review_response=review_response + ) + + def sanitise_skip(self, skip, reviews_remaining_count): + if skip is None: + skip = 0 + + if skip < 0: + skip = 0 + + if reviews_remaining_count == 0: + skip = 0 + elif skip >= reviews_remaining_count: + skip = reviews_remaining_count - 1 + + return skip + + +class ResponseReviewAPI(restful.Resource): + @auth_required + @marshal_with(review_fields) + def get(self): + parser = reqparse.RequestParser() + parser.add_argument('response_id', type=int, required=True) + parser.add_argument('event_id', type=int, required=True) + parser.add_argument('language', type=str, required=True) + args = parser.parse_args() + + response_id = args['response_id'] + event_id = args['event_id'] + + review_form = review_repository.get_review_form(event_id) + if review_form is None: + return REVIEW_FORM_NOT_FOUND + + response = review_repository.get_response_by_reviewer(response_id, g.current_user['id']) + + if response is None: + return RESPONSE_NOT_FOUND + + review_response = review_repository.get_review_response(review_form.id, response_id, g.current_user['id']) + + return ReviewResponseUser(review_form, response, 0, args['language'], review_response=review_response) + + +class ReviewResponseAPI(GetReviewResponseMixin, PostReviewResponseMixin, restful.Resource): + + @auth_required + @marshal_with(review_fields) + def get(self): + args = self.get_req_parser.parse_args() + id = args['id'] + reviewer_user_id = g.current_user['id'] + + review_form_response = review_repository.get_review_response_with_form(id, reviewer_user_id) + + if review_form_response is None: + return REVIEW_RESPONSE_NOT_FOUND + + review_form, review_response = review_form_response + + response = review_repository.get_response_by_review_response(id, reviewer_user_id, + review_form.application_form_id) + + return ReviewResponseUser(review_form, response, 0, args['language'], review_response=review_response) + + @auth_required + @marshal_with(review_response_fields) + def post(self): + args = self.post_req_parser.parse_args() + validation_result = self.validate_scores(args['scores']) + if validation_result is not None: + return validation_result + + response_id = args['response_id'] + review_form_id = args['review_form_id'] + reviewer_user_id = g.current_user['id'] + scores = args['scores'] + language = args['language'] + is_submitted = args['is_submitted'] + + response_reviewer = review_repository.get_response_reviewer(response_id, reviewer_user_id) + if response_reviewer is None: + return FORBIDDEN + + review_response = ReviewResponse(review_form_id, reviewer_user_id, response_id, language) + review_response.review_scores = self.get_review_scores(scores) + if is_submitted: + review_response.submit() + review_repository.add_model(review_response) + + return review_response, 201 + + @auth_required + @marshal_with(review_response_fields) + def put(self): + args = self.post_req_parser.parse_args() + validation_result = self.validate_scores(args['scores']) + if validation_result is not None: + return validation_result + + response_id = args['response_id'] + review_form_id = args['review_form_id'] + reviewer_user_id = g.current_user['id'] + scores = args['scores'] + is_submitted = args['is_submitted'] + + response_reviewer = review_repository.get_response_reviewer(response_id, reviewer_user_id) + if response_reviewer is None: + return FORBIDDEN + + review_response = review_repository.get_review_response(review_form_id, response_id, reviewer_user_id) + if review_response is None: + return REVIEW_RESPONSE_NOT_FOUND + + review_repository.deactivate_review(review_response) + review_response.review_scores = self.get_review_scores(scores) + if is_submitted: + review_response.submit() + db.session.commit() + + return review_response, 200 + + def get_review_scores(self, scores): + review_scores = [] + for score in scores: + review_score = ReviewScore(score['review_question_id'], score['value']) + review_scores.append(review_score) + return review_scores + + def validate_scores(self, scores): + for score in scores: + if 'review_question_id' not in score.keys(): + return self.get_error_message('review_question_id') + if 'value' not in score.keys(): + return self.get_error_message('value') + + def get_error_message(self, key): + return ( + {'message': {key: 'Missing required parameter in the JSON body or the post body or the query string'}}, 400) + + +class ReviewCountView(): + def __init__(self, count): + self.email = count.email + self.user_title = count.user_title + self.firstname = count.firstname + self.lastname = count.lastname + self.reviews_allocated = count.reviews_allocated + self.reviews_completed = count.reviews_completed + self.reviewer_user_id = count.reviewer_user_id + +class ReviewSummaryAPI(GetReviewSummaryMixin, restful.Resource): + @auth_required + def get(self): + args = self.get_req_parser.parse_args() + event_id = args['event_id'] + user_id = g.current_user['id'] + + current_user = user_repository.get_by_id(user_id) + if not current_user.is_event_admin(event_id): + return FORBIDDEN + + config = review_configuration_repository.get_configuration_for_event(event_id) + + return { + 'reviews_unallocated': review_repository.count_unassigned_reviews(event_id, config.num_reviews_required) + } + + +class ReviewAssignmentAPI(GetReviewAssignmentMixin, PostReviewAssignmentMixin, restful.Resource): + reviews_count_fields = { + 'reviewer_user_id': fields.Integer, + 'email': fields.String, + 'user_title': fields.String, + 'firstname': fields.String, + 'lastname': fields.String, + 'reviews_allocated': fields.Integer, + 'reviews_completed': fields.Integer + } + + @auth_required + @marshal_with(reviews_count_fields) + def get(self): + args = self.get_req_parser.parse_args() + event_id = args['event_id'] + user_id = g.current_user['id'] + + current_user = user_repository.get_by_id(user_id) + if not current_user.is_event_admin(event_id): + return FORBIDDEN + + counts = review_repository.count_reviews_allocated_and_completed_per_reviewer(event_id) + views = [ReviewCountView(count) for count in counts] + return views + + @auth_required + def post(self): + args = self.post_req_parser.parse_args() + user_id = g.current_user['id'] + event_id = args['event_id'] + reviewer_user_email = args['reviewer_user_email'] + num_reviews = args['num_reviews'] + + event = event_repository.get_by_id(event_id) + if not event: + return EVENT_NOT_FOUND + + current_user = user_repository.get_by_id(user_id) + if not current_user.is_event_admin(event_id): + return FORBIDDEN + + reviewer_user = user_repository.get_by_email(reviewer_user_email, g.organisation.id) + if reviewer_user is None: + return USER_NOT_FOUND + + if not reviewer_user.is_reviewer(event_id): + _add_reviewer_role(reviewer_user.id, event_id) + + config = review_configuration_repository.get_configuration_for_event(event_id) + + response_ids = self.get_eligible_response_ids(event_id, reviewer_user.id, num_reviews, + config.num_reviews_required) + response_reviewers = [ResponseReviewer(response_id, reviewer_user.id) for response_id in response_ids] + db.session.add_all(response_reviewers) + db.session.commit() + + if len(response_ids) > 0: + email_user( + 'reviews-assigned', + template_parameters=dict( + num_reviews=len(response_ids), + baobab_host=misc.get_baobab_host(), + system_name=g.organisation.system_name, + event_key=event.key + ), + event=event, + user=reviewer_user) + return {}, 201 + + def add_reviewer_role(self, user_id, event_id): + event_role = EventRole('reviewer', user_id, event_id) + db.session.add(event_role) + db.session.commit() + + def get_eligible_response_ids(self, event_id, reviewer_user_id, num_reviews, reviews_required): + candidate_responses = review_repository.get_candidate_response_ids(event_id, reviewer_user_id, reviews_required) + candidate_response_ids = set([r.id for r in candidate_responses]) + + # Now remove any responses that the reviewer is already assigned to + already_assigned = review_repository.get_already_assigned(reviewer_user_id) + already_assigned_ids = set([r.response_id for r in already_assigned]) + responses = list(candidate_response_ids - already_assigned_ids) + + return random.sample(responses, min(len(responses), num_reviews)) + + +_review_history_fields = { + 'response_id': fields.Integer, + 'review_response_id': fields.Integer, + 'submitted_timestamp': fields.DateTime(dt_format='iso8601'), + 'reviewed_user_id': fields.String +} + +review_history_fields = { + 'reviews': fields.List(fields.Nested(_review_history_fields)), + 'num_entries': fields.Integer, + 'current_pagenumber': fields.Integer, + 'total_pages': fields.Integer +} + + +class ReviewHistoryModel: + def __init__(self, review): + self.response_id = review.Response.id + self.review_response_id = review.id + self.submitted_timestamp = review.submitted_timestamp + self.reviewed_user_id = review.AppUser.id + + +class ReviewHistoryAPI(GetReviewHistoryMixin, restful.Resource): + + @auth_required + @marshal_with(review_history_fields) + def get(self): + args = self.get_req_parser.parse_args() + user_id = g.current_user['id'] + event_id = args['event_id'] + page_number = args['page_number'] + limit = args['limit'] + sort_column = args['sort_column'] + + reviewer = user_repository.get_by_id(user_id).is_reviewer(event_id) + if not reviewer: + return FORBIDDEN + + reviews = review_repository.get_review_history(user_id, event_id) + + if sort_column == 'review_response_id': + reviews = reviews.order_by(ReviewResponse.id) + + if sort_column == 'submitted_timestamp': + reviews = reviews.order_by(ReviewResponse.submitted_timestamp) + + reviews = reviews.slice(page_number * limit, page_number * limit + limit).all() + + num_entries = review_repository.get_review_history_count(user_id, event_id) + + total_pages = ceil(float(num_entries) / limit) + + reviews = [ReviewHistoryModel(review) for review in reviews] + return {'reviews': reviews, 'num_entries': num_entries, 'current_pagenumber': page_number, + 'total_pages': total_pages} + + +class ReviewListAPI(restful.Resource): + + @staticmethod + def _serialize_answer(answer, language): + translation = answer.question.get_translation(language) + if not translation: + translation = answer.question.get_translation('en') + LOGGER.warn('Could not find {} translation for question id {}'.format(language, answer.question.id)) + return { + 'headline': translation.headline, + 'value': answer.value_display + } + + @staticmethod + def _serialize_response(response, review_response, language): + info = [ + ReviewListAPI._serialize_answer(answer, language) + for answer in response.answers if answer.question.key == 'review-identifier'] + + submitted = None + if review_response and review_response.submitted_timestamp: + submitted = review_response.submitted_timestamp.isoformat() + + return { + 'response_id': response.id, + 'language': response.language, + 'information': info, + 'started': review_response is not None, + 'submitted': submitted, + 'total_score': review_response.calculate_score() if review_response is not None else 0.0 + } + + @auth_required + def get(self): + parser = reqparse.RequestParser() + parser.add_argument('event_id', type=int, required=True) + parser.add_argument('language', type=str, required=True) + args = parser.parse_args() + event_id = args['event_id'] + user_id = g.current_user['id'] + language = args['language'] + + if not user_repository.get_by_id(user_id).is_reviewer(event_id): + return FORBIDDEN + + responses_to_review = review_repository.get_review_list(user_id, event_id) + + return [ReviewListAPI._serialize_response(response, review_response, language) + for response, review_response in responses_to_review] + +class ResponseReviewAssignmentAPI(restful.Resource): + @event_admin_required + def post(self, event_id): + parser = reqparse.RequestParser() + parser.add_argument('response_ids', type=int, required=True, action='append') + parser.add_argument('reviewer_email', type=str, required=True) + args = parser.parse_args() + + response_ids = args['response_ids'] + reviewer_email = args['reviewer_email'] + + filtered_response_ids = response_repository.filter_ids_to_event(response_ids, event_id) + + if set(filtered_response_ids) != set(response_ids): + return FORBIDDEN + + event = event_repository.get_by_id(event_id) + + reviewer_user = user_repository.get_by_email(reviewer_email, g.organisation.id) + if reviewer_user is None: + return USER_NOT_FOUND + + if not reviewer_user.is_reviewer(event_id): + _add_reviewer_role(reviewer_user.id, event_id) + + response_reviewers = [ResponseReviewer(response_id, reviewer_user.id) for response_id in response_ids] + db.session.add_all(response_reviewers) + db.session.commit() + + if len(response_ids) > 0: + email_user( + 'reviews-assigned', + template_parameters=dict( + num_reviews=len(response_ids), + baobab_host=misc.get_baobab_host(), + system_name=g.organisation.system_name, + event_key=event.key + ), + event=event, + user=reviewer_user) + return {}, 201 + + @event_admin_required + def delete(self, event_id): + parser = reqparse.RequestParser() + parser.add_argument('response_id', type=int, required=True) + parser.add_argument('reviewer_user_id', type=int, required=True) + args = parser.parse_args() + + response_id = args['response_id'] + reviewer_user_id = args['reviewer_user_id'] + + review_form = review_repository.get_review_form(event_id) + if not review_form: + return REVIEW_FORM_NOT_FOUND + + # If the reviewer has already completed the review, action can't be completed + review_response = review_repository.get_review_response(review_form.id, response_id, reviewer_user_id) + if review_response: + return REVIEW_ALREADY_COMPLETED + + review_repository.delete_response_reviewer(response_id, reviewer_user_id) + + return {}, 200 + +class ReviewResponseDetailListAPI(restful.Resource): + @staticmethod + def _serialise_identifier(answer, language): + question_translation = answer.question.get_translation(language) + if question_translation is None: + question_translation = answer.question.get_translation('en') + LOGGER.warn('Could not find {} translation for question id {}'.format(language, answer.question.id)) + + return { + 'headline': question_translation.headline, + 'value': answer.value_display + } + + @staticmethod + def _serialise_score(review_score, language): + review_question_translation = review_score.review_question.get_translation(language) + if review_question_translation is None: + review_question_translation = review_score.review_question.get_translation('en') + LOGGER.warn('Could not find {} translation for review question id {}'.format(language, + review_score.review_question.id)) + + return { + 'review_question_id': review_score.review_question.id, + 'headline': review_question_translation.headline, + 'description': review_question_translation.description, + 'type': review_score.review_question.type, + 'score': review_score.value, + 'weight': review_score.review_question.weight, + } + + @staticmethod + def _serialise_review_response(review_response, language): + return { + 'review_response_id': review_response.id, + 'response_id': review_response.response_id, + + 'reviewer_user_title': review_response.reviewer_user.user_title, + 'reviewer_user_firstname': review_response.reviewer_user.firstname, + 'reviewer_user_lastname': review_response.reviewer_user.lastname, + + 'response_user_title': review_response.response.user.user_title, + 'response_user_firstname': review_response.response.user.firstname, + 'response_user_lastname': review_response.response.user.lastname, + + 'identifiers': [ + ReviewResponseDetailListAPI._serialise_identifier(answer, language) + for answer in review_response.response.answers + if answer.question.is_review_identifier() + ], + + 'scores': [ + ReviewResponseDetailListAPI._serialise_score(review_score, language) + for review_score in review_response.review_scores + if (review_score.review_question.type == 'multi-choice' + or review_score.review_question.type == 'long-text' + or review_score.review_question.type == 'short-text' + ) + ], + + 'total': review_response.calculate_score() + } + + @auth_required + @event_admin_required + def get(self, event_id): + parser = reqparse.RequestParser() + parser.add_argument('language', type=str, required=True) + args = parser.parse_args() + + review_responses = review_repository.get_all_review_responses_by_event(event_id) + return [ + ReviewResponseDetailListAPI._serialise_review_response(review_response, args['language']) + for review_response in review_responses + ], 200 + +class ReviewResponseSummaryListAPI(restful.Resource): + @staticmethod + def _serialise_response(response: Response, review_form: ReviewForm, language: str): + scores = [] + for review_section in review_form.review_sections: + for review_question in review_section.review_questions: + if review_question.weight > 0: + review_question_translation = review_question.get_translation(language) + if not review_question_translation: + review_question_translation = review_question.get_translation('en') + LOGGER.warn('Could not find {} translation for review question id {}'.format(language, review_score.review_question.id)) + + average_score = review_repository.get_average_score_for_review_question(response.id, review_question.id) + + score = { + "review_question_id": review_question.id, + "headline": review_question_translation.headline, + "description": review_question_translation.description, + "type": review_question.type, + "score": average_score, + "weight": review_question.weight + } + scores.append(score) + + response_summary = { + "response_id": response.id, + "response_user_title": response.user.user_title, + "response_user_firstname": response.user.firstname, + "response_user_lastname": response.user.lastname, + + "identifiers": [ + ReviewResponseDetailListAPI._serialise_identifier(answer, response.language) + for answer in response.answers + if answer.question.is_review_identifier() + ], + + "scores": scores, + "total": sum(score['score'] * score['weight'] for score in scores) + } + return response_summary + + @auth_required + @event_admin_required + def get(self, event_id): + parser = reqparse.RequestParser() + parser.add_argument('language', type=str, required=True) + args = parser.parse_args() + + responses = response_repository.get_all_for_event(event_id) + review_form = review_repository.get_review_form(event_id) + + return [ + ReviewResponseSummaryListAPI._serialise_response(response, review_form, args['language']) + for response in responses + ], 200 + + +class ReviewStageAPI(restful.Resource): + + @auth_required + def get(self): + parser = reqparse.RequestParser() + parser.add_argument('event_id', type=int, required=True) + args = parser.parse_args() + + event_id = args['event_id'] + + review_forms = review_repository.get_all_review_forms_for_event(event_id) + current_form = [r for r in review_forms if r.active] + + if not current_form: + return NO_ACTIVE_REVIEW_FORM + + current_form = current_form[0] + + return { + 'current_stage': current_form.stage, + 'total_stages': len(review_forms) + } + + @auth_required + @event_admin_required + def post(self, event_id): + parser = reqparse.RequestParser() + parser.add_argument('stage', type=int, required=True) + args = parser.parse_args() + + stage = args['stage'] + + review_forms = review_repository.get_all_review_forms_for_event(event_id) + selected_form = [r for r in review_forms if r.stage == stage] + + if not selected_form: + return REVIEW_FORM_FOR_STAGE_NOT_FOUND + + selected_form = selected_form[0] + + for form in review_forms: + form.deactivate() + + selected_form.activate() + + db.session.commit() + + return {}, 201 + + +class ReviewFormDetailAPI(restful.Resource): + + @event_admin_required + @marshal_with(review_form_detail_fields) + def get(self, event_id): + req_parser = reqparse.RequestParser() + req_parser.add_argument('stage', type=int, required=False) + args = req_parser.parse_args() + + stage = args['stage'] + + review_form = review_repository.get_review_form(event_id, stage=stage) + + if not review_form: + return REVIEW_FORM_FOR_STAGE_NOT_FOUND + + review_form.event_id = event_id + + return review_form + + @event_admin_required + @marshal_with(review_form_detail_fields) + def post(self, event_id): + dt_format = lambda x: datetime.strptime(x, '%Y-%m-%dT%H:%M:%S') + + req_parser = reqparse.RequestParser() + req_parser.add_argument('application_form_id', type=int, required=True) + req_parser.add_argument('stage', type=int, required=True) + req_parser.add_argument('is_open', type=bool, required=True) + req_parser.add_argument('deadline', type=dt_format, required=True) + req_parser.add_argument('active', type=bool, required=True) + req_parser.add_argument('sections', type=dict, required=True, action='append') + args = req_parser.parse_args() + + application_form_id = args['application_form_id'] + stage = args['stage'] + is_open = args['is_open'] + deadline = args['deadline'] + active = args['active'] + sections_data = args['sections'] + + review_form = review_repository.get_review_form(event_id, stage) + if review_form is not None: + return REVIEW_FORM_EXISTS + + other_review_forms = review_repository.get_review_forms(event_id) + + review_form = ReviewForm( + application_form_id, + deadline, + stage, + # Only make this stage active if there is no other active stage. + active=not any(o.active for o in other_review_forms)) + + review_repository.add_model(review_form) + + for section_data in sections_data: + section = ReviewSection(review_form.id, section_data['order']) + review_repository.add_model(section) + + languages = section_data['name'].keys() + for language in languages: + section_translation = ReviewSectionTranslation( + review_section_id=section.id, + language=language, + headline=section_data['name'][language], + description=section_data['description'][language]) + review_repository.add_model(section_translation) + + for question_data in section_data['questions']: + question = ReviewQuestion( + review_section_id=section.id, + question_id=question_data['question_id'] or None, + type=question_data['type'], + is_required=question_data['is_required'], + order=question_data['order'], + weight=question_data['weight']) + + review_repository.add_model(question) + + languages = question_data['headline'].keys() + for language in languages: + question_translation = ReviewQuestionTranslation( + review_question_id=question.id, + language=language, + description=question_data["description"][language], + headline=question_data["headline"][language], + placeholder=question_data["placeholder"][language], + options=question_data["options"][language], + validation_regex=question_data["validation_regex"][language], + validation_text=question_data["validation_text"][language]) + + review_repository.add_model(question_translation) + + new_review_form = review_repository.get_review_form_by_id(review_form.id) + new_review_form.event_id = event_id + + return new_review_form, 201 + + @event_admin_required + @marshal_with(review_form_detail_fields) + def put(self, event_id): + dt_format = lambda x: datetime.strptime(x, '%Y-%m-%dT%H:%M:%S') + + req_parser = reqparse.RequestParser() + req_parser.add_argument('id', type=int, required=True) + req_parser.add_argument('application_form_id', type=int, required=True) + req_parser.add_argument('stage', type=int, required=True) + req_parser.add_argument('is_open', type=bool, required=True) + req_parser.add_argument('deadline', type=dt_format, required=True) + req_parser.add_argument('sections', type=dict, required=True, action='append') + args = req_parser.parse_args() + + id = args['id'] + application_form_id = args['application_form_id'] + stage = args['stage'] + is_open = args['is_open'] + deadline = args['deadline'] + sections_data = args['sections'] + + review_form = review_repository.get_review_form_by_id(id) + if not review_form: + return REVIEW_FORM_NOT_FOUND + + if application_form_id != review_form.application_form_id: + return UPDATE_CONFLICT + + if event_id != review_form.application_form.event_id: + return UPDATE_CONFLICT + + review_form.stage = stage + review_form.is_open = is_open + review_form.deadline = deadline + + # Note: we don't update the active property which is controlled by + # the ReviewStageAPI + + # Delete questions in the review form that no longer exist + all_question_ids = [q["id"] for s in sections_data for q in s["questions"] if "id" in q] + for section in review_form.review_sections: + for question in section.review_questions: + if question.id not in all_question_ids: + review_repository.delete_review_question(question) + + all_section_ids = [s["id"] for s in sections_data if "id" in s] + for section in review_form.review_sections: + if section.id not in all_section_ids: + review_repository.delete_review_section(section) + + for section_data in sections_data: + if 'id' in section_data: + # If ID is populated, then update the existing section + section = next((s for s in review_form.review_sections if s.id == section_data['id']), None) # type: ReviewSection + if not section: + return SECTION_NOT_FOUND + + current_translations = section.translations # type: Sequence[ReviewSectionTranslation] + for current_translation in current_translations: + current_translation.description = section_data['description'][current_translation.language] + current_translation.headline = section_data['name'][current_translation.language] + + section.order = section_data["order"] + else: + # if not populated, then add new section + section = ReviewSection(review_form.id, section_data["order"]) + review_repository.add_model(section) + + languages = section_data['name'].keys() + for language in languages: + section_translation = ReviewSectionTranslation( + section.id, + language, + section_data['name'][language], + section_data['description'][language]) + review_repository.add_model(section_translation) + + for question_data in section_data["questions"]: + if "id" in question_data: + current_question = next((q for q in section.review_questions if q.id == question_data['id']), None) # type: ReviewQuestion + if not current_question: + return QUESTION_NOT_FOUND + + current_question.question_id = question_data["question_id"] or None + current_question.type = question_data["type"] + current_question.order = question_data["order"] + current_question.is_required = question_data["is_required"] + current_question.weight = question_data["weight"] + + current_translations = current_question.translations # type: Sequence[ReviewQuestionTranslation] + for current_translation in current_translations: + current_translation.description = question_data['description'][current_translation.language] + current_translation.headline = question_data['headline'][current_translation.language] + current_translation.options = question_data['options'][current_translation.language] + current_translation.placeholder = question_data['placeholder'][current_translation.language] + current_translation.validation_regex = question_data['validation_regex'][current_translation.language] + current_translation.validation_text = question_data['validation_text'][current_translation.language] + else: + question = ReviewQuestion( + review_section_id=section.id, + question_id=question_data["question_id"] or None, + type=question_data["type"], + is_required=question_data["is_required"], + order=question_data["order"], + weight=question_data["weight"]) + + review_repository.add_model(question) + + for language in question_data['headline'].keys(): + translation = ReviewQuestionTranslation( + review_question_id=question.id, + language=language, + description=question_data["description"][language], + headline=question_data["headline"][language], + placeholder=question_data["placeholder"][language], + options=question_data["options"][language], + validation_regex=question_data["validation_regex"][language], + validation_text=question_data["validation_text"][language]) + + review_repository.add_model(translation) + + + db.session.commit() + + review_form = review_repository.get_review_form_by_id(id) + review_form.event_id = event_id + + return review_form, 200 diff --git a/api/app/reviews/mixins.py b/api/app/reviews/mixins.py index cdd96f9fa..22638493c 100644 --- a/api/app/reviews/mixins.py +++ b/api/app/reviews/mixins.py @@ -1,41 +1,41 @@ -from flask_restful import reqparse - -class ReviewMixin(object): - req_parser = reqparse.RequestParser() - req_parser.add_argument('event_id', type=int, required=True) - req_parser.add_argument('skip', type=int, required=False) - req_parser.add_argument('language', type=str, required=True) - -class GetReviewResponseMixin(object): - get_req_parser = reqparse.RequestParser() - get_req_parser.add_argument('id', type=int, required=True) - get_req_parser.add_argument('language', type=str, required=True) - -class PostReviewResponseMixin(object): - post_req_parser = reqparse.RequestParser() - post_req_parser.add_argument('review_form_id', type=int, required=True) - post_req_parser.add_argument('response_id', type=int, required=True) - post_req_parser.add_argument('scores', type=dict, required=True, action='append') - post_req_parser.add_argument('language', type=str, required=True) - post_req_parser.add_argument('is_submitted', type=bool, required=True) - -class GetReviewSummaryMixin(object): - get_req_parser = reqparse.RequestParser() - get_req_parser.add_argument('event_id', type=int, required=True) - -class GetReviewAssignmentMixin(object): - get_req_parser = reqparse.RequestParser() - get_req_parser.add_argument('event_id', type=int, required=True) - -class PostReviewAssignmentMixin(object): - post_req_parser = reqparse.RequestParser() - post_req_parser.add_argument('event_id', type=int, required=True) - post_req_parser.add_argument('reviewer_user_email', type=str, required=True) - post_req_parser.add_argument('num_reviews', type=int, required=True) - -class GetReviewHistoryMixin(object): - get_req_parser = reqparse.RequestParser() - get_req_parser.add_argument('event_id', type = int, required = True) - get_req_parser.add_argument('page_number', type = int, required = True) - get_req_parser.add_argument('limit', type = int, required = True) +from flask_restful import reqparse + +class ReviewMixin(object): + req_parser = reqparse.RequestParser() + req_parser.add_argument('event_id', type=int, required=True) + req_parser.add_argument('skip', type=int, required=False) + req_parser.add_argument('language', type=str, required=True) + +class GetReviewResponseMixin(object): + get_req_parser = reqparse.RequestParser() + get_req_parser.add_argument('id', type=int, required=True) + get_req_parser.add_argument('language', type=str, required=True) + +class PostReviewResponseMixin(object): + post_req_parser = reqparse.RequestParser() + post_req_parser.add_argument('review_form_id', type=int, required=True) + post_req_parser.add_argument('response_id', type=int, required=True) + post_req_parser.add_argument('scores', type=dict, required=True, action='append') + post_req_parser.add_argument('language', type=str, required=True) + post_req_parser.add_argument('is_submitted', type=bool, required=True) + +class GetReviewSummaryMixin(object): + get_req_parser = reqparse.RequestParser() + get_req_parser.add_argument('event_id', type=int, required=True) + +class GetReviewAssignmentMixin(object): + get_req_parser = reqparse.RequestParser() + get_req_parser.add_argument('event_id', type=int, required=True) + +class PostReviewAssignmentMixin(object): + post_req_parser = reqparse.RequestParser() + post_req_parser.add_argument('event_id', type=int, required=True) + post_req_parser.add_argument('reviewer_user_email', type=str, required=True) + post_req_parser.add_argument('num_reviews', type=int, required=True) + +class GetReviewHistoryMixin(object): + get_req_parser = reqparse.RequestParser() + get_req_parser.add_argument('event_id', type = int, required = True) + get_req_parser.add_argument('page_number', type = int, required = True) + get_req_parser.add_argument('limit', type = int, required = True) get_req_parser.add_argument('sort_column', type = str, required = True) \ No newline at end of file diff --git a/api/app/reviews/models.py b/api/app/reviews/models.py index 73a87c7dc..f8fe1812c 100644 --- a/api/app/reviews/models.py +++ b/api/app/reviews/models.py @@ -1,257 +1,257 @@ -from datetime import datetime -from app import db -from app.utils import misc -from typing import Any, Callable, Mapping -from datetime import datetime - - -class ReviewForm(db.Model): - id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) - is_open = db.Column(db.Boolean(), nullable=False) - deadline = db.Column(db.DateTime(), nullable=False) - stage = db.Column(db.Integer(), nullable=False) - active = db.Column(db.Boolean(), nullable=False) - - application_form = db.relationship('ApplicationForm', foreign_keys=[application_form_id]) - review_sections = db.relationship('ReviewSection', order_by='ReviewSection.order') - - def __init__(self, application_form_id, deadline, stage, active): - self.application_form_id = application_form_id - self.is_open = True - self.deadline = deadline - self.stage = stage - self.active = active - - def close(self): - self.is_open = False - - def deactivate(self): - self.active = False - - def activate(self): - self.active = True - - -class ReviewSection(db.Model): - id = db.Column(db.Integer(), primary_key=True) - review_form_id = db.Column(db.Integer(), db.ForeignKey('review_form.id'), nullable=False) - order = db.Column(db.Integer(), nullable=False) - - translations = db.relationship('ReviewSectionTranslation', lazy='dynamic') - review_questions = db.relationship('ReviewQuestion', order_by='ReviewQuestion.order') - review_form = db.relationship('ReviewForm', foreign_keys=[review_form_id]) - - def __init__(self, - review_form_id, - order): - self.review_form_id = review_form_id - self.order = order - - def get_translation(self, language): - translation = self.translations.filter_by(language=language).first() - return translation - - def _translations_for_field(self, accessor_fn: Callable[['ReviewSectionTranslation'], Any]) -> Mapping[str, Any]: - translations = {} - for translation in self.translations: - translations[translation.language] = accessor_fn(translation) - return translations - - @property - def headline_translations(self): - return self._translations_for_field(lambda t: t.headline) - - @property - def description_translations(self): - return self._translations_for_field(lambda t: t.description) - - -class ReviewSectionTranslation(db.Model): - __tablename__ = 'review_section_translation' - __table_args__ = tuple([db.UniqueConstraint('review_section_id', 'language', name='uq_review_section_id_language')]) - - id = db.Column(db.Integer(), primary_key=True) - review_section_id = db.Column(db.Integer(), db.ForeignKey('review_section.id'), nullable=False) - language = db.Column(db.String(2), nullable=False) - - headline = db.Column(db.String(), nullable=True) - description = db.Column(db.String(), nullable=True) - - review_section = db.relationship('ReviewSection', foreign_keys=[review_section_id]) - - def __init__(self, review_section_id, language, headline=None, description=None): - self.review_section_id = review_section_id - self.language = language - self.headline = headline - self.description = description - - -class ReviewQuestion(db.Model): - id = db.Column(db.Integer, primary_key=True) - review_section_id = db.Column(db.Integer(), db.ForeignKey('review_section.id'), nullable=False) - question_id = db.Column(db.Integer(), db.ForeignKey('question.id'), nullable=True) - - type = db.Column(db.String(), nullable=False) - - is_required = db.Column(db.Boolean(), nullable=False) - order = db.Column(db.Integer(), nullable=False) - weight = db.Column(db.Float(), nullable=False) - review_section = db.relationship('ReviewSection', foreign_keys=[review_section_id]) - question = db.relationship('Question', foreign_keys=[question_id]) - - translations = db.relationship('ReviewQuestionTranslation', lazy='dynamic') - - def __init__(self, - review_section_id, - question_id, - type, - is_required, - order, - weight): - self.review_section_id = review_section_id - self.question_id = question_id - self.type = type - self.is_required = is_required - self.order = order - self.weight = weight - - def get_translation(self, language): - translation = self.translations.filter_by(language=language).first() - return translation - - def _translations_for_field(self, accessor_fn: Callable[['ReviewQuestionTranslation'], Any]) -> Mapping[str, Any]: - translations = {} - for translation in self.translations: - translations[translation.language] = accessor_fn(translation) - return translations - - @property - def headline_translations(self): - return self._translations_for_field(lambda t: t.headline) - - @property - def description_translations(self): - return self._translations_for_field(lambda t: t.description) - - @property - def placeholder_translations(self): - return self._translations_for_field(lambda t: t.placeholder) - - @property - def options_translations(self): - return self._translations_for_field(lambda t: t.options) - - @property - def validation_regex_translations(self): - return self._translations_for_field(lambda t: t.validation_regex) - - @property - def validation_text_translations(self): - return self._translations_for_field(lambda t: t.validation_text) - - -class ReviewQuestionTranslation(db.Model): - __tablename__ = 'review_question_translation' - __table_args__ = tuple( - [db.UniqueConstraint('review_question_id', 'language', name='uq_review_question_id_language')]) - - id = db.Column(db.Integer(), primary_key=True) - review_question_id = db.Column(db.Integer(), db.ForeignKey('review_question.id'), nullable=False) - language = db.Column(db.String(2), nullable=False) - - description = db.Column(db.String(), nullable=True) - headline = db.Column(db.String(), nullable=True) - placeholder = db.Column(db.String(), nullable=True) - options = db.Column(db.JSON(), nullable=True) - validation_regex = db.Column(db.String(), nullable=True) - validation_text = db.Column(db.String(), nullable=True) - - def __init__(self, - review_question_id, - language, - description=None, - headline=None, - placeholder=None, - options=None, - validation_regex=None, - validation_text=None): - self.review_question_id = review_question_id - self.language = language - self.description = description - self.headline = headline - self.placeholder = placeholder - self.options = options - self.validation_regex = validation_regex - self.validation_text = validation_text - - -class ReviewResponse(db.Model): - id = db.Column(db.Integer(), primary_key=True) - review_form_id = db.Column(db.Integer(), db.ForeignKey('review_form.id'), nullable=False) - reviewer_user_id = db.Column(db.Integer(), db.ForeignKey('app_user.id'), nullable=False) - response_id = db.Column(db.Integer(), db.ForeignKey('response.id'), nullable=False) - submitted_timestamp = db.Column(db.DateTime(), nullable=False) - language = db.Column(db.String(2), nullable=False) - is_submitted = db.Column(db.Boolean(), nullable=False) - submitted_timestamp = db.Column(db.DateTime(), nullable=True) - - review_form = db.relationship('ReviewForm', foreign_keys=[review_form_id]) - reviewer_user = db.relationship('AppUser', foreign_keys=[reviewer_user_id]) - response = db.relationship('Response', foreign_keys=[response_id]) - review_scores = db.relationship('ReviewScore', - primaryjoin="and_(ReviewResponse.id==ReviewScore.review_response_id," - "ReviewScore.is_active==True)") - - def __init__(self, - review_form_id, - reviewer_user_id, - response_id, - language): - self.review_form_id = review_form_id - self.reviewer_user_id = reviewer_user_id - self.response_id = response_id - self.language = language - self.is_submitted = False - - def submit(self): - self.is_submitted = True - self.submitted_timestamp = datetime.now() - - def calculate_score(self): - return sum([ - misc.try_parse_float(score.value) * score.review_question.weight for score in self.review_scores - if score.review_question.weight > 0 - ]) - - -class ReviewScore(db.Model): - id = db.Column(db.Integer(), primary_key=True) - review_response_id = db.Column(db.Integer(), db.ForeignKey('review_response.id'), nullable=False) - review_question_id = db.Column(db.Integer(), db.ForeignKey('review_question.id'), nullable=False) - value = db.Column(db.String(), nullable=False) - is_active = db.Column(db.Boolean(), nullable=False) - created_on = db.Column(db.DateTime, nullable=False, server_default=db.func.now()) - - review_response = db.relationship('ReviewResponse', foreign_keys=[review_response_id]) - review_question = db.relationship('ReviewQuestion', foreign_keys=[review_question_id]) - - def __init__(self, - review_question_id, - value): - self.review_question_id = review_question_id - self.value = value - self.is_active = True - created_on = datetime.now() - - -class ReviewConfiguration(db.Model): - id = db.Column(db.Integer(), primary_key=True) - review_form_id = db.Column(db.Integer(), db.ForeignKey('review_form.id'), nullable=False) - num_reviews_required = db.Column(db.Integer(), nullable=False) - num_optional_reviews = db.Column(db.Integer(), nullable=False) - drop_optional_question_id = db.Column(db.Integer(), db.ForeignKey('review_question.id'), nullable=True) - drop_optional_agreement_values = db.Column(db.String(), nullable=True) - - review_form = db.relationship('ReviewForm', foreign_keys=[review_form_id]) - review_question = db.relationship('ReviewQuestion', foreign_keys=[drop_optional_question_id]) +from datetime import datetime +from app import db +from app.utils import misc +from typing import Any, Callable, Mapping +from datetime import datetime + + +class ReviewForm(db.Model): + id = db.Column(db.Integer(), primary_key=True) + application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) + is_open = db.Column(db.Boolean(), nullable=False) + deadline = db.Column(db.DateTime(), nullable=False) + stage = db.Column(db.Integer(), nullable=False) + active = db.Column(db.Boolean(), nullable=False) + + application_form = db.relationship('ApplicationForm', foreign_keys=[application_form_id]) + review_sections = db.relationship('ReviewSection', order_by='ReviewSection.order') + + def __init__(self, application_form_id, deadline, stage, active): + self.application_form_id = application_form_id + self.is_open = True + self.deadline = deadline + self.stage = stage + self.active = active + + def close(self): + self.is_open = False + + def deactivate(self): + self.active = False + + def activate(self): + self.active = True + + +class ReviewSection(db.Model): + id = db.Column(db.Integer(), primary_key=True) + review_form_id = db.Column(db.Integer(), db.ForeignKey('review_form.id'), nullable=False) + order = db.Column(db.Integer(), nullable=False) + + translations = db.relationship('ReviewSectionTranslation', lazy='dynamic') + review_questions = db.relationship('ReviewQuestion', order_by='ReviewQuestion.order') + review_form = db.relationship('ReviewForm', foreign_keys=[review_form_id]) + + def __init__(self, + review_form_id, + order): + self.review_form_id = review_form_id + self.order = order + + def get_translation(self, language): + translation = self.translations.filter_by(language=language).first() + return translation + + def _translations_for_field(self, accessor_fn: Callable[['ReviewSectionTranslation'], Any]) -> Mapping[str, Any]: + translations = {} + for translation in self.translations: + translations[translation.language] = accessor_fn(translation) + return translations + + @property + def headline_translations(self): + return self._translations_for_field(lambda t: t.headline) + + @property + def description_translations(self): + return self._translations_for_field(lambda t: t.description) + + +class ReviewSectionTranslation(db.Model): + __tablename__ = 'review_section_translation' + __table_args__ = tuple([db.UniqueConstraint('review_section_id', 'language', name='uq_review_section_id_language')]) + + id = db.Column(db.Integer(), primary_key=True) + review_section_id = db.Column(db.Integer(), db.ForeignKey('review_section.id'), nullable=False) + language = db.Column(db.String(2), nullable=False) + + headline = db.Column(db.String(), nullable=True) + description = db.Column(db.String(), nullable=True) + + review_section = db.relationship('ReviewSection', foreign_keys=[review_section_id]) + + def __init__(self, review_section_id, language, headline=None, description=None): + self.review_section_id = review_section_id + self.language = language + self.headline = headline + self.description = description + + +class ReviewQuestion(db.Model): + id = db.Column(db.Integer, primary_key=True) + review_section_id = db.Column(db.Integer(), db.ForeignKey('review_section.id'), nullable=False) + question_id = db.Column(db.Integer(), db.ForeignKey('question.id'), nullable=True) + + type = db.Column(db.String(), nullable=False) + + is_required = db.Column(db.Boolean(), nullable=False) + order = db.Column(db.Integer(), nullable=False) + weight = db.Column(db.Float(), nullable=False) + review_section = db.relationship('ReviewSection', foreign_keys=[review_section_id]) + question = db.relationship('Question', foreign_keys=[question_id]) + + translations = db.relationship('ReviewQuestionTranslation', lazy='dynamic') + + def __init__(self, + review_section_id, + question_id, + type, + is_required, + order, + weight): + self.review_section_id = review_section_id + self.question_id = question_id + self.type = type + self.is_required = is_required + self.order = order + self.weight = weight + + def get_translation(self, language): + translation = self.translations.filter_by(language=language).first() + return translation + + def _translations_for_field(self, accessor_fn: Callable[['ReviewQuestionTranslation'], Any]) -> Mapping[str, Any]: + translations = {} + for translation in self.translations: + translations[translation.language] = accessor_fn(translation) + return translations + + @property + def headline_translations(self): + return self._translations_for_field(lambda t: t.headline) + + @property + def description_translations(self): + return self._translations_for_field(lambda t: t.description) + + @property + def placeholder_translations(self): + return self._translations_for_field(lambda t: t.placeholder) + + @property + def options_translations(self): + return self._translations_for_field(lambda t: t.options) + + @property + def validation_regex_translations(self): + return self._translations_for_field(lambda t: t.validation_regex) + + @property + def validation_text_translations(self): + return self._translations_for_field(lambda t: t.validation_text) + + +class ReviewQuestionTranslation(db.Model): + __tablename__ = 'review_question_translation' + __table_args__ = tuple( + [db.UniqueConstraint('review_question_id', 'language', name='uq_review_question_id_language')]) + + id = db.Column(db.Integer(), primary_key=True) + review_question_id = db.Column(db.Integer(), db.ForeignKey('review_question.id'), nullable=False) + language = db.Column(db.String(2), nullable=False) + + description = db.Column(db.String(), nullable=True) + headline = db.Column(db.String(), nullable=True) + placeholder = db.Column(db.String(), nullable=True) + options = db.Column(db.JSON(), nullable=True) + validation_regex = db.Column(db.String(), nullable=True) + validation_text = db.Column(db.String(), nullable=True) + + def __init__(self, + review_question_id, + language, + description=None, + headline=None, + placeholder=None, + options=None, + validation_regex=None, + validation_text=None): + self.review_question_id = review_question_id + self.language = language + self.description = description + self.headline = headline + self.placeholder = placeholder + self.options = options + self.validation_regex = validation_regex + self.validation_text = validation_text + + +class ReviewResponse(db.Model): + id = db.Column(db.Integer(), primary_key=True) + review_form_id = db.Column(db.Integer(), db.ForeignKey('review_form.id'), nullable=False) + reviewer_user_id = db.Column(db.Integer(), db.ForeignKey('app_user.id'), nullable=False) + response_id = db.Column(db.Integer(), db.ForeignKey('response.id'), nullable=False) + submitted_timestamp = db.Column(db.DateTime(), nullable=False) + language = db.Column(db.String(2), nullable=False) + is_submitted = db.Column(db.Boolean(), nullable=False) + submitted_timestamp = db.Column(db.DateTime(), nullable=True) + + review_form = db.relationship('ReviewForm', foreign_keys=[review_form_id]) + reviewer_user = db.relationship('AppUser', foreign_keys=[reviewer_user_id]) + response = db.relationship('Response', foreign_keys=[response_id]) + review_scores = db.relationship('ReviewScore', + primaryjoin="and_(ReviewResponse.id==ReviewScore.review_response_id," + "ReviewScore.is_active==True)") + + def __init__(self, + review_form_id, + reviewer_user_id, + response_id, + language): + self.review_form_id = review_form_id + self.reviewer_user_id = reviewer_user_id + self.response_id = response_id + self.language = language + self.is_submitted = False + + def submit(self): + self.is_submitted = True + self.submitted_timestamp = datetime.now() + + def calculate_score(self): + return sum([ + misc.try_parse_float(score.value) * score.review_question.weight for score in self.review_scores + if score.review_question.weight > 0 + ]) + + +class ReviewScore(db.Model): + id = db.Column(db.Integer(), primary_key=True) + review_response_id = db.Column(db.Integer(), db.ForeignKey('review_response.id'), nullable=False) + review_question_id = db.Column(db.Integer(), db.ForeignKey('review_question.id'), nullable=False) + value = db.Column(db.String(), nullable=False) + is_active = db.Column(db.Boolean(), nullable=False) + created_on = db.Column(db.DateTime, nullable=False, server_default=db.func.now()) + + review_response = db.relationship('ReviewResponse', foreign_keys=[review_response_id]) + review_question = db.relationship('ReviewQuestion', foreign_keys=[review_question_id]) + + def __init__(self, + review_question_id, + value): + self.review_question_id = review_question_id + self.value = value + self.is_active = True + created_on = datetime.now() + + +class ReviewConfiguration(db.Model): + id = db.Column(db.Integer(), primary_key=True) + review_form_id = db.Column(db.Integer(), db.ForeignKey('review_form.id'), nullable=False) + num_reviews_required = db.Column(db.Integer(), nullable=False) + num_optional_reviews = db.Column(db.Integer(), nullable=False) + drop_optional_question_id = db.Column(db.Integer(), db.ForeignKey('review_question.id'), nullable=True) + drop_optional_agreement_values = db.Column(db.String(), nullable=True) + + review_form = db.relationship('ReviewForm', foreign_keys=[review_form_id]) + review_question = db.relationship('ReviewQuestion', foreign_keys=[drop_optional_question_id]) diff --git a/api/app/reviews/repository.py b/api/app/reviews/repository.py index 3caa02299..eca16e52f 100644 --- a/api/app/reviews/repository.py +++ b/api/app/reviews/repository.py @@ -1,405 +1,405 @@ -from sqlalchemy.sql import exists -from sqlalchemy import and_, or_, func, cast, Date -from app import db -from app.applicationModel.models import ApplicationForm -from app.responses.models import Response, ResponseReviewer -from app.reviews.models import ReviewForm, ReviewResponse, ReviewScore, ReviewSection, ReviewSectionTranslation, ReviewQuestion, ReviewQuestionTranslation, ReviewConfiguration -from app.users.models import AppUser -from app.references.models import Reference -from app.events.models import EventRole -from app.utils import misc - -class ReviewRepository(): - - @staticmethod - def count_reviews_allocated_and_completed_per_reviewer(event_id): - return db.engine.execute(""" - ( - select - app_user.id as reviewer_user_id, - email, - user_title, - firstname, - lastname, - 0 as reviews_allocated, - 0 as reviews_completed - from app_user - join event_role on event_role.user_id = app_user.id - where event_role.role = 'reviewer' - and event_role.event_id = {event_id} - and not exists ( - select 1 - from response_reviewer - inner join response on response_reviewer.response_id = response.id - inner join application_form on response.application_form_id = application_form.id - where response_reviewer.reviewer_user_id = app_user.id - and application_form.event_id = {event_id} - ) - ) - union - ( - select - app_user.id as reviewer_user_id, - email, - user_title, - firstname, - lastname, - count(response_reviewer.id) as reviews_allocated, - count(review_response.id) as reviews_completed - from response_reviewer - join app_user - on response_reviewer.reviewer_user_id = app_user.id - join event_role - on app_user.id = event_role.user_id - join response - on response.id = response_reviewer.response_id - join application_form on response.application_form_id = application_form.id - left join review_response - on review_response.response_id = response.id - and review_response.reviewer_user_id = app_user.id - where - event_role.role = 'reviewer' - and event_role.event_id = {event_id} - and application_form.event_id = {event_id} - group by app_user.id - - ) - """.format(event_id=event_id)) - - @staticmethod - def count_unassigned_reviews(event_id, required_reviews_per_response): - responses = ( - db.session.query(func.count(Response.id)) - .join(ApplicationForm, Response.application_form_id ==ApplicationForm.id) - .filter(ApplicationForm.event_id == event_id) - .filter(Response.is_submitted==True, Response.is_withdrawn==False) - .first()[0] - ) - - reviews = ( - db.session.query(func.count(ResponseReviewer.id)) - .join(Response, ResponseReviewer.response_id ==Response.id) - .join(ApplicationForm, Response.application_form_id ==ApplicationForm.id) - .filter(ApplicationForm.event_id == event_id).first()[0] - ) - - return responses*required_reviews_per_response - reviews - - @staticmethod - def get_review_form(event_id, stage=None): - query = db.session.query(ReviewForm) - - if stage: - query = query.filter_by(stage=stage) - else: - query = query.filter_by(active=True) - - review_form = (query.join(ApplicationForm, ApplicationForm.id==ReviewForm.application_form_id) - .filter_by(event_id=event_id) - .first()) - - return review_form - - @staticmethod - def get_review_forms(event_id): - review_forms = (db.session.query(ReviewForm) - .join(ApplicationForm, ApplicationForm.id==ReviewForm.application_form_id) - .filter_by(event_id=event_id) - .all()) - - return review_forms - - @staticmethod - def get_review_form_by_id(id): - return db.session.query(ReviewForm).get(id) - - @staticmethod - def get_remaining_reviews_count(reviewer_user_id, application_form_id): - remaining = ( - db.session.query(func.count(ResponseReviewer.id)) - .filter_by(reviewer_user_id=reviewer_user_id, active=True) - .join(Response) - .filter_by(is_withdrawn=False, application_form_id=application_form_id, is_submitted=True) - .outerjoin(ReviewResponse, and_(ReviewResponse.response_id==ResponseReviewer.response_id, ReviewResponse.reviewer_user_id==reviewer_user_id)) - .filter(or_(ReviewResponse.id == None, ReviewResponse.is_submitted == False)) - .all()[0][0] - ) - return remaining - - @staticmethod - def get_response_to_review(skip, reviewer_user_id, application_form_id): - response = ( - db.session.query(Response) - .filter_by(is_withdrawn=False, application_form_id=application_form_id, is_submitted=True) - .join(ResponseReviewer) - .filter_by(reviewer_user_id=reviewer_user_id, active=True) - .outerjoin(ReviewResponse, and_(ReviewResponse.response_id==ResponseReviewer.response_id, ReviewResponse.reviewer_user_id==reviewer_user_id)) - .filter(or_(ReviewResponse.id == None, ReviewResponse.is_submitted == False)) - .order_by(ResponseReviewer.response_id) - .offset(skip) - .first() - ) - return response - - @staticmethod - def get_review_response_with_form(id, reviewer_user_id): - review_form_response = ( - db.session.query(ReviewForm, ReviewResponse) - .filter(ReviewForm.active == True) - .join(ReviewResponse) - .filter_by(id=id, reviewer_user_id=reviewer_user_id) - .first() - ) - return review_form_response - - @staticmethod - def get_response_by_review_response(id, reviewer_user_id, application_form_id): - response = ( - db.session.query(Response) - .filter_by(is_withdrawn=False, application_form_id=application_form_id, is_submitted=True) - .join(ResponseReviewer) - .filter_by(reviewer_user_id=reviewer_user_id) - .join(ReviewResponse) - .filter_by(id=id) - .first() - ) - return response - - @staticmethod - def get_response_reviewer(response_id, reviewer_user_id): - response_reviewer = ( - db.session.query(ResponseReviewer) - .filter_by(response_id=response_id, reviewer_user_id=reviewer_user_id) - .first() - ) - return response_reviewer - - @staticmethod - def get_review_response(review_form_id, response_id, reviewer_user_id): - review_response = ( - db.session.query(ReviewResponse) - .filter_by(review_form_id=review_form_id, response_id=response_id, reviewer_user_id=reviewer_user_id) - .first() - ) - return review_response - - @staticmethod - def get_response_by_reviewer(response_id, reviewer_user_id): - return (db.session.query(Response) - .filter_by(id=response_id) - .join(ResponseReviewer, Response.id == ResponseReviewer.response_id) - .filter_by(reviewer_user_id=reviewer_user_id) - .first()) - - @staticmethod - def add_model(model): - db.session.add(model) - db.session.commit() - - @staticmethod - def get_review_history(reviewer_user_id, event_id): - reviews = (db.session.query(ReviewResponse.id, ReviewResponse.submitted_timestamp, AppUser, Response) - .filter(ReviewResponse.reviewer_user_id == reviewer_user_id) - .join(ReviewForm, ReviewForm.id == ReviewResponse.review_form_id) - .filter(ReviewForm.application_form_id == event_id, ReviewForm.active == True) - .join(Response, ReviewResponse.response_id == Response.id) - .join(AppUser, Response.user_id == AppUser.id)) - return reviews - - @staticmethod - def get_review_list(reviewer_user_id, event_id): - reviews = (db.session.query(Response, ReviewResponse) - .join(ResponseReviewer, Response.id == ResponseReviewer.response_id) - .filter_by(reviewer_user_id=reviewer_user_id) - .join(ApplicationForm, Response.application_form_id == ApplicationForm.id) - .filter_by(event_id=event_id) - .outerjoin(ReviewResponse, and_(Response.id == ReviewResponse.response_id, ReviewResponse.reviewer_user_id==reviewer_user_id))) - return reviews - - @staticmethod - def get_review_history_count(reviewer_user_id, event_id): - count = (db.session.query(ReviewResponse) - .filter(ReviewResponse.reviewer_user_id == reviewer_user_id) - .join(ReviewForm, ReviewForm.id == ReviewResponse.review_form_id) - .filter(ReviewForm.active == True) - .join(ApplicationForm, ReviewForm.application_form_id == ApplicationForm.id) - .filter(ApplicationForm.event_id == event_id) - .count()) - return count - - @staticmethod - def get_count_reviews_completed_for_event(event_id): - count = (db.session.query(ReviewResponse) - .join(ReviewForm, ReviewForm.id == ReviewResponse.review_form_id) - .filter(ReviewForm.active == True) - .join(ApplicationForm, ReviewForm.application_form_id == ApplicationForm.id) - .filter(ApplicationForm.event_id == event_id) - .count()) - return count - - @staticmethod - def get_count_reviews_incomplete_for_event(event_id): - count = (db.session.query(ResponseReviewer) - .join(Response, ResponseReviewer.response_id == Response.id) - .join(ApplicationForm, Response.application_form_id == ApplicationForm.id) - .filter(ApplicationForm.event_id == event_id) - .join(ReviewForm, ApplicationForm.id == ReviewForm.application_form_id) - .filter(ReviewForm.active == True) - .outerjoin(ReviewResponse, and_( - ReviewResponse.review_form_id == ReviewForm.id, - ReviewResponse.reviewer_user_id == ResponseReviewer.reviewer_user_id)) - .filter(ReviewResponse.id == None) - .count()) - return count - - @staticmethod - def get_review_complete_timeseries_by_event(event_id): - timeseries = (db.session.query(cast(ReviewResponse.submitted_timestamp, Date), func.count(ReviewResponse.submitted_timestamp)) - .join(ReviewForm, ReviewForm.id == ReviewResponse.review_form_id) - .filter(ReviewForm.active == True) - .join(ApplicationForm, ReviewForm.application_form_id == ApplicationForm.id) - .filter(ApplicationForm.event_id == event_id) - .group_by(cast(ReviewResponse.submitted_timestamp, Date)) - .order_by(cast(ReviewResponse.submitted_timestamp, Date)) - .all()) - return timeseries - - @staticmethod - def get_already_assigned(reviewer_user_id): - already_assigned = db.session.query(ResponseReviewer.response_id) \ - .filter(ResponseReviewer.reviewer_user_id == reviewer_user_id) \ - .all() - return already_assigned - - - @staticmethod - def get_form_id(event_id): - form_id = db.session.query(ApplicationForm.id).filter_by(event_id=event_id).first()[0] - return form_id - - @staticmethod - def deactivate_review(review_response): - scores = db.session.query(ReviewScore).filter(ReviewScore.review_response_id == review_response.id, - ReviewScore.is_active == True).all() - for score in scores: - score.is_active = False - db.session.merge(score) - db.session.commit() - - @staticmethod - def get_reference_models(response_id): - references = db.session.query(Reference).filter(response_id=response_id).all() - return references - - @staticmethod - def get_candidate_response_ids(event_id, reviewer_user_id, reviews_required): - candidate_response_ids = db.session.query(Response.id) \ - .filter(Response.user_id != reviewer_user_id, - Response.is_submitted == True, - Response.is_withdrawn == False) \ - .join(ApplicationForm, Response.application_form_id == ApplicationForm.id) \ - .filter(ApplicationForm.event_id == event_id) \ - .outerjoin(ResponseReviewer, Response.id == ResponseReviewer.response_id) \ - .group_by(Response.id) \ - .having(func.count(ResponseReviewer.reviewer_user_id) < reviews_required) \ - .all() - return candidate_response_ids - - def get_response_reviewers_for_event(event_id): - return (db.session.query(ResponseReviewer) - .join(Response, Response.id == ResponseReviewer.response_id) - .join(ApplicationForm, Response.application_form_id == ApplicationForm.id) - .filter_by(event_id=event_id) - .order_by(ResponseReviewer.response_id) - .all()) - - @staticmethod - def get_review_responses_for_event(event_id): - return (db.session.query(ReviewResponse) - .join(ReviewForm, ReviewResponse.review_form_id == ReviewForm.id) - .filter_by(active=True) - .join(ApplicationForm, ReviewForm.application_form_id == ApplicationForm.id) - .filter_by(event_id=event_id) - .all()) - - @staticmethod - def delete_response_reviewer(response_id, reviewer_user_id): - db.session.query(ResponseReviewer).filter_by(response_id=response_id, reviewer_user_id=reviewer_user_id).delete() - db.session.commit() - - @staticmethod - def get_all_review_responses_by_event(event_id): - return ( - db.session.query(ReviewResponse) - .join(ReviewForm, ReviewForm.id == ReviewResponse.review_form_id) - .filter_by(active=True) - .join(ApplicationForm, ApplicationForm.id == ReviewForm.application_form_id) - .filter_by(event_id=event_id) - .all() - ) - - @staticmethod - def get_average_score_for_review_question(response_id: int, review_question_id: int): - review_score_values = ( - db.session.query(ReviewScore.value) - .filter(ReviewScore.review_question_id == review_question_id, ReviewScore.is_active == True) - .join(ReviewResponse, ReviewResponse.id == ReviewScore.review_response_id) - .join(Response, Response.id == ReviewResponse.response_id) - .filter_by(id=response_id) - .all() - ) - - review_score_values = [misc.try_parse_float(review_score_value[0]) for review_score_value in review_score_values] - - if len(review_score_values) == 0: - average_review_score = 0 - else: - average_review_score = sum(review_score_values) / len(review_score_values) - - return average_review_score - - @staticmethod - def get_all_review_forms_for_event(event_id): - forms = ( - db.session.query(ReviewForm) - .join(ApplicationForm, ApplicationForm.id == ReviewForm.application_form_id) - .filter_by(event_id=event_id) - .all()) - - return forms - - @staticmethod - def delete_review_question(question_to_delete: ReviewQuestion): - db.session.query(ReviewQuestionTranslation).filter_by(review_question_id=question_to_delete.id).delete() - db.session.query(ReviewQuestion).filter_by(id=question_to_delete.id).delete() - db.session.commit() - - @staticmethod - def delete_review_section(section: ReviewSection): - db.session.query(ReviewSectionTranslation).filter_by(review_section_id=section.id).delete() - - for question in section.review_questions: - ReviewRepository.delete_review_question(question) - - db.session.query(ReviewSection).filter_by(id=section.id).delete() - db.session.commit() - -class ReviewConfigurationRepository(): - - @staticmethod - def get_configuration_for_form(review_form_id): - config = (db.session.query(ReviewConfiguration) - .filter_by(review_form_id=review_form_id) - .first()) - return config - - @staticmethod - def get_configuration_for_event(event_id): - config = (db.session.query(ReviewConfiguration) - .join(ReviewForm, ReviewConfiguration.review_form_id == ReviewForm.id) - .filter_by(active=True) - .join(ApplicationForm, ReviewForm.application_form_id == ApplicationForm.id) - .filter(ApplicationForm.event_id == event_id) - .first()) - return config - +from sqlalchemy.sql import exists +from sqlalchemy import and_, or_, func, cast, Date +from app import db +from app.applicationModel.models import ApplicationForm +from app.responses.models import Response, ResponseReviewer +from app.reviews.models import ReviewForm, ReviewResponse, ReviewScore, ReviewSection, ReviewSectionTranslation, ReviewQuestion, ReviewQuestionTranslation, ReviewConfiguration +from app.users.models import AppUser +from app.references.models import Reference +from app.events.models import EventRole +from app.utils import misc + +class ReviewRepository(): + + @staticmethod + def count_reviews_allocated_and_completed_per_reviewer(event_id): + return db.engine.execute(""" + ( + select + app_user.id as reviewer_user_id, + email, + user_title, + firstname, + lastname, + 0 as reviews_allocated, + 0 as reviews_completed + from app_user + join event_role on event_role.user_id = app_user.id + where event_role.role = 'reviewer' + and event_role.event_id = {event_id} + and not exists ( + select 1 + from response_reviewer + inner join response on response_reviewer.response_id = response.id + inner join application_form on response.application_form_id = application_form.id + where response_reviewer.reviewer_user_id = app_user.id + and application_form.event_id = {event_id} + ) + ) + union + ( + select + app_user.id as reviewer_user_id, + email, + user_title, + firstname, + lastname, + count(response_reviewer.id) as reviews_allocated, + count(review_response.id) as reviews_completed + from response_reviewer + join app_user + on response_reviewer.reviewer_user_id = app_user.id + join event_role + on app_user.id = event_role.user_id + join response + on response.id = response_reviewer.response_id + join application_form on response.application_form_id = application_form.id + left join review_response + on review_response.response_id = response.id + and review_response.reviewer_user_id = app_user.id + where + event_role.role = 'reviewer' + and event_role.event_id = {event_id} + and application_form.event_id = {event_id} + group by app_user.id + + ) + """.format(event_id=event_id)) + + @staticmethod + def count_unassigned_reviews(event_id, required_reviews_per_response): + responses = ( + db.session.query(func.count(Response.id)) + .join(ApplicationForm, Response.application_form_id ==ApplicationForm.id) + .filter(ApplicationForm.event_id == event_id) + .filter(Response.is_submitted==True, Response.is_withdrawn==False) + .first()[0] + ) + + reviews = ( + db.session.query(func.count(ResponseReviewer.id)) + .join(Response, ResponseReviewer.response_id ==Response.id) + .join(ApplicationForm, Response.application_form_id ==ApplicationForm.id) + .filter(ApplicationForm.event_id == event_id).first()[0] + ) + + return responses*required_reviews_per_response - reviews + + @staticmethod + def get_review_form(event_id, stage=None): + query = db.session.query(ReviewForm) + + if stage: + query = query.filter_by(stage=stage) + else: + query = query.filter_by(active=True) + + review_form = (query.join(ApplicationForm, ApplicationForm.id==ReviewForm.application_form_id) + .filter_by(event_id=event_id) + .first()) + + return review_form + + @staticmethod + def get_review_forms(event_id): + review_forms = (db.session.query(ReviewForm) + .join(ApplicationForm, ApplicationForm.id==ReviewForm.application_form_id) + .filter_by(event_id=event_id) + .all()) + + return review_forms + + @staticmethod + def get_review_form_by_id(id): + return db.session.query(ReviewForm).get(id) + + @staticmethod + def get_remaining_reviews_count(reviewer_user_id, application_form_id): + remaining = ( + db.session.query(func.count(ResponseReviewer.id)) + .filter_by(reviewer_user_id=reviewer_user_id, active=True) + .join(Response) + .filter_by(is_withdrawn=False, application_form_id=application_form_id, is_submitted=True) + .outerjoin(ReviewResponse, and_(ReviewResponse.response_id==ResponseReviewer.response_id, ReviewResponse.reviewer_user_id==reviewer_user_id)) + .filter(or_(ReviewResponse.id == None, ReviewResponse.is_submitted == False)) + .all()[0][0] + ) + return remaining + + @staticmethod + def get_response_to_review(skip, reviewer_user_id, application_form_id): + response = ( + db.session.query(Response) + .filter_by(is_withdrawn=False, application_form_id=application_form_id, is_submitted=True) + .join(ResponseReviewer) + .filter_by(reviewer_user_id=reviewer_user_id, active=True) + .outerjoin(ReviewResponse, and_(ReviewResponse.response_id==ResponseReviewer.response_id, ReviewResponse.reviewer_user_id==reviewer_user_id)) + .filter(or_(ReviewResponse.id == None, ReviewResponse.is_submitted == False)) + .order_by(ResponseReviewer.response_id) + .offset(skip) + .first() + ) + return response + + @staticmethod + def get_review_response_with_form(id, reviewer_user_id): + review_form_response = ( + db.session.query(ReviewForm, ReviewResponse) + .filter(ReviewForm.active == True) + .join(ReviewResponse) + .filter_by(id=id, reviewer_user_id=reviewer_user_id) + .first() + ) + return review_form_response + + @staticmethod + def get_response_by_review_response(id, reviewer_user_id, application_form_id): + response = ( + db.session.query(Response) + .filter_by(is_withdrawn=False, application_form_id=application_form_id, is_submitted=True) + .join(ResponseReviewer) + .filter_by(reviewer_user_id=reviewer_user_id) + .join(ReviewResponse) + .filter_by(id=id) + .first() + ) + return response + + @staticmethod + def get_response_reviewer(response_id, reviewer_user_id): + response_reviewer = ( + db.session.query(ResponseReviewer) + .filter_by(response_id=response_id, reviewer_user_id=reviewer_user_id) + .first() + ) + return response_reviewer + + @staticmethod + def get_review_response(review_form_id, response_id, reviewer_user_id): + review_response = ( + db.session.query(ReviewResponse) + .filter_by(review_form_id=review_form_id, response_id=response_id, reviewer_user_id=reviewer_user_id) + .first() + ) + return review_response + + @staticmethod + def get_response_by_reviewer(response_id, reviewer_user_id): + return (db.session.query(Response) + .filter_by(id=response_id) + .join(ResponseReviewer, Response.id == ResponseReviewer.response_id) + .filter_by(reviewer_user_id=reviewer_user_id) + .first()) + + @staticmethod + def add_model(model): + db.session.add(model) + db.session.commit() + + @staticmethod + def get_review_history(reviewer_user_id, event_id): + reviews = (db.session.query(ReviewResponse.id, ReviewResponse.submitted_timestamp, AppUser, Response) + .filter(ReviewResponse.reviewer_user_id == reviewer_user_id) + .join(ReviewForm, ReviewForm.id == ReviewResponse.review_form_id) + .filter(ReviewForm.application_form_id == event_id, ReviewForm.active == True) + .join(Response, ReviewResponse.response_id == Response.id) + .join(AppUser, Response.user_id == AppUser.id)) + return reviews + + @staticmethod + def get_review_list(reviewer_user_id, event_id): + reviews = (db.session.query(Response, ReviewResponse) + .join(ResponseReviewer, Response.id == ResponseReviewer.response_id) + .filter_by(reviewer_user_id=reviewer_user_id) + .join(ApplicationForm, Response.application_form_id == ApplicationForm.id) + .filter_by(event_id=event_id) + .outerjoin(ReviewResponse, and_(Response.id == ReviewResponse.response_id, ReviewResponse.reviewer_user_id==reviewer_user_id))) + return reviews + + @staticmethod + def get_review_history_count(reviewer_user_id, event_id): + count = (db.session.query(ReviewResponse) + .filter(ReviewResponse.reviewer_user_id == reviewer_user_id) + .join(ReviewForm, ReviewForm.id == ReviewResponse.review_form_id) + .filter(ReviewForm.active == True) + .join(ApplicationForm, ReviewForm.application_form_id == ApplicationForm.id) + .filter(ApplicationForm.event_id == event_id) + .count()) + return count + + @staticmethod + def get_count_reviews_completed_for_event(event_id): + count = (db.session.query(ReviewResponse) + .join(ReviewForm, ReviewForm.id == ReviewResponse.review_form_id) + .filter(ReviewForm.active == True) + .join(ApplicationForm, ReviewForm.application_form_id == ApplicationForm.id) + .filter(ApplicationForm.event_id == event_id) + .count()) + return count + + @staticmethod + def get_count_reviews_incomplete_for_event(event_id): + count = (db.session.query(ResponseReviewer) + .join(Response, ResponseReviewer.response_id == Response.id) + .join(ApplicationForm, Response.application_form_id == ApplicationForm.id) + .filter(ApplicationForm.event_id == event_id) + .join(ReviewForm, ApplicationForm.id == ReviewForm.application_form_id) + .filter(ReviewForm.active == True) + .outerjoin(ReviewResponse, and_( + ReviewResponse.review_form_id == ReviewForm.id, + ReviewResponse.reviewer_user_id == ResponseReviewer.reviewer_user_id)) + .filter(ReviewResponse.id == None) + .count()) + return count + + @staticmethod + def get_review_complete_timeseries_by_event(event_id): + timeseries = (db.session.query(cast(ReviewResponse.submitted_timestamp, Date), func.count(ReviewResponse.submitted_timestamp)) + .join(ReviewForm, ReviewForm.id == ReviewResponse.review_form_id) + .filter(ReviewForm.active == True) + .join(ApplicationForm, ReviewForm.application_form_id == ApplicationForm.id) + .filter(ApplicationForm.event_id == event_id) + .group_by(cast(ReviewResponse.submitted_timestamp, Date)) + .order_by(cast(ReviewResponse.submitted_timestamp, Date)) + .all()) + return timeseries + + @staticmethod + def get_already_assigned(reviewer_user_id): + already_assigned = db.session.query(ResponseReviewer.response_id) \ + .filter(ResponseReviewer.reviewer_user_id == reviewer_user_id) \ + .all() + return already_assigned + + + @staticmethod + def get_form_id(event_id): + form_id = db.session.query(ApplicationForm.id).filter_by(event_id=event_id).first()[0] + return form_id + + @staticmethod + def deactivate_review(review_response): + scores = db.session.query(ReviewScore).filter(ReviewScore.review_response_id == review_response.id, + ReviewScore.is_active == True).all() + for score in scores: + score.is_active = False + db.session.merge(score) + db.session.commit() + + @staticmethod + def get_reference_models(response_id): + references = db.session.query(Reference).filter(response_id=response_id).all() + return references + + @staticmethod + def get_candidate_response_ids(event_id, reviewer_user_id, reviews_required): + candidate_response_ids = db.session.query(Response.id) \ + .filter(Response.user_id != reviewer_user_id, + Response.is_submitted == True, + Response.is_withdrawn == False) \ + .join(ApplicationForm, Response.application_form_id == ApplicationForm.id) \ + .filter(ApplicationForm.event_id == event_id) \ + .outerjoin(ResponseReviewer, Response.id == ResponseReviewer.response_id) \ + .group_by(Response.id) \ + .having(func.count(ResponseReviewer.reviewer_user_id) < reviews_required) \ + .all() + return candidate_response_ids + + def get_response_reviewers_for_event(event_id): + return (db.session.query(ResponseReviewer) + .join(Response, Response.id == ResponseReviewer.response_id) + .join(ApplicationForm, Response.application_form_id == ApplicationForm.id) + .filter_by(event_id=event_id) + .order_by(ResponseReviewer.response_id) + .all()) + + @staticmethod + def get_review_responses_for_event(event_id): + return (db.session.query(ReviewResponse) + .join(ReviewForm, ReviewResponse.review_form_id == ReviewForm.id) + .filter_by(active=True) + .join(ApplicationForm, ReviewForm.application_form_id == ApplicationForm.id) + .filter_by(event_id=event_id) + .all()) + + @staticmethod + def delete_response_reviewer(response_id, reviewer_user_id): + db.session.query(ResponseReviewer).filter_by(response_id=response_id, reviewer_user_id=reviewer_user_id).delete() + db.session.commit() + + @staticmethod + def get_all_review_responses_by_event(event_id): + return ( + db.session.query(ReviewResponse) + .join(ReviewForm, ReviewForm.id == ReviewResponse.review_form_id) + .filter_by(active=True) + .join(ApplicationForm, ApplicationForm.id == ReviewForm.application_form_id) + .filter_by(event_id=event_id) + .all() + ) + + @staticmethod + def get_average_score_for_review_question(response_id: int, review_question_id: int): + review_score_values = ( + db.session.query(ReviewScore.value) + .filter(ReviewScore.review_question_id == review_question_id, ReviewScore.is_active == True) + .join(ReviewResponse, ReviewResponse.id == ReviewScore.review_response_id) + .join(Response, Response.id == ReviewResponse.response_id) + .filter_by(id=response_id) + .all() + ) + + review_score_values = [misc.try_parse_float(review_score_value[0]) for review_score_value in review_score_values] + + if len(review_score_values) == 0: + average_review_score = 0 + else: + average_review_score = sum(review_score_values) / len(review_score_values) + + return average_review_score + + @staticmethod + def get_all_review_forms_for_event(event_id): + forms = ( + db.session.query(ReviewForm) + .join(ApplicationForm, ApplicationForm.id == ReviewForm.application_form_id) + .filter_by(event_id=event_id) + .all()) + + return forms + + @staticmethod + def delete_review_question(question_to_delete: ReviewQuestion): + db.session.query(ReviewQuestionTranslation).filter_by(review_question_id=question_to_delete.id).delete() + db.session.query(ReviewQuestion).filter_by(id=question_to_delete.id).delete() + db.session.commit() + + @staticmethod + def delete_review_section(section: ReviewSection): + db.session.query(ReviewSectionTranslation).filter_by(review_section_id=section.id).delete() + + for question in section.review_questions: + ReviewRepository.delete_review_question(question) + + db.session.query(ReviewSection).filter_by(id=section.id).delete() + db.session.commit() + +class ReviewConfigurationRepository(): + + @staticmethod + def get_configuration_for_form(review_form_id): + config = (db.session.query(ReviewConfiguration) + .filter_by(review_form_id=review_form_id) + .first()) + return config + + @staticmethod + def get_configuration_for_event(event_id): + config = (db.session.query(ReviewConfiguration) + .join(ReviewForm, ReviewConfiguration.review_form_id == ReviewForm.id) + .filter_by(active=True) + .join(ApplicationForm, ReviewForm.application_form_id == ApplicationForm.id) + .filter(ApplicationForm.event_id == event_id) + .first()) + return config + diff --git a/api/app/reviews/tests.py b/api/app/reviews/tests.py index cd3828848..c22ae11ea 100644 --- a/api/app/reviews/tests.py +++ b/api/app/reviews/tests.py @@ -1,2948 +1,2948 @@ -from datetime import datetime -import json -import copy -import itertools - -from app import db, LOGGER -from app.utils.testing import ApiTestCase -from app.events.models import Event, EventRole -from app.users.models import AppUser, UserCategory, Country -from app.applicationModel.models import ApplicationForm, Question, Section -from app.responses.models import Response, Answer, ResponseReviewer - -from app.references.models import ReferenceRequest, Reference -from app.references.repository import ReferenceRequestRepository as reference_request_repository -from app.reviews.models import ReviewForm, ReviewQuestion, ReviewResponse, ReviewScore, ReviewConfiguration - -from app.reviews.models import ReviewForm, ReviewQuestion, ReviewQuestionTranslation, ReviewResponse, ReviewScore, ReviewConfiguration -from app.utils.errors import REVIEW_RESPONSE_NOT_FOUND, FORBIDDEN, USER_NOT_FOUND -from nose.plugins.skip import SkipTest -from app.organisation.models import Organisation - -from parameterized import parameterized - -class ReviewsApiTest(ApiTestCase): - - def seed_static_data(self): - self.add_organisation('Deep Learning Indaba 2019', 'blah.png', 'blah_big.png') - self.add_organisation('Deep Learning Indaba 2020', 'blah.png', 'blah_big.png') - user_categories = [ - UserCategory('Honours'), - UserCategory('Student'), - UserCategory('MSc'), - UserCategory('PhD') - ] - db.session.add_all(user_categories) - db.session.commit() - - countries = [ - Country('Egypt'), - Country('Botswana'), - Country('Namibia'), - Country('Zimbabwe'), - Country('Mozambique'), - Country('Ghana'), - Country('Nigeria') - ] - db.session.add_all(countries) - db.session.commit() - - reviewer1 = AppUser('r1@r.com', 'reviewer', '1', 'Mr', password='abc', organisation_id=1,) - reviewer2 = AppUser('r2@r.com', 'reviewer', '2', 'Ms', password='abc', organisation_id=1,) - reviewer3 = AppUser('r3@r.com', 'reviewer', '3', 'Mr', password='abc', organisation_id=1,) - reviewer4 = AppUser('r4@r.com', 'reviewer', '4', 'Ms', password='abc', organisation_id=1,) - candidate1 = AppUser('c1@c.com', 'candidate', '1', 'Mr', password='abc', organisation_id=1,) - candidate2 = AppUser('c2@c.com', 'candidate', '2', 'Ms', password='abc', organisation_id=1,) - candidate3 = AppUser('c3@c.com', 'candidate', '3', 'Mr', password='abc', organisation_id=1,) - candidate4 = AppUser('c4@c.com', 'candidate', '4', 'Ms', password='abc', organisation_id=1,) - system_admin = AppUser('sa@sa.com', 'system_admin', '1', 'Ms', password='abc', organisation_id=1, is_admin=True) - event_admin = AppUser('ea@ea.com', 'event_admin', '1', 'Ms', password='abc',organisation_id=1) - users = [reviewer1, reviewer2, reviewer3, reviewer4, candidate1, candidate2, candidate3, candidate4, system_admin, event_admin] - for user in users: - user.verify() - db.session.add_all(users) - db.session.commit() - - events = [ - self.add_event({'en': 'indaba 2019'}, {'en': 'The Deep Learning Indaba 2019, Kenyatta University, Nairobi, Kenya '}, datetime(2019, 8, 25), datetime(2019, 8, 31), - 'KENYADABA2019'), - self.add_event({'en': 'indaba 2020'}, {'en': 'The Deep Learning Indaba 2018, Stellenbosch University, South Africa'}, datetime(2018, 9, 9), datetime(2018, 9, 15), - 'INDABA2020', 2) - ] - db.session.commit() - - event_roles = [ - EventRole('admin', 10, 1), - EventRole('reviewer', 3, 1) - ] - db.session.add_all(event_roles) - db.session.commit() - - application_forms = [ - self.create_application_form(1, True, False), - self.create_application_form(2, False, False) - ] - db.session.add_all(application_forms) - db.session.commit() - - sections = [ - Section(1, 1), - Section(2, 1) - ] - db.session.add_all(sections) - db.session.commit() - - options = [ - { - "value": "indaba-2017", - "label": "Yes, I attended the 2017 Indaba" - }, - { - "value": "indaba-2018", - "label": "Yes, I attended the 2018 Indaba" - }, - { - "value": "indaba-2017-2018", - "label": "Yes, I attended both Indabas" - }, - { - "value": "none", - "label": "No" - } - ] - questions = [ - Question(1, 1, 1, 'long_text'), - Question(1, 1, 2, 'long_text'), - Question(2, 2, 1, 'long_text'), - Question(2, 2, 2, 'long_text'), - Question(1, 1, 3, 'multi-choice') - ] - db.session.add_all(questions) - db.session.commit() - - self.add_question_translation(1, 'en', 'Question 1') - self.add_question_translation(2, 'en', 'Question 2') - self.add_question_translation(3, 'en', 'Question 3') - self.add_question_translation(4, 'en', 'Question 4') - self.add_question_translation(5, 'en', 'Did you attend the 2017 or 2018 Indaba', options=options) - - closed_review = ReviewForm(2, datetime(2018, 4, 30), 1, True) - closed_review.close() - inactive_review = ReviewForm(2, datetime(2018, 4, 30), 2, False) - review_forms = [ - ReviewForm(1, datetime(2019, 4, 30), 1, True), - ReviewForm(1, datetime(2019, 4, 30), 2, False), - closed_review, - inactive_review - ] - db.session.add_all(review_forms) - db.session.commit() - - review_configs = [ - ReviewConfiguration(review_form_id=review_forms[0].id, num_reviews_required=3, num_optional_reviews=0), - ReviewConfiguration(review_form_id=review_forms[1].id, num_reviews_required=3, num_optional_reviews=0) - ] - db.session.add_all(review_configs) - db.session.commit() - - review_section1 = self.add_review_section(review_forms[0].id) - self.add_review_section_translation(review_section1.id, 'en', 'Review Section 1 English', 'Review Section 1 Description English') - self.add_review_section_translation(review_section1.id, 'fr', 'Review Section 1 French', 'Review Section 1 Description French') - review_section2 = self.add_review_section(review_forms[1].id) - self.add_review_section_translation(review_section2.id, 'en', 'Review Section 2', 'Review Section 2 Description') - - review_questions = [ - ReviewQuestion(review_section1.id, 1, 'multi-choice', True, 1, 0), - ReviewQuestion(review_section1.id, 2, 'multi-choice', True, 2, 0), - ReviewQuestion(review_section2.id, 3, 'multi-choice', True, 1, 0), - ReviewQuestion(review_section2.id, 4, 'information', False, 2, 0) - ] - db.session.add_all(review_questions) - db.session.commit() - - review_question_translations = [ - ReviewQuestionTranslation(review_questions[0].id, 'en'), - ReviewQuestionTranslation(review_questions[0].id, 'fr'), - ReviewQuestionTranslation(review_questions[1].id, 'en', headline='English Headline', description='English Description', placeholder='English Placeholder', options=[{'label': 'en1', 'value': 'en'}], validation_regex='EN Regex', validation_text='EN Validation Message'), - ReviewQuestionTranslation(review_questions[1].id, 'fr', headline='French Headline', description='French Description', placeholder='French Placeholder', options=[{'label': 'fr1', 'value': 'fr'}], validation_regex='FR Regex', validation_text='FR Validation Message'), - ReviewQuestionTranslation(review_questions[2].id, 'en'), - ReviewQuestionTranslation(review_questions[2].id, 'fr'), - ReviewQuestionTranslation(review_questions[3].id, 'en'), - ReviewQuestionTranslation(review_questions[3].id, 'fr'), - ] - db.session.add_all(review_question_translations) - db.session.commit() - - self.add_email_template('reviews-assigned') - - def get_auth_header_for(self, email): - body = { - 'email': email, - 'password': 'abc' - } - response = self.app.post('api/v1/authenticate', data=body) - data = json.loads(response.data) - header = {'Authorization': data['token']} - return header - - def setup_one_reviewer_one_candidate(self, active=True): - response = self.add_response(1, 5, is_submitted=True) - - answers = [ - Answer(1, 1, 'I will learn alot.'), - Answer(1, 2, 'I will share by doing talks.') - ] - db.session.add_all(answers) - db.session.commit() - - response_reviewers = [ - ResponseReviewer(1, 1) # assign reviewer 1 to candidate 1 response - ] - if not active: - response_reviewers[0].deactivate() - db.session.add_all(response_reviewers) - db.session.commit() - - def test_one_reviewer_one_candidate(self): - self.seed_static_data() - self.setup_one_reviewer_one_candidate() - header = self.get_auth_header_for('r1@r.com') - params = {'event_id': 1, 'language': 'en'} - - response = self.app.get('/api/v1/review', headers=header, data=params) - data = json.loads(response.data) - - self.assertEqual(data['reviews_remaining_count'], 1) - - def test_one_reviewer_one_candidate_inactive(self): - self.seed_static_data() - self.setup_one_reviewer_one_candidate(active=False) - header = self.get_auth_header_for('r1@r.com') - params = {'event_id': 1, 'language': 'en'} - - response = self.app.get('/api/v1/review', headers=header, data=params) - data = json.loads(response.data) - - self.assertEqual(data['reviews_remaining_count'], 0) - self.assertEqual(data['response']['id'], 0) - - @parameterized.expand([ - (True,), (False,) - ]) - def test_one_reviewer_one_candidate_review_summary(self, active): - self.seed_static_data() - self.setup_one_reviewer_one_candidate(active=active) - header = self.get_auth_header_for('ea@ea.com') - params = {'event_id': 1} - - response = self.app.get('/api/v1/reviewassignment/summary', headers=header, data=params) - data = json.loads(response.data) - - self.assertEqual(data['reviews_unallocated'], 2) - - def setup_responses_and_no_reviewers(self): - response = self.add_response(1, 5, is_submitted=True) - - answers = [ - Answer(1, 1, 'I will learn alot.'), - Answer(1, 2, 'I will share by doing talks.') - ] - db.session.add_all(answers) - db.session.commit() - - def test_no_response_reviewers(self): - self.seed_static_data() - self.setup_responses_and_no_reviewers() - header = self.get_auth_header_for('r1@r.com') - params = {'event_id': 1, 'language': 'en'} - - response = self.app.get('/api/v1/review', headers=header, data=params) - data = json.loads(response.data) - - self.assertEqual(data['reviews_remaining_count'], 0) - - def test_no_response_reviewers_reviews_unallocated(self): - self.seed_static_data() - self.setup_responses_and_no_reviewers() - header = self.get_auth_header_for('ea@ea.com') - params = {'event_id': 1} - response = self.app.get('/api/v1/reviewassignment/summary', headers=header, data=params) - data = json.loads(response.data) - - self.assertEqual(data['reviews_unallocated'], 3) - - def setup_one_reviewer_three_candidates(self): - self.add_response(application_form_id=1, user_id=5, is_submitted=True) - self.add_response(application_form_id=1, user_id=6, is_submitted=True) - self.add_response(application_form_id=1, user_id=7, is_submitted=True) - - answers = [ - Answer(1, 1, 'I will learn alot.'), - Answer(1, 2, 'I will share by doing talks.'), - Answer(2, 1, 'I want to do a PhD.'), - Answer(2, 2, 'I will share by writing a blog.'), - Answer(3, 1, 'I want to solve new problems.'), - Answer(3, 2, 'I will share by tutoring.'), - ] - db.session.add_all(answers) - db.session.commit() - - response_reviewers = [ - ResponseReviewer(1, 1), - ResponseReviewer(2, 1), - ResponseReviewer(3, 1) - ] - response_reviewers[1].deactivate() - - db.session.add_all(response_reviewers) - db.session.commit() - - def test_one_reviewer_three_candidates(self): - self.seed_static_data() - self.setup_one_reviewer_three_candidates() - header = self.get_auth_header_for('r1@r.com') - params = {'event_id': 1, 'language': 'en'} - - response = self.app.get('/api/v1/review', headers=header, data=params) - data = json.loads(response.data) - - self.assertEqual(data['reviews_remaining_count'], 2) - - def setup_one_reviewer_three_candidates_and_one_completed_review(self): - self.add_response(1, 5, is_submitted=True) - self.add_response(1, 6, is_submitted=True) - self.add_response(1, 7, is_submitted=True) - - answers = [ - Answer(1, 1, 'I will learn alot.'), - Answer(1, 2, 'I will share by doing talks.'), - Answer(2, 1, 'I want to do a PhD.'), - Answer(2, 2, 'I will share by writing a blog.'), - Answer(3, 1, 'I want to solve new problems.'), - Answer(3, 2, 'I will share by tutoring.') - ] - db.session.add_all(answers) - db.session.commit() - - response_reviewers = [ - ResponseReviewer(1, 1), - ResponseReviewer(2, 1), - ResponseReviewer(3, 1) - ] - db.session.add_all(response_reviewers) - db.session.commit() - - review_response = ReviewResponse(1, 1, 1, 'en') - review_response.submit() - db.session.add(review_response) - db.session.commit() - - def test_one_reviewer_three_candidates_and_one_completed_review(self): - self.seed_static_data() - self.setup_one_reviewer_three_candidates_and_one_completed_review() - header = self.get_auth_header_for('r1@r.com') - params = {'event_id': 1, 'language': 'en'} - - response = self.app.get('/api/v1/review', headers=header, data=params) - data = json.loads(response.data) - - self.assertEqual(data['reviews_remaining_count'], 2) - - def setup_one_reviewer_three_candidates_with_one_withdrawn_response_and_one_unsubmitted_response(self): - withdrawn_response = self.add_response(1, 5, is_withdrawn=True) - submitted_response = self.add_response(1, 7, is_submitted=True) - self.add_response(1, 6) - - answers = [ - Answer(1, 1, 'I will learn alot.'), - Answer(1, 2, 'I will share by doing talks.'), - Answer(2, 1, 'I want to do a PhD.'), - Answer(2, 2, 'I will share by writing a blog.'), - Answer(3, 1, 'I want to solve new problems.'), - Answer(3, 2, 'I will share by tutoring.') - ] - db.session.add_all(answers) - db.session.commit() - - response_reviewers = [ - ResponseReviewer(1, 1), - ResponseReviewer(2, 1), - ResponseReviewer(3, 1) - ] - db.session.add_all(response_reviewers) - db.session.commit() - - def test_one_reviewer_three_candidates_with_one_withdrawn_response_and_one_unsubmitted_response(self): - self.seed_static_data() - self.setup_one_reviewer_three_candidates_with_one_withdrawn_response_and_one_unsubmitted_response() - header = self.get_auth_header_for('r1@r.com') - params = {'event_id': 1, 'language': 'en'} - - response = self.app.get('/api/v1/review', headers=header, data=params) - data = json.loads(response.data) - - self.assertEqual(data['reviews_remaining_count'], 1) - - def setup_multiple_reviewers_with_different_subsets_of_candidates_and_reviews_completed(self): - self.add_response(1, 5, is_submitted=True) - self.add_response(1, 6, is_submitted=True) - self.add_response(1, 7, is_submitted=True) - self.add_response(1, 8, is_submitted=True) - - answers = [ - Answer(1, 1, 'I will learn alot.'), - Answer(1, 2, 'I will share by doing talks.'), - Answer(2, 1, 'I want to do a PhD.'), - Answer(2, 2, 'I will share by writing a blog.'), - Answer(3, 1, 'I want to solve new problems.'), - Answer(3, 2, 'I will share by tutoring.'), - Answer(4, 1, 'I want to exchange ideas with like minded people'), - Answer(4, 2, 'I will mentor people interested in ML.') - ] - db.session.add_all(answers) - db.session.commit() - - response_reviewers = [ - ResponseReviewer(1, 1), - ResponseReviewer(2, 1), - ResponseReviewer(3, 1), - - ResponseReviewer(2, 2), - ResponseReviewer(3, 2), - - ResponseReviewer(1, 3), - ResponseReviewer(2, 3), - ResponseReviewer(3, 3), - ResponseReviewer(4, 3), - - ResponseReviewer(1, 4), - ResponseReviewer(2, 4) - ] - response_reviewers[-1].deactivate() - db.session.add_all(response_reviewers) - db.session.commit() - - review_responses = [ - ReviewResponse(1, 2, 2, 'en'), - ReviewResponse(1, 3, 1, 'en'), - ReviewResponse(1, 3, 2, 'en'), - ReviewResponse(1, 4, 1, 'en') - ] - for rr in review_responses: - rr.submit() - - db.session.add_all(review_responses) - db.session.commit() - - def test_multiple_reviewers_with_different_subsets_of_candidates_and_reviews_completed(self): - self.seed_static_data() - self.setup_multiple_reviewers_with_different_subsets_of_candidates_and_reviews_completed() - params = {'event_id': 1, 'language': 'en'} - - header = self.get_auth_header_for('r1@r.com') - response1 = self.app.get('/api/v1/review', headers=header, data=params) - data1 = json.loads(response1.data) - header = self.get_auth_header_for('r2@r.com') - response2 = self.app.get('/api/v1/review', headers=header, data=params) - data2 = json.loads(response2.data) - header = self.get_auth_header_for('r3@r.com') - response3 = self.app.get('/api/v1/review', headers=header, data=params) - data3 = json.loads(response3.data) - header = self.get_auth_header_for('r4@r.com') - response4 = self.app.get('/api/v1/review', headers=header, data=params) - data4 = json.loads(response4.data) - - self.assertEqual(data1['reviews_remaining_count'], 3) - self.assertEqual(data2['reviews_remaining_count'], 1) - self.assertEqual(data3['reviews_remaining_count'], 2) - self.assertEqual(data4['reviews_remaining_count'], 0) - - def test_skipping(self): - self.seed_static_data() - self.setup_one_reviewer_three_candidates() - params = {'event_id': 1, 'skip': 1, 'language': 'en'} - header = self.get_auth_header_for('r1@r.com') - - response = self.app.get('/api/v1/review', headers=header, data=params) - data = json.loads(response.data) - - self.assertEqual(data['response']['user_id'], 7) - self.assertEqual(data['response']['answers'][0]['value'], 'I want to solve new problems.') - - def test_high_skip_defaults_to_last_review(self): - self.seed_static_data() - self.setup_one_reviewer_three_candidates() - params = {'event_id': 1, 'skip': 5, 'language': 'en'} - header = self.get_auth_header_for('r1@r.com') - - response = self.app.get('/api/v1/review', headers=header, data=params) - data = json.loads(response.data) - - self.assertEqual(data['response']['user_id'], 7) - self.assertEqual(data['response']['answers'][1]['value'], 'I will share by tutoring.') - - def setup_candidate_who_has_applied_to_multiple_events(self): - user_id = 5 - - self.add_response(application_form_id=1, user_id=user_id, is_submitted=True) - self.add_response(application_form_id=2, user_id=user_id, is_submitted=True) - - answers = [ - Answer(1, 1, 'I will learn alot.'), - Answer(1, 2, 'I will share by doing talks.'), - Answer(2, 3, 'Yes I worked on a vision task.'), - Answer(2, 4, 'Yes I want the travel award.') - ] - db.session.add_all(answers) - db.session.commit() - - response_reviewers = [ - ResponseReviewer(1, 1), - ResponseReviewer(2, 1) - ] - db.session.add_all(response_reviewers) - db.session.commit() - - def test_filtering_on_event_when_candidate_has_applied_to_more_than(self): - self.seed_static_data() - self.setup_candidate_who_has_applied_to_multiple_events() - params = {'event_id': 2, 'language': 'en'} - header = self.get_auth_header_for('r1@r.com') - - response = self.app.get('/api/v1/review', headers=header, data=params) - data = json.loads(response.data) - - self.assertEqual(data['reviews_remaining_count'], 1) - self.assertEqual(data['response']['user_id'], 5) - self.assertEqual(data['response']['answers'][0]['value'], 'Yes I worked on a vision task.') - - def setup_multi_choice_answer(self): - self.add_response(1, 5, is_submitted=True) - - answer = Answer(1, 5, 'indaba-2017') - db.session.add(answer) - db.session.commit() - - response_reviewer = ResponseReviewer(1, 1) - db.session.add(response_reviewer) - db.session.commit() - - def test_multi_choice_answers_use_label_instead_of_value(self): - self.seed_static_data() - self.setup_multi_choice_answer() - params = {'event_id': 1, 'language': 'en'} - header = self.get_auth_header_for('r1@r.com') - - response = self.app.get('/api/v1/review', headers=header, data=params) - data = json.loads(response.data) - print(data) - - self.assertEqual(data['response']['answers'][0]['value'], 'Yes, I attended the 2017 Indaba') - - def test_review_response_not_found(self): - self.seed_static_data() - params = {'id': 55, 'language': 'en'} - header = self.get_auth_header_for('r1@r.com') - - response = self.app.get('/api/v1/reviewresponse', headers=header, data=params) - data = json.loads(response.data) - - self.assertEqual(response.status_code, REVIEW_RESPONSE_NOT_FOUND[1]) - - def setup_review_response(self): - self.add_response(1, 5, is_submitted=True) - - answer = Answer(1, 1, 'To learn alot') - db.session.add(answer) - db.session.commit() - - self.review_response = ReviewResponse(1, 1, 1, 'en') - self.review_response.review_scores.append(ReviewScore(1, 'answer1')) - self.review_response.review_scores.append(ReviewScore(2, 'answer2')) - db.session.add(self.review_response) - db.session.commit() - - db.session.flush() - - def test_review_response(self): - self.seed_static_data() - self.setup_review_response() - params = {'id': self.review_response.id, 'language': 'en'} - header = self.get_auth_header_for('r1@r.com') - - response = self.app.get('/api/v1/reviewresponse', headers=header, data=params) - data = json.loads(response.data) - - print(data) - - self.assertEqual(data['review_form']['id'], 1) - self.assertEqual(data['review_response']['reviewer_user_id'], 1) - self.assertEqual(data['review_response']['response_id'], 1) - self.assertEqual(data['review_response']['scores'][0]['value'], 'answer1') - self.assertEqual(data['review_response']['scores'][1]['value'], 'answer2') - - def test_prevent_saving_review_response_reviewer_was_not_assigned_to_response(self): - self.seed_static_data() - params = json.dumps({'review_form_id': 1, 'response_id': 1, 'scores': [{'review_question_id': 1, 'value': 'test_answer'}], 'language': 'en', 'is_submitted': False}) - header = self.get_auth_header_for('r1@r.com') - - response = self.app.post('/api/v1/reviewresponse', headers=header, data=params, content_type='application/json') - - self.assertEqual(response.status_code, FORBIDDEN[1]) - - def test_can_still_submit_inactive_response_reviewer(self): - self.seed_static_data() - self.setup_one_reviewer_three_candidates() - params = json.dumps({'review_form_id': 1, 'response_id': 2, 'scores': [{'review_question_id': 1, 'value': 'test_answer'}], 'language': 'en', 'is_submitted': True}) - header = self.get_auth_header_for('r1@r.com') - - response = self.app.post('/api/v1/reviewresponse', headers=header, data=params, content_type='application/json') - - self.assertEqual(response.status_code, 201) - - def setup_response_reviewer(self): - self.add_response(1, 5, is_submitted=True) - - response_reviewer = ResponseReviewer(1, 1) - db.session.add(response_reviewer) - db.session.commit() - - def test_saving_review_response(self): - self.seed_static_data() - self.setup_response_reviewer() - params = json.dumps({'review_form_id': 1, 'response_id': 1, 'scores': [{'review_question_id': 1, 'value': 'test_answer'}], 'language': 'en', 'is_submitted': False}) - header = self.get_auth_header_for('r1@r.com') - - response = self.app.post('/api/v1/reviewresponse', headers=header, data=params, content_type='application/json') - - review_scores = db.session.query(ReviewScore).filter_by(review_response_id=1, is_active=True).all() - self.assertEqual(response.status_code, 201) - self.assertEqual(len(review_scores), 1) - self.assertEqual(review_scores[0].value, 'test_answer') - - def setup_existing_review_response(self): - self.add_response(1, 5, is_submitted=True) - - response_reviewer = ResponseReviewer(1, 1) - db.session.add(response_reviewer) - db.session.commit() - - review_response = ReviewResponse(1, 1, 1, 'en') - review_response.submit() - review_response.review_scores = [ReviewScore(1, 'test_answer1'), ReviewScore(2, 'test_answer2')] - db.session.add(review_response) - db.session.commit() - - def test_updating_review_response(self): - self.seed_static_data() - self.setup_existing_review_response() - params = json.dumps({'review_form_id': 1, 'response_id': 1, 'scores': [{'review_question_id': 1, 'value': 'test_answer3'}, {'review_question_id': 2, 'value': 'test_answer4'}], 'language': 'en', 'is_submitted': True}) - header = self.get_auth_header_for('r1@r.com') - - response = self.app.put('/api/v1/reviewresponse', headers=header, data=params, content_type='application/json') - - review_scores = db.session.query(ReviewScore).filter_by(review_response_id=1, is_active=True).order_by(ReviewScore.review_question_id).all() - self.assertEqual(response.status_code, 200) - self.assertEqual(len(review_scores), 2) - self.assertEqual(review_scores[0].value, 'test_answer3') - self.assertEqual(review_scores[1].value, 'test_answer4') - - def test_user_cant_assign_responsesreviewer_without_system_or_event_admin_role(self): - self.seed_static_data() - params = {'event_id': 1, 'reviewer_user_email': 'r2@r.com', 'num_reviews': 10} - header = self.get_auth_header_for('c1@c.com') - - response = self.app.post('/api/v1/reviewassignment', headers=header, data=params) - - self.assertEqual(response.status_code, FORBIDDEN[1]) - - def test_reviewer_not_found(self): - self.seed_static_data() - params = {'event_id': 1, 'reviewer_user_email': 'non_existent@user.com', 'num_reviews': 10} - header = self.get_auth_header_for('sa@sa.com') - - response = self.app.post('/api/v1/reviewassignment', headers=header, data=params) - - self.assertEqual(response.status_code, USER_NOT_FOUND[1]) - - def test_add_reviewer_with_no_roles(self): - self.seed_static_data() - params = {'event_id': 1, 'reviewer_user_email': 'r1@r.com', 'num_reviews': 10} - header = self.get_auth_header_for('ea@ea.com') - - response = self.app.post('/api/v1/reviewassignment', headers=header, data=params) - - event_roles = db.session.query(EventRole).filter_by(user_id=1, event_id=1).all() - self.assertEqual(len(event_roles), 1) - self.assertEqual(event_roles[0].role, 'reviewer') - - def test_add_reviewer_with_a_role(self): - self.seed_static_data() - params = {'event_id': 1, 'reviewer_user_email': 'ea@ea.com', 'num_reviews': 10} - header = self.get_auth_header_for('sa@sa.com') - - response = self.app.post('/api/v1/reviewassignment', headers=header, data=params) - - event_roles = db.session.query(EventRole).filter_by(user_id=10, event_id=1).order_by(EventRole.id).all() - self.assertEqual(len(event_roles), 2) - self.assertEqual(event_roles[0].role, 'admin') - self.assertEqual(event_roles[1].role, 'reviewer') - - def setup_responses_without_reviewers(self): - self.add_response(1, 5, is_submitted=True) - self.add_response(1, 6, is_submitted=True) - self.add_response(1, 7, is_submitted=True) - self.add_response(1, 8, is_submitted=True) - - def test_adding_first_reviewer(self): - self.seed_static_data() - self.setup_responses_without_reviewers() - params = {'event_id': 1, 'reviewer_user_email': 'r3@r.com', 'num_reviews': 4} - header = self.get_auth_header_for('ea@ea.com') - - response = self.app.post('/api/v1/reviewassignment', headers=header, data=params) - response_reviewers = db.session.query(ResponseReviewer).filter_by(reviewer_user_id=3).all() - self.assertEqual(response.status_code, 201) - self.assertEqual(len(response_reviewers), 4) - - def test_limit_of_num_reviews(self): - self.seed_static_data() - self.setup_responses_without_reviewers() - params = {'event_id': 1, 'reviewer_user_email': 'r3@r.com', 'num_reviews': 3} - header = self.get_auth_header_for('ea@ea.com') - - response = self.app.post('/api/v1/reviewassignment', headers=header, data=params) - - response_reviewers = db.session.query(ResponseReviewer).filter_by(reviewer_user_id=3).all() - self.assertEqual(len(response_reviewers), 3) - - def setup_reviewer_with_own_response(self): - self.add_response(1, 3, is_submitted=True) # reviewer - self.add_response(1, 5, is_submitted=True) # someone else - - def test_reviewer_does_not_get_assigned_to_own_response(self): - self.seed_static_data() - self.setup_reviewer_with_own_response() - params = {'event_id': 1, 'reviewer_user_email': 'r3@r.com', 'num_reviews': 3} - header = self.get_auth_header_for('ea@ea.com') - - response = self.app.post('/api/v1/reviewassignment', headers=header, data=params) - - response_reviewers = db.session.query(ResponseReviewer).filter_by(reviewer_user_id=3).all() - self.assertEqual(len(response_reviewers), 1) - self.assertEqual(response_reviewers[0].response_id, 2) - - def setup_withdrawn_and_unsubmitted_responses(self): - self.add_response(1, 5) - self.add_response(1, 6, is_withdrawn=True) - self.add_response(1, 7, is_submitted=True) - - def test_withdrawn_and_unsubmitted_responses_are_not_assigned_reviewers(self): - self.seed_static_data() - self.setup_withdrawn_and_unsubmitted_responses() - params = {'event_id': 1, 'reviewer_user_email': 'r3@r.com', 'num_reviews': 3} - header = self.get_auth_header_for('ea@ea.com') - - response = self.app.post('/api/v1/reviewassignment', headers=header, data=params) - - response_reviewers = db.session.query(ResponseReviewer).filter_by(reviewer_user_id=3).all() - self.assertEqual(len(response_reviewers), 1) - self.assertEqual(response_reviewers[0].response_id, 3) - - def setup_response_with_three_reviewers(self): - response = self.add_response(1, 5, is_submitted=True) - - response_reviewers = [ - ResponseReviewer(1, 1), - ResponseReviewer(1, 2), - ResponseReviewer(1, 4) - ] - db.session.add_all(response_reviewers) - db.session.commit() - - def test_response_with_three_reviewers_does_not_get_assigned_another_reviewer(self): - self.seed_static_data() - self.setup_response_with_three_reviewers() - params = {'event_id': 1, 'reviewer_user_email': 'r3@r.com', 'num_reviews': 3} - header = self.get_auth_header_for('ea@ea.com') - - response = self.app.post('/api/v1/reviewassignment', headers=header, data=params) - - response_reviewers = db.session.query(ResponseReviewer).filter_by(reviewer_user_id=3).all() - self.assertEqual(len(response_reviewers), 0) - - def setup_responsereview_with_different_reviewer(self): - self.add_response(1, 5, is_submitted=True) - - response_reviewer = ResponseReviewer(1, 1) - db.session.add(response_reviewer) - db.session.commit() - - def test_response_will_get_multiple_reviewers_assigned(self): - self.seed_static_data() - self.setup_responsereview_with_different_reviewer() - params = {'event_id': 1, 'reviewer_user_email': 'r3@r.com', 'num_reviews': 3} - header = self.get_auth_header_for('ea@ea.com') - - response = self.app.post('/api/v1/reviewassignment', headers=header, data=params) - response_reviewers = db.session.query(ResponseReviewer).order_by(ResponseReviewer.reviewer_user_id).all() - - self.assertEqual(len(response_reviewers), 2) - self.assertEqual(response_reviewers[0].reviewer_user_id, 1) - self.assertEqual(response_reviewers[1].reviewer_user_id, 3) - - def setup_reviewer_is_not_assigned_to_response_more_than_once(self): - self.add_response(1, 5, is_submitted=True) - - def setup_count_reviews_allocated_and_completed(self): - db.session.add_all([ - EventRole('reviewer', 1, 1), - EventRole('reviewer', 2, 1), - EventRole('reviewer', 3, 1), - EventRole('reviewer', 4, 1) - ]) - - self.add_response(1, 5, is_submitted=True) #1 - self.add_response(1, 6, is_submitted=True) #2 - self.add_response(1, 7, is_submitted=True) #3 - self.add_response(1, 8, is_submitted=True) #4 - self.add_response(2, 5, is_submitted=True) #5 - self.add_response(2, 6, is_submitted=True) #6 - - response_reviewers = [ - ResponseReviewer(1, 2), - ResponseReviewer(2, 2), - ResponseReviewer(3, 2), - ResponseReviewer(4, 2), - ResponseReviewer(6, 2), - - ResponseReviewer(2, 3), - ResponseReviewer(4, 3), - - ResponseReviewer(3, 4), - - ResponseReviewer(5, 1), - - ] - - - db.session.add_all(response_reviewers) - # review form, review_user_id, response_id - review_responses = [ - ReviewResponse(1, 3, 2, 'en'), - ReviewResponse(1, 3, 4, 'en'), - ReviewResponse(1, 2, 1, 'en'), - ReviewResponse(1, 2, 3, 'en'), - ReviewResponse(1, 2, 4, 'en'), - ReviewResponse(2, 1, 5, 'en'), - ReviewResponse(2, 2, 6, 'en') - ] - db.session.add_all(review_responses) - - db.session.commit() - - # response 1 - 1 review assigned - 1 complete - # response 2 - 2 reviews - 1 complete - # response 3 - 2 reviews - 1 complete - # response 4 - 2 reviews - 1 complete - # response 5 - 1 review - 1 complete - # response 6 - 1 review - 1 complete - - # reviewer 1 - 1 review assigned (1 from event 2) - 1 complete - # reviewer 2 - 5 reviews assigned (1 from event 2)- 3 complete - # reviewer 3 - 2 reviews assigned - 2 complete - # reviewer 4 - 1 review assigned - none complete - - # total assigned reviews: 9 - # total required review = 6*3 = 18 - # total unallocated: 18 - 9 = 9 - # total completed reviews: 6 - - @SkipTest - def test_count_reviews_allocated_and_completed(self): - self.seed_static_data() - self.setup_count_reviews_allocated_and_completed() - header = self.get_auth_header_for('ea@ea.com') - params = {'event_id': 1} - - response = self.app.get('/api/v1/reviewassignment', headers=header, data=params) - - data = json.loads(response.data) - data = sorted(data, key=lambda k: k['email']) - LOGGER.debug(data) - self.assertEqual(len(data),3) - self.assertEqual(data[0]['email'], 'r2@r.com') - self.assertEqual(data[0]['reviews_allocated'], 4) - self.assertEqual(data[0]['reviews_completed'], 3) - self.assertEqual(data[1]['email'], 'r3@r.com') - self.assertEqual(data[1]['reviews_allocated'], 2) - self.assertEqual(data[1]['reviews_completed'], 2) - self.assertEqual(data[2]['email'], 'r4@r.com') - self.assertEqual(data[2]['reviews_allocated'], 1) - self.assertEqual(data[2]['reviews_completed'], 0) - - def test_reviewer_is_not_assigned_to_response_more_than_once(self): - self.seed_static_data() - self.setup_reviewer_is_not_assigned_to_response_more_than_once() - params = {'event_id': 1, 'reviewer_user_email': 'r3@r.com', 'num_reviews': 3} - header = self.get_auth_header_for('ea@ea.com') - - response = self.app.post('/api/v1/reviewassignment', headers=header, data=params) - response2 = self.app.post('/api/v1/reviewassignment', headers=header, data=params) - response_reviewers = db.session.query(ResponseReviewer).all() - - self.assertEqual(len(response_reviewers), 1) - - def setup_reviewer_responses_finalverdict_reviewquestion_reviewresponses_and_scores(self): - second_reviewer = EventRole('reviewer', 2, 1) - db.session.add(second_reviewer) - db.session.commit() - - users_id = [5,6,7] - self.add_response(application_form_id=1, user_id=users_id[0], is_submitted=True) - self.add_response(application_form_id=1, user_id=users_id[1], is_submitted=True) - self.add_response(application_form_id=1, user_id=users_id[2], is_submitted=True) - - final_verdict_options = [ - {'label': 'Yes', 'value': 2}, - {'label': 'No', 'value': 0}, - {'label': 'Maybe', 'value': 1}, - ] - verdict_question = ReviewQuestion(1, None, 'multi-choice', True, 3, 0) - db.session.add(verdict_question) - db.session.commit() - verdict_question_translation = ReviewQuestionTranslation(verdict_question.id, 'en', headline='Final Verdict', options=final_verdict_options) - db.session.add(verdict_question_translation) - db.session.commit() - - review_responses = [ - ReviewResponse(1,3,1, 'en'), - ReviewResponse(1,3,2, 'en'), - ReviewResponse(1,2,1, 'en'), - ReviewResponse(1,2,2, 'en'), - ReviewResponse(1,3,3, 'en') - ] - review_responses[0].review_scores = [ReviewScore(1, '23'), ReviewScore(5, '1')] - review_responses[1].review_scores = [ReviewScore(1, '55'), ReviewScore(5, '2')] - review_responses[2].review_scores = [ReviewScore(1, '45'), ReviewScore(2, '67'), ReviewScore(5, 'No')] - review_responses[3].review_scores = [ReviewScore(1, '220'), ReviewScore(5, '2')] - review_responses[4].review_scores = [ReviewScore(1, '221'), ReviewScore(5, '1')] - db.session.add_all(review_responses) - db.session.commit() - - return users_id - - - def test_review_history_returned(self): - self.seed_static_data() - users_id = self.setup_reviewer_responses_finalverdict_reviewquestion_reviewresponses_and_scores() - - params ={'event_id' : 1, 'page_number' : 0, 'limit' : 10, 'sort_column' : 'review_response_id'} - header = self.get_auth_header_for('r3@r.com') - - response = self.app.get('/api/v1/reviewhistory', headers=header, data=params) - data = json.loads(response.data) - - self.assertEqual(len(data['reviews']), 3) - self.assertEqual(data['num_entries'], 3) - - self.assertEqual(data['reviews'][0]['review_response_id'], 1) - self.assertEqual(data['reviews'][0]['reviewed_user_id'], str(users_id[0])) - - self.assertEqual(data['reviews'][1]['review_response_id'], 2) - self.assertEqual(data['reviews'][1]['reviewed_user_id'], str(users_id[1])) - - self.assertEqual(data['reviews'][2]['review_response_id'], 5) - self.assertEqual(data['reviews'][2]['reviewed_user_id'], str(users_id[2])) - - def test_brings_back_only_logged_in_reviewer_reviewresponses(self): - self.seed_static_data() - self.setup_reviewer_responses_finalverdict_reviewquestion_reviewresponses_and_scores() - - params ={'event_id' : 1, 'page_number' : 0, 'limit' : 10, 'sort_column' : 'review_response_id'} - header = self.get_auth_header_for('r2@r.com') - - response = self.app.get('/api/v1/reviewhistory', headers=header, data=params) - data = json.loads(response.data) - - self.assertEqual(len(data['reviews']), 2) - self.assertEqual(data['reviews'][0]['review_response_id'], 3) - self.assertEqual(data['reviews'][1]['review_response_id'], 4) - - def test_logged_in_user_not_reviewer(self): - self.seed_static_data() - self.setup_reviewer_responses_finalverdict_reviewquestion_reviewresponses_and_scores() - - params ={'event_id' : 1, 'page_number' : 0, 'limit' : 10, 'sort_column' : 'review_response_id'} - header = self.get_auth_header_for('c1@c.com') - - response = self.app.get('/api/v1/reviewhistory', headers=header, data=params) - - self.assertEqual(response.status_code, FORBIDDEN[1]) - - def setup_reviewer_with_no_reviewresponses(self): - reviewer = EventRole('reviewer', 1, 1) - db.session.add(reviewer) - db.session.commit() - - def test_reviewer_with_no_reviewresponses(self): - self.seed_static_data() - self.setup_reviewer_responses_finalverdict_reviewquestion_reviewresponses_and_scores() - self.setup_reviewer_with_no_reviewresponses() - - params ={'event_id' : 1, 'page_number' : 0, 'limit' : 10, 'sort_column' : 'review_response_id'} - header = self.get_auth_header_for('r1@r.com') - - response = self.app.get('/api/v1/reviewhistory', headers=header, data=params) - data = json.loads(response.data) - - self.assertEqual(data['num_entries'], 0) - self.assertEqual(data['reviews'], []) - - def test_order_by_reviewresponseid(self): - self.seed_static_data() - self.setup_reviewer_responses_finalverdict_reviewquestion_reviewresponses_and_scores() - - params ={'event_id' : 1, 'page_number' : 0, 'limit' : 10, 'sort_column' : 'review_response_id'} - header = self.get_auth_header_for('r3@r.com') - - response = self.app.get('/api/v1/reviewhistory', headers=header, data=params) - data = json.loads(response.data) - - self.assertEqual(data['reviews'][0]['review_response_id'], 1) - self.assertEqual(data['reviews'][1]['review_response_id'], 2) - self.assertEqual(data['reviews'][2]['review_response_id'], 5) - - def setup_reviewresponses_with_unordered_timestamps(self): - final_verdict_options = [ - {'label': 'Yes', 'value': 2}, - {'label': 'No', 'value': 0}, - {'label': 'Maybe', 'value': 1}, - ] - - verdict_question = ReviewQuestion(1, None, 'multi-choice', True, 3, 0) - db.session.add(verdict_question) - db.session.commit() - verdict_question_translation = ReviewQuestionTranslation(verdict_question.id, 'en', headline='Final Verdict', options=final_verdict_options) - db.session.add(verdict_question_translation) - db.session.commit() - - self.add_response(1, 5, is_submitted=True) - self.add_response(1, 6, is_submitted=True) - self.add_response(1, 7, is_submitted=True) - - review_response_1 = ReviewResponse(1,3,1, 'en') - review_response_2 = ReviewResponse(1,3,2, 'en') - review_response_3 = ReviewResponse(1,3,3, 'en') - review_response_1.submitted_timestamp = datetime(2019, 1, 1) - review_response_2.submitted_timestamp = datetime(2018, 1, 1) - review_response_3.submitted_timestamp = datetime(2018, 6, 6) - review_responses = [review_response_1, review_response_2, review_response_3] - review_responses[0].review_scores = [ReviewScore(1, '67'), ReviewScore(5, 'Yes')] - review_responses[1].review_scores = [ReviewScore(1, '23'), ReviewScore(5, 'Yes')] - review_responses[2].review_scores = [ReviewScore(1, '53'), ReviewScore(5, 'Yes')] - db.session.add_all(review_responses) - db.session.commit() - - def test_order_by_submittedtimestamp(self): - self.seed_static_data() - self.setup_reviewresponses_with_unordered_timestamps() - - params ={'event_id' : 1, 'page_number' : 0, 'limit' : 10, 'sort_column' : 'submitted_timestamp'} - header = self.get_auth_header_for('r3@r.com') - - response = self.app.get('/api/v1/reviewhistory', headers=header, data=params) - data = json.loads(response.data) - LOGGER.debug(data) - self.assertEqual(data['reviews'][0]['submitted_timestamp'], '2018-01-01T00:00:00') - self.assertEqual(data['reviews'][1]['submitted_timestamp'], '2018-06-06T00:00:00') - self.assertEqual(data['reviews'][2]['submitted_timestamp'], '2019-01-01T00:00:00') - - # TODO re-add these tests once we can get the info outside of AppUser - @SkipTest - def test_order_by_nationalitycountry(self): - self.seed_static_data() - self.setup_reviewer_responses_finalverdict_reviewquestion_reviewresponses_and_scores() - - params ={'event_id' : 1, 'page_number' : 0, 'limit' : 10, 'sort_column' : 'nationality_country'} - header = self.get_auth_header_for('r3@r.com') - - response = self.app.get('/api/v1/reviewhistory', headers=header, data=params) - data = json.loads(response.data) - - self.assertEqual(data['reviews'][0]['nationality_country'], 'Botswana') - self.assertEqual(data['reviews'][1]['nationality_country'], 'South Africa') - self.assertEqual(data['reviews'][2]['nationality_country'], 'Zimbabwe') - - # TODO re-add these tests once we can get the info outside of AppUser - @SkipTest - def test_order_by_residencecountry(self): - self.seed_static_data() - self.setup_reviewer_responses_finalverdict_reviewquestion_reviewresponses_and_scores() - - params ={'event_id' : 1, 'page_number' : 0, 'limit' : 10, 'sort_column' : 'residence_country'} - header = self.get_auth_header_for('r3@r.com') - - response = self.app.get('/api/v1/reviewhistory', headers=header, data=params) - data = json.loads(response.data) - - self.assertEqual(data['reviews'][0]['residence_country'], 'Egypt') - self.assertEqual(data['reviews'][1]['residence_country'], 'Mozambique') - self.assertEqual(data['reviews'][2]['residence_country'], 'Namibia') - - # TODO re-add these tests once we can get the info outside of AppUser - @SkipTest - def test_order_by_affiliation(self): - self.seed_static_data() - self.setup_reviewer_responses_finalverdict_reviewquestion_reviewresponses_and_scores() - - params ={'event_id' : 1, 'page_number' : 0, 'limit' : 10, 'sort_column' : 'affiliation'} - header = self.get_auth_header_for('r3@r.com') - - response = self.app.get('/api/v1/reviewhistory', headers=header, data=params) - data = json.loads(response.data) - - self.assertEqual(data['reviews'][0]['affiliation'], 'RU') - self.assertEqual(data['reviews'][1]['affiliation'], 'UFH') - self.assertEqual(data['reviews'][2]['affiliation'], 'UWC') - - # TODO re-add these tests once we can get the info outside of AppUser - @SkipTest - def test_order_by_department(self): - self.seed_static_data() - self.setup_reviewer_responses_finalverdict_reviewquestion_reviewresponses_and_scores() - - params ={'event_id' : 1, 'page_number' : 0, 'limit' : 10, 'sort_column' : 'department'} - header = self.get_auth_header_for('r3@r.com') - - response = self.app.get('/api/v1/reviewhistory', headers=header, data=params) - data = json.loads(response.data) - - self.assertEqual(data['reviews'][0]['department'], 'CS') # ascii ordering orders capital letters before lowercase - self.assertEqual(data['reviews'][1]['department'], 'Chem') - self.assertEqual(data['reviews'][2]['department'], 'Phys') - - @SkipTest - def test_order_by_usercategory(self): - self.seed_static_data() - self.setup_reviewer_responses_finalverdict_reviewquestion_reviewresponses_and_scores() - - params ={'event_id' : 1, 'page_number' : 0, 'limit' : 10, 'sort_column' : 'user_category'} - header = self.get_auth_header_for('r3@r.com') - - response = self.app.get('/api/v1/reviewhistory', headers=header, data=params) - data = json.loads(response.data) - - self.assertEqual(data['reviews'][0]['user_category'], 'Honours') - self.assertEqual(data['reviews'][1]['user_category'], 'MSc') - self.assertEqual(data['reviews'][2]['user_category'], 'Student') - - def setup_two_extra_responses_for_reviewer3(self): - self.add_response(1, 8, is_submitted=True) - self.add_response(1, 1, is_submitted=True) - - review_responses = [ - ReviewResponse(1,3,4, 'en'), - ReviewResponse(1,3,5, 'en') - ] - review_responses[0].review_scores = [ReviewScore(1, '89'), ReviewScore(5, 'Maybe')] - review_responses[1].review_scores = [ReviewScore(1, '75'), ReviewScore(5, 'Yes')] - db.session.add_all(review_responses) - db.session.commit() - - def test_first_page_in_pagination(self): - self.seed_static_data() - self.setup_reviewer_responses_finalverdict_reviewquestion_reviewresponses_and_scores() - self.setup_two_extra_responses_for_reviewer3() - - params ={'event_id' : 1, 'page_number' : 0, 'limit' : 2, 'sort_column' : 'review_response_id'} - header = self.get_auth_header_for('r3@r.com') - - response = self.app.get('/api/v1/reviewhistory', headers=header, data=params) - data = json.loads(response.data) - - self.assertEqual(len(data['reviews']), 2) - self.assertEqual(data['num_entries'], 5) - - self.assertEqual(data['reviews'][0]['review_response_id'], 1) - self.assertEqual(data['reviews'][1]['review_response_id'], 2) - - def test_middle_page_in_pagination(self): - self.seed_static_data() - self.setup_reviewer_responses_finalverdict_reviewquestion_reviewresponses_and_scores() - self.setup_two_extra_responses_for_reviewer3() - - params ={'event_id' : 1, 'page_number' : 1, 'limit' : 2, 'sort_column' : 'review_response_id'} - header = self.get_auth_header_for('r3@r.com') - - response = self.app.get('/api/v1/reviewhistory', headers=header, data=params) - data = json.loads(response.data) - - self.assertEqual(len(data['reviews']), 2) - self.assertEqual(data['num_entries'], 5) - - self.assertEqual(data['reviews'][0]['review_response_id'], 5) - self.assertEqual(data['reviews'][1]['review_response_id'], 6) - - def test_last_page_in_pagination(self): - self.seed_static_data() - self.setup_reviewer_responses_finalverdict_reviewquestion_reviewresponses_and_scores() - self.setup_two_extra_responses_for_reviewer3() - - params ={'event_id' : 1, 'page_number' : 2, 'limit' : 2, 'sort_column' : 'review_response_id'} - header = self.get_auth_header_for('r3@r.com') - - response = self.app.get('/api/v1/reviewhistory', headers=header, data=params) - data = json.loads(response.data) - - self.assertEqual(len(data['reviews']), 1) - self.assertEqual(data['num_entries'], 5) - self.assertEqual(data['reviews'][0]['review_response_id'], 7) - - def test_total_number_of_pages_greater_than_zero(self): - self.seed_static_data() - self.setup_reviewer_responses_finalverdict_reviewquestion_reviewresponses_and_scores() - self.setup_two_extra_responses_for_reviewer3() - - params ={'event_id' : 1, 'page_number' : 2, 'limit' : 2, 'sort_column' : 'review_response_id'} - header = self.get_auth_header_for('r3@r.com') - - response = self.app.get('/api/v1/reviewhistory', headers=header, data=params) - data = json.loads(response.data) - - self.assertEqual(data['total_pages'], 3) - - def test_total_number_of_pages_when_zero(self): - self.seed_static_data() - self.setup_reviewer_with_no_reviewresponses() - - params ={'event_id' : 1, 'page_number' : 2, 'limit' : 2, 'sort_column' : 'review_response_id'} - header = self.get_auth_header_for('r1@r.com') - - response = self.app.get('/api/v1/reviewhistory', headers=header, data=params) - data = json.loads(response.data) - - self.assertEqual(data['total_pages'], 0) - - - def test_review_form_language(self): - """Test that the review form questions are returned in the correct language.""" - self.seed_static_data() - params ={'event_id' : 1, 'language': 'en'} - header = self.get_auth_header_for('r1@r.com') - - response = self.app.get('/api/v1/review', headers=header, data=params) - data = json.loads(response.data) - - print(data['review_form']['review_sections']) - - self.assertEqual(data['review_form']['review_sections'][0]['headline'], 'Review Section 1 English') - self.assertEqual(data['review_form']['review_sections'][0]['description'], 'Review Section 1 Description English') - - self.assertEqual(len(data['review_form']['review_sections'][0]['review_questions']), 2) - - self.assertEqual(data['review_form']['review_sections'][0]['review_questions'][1]['description'], 'English Description') - self.assertEqual(data['review_form']['review_sections'][0]['review_questions'][1]['headline'], 'English Headline') - self.assertEqual(data['review_form']['review_sections'][0]['review_questions'][1]['placeholder'], 'English Placeholder') - self.assertDictEqual(data['review_form']['review_sections'][0]['review_questions'][1]['options'][0], {'label': 'en1', 'value': 'en'}) - self.assertEqual(data['review_form']['review_sections'][0]['review_questions'][1]['validation_regex'], 'EN Regex') - self.assertEqual(data['review_form']['review_sections'][0]['review_questions'][1]['validation_text'], 'EN Validation Message') - - params ={'event_id' : 1, 'language': 'fr'} - - response = self.app.get('/api/v1/review', headers=header, data=params) - data = json.loads(response.data) - - self.assertEqual(data['review_form']['review_sections'][0]['headline'], 'Review Section 1 French') - self.assertEqual(data['review_form']['review_sections'][0]['description'], 'Review Section 1 Description French') - - self.assertEqual(len(data['review_form']['review_sections'][0]['review_questions']), 2) - - self.assertEqual(data['review_form']['review_sections'][0]['review_questions'][1]['description'], 'French Description') - self.assertEqual(data['review_form']['review_sections'][0]['review_questions'][1]['headline'], 'French Headline') - self.assertEqual(data['review_form']['review_sections'][0]['review_questions'][1]['placeholder'], 'French Placeholder') - self.assertDictEqual(data['review_form']['review_sections'][0]['review_questions'][1]['options'][0], {'label': 'fr1', 'value': 'fr'}) - self.assertEqual(data['review_form']['review_sections'][0]['review_questions'][1]['validation_regex'], 'FR Regex') - self.assertEqual(data['review_form']['review_sections'][0]['review_questions'][1]['validation_text'], 'FR Validation Message') - - -class ReviewListAPITest(ApiTestCase): - - def seed_static_data(self): - self.event1 = self.add_event(key='event1') - self.event2 = self.add_event(key='event2') - - self.reviewer1 = self.add_user('reviewer1@mail.com') - self.reviewer2 = self.add_user('reviewer2@mail.com') - self.event1.add_event_role('reviewer', self.reviewer1.id) - self.event2.add_event_role('reviewer', self.reviewer1.id) - self.event1.add_event_role('reviewer', self.reviewer2.id) - self.event2.add_event_role('reviewer', self.reviewer2.id) - - self.user1 = self.add_user('user1@mail.com') - user2 = self.add_user('user2@mail.com') - user3 = self.add_user('user3@mail.com') - user4 = self.add_user('user4@mail.com') - - application_form1 = self.create_application_form(self.event1.id) - section1 = self.add_section(application_form1.id) - question1 = self.add_question(application_form1.id, section1.id) - question1.key = 'review-identifier' - self.add_question_translation(question1.id, 'en', 'Headline 1 EN') - self.add_question_translation(question1.id, 'fr', 'Headline 1 FR') - question2 = self.add_question(application_form1.id, section1.id) - question2.key = 'review-identifier' - self.add_question_translation(question2.id, 'en', 'Headline 2 EN') - self.add_question_translation(question2.id, 'fr', 'Headline 2 FR') - question3 = self.add_question(application_form1.id, section1.id) - self.add_question_translation(question3.id, 'en', 'Headline 3 EN') - self.add_question_translation(question3.id, 'fr', 'Headline 3 FR') - - review_form1 = self.add_review_form(application_form1.id) - - review_form1_section1 = self.add_review_section(review_form1.id) - self.add_review_section_translation(review_form1_section1.id, 'en', 'Review Section 1 en', 'Review Section 1 en') - self.add_review_section_translation(review_form1_section1.id, 'fr', 'Review Section 1 fr', 'Review Section 1 fr') - review_form1_section2 = self.add_review_section(review_form1.id) - self.add_review_section_translation(review_form1_section2.id, 'en', 'Review Section 2 en', 'Review Section 2 en') - self.add_review_section_translation(review_form1_section2.id, 'fr', 'Review Section 2 fr', 'Review Section 2 fr') - - review_form1_section2 = self.add_review_section(review_form1.id) - review_q1 = self.add_review_question(review_form1_section1.id, weight=1) - review_q1_translation_en = self.add_review_question_translation(review_q1.id, 'en', headline='Heading En') - review_q1_translation_fr = self.add_review_question_translation(review_q1.id, 'fr', headline='Heading Fr') - review_q2 = self.add_review_question(review_form1_section1.id, weight=0) - review_q2_translation_en = self.add_review_question_translation(review_q2.id, 'en', headline='Heading En') - review_q2_translation_fr = self.add_review_question_translation(review_q2.id, 'fr', headline='Heading Fr') - review_q3 = self.add_review_question(review_form1_section2.id, weight=1) - review_q3_translation_en = self.add_review_question_translation(review_q3.id, 'en', headline='Heading En') - review_q3_translation_fr = self.add_review_question_translation(review_q3.id, 'fr', headline='Heading Fr') - - application_form2 = self.create_application_form(self.event2.id) - review_form2 = self.add_review_form(application_form2.id) - - event1_response1 = self.add_response(application_form1.id, self.user1.id, is_submitted=True) - self.add_answer(event1_response1.id, question1.id, 'First answer') - self.add_answer(event1_response1.id, question2.id, 'Second answer') - self.add_answer(event1_response1.id, question3.id, 'Third answer') - - event1_response2 = self.add_response(application_form1.id, user2.id, is_submitted=True, language='fr') - self.add_answer(event1_response2.id, question1.id, 'Forth answer') - self.add_answer(event1_response2.id, question2.id, 'Fifth answer') - self.add_answer(event1_response2.id, question3.id, 'Sixth answer') - - event1_response3 = self.add_response(application_form1.id, user3.id, is_submitted=True) - self.add_answer(event1_response3.id, question1.id, 'Seventh answer') - self.add_answer(event1_response3.id, question2.id, 'Eigth answer') - self.add_answer(event1_response3.id, question3.id, 'Ninth answer') - - event1_response4 = self.add_response(application_form1.id, user4.id, is_submitted=True, language='zu') - self.add_answer(event1_response4.id, question1.id, 'Tenth answer') - self.add_answer(event1_response4.id, question2.id, 'Eleventh answer') - self.add_answer(event1_response4.id, question3.id, 'Twelfth answer') - - event2_response1 = self.add_response(application_form2.id, self.user1.id, is_submitted=True) - event2_response2 = self.add_response(application_form2.id, user2.id, is_submitted=True, language='fr') - event2_response3 = self.add_response(application_form2.id, user3.id, is_submitted=True) - - # Review 1 completed review - self.add_response_reviewer(event1_response1.id, self.reviewer1.id) - review_response1 = self.add_review_response(self.reviewer1.id, event1_response1.id, review_form1.id) - review_response1.submit() - self.add_review_score(review_response1.id, review_q1.id, 10.5) - self.add_review_score(review_response1.id, review_q2.id, 100) - self.add_review_score(review_response1.id, review_q3.id, 'Hello world') - - # Reviewer 1 incomplete review - self.review_response1_submitted = review_response1.submitted_timestamp.isoformat() - self.add_response_reviewer(event1_response2.id, self.reviewer1.id) - review_response2 = self.add_review_response(self.reviewer1.id, event1_response2.id, review_form1.id) - self.add_review_score(review_response2.id, review_q1.id, 13) - - # Reviewer 1 not started review - self.add_response_reviewer(event1_response3.id, self.reviewer1.id) - - # Confounders - self.add_response_reviewer(event1_response2.id, self.reviewer2.id) - self.add_response_reviewer(event1_response3.id, self.reviewer2.id) - self.add_response_reviewer(event2_response1.id, self.reviewer1.id) - self.add_response_reviewer(event2_response2.id, self.reviewer2.id) - - def test_review_list(self): - self.seed_static_data() - params ={'event_id' : 1, 'language': 'en'} - - response = self.app.get( - '/api/v1/reviewlist', - headers=self.get_auth_header_for('reviewer1@mail.com'), - data=params) - - data = json.loads(response.data) - - self.assertEqual(len(data), 3) - - self.assertEqual(data[0]['response_id'], 1) - self.assertEqual(data[0]['language'], 'en') - self.assertEqual(len(data[0]['information']), 2) - self.assertEqual(data[0]['information'][0]['headline'], 'Headline 1 EN') - self.assertEqual(data[0]['information'][0]['value'], 'First answer') - self.assertEqual(data[0]['information'][1]['headline'], 'Headline 2 EN') - self.assertEqual(data[0]['information'][1]['value'], 'Second answer') - self.assertTrue(data[0]['started']) - self.assertEqual(data[0]['submitted'], self.review_response1_submitted) - self.assertEqual(data[0]['total_score'], 10.5) - - self.assertEqual(data[1]['response_id'], 2) - self.assertEqual(data[1]['language'], 'fr') - self.assertEqual(len(data[1]['information']), 2) - self.assertEqual(data[1]['information'][0]['headline'], 'Headline 1 EN') - self.assertEqual(data[1]['information'][0]['value'], 'Forth answer') - self.assertEqual(data[1]['information'][1]['headline'], 'Headline 2 EN') - self.assertEqual(data[1]['information'][1]['value'], 'Fifth answer') - self.assertTrue(data[1]['started']) - self.assertIsNone(data[1]['submitted']) - self.assertEqual(data[1]['total_score'], 13) - - self.assertEqual(data[2]['response_id'], 3) - self.assertEqual(data[2]['language'], 'en') - self.assertEqual(len(data[2]['information']), 2) - self.assertEqual(data[2]['information'][0]['headline'], 'Headline 1 EN') - self.assertEqual(data[2]['information'][0]['value'], 'Seventh answer') - self.assertEqual(data[2]['information'][1]['headline'], 'Headline 2 EN') - self.assertEqual(data[2]['information'][1]['value'], 'Eigth answer') - self.assertFalse(data[2]['started']) - self.assertIsNone(data[2]['submitted']) - self.assertEqual(data[2]['total_score'], 0.0) - -class ResponseReviewerAssignmentApiTest(ApiTestCase): - def seed_static_data(self): - self.event = self.add_event(key='event1') - self.event2 = self.add_event(key='event2') - self.event_admin = self.add_user('eventadmin@mail.com') - self.reviewer = self.add_user('reviewer@mail.com') - self.reviewer_user_id = self.reviewer.id - - self.user1 = self.add_user('user1@mail.com') - self.user2 = self.add_user('user2@mail.com') - self.user3 = self.add_user('user3@mail.com') - - self.event.add_event_role('admin', self.event_admin.id) - - application_form = self.create_application_form(self.event.id) - application_form2 = self.create_application_form(self.event2.id) - self.response1 = self.add_response(application_form.id, self.user1.id, is_submitted=True) - self.response2 = self.add_response(application_form.id, self.user2.id, is_submitted=True) - self.response3 = self.add_response(application_form.id, self.user3.id, is_submitted=True) - - self.add_review_form(application_form.id) - self.add_review_form(application_form2.id) - - self.event2_response_id = self.add_response(application_form2.id, self.user1.id, is_submitted=True).id - - self.add_email_template('reviews-assigned') - - def test_responses_assigned(self): - self.seed_static_data() - - params = {'event_id' : 1, 'response_ids': [1, 2], 'reviewer_email': 'reviewer@mail.com'} - - response = self.app.post( - '/api/v1/assignresponsereviewer', - headers=self.get_auth_header_for('eventadmin@mail.com'), - data=params) - - self.assertEqual(response.status_code, 201) - - response_reviewers = (db.session.query(ResponseReviewer) - .join(Response, ResponseReviewer.response_id == Response.id) - .filter_by(application_form_id=1).all()) - - self.assertEqual(len(response_reviewers), 2) - - for rr in response_reviewers: - self.assertEqual(rr.reviewer_user_id, self.reviewer_user_id) - - def test_response_for_different_event_forbidden(self): - self.seed_static_data() - - params = {'event_id' : 1, 'response_ids': [1, 2, self.event2_response_id], 'reviewer_email': 'reviewer@mail.com'} - - response = self.app.post( - '/api/v1/assignresponsereviewer', - headers=self.get_auth_header_for('eventadmin@mail.com'), - data=params) - - self.assertEqual(response.status_code, 403) - - def test_delete(self): - """Test that a review assignment can be deleted.""" - self.seed_static_data() - - # Assign a reviewer - response_id = self.response1.id - self.add_response_reviewer(response_id, self.reviewer_user_id) - - params = {'event_id' : 1, 'response_id': self.response1.id, 'reviewer_user_id': self.reviewer_user_id} - - response = self.app.delete( - '/api/v1/assignresponsereviewer', - headers=self.get_auth_header_for('eventadmin@mail.com'), - data=params) - - self.assertEqual(response.status_code, 200) - - # Check that it was actually deleted - response_reviewer = db.session.query(ResponseReviewer).filter_by(response_id=response_id, reviewer_user_id=self.reviewer_user_id).first() - self.assertIsNone(response_reviewer) - - def test_delete_not_allowed_if_completed(self): - """Test that a review assignment can't be deleted if the review has been completed.""" - self.seed_static_data() - - # Assign a reviewer and create a review resposne - response_id = self.response1.id - self.add_response_reviewer(response_id, self.reviewer_user_id) - self.add_review_response(self.reviewer_user_id, response_id) - - params = {'event_id' : 1, 'response_id': self.response1.id, 'reviewer_user_id': self.reviewer_user_id} - - response = self.app.delete( - '/api/v1/assignresponsereviewer', - headers=self.get_auth_header_for('eventadmin@mail.com'), - data=params) - - self.assertEqual(response.status_code, 400) - - -class ReferenceReviewRequest(ApiTestCase): - def static_seed_data(self): - # User, country and organisation is set up by ApiTestCase - self.first_user = self.add_user('firstuser@mail.com', 'First', 'User', 'Mx') - - self.event = self.add_event() - - self.system_admin = self.add_user('sa@sa.com', is_admin=True) - non_admin_users = self.add_n_users(3) - users = non_admin_users.append(self.system_admin) - self.event.add_event_role('admin', 4), - self.event.add_event_role('reviewer', 1), - - db.session.commit() - - application_form = [ - self.create_application_form(1, True, False), - ] - - closed_review = ReviewForm(1, datetime(2018, 4, 30), 1, True) - inactive_review = ReviewForm(1, datetime(2018, 4, 30), 2, False) - closed_review.close() - review_forms = [ - ReviewForm(1, datetime(2019, 4, 30), 1, True), - ReviewForm(1, datetime(2019, 4, 30), 2, False), - closed_review, - inactive_review - ] - db.session.add_all(review_forms) - db.session.commit() - - self.test_response = self.add_response(1, self.first_user.id, language='en', is_submitted=True) - - self.response_review = self.add_response_reviewer(self.test_response.id, self.first_user.id) - - self.first_headers = self.get_auth_header_for("firstuser@mail.com") - - db.session.flush() - - def test_get_reference_request_by_event_id(self): - self.static_seed_data() - reference_req = ReferenceRequest(1, 'Mr', 'John', 'Snow', 'Supervisor', 'common@email.com') - reference_request_repository.create(reference_req) - REFERENCE_DETAIL = { - 'token': reference_req.token, - 'uploaded_document': 'DOCT-UPLOAD-78999', - } - - response = self.app.post( - '/api/v1/reference', data=REFERENCE_DETAIL, headers=self.first_headers) - self.assertEqual(response.status_code, 201) - - params = {'event_id': 1, 'language': 'en'} - response = self.app.get('/api/v1/review', headers=self.get_auth_header_for('firstuser@mail.com'), data=params) - - data = json.loads(response.data) - - self.assertEqual(response.status_code, 200) - self.assertEqual(len(data), 8) - self.assertEqual(len(data['references']), 1) - self.assertDictEqual(data['references'][0], { - u'title': u'Mr', - u'firstname': u'John', - u'lastname': u'Snow', - u'relation': u'Supervisor', - u'uploaded_document': u'DOCT-UPLOAD-78999', - }) - - def test_get_reference_request_with_two_references(self): - """ - In this test, there are two references requested and submitted - """ - self.static_seed_data() - reference_req = ReferenceRequest(1, 'Mr', 'John', 'Snow', 'Supervisor', 'common@email.com') - reference_req2 = ReferenceRequest(1, 'Mrs', 'Jane', 'Jones', 'Manager', 'jane@email.com') - reference_request_repository.create(reference_req) - reference_request_repository.create(reference_req2) - - REFERENCE_DETAIL = { - 'token': reference_req.token, - 'uploaded_document': 'DOCT-UPLOAD-78999', - } - REFERENCE_DETAIL_2 = { - 'token': reference_req2.token, - 'uploaded_document': 'DOCT-UPLOAD-78979', - } - response = self.app.post( - '/api/v1/reference', data=REFERENCE_DETAIL, headers=self.first_headers) - self.assertEqual(response.status_code, 201) - - response = self.app.post( - '/api/v1/reference', data=REFERENCE_DETAIL_2, headers=self.first_headers) - self.assertEqual(response.status_code, 201) - - params = {'event_id': 1, 'language': 'en'} - response = self.app.get('/api/v1/review', headers=self.get_auth_header_for('firstuser@mail.com'), data=params) - - data = json.loads(response.data) - - self.assertEqual(response.status_code, 200) - self.assertEqual(len(data), 8) - self.assertEqual(len(data['references']), 2) - self.assertDictEqual(data['references'][0], { - u'title': u'Mr', - u'firstname': u'John', - u'lastname': u'Snow', - u'relation': u'Supervisor', - u'uploaded_document': u'DOCT-UPLOAD-78999', - }) - - self.assertDictEqual(data['references'][1], { - u'title': u'Mrs', - u'firstname': u'Jane', - u'lastname': u'Jones', - u'relation': u'Manager', - u'uploaded_document': u'DOCT-UPLOAD-78979', - }) - - def test_get_reference_not_yet_submitted(self): - """ - In this test, a reference has been requested, but not yet submitted by the referee (id exists, but no reference yet) - """ - self.static_seed_data() - reference_req = ReferenceRequest(1, 'Mr', 'John', 'Snow', 'Supervisor', 'common@email.com') - reference_request_repository.create(reference_req) - - params = {'event_id': 1, 'language': 'en'} - response = self.app.get('/api/v1/review', headers=self.get_auth_header_for('firstuser@mail.com'), data=params) - - data = json.loads(response.data) - - self.assertEqual(response.status_code, 200) - self.assertEqual(len(data), 8) - self.assertEqual(data.get('references', None), None) - - def test_get_reference_submitted_later(self): - """ - In this test, a reference has been requested, but the reference is only submitted after the second check - """ - self.static_seed_data() - reference_req = ReferenceRequest(1, 'Mr', 'John', 'Snow', 'Supervisor', 'common@email.com') - reference_request_repository.create(reference_req) - - params = {'event_id': 1, 'language': 'en'} - - REFERENCE_DETAIL = { - 'token': reference_req.token, - 'uploaded_document': 'DOCT-UPLOAD-78999', - } - - response = self.app.get('/api/v1/review', headers=self.get_auth_header_for('firstuser@mail.com'), - data=params) - - data = json.loads(response.data) - - self.assertEqual(response.status_code, 200) - self.assertEqual(len(data), 8) - self.assertEqual(data.get('references', None), None) - - response = self.app.post( - '/api/v1/reference', data=REFERENCE_DETAIL, headers=self.first_headers) - self.assertEqual(response.status_code, 201) - - params = {'event_id': 1, 'language': 'en'} - response = self.app.get('/api/v1/review', headers=self.get_auth_header_for('firstuser@mail.com'), data=params) - - data = json.loads(response.data) - - self.assertEqual(response.status_code, 200) - self.assertEqual(len(data), 8) - self.assertEqual(len(data['references']), 1) - self.assertDictEqual(data['references'][0], { - u'title': u'Mr', - u'firstname': u'John', - u'lastname': u'Snow', - u'relation': u'Supervisor', - u'uploaded_document': u'DOCT-UPLOAD-78999', - }) - - def test_two_references_only_one_submitted(self): - """ - In this test, two references have been requested, but only one submitted (two ids exists, one reference submitted) - """ - self.static_seed_data() - reference_req = ReferenceRequest(1, 'Mr', 'John', 'Snow', 'Supervisor', 'common@email.com') - reference_req2 = ReferenceRequest(1, 'Mrs', 'Jane', 'Jones', 'Manager', 'jane@email.com') - reference_request_repository.create(reference_req) - reference_request_repository.create(reference_req2) - - REFERENCE_DETAIL_2 = { - 'token': reference_req2.token, - 'uploaded_document': 'DOCT-UPLOAD-78979', - } - - response = self.app.post( - '/api/v1/reference', data=REFERENCE_DETAIL_2, headers=self.first_headers) - self.assertEqual(response.status_code, 201) - - params = {'event_id': 1, 'language': 'en'} - response = self.app.get('/api/v1/review', headers=self.get_auth_header_for('firstuser@mail.com'), data=params) - - data = json.loads(response.data) - - self.assertEqual(response.status_code, 200) - self.assertEqual(len(data), 8) - self.assertEqual(len(data['references']), 1) - - self.assertDictEqual(data['references'][0], { - u'title': u'Mrs', - u'firstname': u'Jane', - u'lastname': u'Jones', - u'relation': u'Manager', - u'uploaded_document': u'DOCT-UPLOAD-78979', - }) - - def test_get_review_no_reference(self): - """ - In this test, there is no reference attached to the application at all for review (i.e. no reference request) - """ - self.static_seed_data() - - params = {'event_id': 1, 'language': 'en'} - response = self.app.get('/api/v1/review', headers=self.get_auth_header_for('firstuser@mail.com'), data=params) - - data = json.loads(response.data) - self.assertEqual(response.status_code, 200) - self.assertEqual(len(data), 8) - self.assertEqual(data.get('references', None), None) - - -class ReviewResponseDetailListApiTest(ApiTestCase): - def seed_static_data(self): - self.user1 = self.add_user('user1@mail.com', 'Jane', 'Bloggs', 'Ms') - self.user2 = self.add_user('user2@mail.com', 'Alex', 'Person', 'Dr') - self.reviewer = self.add_user('reviewer@mail.com', 'Joe', 'Soap', 'Mr') - self.reviewer2 = self.add_user('reviewer2@mail.com', 'Jenny', 'Sharp', 'Ms') - self.event_admin = self.add_user('event_admin@mail.com') - - self.event = self.add_event() - self.event2 = self.add_event(key='Empty') - self.event.add_event_role('admin', self.event_admin.id) - self.event2.add_event_role('admin', self.event_admin.id) - - self.application_form = self.create_application_form(self.event.id) - self.section = self.add_section(self.application_form.id) - self.question1 = self.add_question( - self.application_form.id, - self.section.id, - order=1, - key='review-identifier') - self.question_translation1 = self.add_question_translation( - self.question1.id, - 'en', - 'Organisation') - self.question2 = self.add_question( - self.application_form.id, - self.section.id, - order=2, - key='review-identifier') - self.question_translation2 = self.add_question_translation( - self.question2.id, - 'en', - 'Country') - self.question3 = self.add_question( - self.application_form.id, - self.section.id, - order=3, - ) - self.question_translation3 = self.add_question_translation( - self.question3.id, - 'en', - 'Non-review question' - ) - - self.response1 = self.add_response( - self.application_form.id, - self.user1.id, - is_submitted=True) - self.answer1 = self.add_answer(self.response1.id, self.question1.id, 'Pets R Us') - self.answer2 = self.add_answer(self.response1.id, self.question2.id, 'Nigeria') - self.answer3 = self.add_answer(self.response1.id, self.question3.id, 'Non-review answer') - - self.response2 = self.add_response( - self.application_form.id, - self.user2.id, - is_submitted=True) - self.answer4 = self.add_answer(self.response2.id, self.question1.id, 'Nokia') - self.answer5 = self.add_answer(self.response2.id, self.question2.id, 'South Africa') - self.answer6 = self.add_answer(self.response2.id, self.question3.id, 'Non-review answer 2') - - self.review_form = self.add_review_form(self.application_form.id) - self.review_section1 = self.add_review_section(self.review_form.id) - self.review_section1_translation1 = self.add_review_section_translation( - self.review_section1.id, - 'en') - self.review_section2 = self.add_review_section(self.review_form.id) - self.review_section2_translation1 = self.add_review_section_translation( - self.review_section2.id, - 'en' - 'Review Section 2', - 'Review Section 2 Description') - self.review_question1 = self.add_review_question(self.review_section1.id, type='short-text', weight=1) - self.review_question_translation1 = self.add_review_question_translation( - self.review_question1.id, - 'en', - headline='Ethical Considerations', - description="How ethical is the candidate's proposal from 1 to 5?") - self.review_question2 = self.add_review_question(self.review_section1.id, type='long-text', weight=0) - self.review_question_translation2 = self.add_review_question_translation( - self.review_question2.id, - 'en', - headline=None, - description='Comments for the candidate' - ) - self.review_question3 = self.add_review_question(self.review_section2.id, type='multi-choice', weight=2) - self.review_question_translation3 = self.add_review_question_translation( - self.review_question3.id, - 'en', - headline=None, - description='What is your overall rating?' - ) - self.review_question4 = self.add_review_question(self.review_section2.id, type='checkbox', weight=0) - self.review_question_translation4 = self.add_review_question_translation( - self.review_question4.id, - 'en', - headline='Yes/No Question' - ) - - self.review_response1 = self.add_review_response( - self.reviewer.id, - self.response1.id, - self.review_form.id, - is_submitted=True) - self.review_score1 = self.add_review_score(self.review_response1.id, self.review_question1.id, '4') - self.review_score2 = self.add_review_score(self.review_response1.id, self.review_question2.id, 'This is a very good proposal') - self.review_score3 = self.add_review_score(self.review_response1.id, self.review_question3.id, '5') - self.review_score4 = self.add_review_score(self.review_response1.id, self.review_question4.id, 'Yes') - - self.review_response2 = self.add_review_response( - self.reviewer2.id, - self.response2.id, - self.review_form.id, - is_submitted=True) - self.review_score5 = self.add_review_score(self.review_response2.id, self.review_question1.id, '3') - self.review_score6 = self.add_review_score(self.review_response2.id, self.review_question2.id, 'Not bad!') - self.review_score7 = self.add_review_score(self.review_response2.id, self.review_question3.id, '3') - self.review_score8 = self.add_review_score(self.review_response2.id, self.review_question4.id, 'No') - - def test_not_authed(self): - response = self.app.get('/api/v1/reviewresponsedetaillist') - self.assertEqual(response.status_code, 401) - - def test_not_event_admin(self): - self.seed_static_data() - - response = self.app.get( - '/api/v1/reviewresponsedetaillist', - headers=self.get_auth_header_for('user1@mail.com'), - data={'event_id': 1} - ) - - self.assertEqual(response.status_code, 403) - - def test_no_review_responses(self): - self.seed_static_data() - - response = self.app.get( - '/api/v1/reviewresponsedetaillist', - headers=self.get_auth_header_for('event_admin@mail.com'), - data={'event_id': 2, 'language': 'en'} - ) - data = json.loads(response.data) - - print(data) - - self.assertEqual(data, []) - - def test_review_responses_for_event(self): - self.seed_static_data() - - response = self.app.get( - '/api/v1/reviewresponsedetaillist', - headers=self.get_auth_header_for('event_admin@mail.com'), - data={'event_id': 1, 'language': 'en'} - ) - - data = json.loads(response.data) - - self.assertEqual(len(data), 2) - - self.assertEqual(data[0]['review_response_id'], 1) - self.assertEqual(data[0]['response_id'], 1) - - self.assertEqual(data[0]['reviewer_user_title'], 'Mr') - self.assertEqual(data[0]['reviewer_user_firstname'], 'Joe') - self.assertEqual(data[0]['reviewer_user_lastname'], 'Soap') - - self.assertEqual(data[0]['response_user_title'], 'Ms') - self.assertEqual(data[0]['response_user_firstname'], 'Jane') - self.assertEqual(data[0]['response_user_lastname'], 'Bloggs') - - self.assertEqual(len(data[0]['identifiers']), 2) - self.assertEqual(data[0]['identifiers'][0]['headline'], 'Organisation') - self.assertEqual(data[0]['identifiers'][0]['value'], 'Pets R Us') - self.assertEqual(data[0]['identifiers'][1]['headline'], 'Country') - self.assertEqual(data[0]['identifiers'][1]['value'], 'Nigeria') - - self.assertEqual(len(data[0]['scores']), 3) - 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]['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]['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]['weight'], 2) - - self.assertEqual(data[0]['total'], 14) - - - self.assertEqual(data[1]['review_response_id'], 2) - self.assertEqual(data[1]['response_id'], 2) - - self.assertEqual(data[1]['reviewer_user_title'], 'Ms') - self.assertEqual(data[1]['reviewer_user_firstname'], 'Jenny') - self.assertEqual(data[1]['reviewer_user_lastname'], 'Sharp') - - self.assertEqual(data[1]['response_user_title'], 'Dr') - self.assertEqual(data[1]['response_user_firstname'], 'Alex') - self.assertEqual(data[1]['response_user_lastname'], 'Person') - - self.assertEqual(len(data[1]['identifiers']), 2) - self.assertEqual(data[1]['identifiers'][0]['headline'], 'Organisation') - self.assertEqual(data[1]['identifiers'][0]['value'], 'Nokia') - self.assertEqual(data[1]['identifiers'][1]['headline'], 'Country') - self.assertEqual(data[1]['identifiers'][1]['value'], 'South Africa') - - self.assertEqual(len(data[1]['scores']), 3) - 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]['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]['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]['weight'], 2) - - self.assertEqual(data[1]['total'], 9) - - -class ReviewResponseSummaryListApiTest(ApiTestCase): - def seed_static_data(self): - self.user1 = self.add_user('user1@mail.com', 'Jane', 'Bloggs', 'Ms') - self.user2 = self.add_user('user2@mail.com', 'Alex', 'Person', 'Dr') - self.reviewer = self.add_user('reviewer@mail.com', 'Joe', 'Soap', 'Mr') - self.reviewer2 = self.add_user('reviewer2@mail.com', 'Jenny', 'Sharp', 'Ms') - self.event_admin = self.add_user('event_admin@mail.com') - - self.event = self.add_event() - self.event2 = self.add_event(key='Empty') - self.event.add_event_role('admin', self.event_admin.id) - self.event2.add_event_role('admin', self.event_admin.id) - - self.application_form = self.create_application_form(self.event.id) - self.section = self.add_section(self.application_form.id) - self.question1 = self.add_question( - self.application_form.id, - self.section.id, - order=1, - key='review-identifier') - self.question_translation1 = self.add_question_translation( - self.question1.id, - 'en', - 'Organisation') - self.question2 = self.add_question( - self.application_form.id, - self.section.id, - order=2, - key='review-identifier') - self.question_translation2 = self.add_question_translation( - self.question2.id, - 'en', - 'Country') - self.question3 = self.add_question( - self.application_form.id, - self.section.id, - order=3, - ) - self.question_translation3 = self.add_question_translation( - self.question3.id, - 'en', - 'Non-review question' - ) - - self.response1 = self.add_response( - self.application_form.id, - self.user1.id, - is_submitted=True) - self.answer1 = self.add_answer(self.response1.id, self.question1.id, 'Pets R Us') - self.answer2 = self.add_answer(self.response1.id, self.question2.id, 'Nigeria') - self.answer3 = self.add_answer(self.response1.id, self.question3.id, 'Non-review answer') - - self.response2 = self.add_response( - self.application_form.id, - self.user2.id, - is_submitted=True) - self.answer4 = self.add_answer(self.response2.id, self.question1.id, 'Nokia') - self.answer5 = self.add_answer(self.response2.id, self.question2.id, 'South Africa') - self.answer6 = self.add_answer(self.response2.id, self.question3.id, 'Non-review answer 2') - - self.review_form = self.add_review_form(self.application_form.id) - self.review_section1 = self.add_review_section(self.review_form.id) - self.review_section1_translation1 = self.add_review_section_translation( - self.review_section1.id, - 'en') - self.review_section2 = self.add_review_section(self.review_form.id) - self.review_section2_translation1 = self.add_review_section_translation( - self.review_section2.id, - 'en' - 'Review Section 2', - 'Review Section 2 Description') - self.review_question1 = self.add_review_question(self.review_section1.id, type='short-text', weight=1) - self.review_question_translation1 = self.add_review_question_translation( - self.review_question1.id, - 'en', - headline='Ethical Considerations', - description="How ethical is the candidate's proposal from 1 to 5?") - self.review_question2 = self.add_review_question(self.review_section1.id, type='long-text', weight=0) - self.review_question_translation2 = self.add_review_question_translation( - self.review_question2.id, - 'en', - headline=None, - description='Comments for the candidate' - ) - self.review_question3 = self.add_review_question(self.review_section2.id, type='multi-choice', weight=2) - self.review_question_translation3 = self.add_review_question_translation( - self.review_question3.id, - 'en', - headline=None, - description='What is your overall rating?' - ) - self.review_question4 = self.add_review_question(self.review_section2.id, type='checkbox', weight=0) - self.review_question_translation4 = self.add_review_question_translation( - self.review_question4.id, - 'en', - headline='Yes/No Question' - ) - - # reviewer 1 for first response - self.review_response1 = self.add_review_response( - self.reviewer.id, - self.response1.id, - self.review_form.id, - is_submitted=True) - self.review_score1 = self.add_review_score(self.review_response1.id, self.review_question1.id, '4') - self.review_score2 = self.add_review_score(self.review_response1.id, self.review_question2.id, 'This is a very good proposal') - self.review_score3 = self.add_review_score(self.review_response1.id, self.review_question3.id, '5') - self.review_score4 = self.add_review_score(self.review_response1.id, self.review_question4.id, 'Yes') - - # reviewer 2 for first response - self.review_response3 = self.add_review_response( - self.reviewer2.id, - self.response1.id, - self.review_form.id, - is_submitted=True) - self.review_score9 = self.add_review_score(self.review_response3.id, self.review_question1.id, '3') - self.review_score10 = self.add_review_score(self.review_response3.id, self.review_question2.id, 'This is a good proposal') - self.review_score11 = self.add_review_score(self.review_response3.id, self.review_question3.id, '4') - self.review_score12 = self.add_review_score(self.review_response3.id, self.review_question4.id, 'Yes') - - # reviewer 2 for second response - self.review_response2 = self.add_review_response( - self.reviewer2.id, - self.response2.id, - self.review_form.id, - is_submitted=True) - self.review_score5 = self.add_review_score(self.review_response2.id, self.review_question1.id, '3') - self.review_score6 = self.add_review_score(self.review_response2.id, self.review_question2.id, 'Close to bad!') - self.review_score7 = self.add_review_score(self.review_response2.id, self.review_question3.id, '2') - self.review_score8 = self.add_review_score(self.review_response2.id, self.review_question4.id, 'No') - - # reviewer 1 for second response - self.review_response4 = self.add_review_response( - self.reviewer.id, - self.response2.id, - self.review_form.id, - is_submitted=True) - self.review_score13 = self.add_review_score(self.review_response4.id, self.review_question1.id, '2') - self.review_score14 = self.add_review_score(self.review_response4.id, self.review_question2.id, 'Bad!') - self.review_score15 = self.add_review_score(self.review_response4.id, self.review_question3.id, '1') - self.review_score16 = self.add_review_score(self.review_response4.id, self.review_question4.id, 'No') - - def test_not_authed(self): - response = self.app.get('/api/v1/reviewresponsesummarylist') - self.assertEqual(response.status_code, 401) - - def test_not_event_admin(self): - self.seed_static_data() - - response = self.app.get( - '/api/v1/reviewresponsesummarylist', - headers=self.get_auth_header_for('user1@mail.com'), - data={'event_id': 1} - ) - - self.assertEqual(response.status_code, 403) - - def test_review_response_summary_for_event(self): - self.seed_static_data() - - response = self.app.get( - '/api/v1/reviewresponsesummarylist', - headers=self.get_auth_header_for('event_admin@mail.com'), - data={'event_id': 1, 'language': 'en'} - ) - - data = json.loads(response.data) - - self.assertEqual(len(data), 2) - - self.assertEqual(data[0]['response_id'], 1) - self.assertEqual(data[0]['response_user_title'], 'Ms') - self.assertEqual(data[0]['response_user_firstname'], 'Jane') - self.assertEqual(data[0]['response_user_lastname'], 'Bloggs') - - self.assertEqual(len(data[0]['identifiers']), 2) - self.assertEqual(data[0]['identifiers'][0]['headline'], 'Organisation') - self.assertEqual(data[0]['identifiers'][0]['value'], 'Pets R Us') - self.assertEqual(data[0]['identifiers'][1]['headline'], 'Country') - self.assertEqual(data[0]['identifiers'][1]['value'], 'Nigeria') - - self.assertEqual(len(data[0]['scores']), 2) - 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'], 3.5) - self.assertEqual(data[0]['scores'][0]['weight'], 1) - - self.assertEqual(data[0]['scores'][1]['headline'], None) - self.assertEqual(data[0]['scores'][1]['description'], 'What is your overall rating?') - self.assertEqual(data[0]['scores'][1]['type'], 'multi-choice') - self.assertEqual(data[0]['scores'][1]['score'], 4.5) - self.assertEqual(data[0]['scores'][1]['weight'], 2) - - self.assertEqual(data[0]['total'], 12.5) - - - self.assertEqual(data[1]['response_id'], 2) - self.assertEqual(data[1]['response_user_title'], 'Dr') - self.assertEqual(data[1]['response_user_firstname'], 'Alex') - self.assertEqual(data[1]['response_user_lastname'], 'Person') - - self.assertEqual(len(data[1]['identifiers']), 2) - self.assertEqual(data[1]['identifiers'][0]['headline'], 'Organisation') - self.assertEqual(data[1]['identifiers'][0]['value'], 'Nokia') - self.assertEqual(data[1]['identifiers'][1]['headline'], 'Country') - self.assertEqual(data[1]['identifiers'][1]['value'], 'South Africa') - - self.assertEqual(len(data[1]['scores']), 2) - 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'], 2.5) - self.assertEqual(data[1]['scores'][0]['weight'], 1) - - self.assertEqual(data[1]['scores'][1]['headline'], None) - self.assertEqual(data[1]['scores'][1]['description'], 'What is your overall rating?') - self.assertEqual(data[1]['scores'][1]['type'], 'multi-choice') - self.assertEqual(data[1]['scores'][1]['score'], 1.5) - self.assertEqual(data[1]['scores'][1]['weight'], 2) - - self.assertEqual(data[1]['total'], 5.5) - - -class ReviewStageAPITest(ApiTestCase): - def seed_static_data(self, activate_second_stage=True): - first_event = self.add_event() - self.create_application_form() - - event_admin = self.add_user("event@admin.com") - first_event.add_event_role('admin', event_admin.id) - - self.add_review_form(stage=1, active=False) - self.add_review_form(stage=2, active=activate_second_stage) - self.add_review_form(stage=3, active=False) - - # An event with no review form - no_form_event = self.add_event(key="no_form_event") - self.no_form_event_id = no_form_event.id - no_form_event.add_event_role('admin', event_admin.id) - - db.session.commit() - self.event_admin_headers = self.get_auth_header_for("event@admin.com") - - def test_get(self): - """Test that the get method returns the correct current stage and number of stages.""" - self.seed_static_data() - - response = self.app.get( - '/api/v1/reviewstage', - headers=self.event_admin_headers, - data={'event_id': 1} - ) - - data = json.loads(response.data) - - self.assertEqual(data['current_stage'], 2) - self.assertEqual(data['total_stages'], 3) - - def test_get_no_active(self): - """Test that the get method returns a 404 when ther is no active review form.""" - self.seed_static_data(activate_second_stage=False) - - response = self.app.get( - '/api/v1/reviewstage', - headers=self.event_admin_headers, - data={'event_id': 1} - ) - - self.assertEqual(response.status_code, 404) - - def test_get_no_form(self): - """Test that the get method returns a 404 when there are no review forms for the event.""" - self.seed_static_data() - - response = self.app.get( - '/api/v1/reviewstage', - headers=self.event_admin_headers, - data={'event_id': self.no_form_event_id} - ) - - self.assertEqual(response.status_code, 404) - - def test_post(self): - """Test that the review stage can be updated.""" - self.seed_static_data() - - response = self.app.post( - '/api/v1/reviewstage', - headers=self.event_admin_headers, - data={'event_id': 1, 'stage': 3} - ) - - self.assertEqual(response.status_code, 201) - - response = self.app.get( - '/api/v1/reviewstage', - headers=self.event_admin_headers, - data={'event_id': 1} - ) - - data = json.loads(response.data) - - self.assertEqual(data['current_stage'], 3) - self.assertEqual(data['total_stages'], 3) - - def test_post_no_form(self): - """Test that the post method returns a 404 when there are no review forms for the event.""" - self.seed_static_data() - - response = self.app.post( - '/api/v1/reviewstage', - headers=self.event_admin_headers, - data={'event_id': self.no_form_event_id, 'stage': 1} - ) - - self.assertEqual(response.status_code, 404) - - def test_post_no_form(self): - """Test that the post method returns a 404 when trying to activate a stage that doesn't exist.""" - self.seed_static_data() - - response = self.app.post( - '/api/v1/reviewstage', - headers=self.event_admin_headers, - data={'event_id': 1, 'stage': 4} - ) - - self.assertEqual(response.status_code, 404) - - -REVIEW_FORM_POST = { - "event_id": 1, - "application_form_id": 1, - "stage": 2, - "is_open": True, - "deadline": datetime(2024, 6, 1).strftime('%Y-%m-%dT%H:%M:%S'), - "active": True, - "sections": [ - { - "order": 1, - "name": { - "en": "Section 1", - "fr": "Section 1" - }, - "description": { - "en": "This is the first section", - "fr": "Ceci est la première section" - }, - "questions": [ - { - "type": "short-text", - "is_required": False, - "order": 1, - "weight": 0.0, - "question_id": 0, - "headline": { - "en": "Question 1", - "fr": "Question 1" - }, - "description": { - "en": "This is the first question", - "fr": "C'est la première question" - }, - "placeholder": { - "en": "Enter something interesting", - "fr": "Entrez quelque chose d'intéressant" - }, - "options": { - "en": None, - "fr": None - }, - "validation_regex": { - "en": "(a|b|c|d)", - "fr": "(a|b|c|d)" - }, - "validation_text": { - "en": "You must enter a, b, c or d", - "fr": "Vous devez entrer a, b, c ou d" - } - }, - { - "type": "radio", - "is_required": True, - "order": 2, - "weight": 1.5, - "question_id": 0, - "headline": { - "en": "Question 2", - "fr": "Question 2" - }, - "description": { - "en": "This is the second question", - "fr": "C'est la deuxième question" - }, - "placeholder": { - "en": None, - "fr": None - }, - "options": { - "en": [ - {"value": "0", "label": "Bad"}, - {"value": "1", "label": "Average"}, - {"value": "2", "label": "Good"}, - ], - "fr": [ - {"value": "0", "label": "Mal"}, - {"value": "1", "label": "Moyenne"}, - {"value": "2", "label": "Bien"}, - ], - }, - "validation_regex": { - "en": None, - "fr": None - }, - "validation_text": { - "en": None, - "fr": None - } - } - ] - }, - { - "order": 2, - "name": { - "en": "Section 2", - "fr": "Section 2" - }, - "description": { - "en": "This is the first section", - "fr": "Ceci est la deuxième section" - }, - "questions": [ - { - "type": "information", - "is_required": False, - "order": 1, - "weight": 0.0, - "question_id": 1, - "headline": { - "en": "Question 1 from application form", - "fr": "Question 1 from application form" - }, - "description": { - "en": "This is the first question from the application form", - "fr": "C'est la première question du formulaire de candidature" - }, - "placeholder": { - "en": None, - "fr": None - }, - "options": { - "en": None, - "fr": None - }, - "validation_regex": { - "en": None, - "fr": None - }, - "validation_text": { - "en": None, - "fr": None - } - } - ] - } - ] -} - - -REVIEW_FORM_PUT = { - "id": 1, - "event_id": 1, - "application_form_id": 1, - "stage": 3, # Updated - "is_open": True, - "deadline": datetime(2023, 2, 2).strftime('%Y-%m-%dT%H:%M:%S'), # Updated - "active": True, - "sections": [ - { # Updated first section - "id": 1, - "order": 2, # Updated - "name": { - "en": "Section 1 Updated", - "fr": "Section 1 Updated" - }, - "description": { - "en": "This is the first section updated", - "fr": "Ceci est la première section updated" - }, - "questions": [ - { - "id": 1, # Updated - "type": "long-text", # Updated - "is_required": True, - "order": 10, - "weight": 1.0, - "question_id": 0, - "headline": { - "en": "Question 1 Updated", - "fr": "Question 1 Updated" - }, - "description": { - "en": "This is the first question Updated", - "fr": "C'est la première question Updated" - }, - "placeholder": { - "en": "Enter something interesting updated", - "fr": "Entrez quelque chose d'intéressant updated" - }, - "options": { - "en": {"blah": "blah"}, - "fr": {"blah": "blah"} - }, - "validation_regex": { - "en": "(a|b|c|d|e)", - "fr": "(a|b|c|d|e)" - }, - "validation_text": { - "en": "You must enter a, b, c or d, or e", - "fr": "Vous devez entrer a, b, c, d, ou e" - } - }, - # Deleted question 2 - { # New question - "type": "radio", - "is_required": True, - "order": 20, - "weight": 2.5, - "question_id": 0, - "headline": { - "en": "Question 3", - "fr": "Question 3" - }, - "description": { - "en": "This is the third question", - "fr": "C'est la deuxième question" - }, - "placeholder": { - "en": "hello", - "fr": "world" - }, - "options": { - "en": [ - {"value": "0", "label": "Bad"}, - {"value": "1", "label": "Average"}, - {"value": "2", "label": "Good"}, - ], - "fr": [ - {"value": "0", "label": "Mal"}, - {"value": "1", "label": "Moyenne"}, - {"value": "2", "label": "Bien"}, - ], - }, - "validation_regex": { - "en": None, - "fr": None - }, - "validation_text": { - "en": None, - "fr": None - } - } - ] - }, - # Deleted section 2 - { # New section - "order": 3, - "name": { - "en": "Section 2 New", - "fr": "Section 2 New" - }, - "description": { - "en": "This is the new second section", - "fr": "Ceci est la neue deuxième section" - }, - "questions": [ - { - "type": "information", - "is_required": False, - "order": 1, - "weight": 0.0, - "question_id": 1, - "headline": { - "en": "Question 1 from application form", - "fr": "Question 1 from application form" - }, - "description": { - "en": "This is the first question from the application form", - "fr": "C'est la première question du formulaire de candidature" - }, - "placeholder": { - "en": None, - "fr": None - }, - "options": { - "en": None, - "fr": None - }, - "validation_regex": { - "en": None, - "fr": None - }, - "validation_text": { - "en": None, - "fr": None - } - } - ] - } - ] -} - -class ReviewFormDetailAPITest(ApiTestCase): - - def seed_static_data(self): - first_event = self.add_event() - self.first_event_id = first_event.id - - second_event = self.add_event(key="event2") - self.no_form_event_id = second_event.id - - app_form = self.create_application_form() - section = self.add_section(app_form.id) - self.question = self.add_question(app_form.id, section.id) - - event_admin = self.add_user("event@admin.com") - first_event.add_event_role('admin', event_admin.id) - second_event.add_event_role('admin', event_admin.id) - db.session.commit() - - def add_review_forms(self): - # First stage review form - form1 = self.add_review_form(stage=1, active=False, deadline=datetime(2021, 6, 1)) - form1_section1 = self.add_review_section(form1.id) - self.add_review_section_translation(form1_section1.id, 'en', 'Form 1 Section 1 EN', 'Form 1 Section 1 Description EN') - self.add_review_section_translation(form1_section1.id, 'fr', 'Form 1 Section 1 FR', 'Form 1 Section 1 Description FR') - - form1_section1_question1 = self.add_review_question(form1_section1.id, weight=1, type='numeric-text') - self.add_review_question_translation( - form1_section1_question1.id, - language='en', - headline='Review Question 1 EN', - description='Review Question 1 Description EN', - placeholder='Placeholder Question 1 EN', - validation_regex='(a|b|c|d)', - validation_text='Validation text question 1 EN') - self.add_review_question_translation( - form1_section1_question1.id, - language='fr', - headline='Review Question 1 FR', - description='Review Question 1 Description FR', - placeholder='Placeholder Question 1 FR', - validation_regex='(a|b|c|d)', - validation_text='Validation text question 1 FR') - - form1_section1_question2 = self.add_review_question(form1_section1.id, type='information', order=2, question_id=self.question.id) - self.add_review_question_translation( - form1_section1_question2.id, - language='en', - headline='Review Question 2 EN', - description='Review Question 2 Description EN') - self.add_review_question_translation( - form1_section1_question2.id, - language='fr', - headline='Review Question 2 FR', - description='Review Question 2 Description FR') - - form1_section2 = self.add_review_section(form1.id, order=2) - self.add_review_section_translation(form1_section2.id, 'en', 'Form 1 Section 2 EN', 'Form 1 Section 2 Description EN') - self.add_review_section_translation(form1_section2.id, 'fr', 'Form 1 Section 2 FR', 'Form 1 Section 2 Description FR') - - form1_section2_question1 = self.add_review_question(form1_section2.id, weight=1, type='radio') - self.add_review_question_translation( - form1_section2_question1.id, - language='en', - headline='Review Question 3 EN', - description='Review Question 3 Description EN', - options=[ - {'value': '0', 'label': 'Bad EN'}, - {'value': '1', 'label': 'Average EN'}, - {'value': '2', 'label': 'Good EN'} - ]) - self.add_review_question_translation( - form1_section2_question1.id, - language='fr', - headline='Review Question 3 FR', - description='Review Question 3 Description FR', - options=[ - {'value': '0', 'label': 'Bad FR'}, - {'value': '1', 'label': 'Average FR'}, - {'value': '2', 'label': 'Good FR'} - ]) - - # Second stage review form - form2 = self.add_review_form(stage=2, active=True, deadline=datetime(2021, 8, 1)) - - form2_section1 = self.add_review_section(form2.id) - self.add_review_section_translation(form2_section1.id, 'en', 'Form 2 Section 1 EN', 'Form 2 Section 1 Description EN') - self.add_review_section_translation(form2_section1.id, 'fr', 'Form 2 Section 1 FR', 'Form 2 Section 1 Description FR') - - form2_section1_question1 = self.add_review_question(form2_section1.id, type='information', question_id=self.question.id) - self.add_review_question_translation( - form2_section1_question1.id, - language='en', - headline='Stage 2 Review Question 1 EN', - description='Stage 2 Review Question 1 Description EN') - self.add_review_question_translation( - form2_section1_question1.id, - language='fr', - headline='Stage 2 Review Question 1 FR', - description='Stage 2 Review Question 1 Description FR') - - db.session.commit() - - def test_get_without_stage(self): - """Test that the get method returns the correct stage when a stage parameter isn't provided.""" - self.seed_static_data() - self.add_review_forms() - - response = self.app.get( - '/api/v1/review-form-detail', - headers=self.get_auth_header_for("event@admin.com"), - data={'event_id': self.first_event_id} - ) - - data = json.loads(response.data) - - self.assertEqual(data['stage'], 2) - - def test_get_first_stage(self): - """Check that the get method returns the form for stage 1.""" - self.seed_static_data() - self.add_review_forms() - - response = self.app.get( - '/api/v1/review-form-detail', - headers=self.get_auth_header_for("event@admin.com"), - data={'event_id': self.first_event_id, 'stage': 1} - ) - - data = json.loads(response.data) - - self.assertEqual(data['id'], 1) - self.assertEqual(data['event_id'], self.first_event_id) - self.assertEqual(data['application_form_id'], 1) - self.assertEqual(data['is_open'], True) - self.assertEqual(data['deadline'], "2021-06-01T00:00:00") - self.assertEqual(data['active'], False) - self.assertEqual(data['stage'], 1) - self.assertEqual(len(data['sections']), 2) - - section1 = data['sections'][0] - self.assertEqual(section1['order'], 1) - self.assertEqual(section1['name']['en'], 'Form 1 Section 1 EN') - self.assertEqual(section1['name']['fr'], 'Form 1 Section 1 FR') - self.assertEqual(section1['description']['en'], 'Form 1 Section 1 Description EN') - self.assertEqual(section1['description']['fr'], 'Form 1 Section 1 Description FR') - self.assertEqual(len(section1['questions']), 2) - - question1 = section1['questions'][0] - self.assertEqual(question1['order'], 1) - self.assertEqual(question1['weight'], 1) - self.assertEqual(question1['type'], 'numeric-text') - self.assertEqual(question1['question_id'], 0) - self.assertEqual(question1['headline']['en'], 'Review Question 1 EN') - self.assertEqual(question1['headline']['fr'], 'Review Question 1 FR') - self.assertEqual(question1['description']['en'], 'Review Question 1 Description EN') - self.assertEqual(question1['description']['fr'], 'Review Question 1 Description FR') - self.assertEqual(question1['placeholder']['en'], 'Placeholder Question 1 EN') - self.assertEqual(question1['placeholder']['fr'], 'Placeholder Question 1 FR') - self.assertEqual(question1['validation_regex']['en'], '(a|b|c|d)') - self.assertEqual(question1['validation_regex']['fr'], '(a|b|c|d)') - self.assertEqual(question1['validation_text']['en'], 'Validation text question 1 EN') - self.assertEqual(question1['validation_text']['fr'], 'Validation text question 1 FR') - self.assertEqual(question1['options']['en'], None) - self.assertEqual(question1['options']['fr'], None) - - question2 = section1['questions'][1] - self.assertEqual(question2['order'], 2) - self.assertEqual(question2['weight'], 0) - self.assertEqual(question2['type'], 'information') - self.assertEqual(question2['question_id'], 1) - self.assertEqual(question2['headline']['en'], 'Review Question 2 EN') - self.assertEqual(question2['headline']['fr'], 'Review Question 2 FR') - self.assertEqual(question2['description']['en'], 'Review Question 2 Description EN') - self.assertEqual(question2['description']['fr'], 'Review Question 2 Description FR') - self.assertEqual(question2['placeholder']['en'], None) - self.assertEqual(question2['placeholder']['fr'], None) - self.assertEqual(question2['validation_regex']['en'], None) - self.assertEqual(question2['validation_regex']['fr'], None) - self.assertEqual(question2['validation_text']['en'], None) - self.assertEqual(question2['validation_text']['fr'], None) - self.assertEqual(question2['options']['en'], None) - self.assertEqual(question2['options']['fr'], None) - - section2 = data['sections'][1] - self.assertEqual(section2['order'], 2) - self.assertEqual(section2['name']['en'], 'Form 1 Section 2 EN') - self.assertEqual(section2['name']['fr'], 'Form 1 Section 2 FR') - self.assertEqual(section2['description']['en'], 'Form 1 Section 2 Description EN') - self.assertEqual(section2['description']['fr'], 'Form 1 Section 2 Description FR') - self.assertEqual(len(section2['questions']), 1) - - question3 = section2['questions'][0] - self.assertEqual(question3['order'], 1) - self.assertEqual(question3['weight'], 1) - self.assertEqual(question3['type'], 'radio') - self.assertEqual(question3['question_id'], 0) - self.assertEqual(question3['headline']['en'], 'Review Question 3 EN') - self.assertEqual(question3['headline']['fr'], 'Review Question 3 FR') - self.assertEqual(question3['description']['en'], 'Review Question 3 Description EN') - self.assertEqual(question3['description']['fr'], 'Review Question 3 Description FR') - self.assertEqual(question3['placeholder']['en'], None) - self.assertEqual(question3['placeholder']['fr'], None) - self.assertEqual(question3['validation_regex']['en'], None) - self.assertEqual(question3['validation_regex']['fr'], None) - self.assertEqual(question3['validation_text']['en'], None) - self.assertEqual(question3['validation_text']['fr'], None) - self.assertEqual(len(question3['options']['en']), 3) - self.assertEqual(len(question3['options']['fr']), 3) - - def test_get_second_stage(self): - """Check that the get method returns the form for stage 2.""" - self.seed_static_data() - self.add_review_forms() - - response = self.app.get( - '/api/v1/review-form-detail', - headers=self.get_auth_header_for("event@admin.com"), - data={'event_id': self.first_event_id, 'stage': 2} - ) - - data = json.loads(response.data) - - self.assertEqual(data['id'], 2) - self.assertEqual(data['event_id'], self.first_event_id) - self.assertEqual(data['application_form_id'], 1) - self.assertEqual(data['is_open'], True) - self.assertEqual(data['deadline'], "2021-08-01T00:00:00") - self.assertEqual(data['active'], True) - self.assertEqual(data['stage'], 2) - self.assertEqual(len(data['sections']), 1) - - section1 = data['sections'][0] - self.assertEqual(section1['order'], 1) - self.assertEqual(section1['name']['en'], 'Form 2 Section 1 EN') - self.assertEqual(section1['name']['fr'], 'Form 2 Section 1 FR') - self.assertEqual(section1['description']['en'], 'Form 2 Section 1 Description EN') - self.assertEqual(section1['description']['fr'], 'Form 2 Section 1 Description FR') - self.assertEqual(len(section1['questions']), 1) - - question1 = section1['questions'][0] - self.assertEqual(question1['order'], 1) - self.assertEqual(question1['weight'], 0) - self.assertEqual(question1['type'], 'information') - self.assertEqual(question1['question_id'], 1) - self.assertEqual(question1['headline']['en'], 'Stage 2 Review Question 1 EN') - self.assertEqual(question1['headline']['fr'], 'Stage 2 Review Question 1 FR') - self.assertEqual(question1['description']['en'], 'Stage 2 Review Question 1 Description EN') - self.assertEqual(question1['description']['fr'], 'Stage 2 Review Question 1 Description FR') - self.assertEqual(question1['placeholder']['en'], None) - self.assertEqual(question1['placeholder']['fr'], None) - self.assertEqual(question1['validation_regex']['en'], None) - self.assertEqual(question1['validation_regex']['fr'], None) - self.assertEqual(question1['validation_text']['en'], None) - self.assertEqual(question1['validation_text']['fr'], None) - self.assertEqual(question1['options']['en'], None) - self.assertEqual(question1['options']['fr'], None) - - def test_get_new_stage(self): - """Check that the get method returns not found for a stage that doesn't exist.""" - self.seed_static_data() - self.add_review_forms() - - response = self.app.get( - '/api/v1/review-form-detail', - headers=self.get_auth_header_for("event@admin.com"), - data={'event_id': self.first_event_id, 'stage': 3} - ) - - self.assertEqual(response.status_code, 404) - - def test_get_no_form(self): - """Check that the get method returns not found for an event that has no review form.""" - self.seed_static_data() - self.add_review_forms() - - response = self.app.get( - '/api/v1/review-form-detail', - headers=self.get_auth_header_for("event@admin.com"), - data={'event_id': self.no_form_event_id, 'stage': 1} - ) - - self.assertEqual(response.status_code, 404) - - def test_post(self): - """Check that the post method works.""" - self.seed_static_data() - - response = self.app.post( - '/api/v1/review-form-detail', - headers=self.get_auth_header_for("event@admin.com"), - data=json.dumps(REVIEW_FORM_POST), - content_type='application/json',) - - self.assertEqual(response.status_code, 201) - - response = self.app.get( - '/api/v1/review-form-detail', - headers=self.get_auth_header_for("event@admin.com"), - data={'event_id': REVIEW_FORM_POST["event_id"], 'stage': REVIEW_FORM_POST["stage"]} - ) - - data = json.loads(response.data) - - self.assertTrue("id" in data and data["id"] is not None) - del data["id"] - - self.assertTrue("id" in data["sections"][0] and data["sections"][0]["id"] is not None) - del data["sections"][0]["id"] - - self.assertTrue("id" in data["sections"][1] and data["sections"][1]["id"] is not None) - del data["sections"][1]["id"] - - self.assertTrue("id" in data["sections"][0]["questions"][0] and data["sections"][0]["questions"][0]["id"] is not None) - del data["sections"][0]["questions"][0]["id"] - - self.assertTrue("id" in data["sections"][0]["questions"][1] and data["sections"][0]["questions"][1]["id"] is not None) - del data["sections"][0]["questions"][1]["id"] - - self.assertTrue("id" in data["sections"][1]["questions"][0] and data["sections"][1]["questions"][0]["id"] is not None) - del data["sections"][1]["questions"][0]["id"] - - self.maxDiff = 6000 - self.assertEqual(data, REVIEW_FORM_POST) - - def test_put(self): - self.seed_static_data() - - # First create the review form - response = self.app.post( - '/api/v1/review-form-detail', - headers=self.get_auth_header_for("event@admin.com"), - data=json.dumps(REVIEW_FORM_POST), - content_type='application/json',) - - self.assertEqual(response.status_code, 201) - - # Now update it - response = self.app.put( - '/api/v1/review-form-detail', - headers=self.get_auth_header_for("event@admin.com"), - data=json.dumps(REVIEW_FORM_PUT), - content_type='application/json' - ) - - data = json.loads(response.data) - - del data["sections"][1]["id"] - del data["sections"][0]["questions"][1]["id"] - del data["sections"][1]["questions"][0]["id"] - self.maxDiff = None - self.assertEqual(data, REVIEW_FORM_PUT) +from datetime import datetime +import json +import copy +import itertools + +from app import db, LOGGER +from app.utils.testing import ApiTestCase +from app.events.models import Event, EventRole +from app.users.models import AppUser, UserCategory, Country +from app.applicationModel.models import ApplicationForm, Question, Section +from app.responses.models import Response, Answer, ResponseReviewer + +from app.references.models import ReferenceRequest, Reference +from app.references.repository import ReferenceRequestRepository as reference_request_repository +from app.reviews.models import ReviewForm, ReviewQuestion, ReviewResponse, ReviewScore, ReviewConfiguration + +from app.reviews.models import ReviewForm, ReviewQuestion, ReviewQuestionTranslation, ReviewResponse, ReviewScore, ReviewConfiguration +from app.utils.errors import REVIEW_RESPONSE_NOT_FOUND, FORBIDDEN, USER_NOT_FOUND +from nose.plugins.skip import SkipTest +from app.organisation.models import Organisation + +from parameterized import parameterized + +class ReviewsApiTest(ApiTestCase): + + def seed_static_data(self): + self.add_organisation('Deep Learning Indaba 2019', 'blah.png', 'blah_big.png') + self.add_organisation('Deep Learning Indaba 2020', 'blah.png', 'blah_big.png') + user_categories = [ + UserCategory('Honours'), + UserCategory('Student'), + UserCategory('MSc'), + UserCategory('PhD') + ] + db.session.add_all(user_categories) + db.session.commit() + + countries = [ + Country('Egypt'), + Country('Botswana'), + Country('Namibia'), + Country('Zimbabwe'), + Country('Mozambique'), + Country('Ghana'), + Country('Nigeria') + ] + db.session.add_all(countries) + db.session.commit() + + reviewer1 = AppUser('r1@r.com', 'reviewer', '1', 'Mr', password='abc', organisation_id=1,) + reviewer2 = AppUser('r2@r.com', 'reviewer', '2', 'Ms', password='abc', organisation_id=1,) + reviewer3 = AppUser('r3@r.com', 'reviewer', '3', 'Mr', password='abc', organisation_id=1,) + reviewer4 = AppUser('r4@r.com', 'reviewer', '4', 'Ms', password='abc', organisation_id=1,) + candidate1 = AppUser('c1@c.com', 'candidate', '1', 'Mr', password='abc', organisation_id=1,) + candidate2 = AppUser('c2@c.com', 'candidate', '2', 'Ms', password='abc', organisation_id=1,) + candidate3 = AppUser('c3@c.com', 'candidate', '3', 'Mr', password='abc', organisation_id=1,) + candidate4 = AppUser('c4@c.com', 'candidate', '4', 'Ms', password='abc', organisation_id=1,) + system_admin = AppUser('sa@sa.com', 'system_admin', '1', 'Ms', password='abc', organisation_id=1, is_admin=True) + event_admin = AppUser('ea@ea.com', 'event_admin', '1', 'Ms', password='abc',organisation_id=1) + users = [reviewer1, reviewer2, reviewer3, reviewer4, candidate1, candidate2, candidate3, candidate4, system_admin, event_admin] + for user in users: + user.verify() + db.session.add_all(users) + db.session.commit() + + events = [ + self.add_event({'en': 'indaba 2019'}, {'en': 'The Deep Learning Indaba 2019, Kenyatta University, Nairobi, Kenya '}, datetime(2019, 8, 25), datetime(2019, 8, 31), + 'KENYADABA2019'), + self.add_event({'en': 'indaba 2020'}, {'en': 'The Deep Learning Indaba 2018, Stellenbosch University, South Africa'}, datetime(2018, 9, 9), datetime(2018, 9, 15), + 'INDABA2020', 2) + ] + db.session.commit() + + event_roles = [ + EventRole('admin', 10, 1), + EventRole('reviewer', 3, 1) + ] + db.session.add_all(event_roles) + db.session.commit() + + application_forms = [ + self.create_application_form(1, True, False), + self.create_application_form(2, False, False) + ] + db.session.add_all(application_forms) + db.session.commit() + + sections = [ + Section(1, 1), + Section(2, 1) + ] + db.session.add_all(sections) + db.session.commit() + + options = [ + { + "value": "indaba-2017", + "label": "Yes, I attended the 2017 Indaba" + }, + { + "value": "indaba-2018", + "label": "Yes, I attended the 2018 Indaba" + }, + { + "value": "indaba-2017-2018", + "label": "Yes, I attended both Indabas" + }, + { + "value": "none", + "label": "No" + } + ] + questions = [ + Question(1, 1, 1, 'long_text'), + Question(1, 1, 2, 'long_text'), + Question(2, 2, 1, 'long_text'), + Question(2, 2, 2, 'long_text'), + Question(1, 1, 3, 'multi-choice') + ] + db.session.add_all(questions) + db.session.commit() + + self.add_question_translation(1, 'en', 'Question 1') + self.add_question_translation(2, 'en', 'Question 2') + self.add_question_translation(3, 'en', 'Question 3') + self.add_question_translation(4, 'en', 'Question 4') + self.add_question_translation(5, 'en', 'Did you attend the 2017 or 2018 Indaba', options=options) + + closed_review = ReviewForm(2, datetime(2018, 4, 30), 1, True) + closed_review.close() + inactive_review = ReviewForm(2, datetime(2018, 4, 30), 2, False) + review_forms = [ + ReviewForm(1, datetime(2019, 4, 30), 1, True), + ReviewForm(1, datetime(2019, 4, 30), 2, False), + closed_review, + inactive_review + ] + db.session.add_all(review_forms) + db.session.commit() + + review_configs = [ + ReviewConfiguration(review_form_id=review_forms[0].id, num_reviews_required=3, num_optional_reviews=0), + ReviewConfiguration(review_form_id=review_forms[1].id, num_reviews_required=3, num_optional_reviews=0) + ] + db.session.add_all(review_configs) + db.session.commit() + + review_section1 = self.add_review_section(review_forms[0].id) + self.add_review_section_translation(review_section1.id, 'en', 'Review Section 1 English', 'Review Section 1 Description English') + self.add_review_section_translation(review_section1.id, 'fr', 'Review Section 1 French', 'Review Section 1 Description French') + review_section2 = self.add_review_section(review_forms[1].id) + self.add_review_section_translation(review_section2.id, 'en', 'Review Section 2', 'Review Section 2 Description') + + review_questions = [ + ReviewQuestion(review_section1.id, 1, 'multi-choice', True, 1, 0), + ReviewQuestion(review_section1.id, 2, 'multi-choice', True, 2, 0), + ReviewQuestion(review_section2.id, 3, 'multi-choice', True, 1, 0), + ReviewQuestion(review_section2.id, 4, 'information', False, 2, 0) + ] + db.session.add_all(review_questions) + db.session.commit() + + review_question_translations = [ + ReviewQuestionTranslation(review_questions[0].id, 'en'), + ReviewQuestionTranslation(review_questions[0].id, 'fr'), + ReviewQuestionTranslation(review_questions[1].id, 'en', headline='English Headline', description='English Description', placeholder='English Placeholder', options=[{'label': 'en1', 'value': 'en'}], validation_regex='EN Regex', validation_text='EN Validation Message'), + ReviewQuestionTranslation(review_questions[1].id, 'fr', headline='French Headline', description='French Description', placeholder='French Placeholder', options=[{'label': 'fr1', 'value': 'fr'}], validation_regex='FR Regex', validation_text='FR Validation Message'), + ReviewQuestionTranslation(review_questions[2].id, 'en'), + ReviewQuestionTranslation(review_questions[2].id, 'fr'), + ReviewQuestionTranslation(review_questions[3].id, 'en'), + ReviewQuestionTranslation(review_questions[3].id, 'fr'), + ] + db.session.add_all(review_question_translations) + db.session.commit() + + self.add_email_template('reviews-assigned') + + def get_auth_header_for(self, email): + body = { + 'email': email, + 'password': 'abc' + } + response = self.app.post('api/v1/authenticate', data=body) + data = json.loads(response.data) + header = {'Authorization': data['token']} + return header + + def setup_one_reviewer_one_candidate(self, active=True): + response = self.add_response(1, 5, is_submitted=True) + + answers = [ + Answer(1, 1, 'I will learn alot.'), + Answer(1, 2, 'I will share by doing talks.') + ] + db.session.add_all(answers) + db.session.commit() + + response_reviewers = [ + ResponseReviewer(1, 1) # assign reviewer 1 to candidate 1 response + ] + if not active: + response_reviewers[0].deactivate() + db.session.add_all(response_reviewers) + db.session.commit() + + def test_one_reviewer_one_candidate(self): + self.seed_static_data() + self.setup_one_reviewer_one_candidate() + header = self.get_auth_header_for('r1@r.com') + params = {'event_id': 1, 'language': 'en'} + + response = self.app.get('/api/v1/review', headers=header, data=params) + data = json.loads(response.data) + + self.assertEqual(data['reviews_remaining_count'], 1) + + def test_one_reviewer_one_candidate_inactive(self): + self.seed_static_data() + self.setup_one_reviewer_one_candidate(active=False) + header = self.get_auth_header_for('r1@r.com') + params = {'event_id': 1, 'language': 'en'} + + response = self.app.get('/api/v1/review', headers=header, data=params) + data = json.loads(response.data) + + self.assertEqual(data['reviews_remaining_count'], 0) + self.assertEqual(data['response']['id'], 0) + + @parameterized.expand([ + (True,), (False,) + ]) + def test_one_reviewer_one_candidate_review_summary(self, active): + self.seed_static_data() + self.setup_one_reviewer_one_candidate(active=active) + header = self.get_auth_header_for('ea@ea.com') + params = {'event_id': 1} + + response = self.app.get('/api/v1/reviewassignment/summary', headers=header, data=params) + data = json.loads(response.data) + + self.assertEqual(data['reviews_unallocated'], 2) + + def setup_responses_and_no_reviewers(self): + response = self.add_response(1, 5, is_submitted=True) + + answers = [ + Answer(1, 1, 'I will learn alot.'), + Answer(1, 2, 'I will share by doing talks.') + ] + db.session.add_all(answers) + db.session.commit() + + def test_no_response_reviewers(self): + self.seed_static_data() + self.setup_responses_and_no_reviewers() + header = self.get_auth_header_for('r1@r.com') + params = {'event_id': 1, 'language': 'en'} + + response = self.app.get('/api/v1/review', headers=header, data=params) + data = json.loads(response.data) + + self.assertEqual(data['reviews_remaining_count'], 0) + + def test_no_response_reviewers_reviews_unallocated(self): + self.seed_static_data() + self.setup_responses_and_no_reviewers() + header = self.get_auth_header_for('ea@ea.com') + params = {'event_id': 1} + response = self.app.get('/api/v1/reviewassignment/summary', headers=header, data=params) + data = json.loads(response.data) + + self.assertEqual(data['reviews_unallocated'], 3) + + def setup_one_reviewer_three_candidates(self): + self.add_response(application_form_id=1, user_id=5, is_submitted=True) + self.add_response(application_form_id=1, user_id=6, is_submitted=True) + self.add_response(application_form_id=1, user_id=7, is_submitted=True) + + answers = [ + Answer(1, 1, 'I will learn alot.'), + Answer(1, 2, 'I will share by doing talks.'), + Answer(2, 1, 'I want to do a PhD.'), + Answer(2, 2, 'I will share by writing a blog.'), + Answer(3, 1, 'I want to solve new problems.'), + Answer(3, 2, 'I will share by tutoring.'), + ] + db.session.add_all(answers) + db.session.commit() + + response_reviewers = [ + ResponseReviewer(1, 1), + ResponseReviewer(2, 1), + ResponseReviewer(3, 1) + ] + response_reviewers[1].deactivate() + + db.session.add_all(response_reviewers) + db.session.commit() + + def test_one_reviewer_three_candidates(self): + self.seed_static_data() + self.setup_one_reviewer_three_candidates() + header = self.get_auth_header_for('r1@r.com') + params = {'event_id': 1, 'language': 'en'} + + response = self.app.get('/api/v1/review', headers=header, data=params) + data = json.loads(response.data) + + self.assertEqual(data['reviews_remaining_count'], 2) + + def setup_one_reviewer_three_candidates_and_one_completed_review(self): + self.add_response(1, 5, is_submitted=True) + self.add_response(1, 6, is_submitted=True) + self.add_response(1, 7, is_submitted=True) + + answers = [ + Answer(1, 1, 'I will learn alot.'), + Answer(1, 2, 'I will share by doing talks.'), + Answer(2, 1, 'I want to do a PhD.'), + Answer(2, 2, 'I will share by writing a blog.'), + Answer(3, 1, 'I want to solve new problems.'), + Answer(3, 2, 'I will share by tutoring.') + ] + db.session.add_all(answers) + db.session.commit() + + response_reviewers = [ + ResponseReviewer(1, 1), + ResponseReviewer(2, 1), + ResponseReviewer(3, 1) + ] + db.session.add_all(response_reviewers) + db.session.commit() + + review_response = ReviewResponse(1, 1, 1, 'en') + review_response.submit() + db.session.add(review_response) + db.session.commit() + + def test_one_reviewer_three_candidates_and_one_completed_review(self): + self.seed_static_data() + self.setup_one_reviewer_three_candidates_and_one_completed_review() + header = self.get_auth_header_for('r1@r.com') + params = {'event_id': 1, 'language': 'en'} + + response = self.app.get('/api/v1/review', headers=header, data=params) + data = json.loads(response.data) + + self.assertEqual(data['reviews_remaining_count'], 2) + + def setup_one_reviewer_three_candidates_with_one_withdrawn_response_and_one_unsubmitted_response(self): + withdrawn_response = self.add_response(1, 5, is_withdrawn=True) + submitted_response = self.add_response(1, 7, is_submitted=True) + self.add_response(1, 6) + + answers = [ + Answer(1, 1, 'I will learn alot.'), + Answer(1, 2, 'I will share by doing talks.'), + Answer(2, 1, 'I want to do a PhD.'), + Answer(2, 2, 'I will share by writing a blog.'), + Answer(3, 1, 'I want to solve new problems.'), + Answer(3, 2, 'I will share by tutoring.') + ] + db.session.add_all(answers) + db.session.commit() + + response_reviewers = [ + ResponseReviewer(1, 1), + ResponseReviewer(2, 1), + ResponseReviewer(3, 1) + ] + db.session.add_all(response_reviewers) + db.session.commit() + + def test_one_reviewer_three_candidates_with_one_withdrawn_response_and_one_unsubmitted_response(self): + self.seed_static_data() + self.setup_one_reviewer_three_candidates_with_one_withdrawn_response_and_one_unsubmitted_response() + header = self.get_auth_header_for('r1@r.com') + params = {'event_id': 1, 'language': 'en'} + + response = self.app.get('/api/v1/review', headers=header, data=params) + data = json.loads(response.data) + + self.assertEqual(data['reviews_remaining_count'], 1) + + def setup_multiple_reviewers_with_different_subsets_of_candidates_and_reviews_completed(self): + self.add_response(1, 5, is_submitted=True) + self.add_response(1, 6, is_submitted=True) + self.add_response(1, 7, is_submitted=True) + self.add_response(1, 8, is_submitted=True) + + answers = [ + Answer(1, 1, 'I will learn alot.'), + Answer(1, 2, 'I will share by doing talks.'), + Answer(2, 1, 'I want to do a PhD.'), + Answer(2, 2, 'I will share by writing a blog.'), + Answer(3, 1, 'I want to solve new problems.'), + Answer(3, 2, 'I will share by tutoring.'), + Answer(4, 1, 'I want to exchange ideas with like minded people'), + Answer(4, 2, 'I will mentor people interested in ML.') + ] + db.session.add_all(answers) + db.session.commit() + + response_reviewers = [ + ResponseReviewer(1, 1), + ResponseReviewer(2, 1), + ResponseReviewer(3, 1), + + ResponseReviewer(2, 2), + ResponseReviewer(3, 2), + + ResponseReviewer(1, 3), + ResponseReviewer(2, 3), + ResponseReviewer(3, 3), + ResponseReviewer(4, 3), + + ResponseReviewer(1, 4), + ResponseReviewer(2, 4) + ] + response_reviewers[-1].deactivate() + db.session.add_all(response_reviewers) + db.session.commit() + + review_responses = [ + ReviewResponse(1, 2, 2, 'en'), + ReviewResponse(1, 3, 1, 'en'), + ReviewResponse(1, 3, 2, 'en'), + ReviewResponse(1, 4, 1, 'en') + ] + for rr in review_responses: + rr.submit() + + db.session.add_all(review_responses) + db.session.commit() + + def test_multiple_reviewers_with_different_subsets_of_candidates_and_reviews_completed(self): + self.seed_static_data() + self.setup_multiple_reviewers_with_different_subsets_of_candidates_and_reviews_completed() + params = {'event_id': 1, 'language': 'en'} + + header = self.get_auth_header_for('r1@r.com') + response1 = self.app.get('/api/v1/review', headers=header, data=params) + data1 = json.loads(response1.data) + header = self.get_auth_header_for('r2@r.com') + response2 = self.app.get('/api/v1/review', headers=header, data=params) + data2 = json.loads(response2.data) + header = self.get_auth_header_for('r3@r.com') + response3 = self.app.get('/api/v1/review', headers=header, data=params) + data3 = json.loads(response3.data) + header = self.get_auth_header_for('r4@r.com') + response4 = self.app.get('/api/v1/review', headers=header, data=params) + data4 = json.loads(response4.data) + + self.assertEqual(data1['reviews_remaining_count'], 3) + self.assertEqual(data2['reviews_remaining_count'], 1) + self.assertEqual(data3['reviews_remaining_count'], 2) + self.assertEqual(data4['reviews_remaining_count'], 0) + + def test_skipping(self): + self.seed_static_data() + self.setup_one_reviewer_three_candidates() + params = {'event_id': 1, 'skip': 1, 'language': 'en'} + header = self.get_auth_header_for('r1@r.com') + + response = self.app.get('/api/v1/review', headers=header, data=params) + data = json.loads(response.data) + + self.assertEqual(data['response']['user_id'], 7) + self.assertEqual(data['response']['answers'][0]['value'], 'I want to solve new problems.') + + def test_high_skip_defaults_to_last_review(self): + self.seed_static_data() + self.setup_one_reviewer_three_candidates() + params = {'event_id': 1, 'skip': 5, 'language': 'en'} + header = self.get_auth_header_for('r1@r.com') + + response = self.app.get('/api/v1/review', headers=header, data=params) + data = json.loads(response.data) + + self.assertEqual(data['response']['user_id'], 7) + self.assertEqual(data['response']['answers'][1]['value'], 'I will share by tutoring.') + + def setup_candidate_who_has_applied_to_multiple_events(self): + user_id = 5 + + self.add_response(application_form_id=1, user_id=user_id, is_submitted=True) + self.add_response(application_form_id=2, user_id=user_id, is_submitted=True) + + answers = [ + Answer(1, 1, 'I will learn alot.'), + Answer(1, 2, 'I will share by doing talks.'), + Answer(2, 3, 'Yes I worked on a vision task.'), + Answer(2, 4, 'Yes I want the travel award.') + ] + db.session.add_all(answers) + db.session.commit() + + response_reviewers = [ + ResponseReviewer(1, 1), + ResponseReviewer(2, 1) + ] + db.session.add_all(response_reviewers) + db.session.commit() + + def test_filtering_on_event_when_candidate_has_applied_to_more_than(self): + self.seed_static_data() + self.setup_candidate_who_has_applied_to_multiple_events() + params = {'event_id': 2, 'language': 'en'} + header = self.get_auth_header_for('r1@r.com') + + response = self.app.get('/api/v1/review', headers=header, data=params) + data = json.loads(response.data) + + self.assertEqual(data['reviews_remaining_count'], 1) + self.assertEqual(data['response']['user_id'], 5) + self.assertEqual(data['response']['answers'][0]['value'], 'Yes I worked on a vision task.') + + def setup_multi_choice_answer(self): + self.add_response(1, 5, is_submitted=True) + + answer = Answer(1, 5, 'indaba-2017') + db.session.add(answer) + db.session.commit() + + response_reviewer = ResponseReviewer(1, 1) + db.session.add(response_reviewer) + db.session.commit() + + def test_multi_choice_answers_use_label_instead_of_value(self): + self.seed_static_data() + self.setup_multi_choice_answer() + params = {'event_id': 1, 'language': 'en'} + header = self.get_auth_header_for('r1@r.com') + + response = self.app.get('/api/v1/review', headers=header, data=params) + data = json.loads(response.data) + print(data) + + self.assertEqual(data['response']['answers'][0]['value'], 'Yes, I attended the 2017 Indaba') + + def test_review_response_not_found(self): + self.seed_static_data() + params = {'id': 55, 'language': 'en'} + header = self.get_auth_header_for('r1@r.com') + + response = self.app.get('/api/v1/reviewresponse', headers=header, data=params) + data = json.loads(response.data) + + self.assertEqual(response.status_code, REVIEW_RESPONSE_NOT_FOUND[1]) + + def setup_review_response(self): + self.add_response(1, 5, is_submitted=True) + + answer = Answer(1, 1, 'To learn alot') + db.session.add(answer) + db.session.commit() + + self.review_response = ReviewResponse(1, 1, 1, 'en') + self.review_response.review_scores.append(ReviewScore(1, 'answer1')) + self.review_response.review_scores.append(ReviewScore(2, 'answer2')) + db.session.add(self.review_response) + db.session.commit() + + db.session.flush() + + def test_review_response(self): + self.seed_static_data() + self.setup_review_response() + params = {'id': self.review_response.id, 'language': 'en'} + header = self.get_auth_header_for('r1@r.com') + + response = self.app.get('/api/v1/reviewresponse', headers=header, data=params) + data = json.loads(response.data) + + print(data) + + self.assertEqual(data['review_form']['id'], 1) + self.assertEqual(data['review_response']['reviewer_user_id'], 1) + self.assertEqual(data['review_response']['response_id'], 1) + self.assertEqual(data['review_response']['scores'][0]['value'], 'answer1') + self.assertEqual(data['review_response']['scores'][1]['value'], 'answer2') + + def test_prevent_saving_review_response_reviewer_was_not_assigned_to_response(self): + self.seed_static_data() + params = json.dumps({'review_form_id': 1, 'response_id': 1, 'scores': [{'review_question_id': 1, 'value': 'test_answer'}], 'language': 'en', 'is_submitted': False}) + header = self.get_auth_header_for('r1@r.com') + + response = self.app.post('/api/v1/reviewresponse', headers=header, data=params, content_type='application/json') + + self.assertEqual(response.status_code, FORBIDDEN[1]) + + def test_can_still_submit_inactive_response_reviewer(self): + self.seed_static_data() + self.setup_one_reviewer_three_candidates() + params = json.dumps({'review_form_id': 1, 'response_id': 2, 'scores': [{'review_question_id': 1, 'value': 'test_answer'}], 'language': 'en', 'is_submitted': True}) + header = self.get_auth_header_for('r1@r.com') + + response = self.app.post('/api/v1/reviewresponse', headers=header, data=params, content_type='application/json') + + self.assertEqual(response.status_code, 201) + + def setup_response_reviewer(self): + self.add_response(1, 5, is_submitted=True) + + response_reviewer = ResponseReviewer(1, 1) + db.session.add(response_reviewer) + db.session.commit() + + def test_saving_review_response(self): + self.seed_static_data() + self.setup_response_reviewer() + params = json.dumps({'review_form_id': 1, 'response_id': 1, 'scores': [{'review_question_id': 1, 'value': 'test_answer'}], 'language': 'en', 'is_submitted': False}) + header = self.get_auth_header_for('r1@r.com') + + response = self.app.post('/api/v1/reviewresponse', headers=header, data=params, content_type='application/json') + + review_scores = db.session.query(ReviewScore).filter_by(review_response_id=1, is_active=True).all() + self.assertEqual(response.status_code, 201) + self.assertEqual(len(review_scores), 1) + self.assertEqual(review_scores[0].value, 'test_answer') + + def setup_existing_review_response(self): + self.add_response(1, 5, is_submitted=True) + + response_reviewer = ResponseReviewer(1, 1) + db.session.add(response_reviewer) + db.session.commit() + + review_response = ReviewResponse(1, 1, 1, 'en') + review_response.submit() + review_response.review_scores = [ReviewScore(1, 'test_answer1'), ReviewScore(2, 'test_answer2')] + db.session.add(review_response) + db.session.commit() + + def test_updating_review_response(self): + self.seed_static_data() + self.setup_existing_review_response() + params = json.dumps({'review_form_id': 1, 'response_id': 1, 'scores': [{'review_question_id': 1, 'value': 'test_answer3'}, {'review_question_id': 2, 'value': 'test_answer4'}], 'language': 'en', 'is_submitted': True}) + header = self.get_auth_header_for('r1@r.com') + + response = self.app.put('/api/v1/reviewresponse', headers=header, data=params, content_type='application/json') + + review_scores = db.session.query(ReviewScore).filter_by(review_response_id=1, is_active=True).order_by(ReviewScore.review_question_id).all() + self.assertEqual(response.status_code, 200) + self.assertEqual(len(review_scores), 2) + self.assertEqual(review_scores[0].value, 'test_answer3') + self.assertEqual(review_scores[1].value, 'test_answer4') + + def test_user_cant_assign_responsesreviewer_without_system_or_event_admin_role(self): + self.seed_static_data() + params = {'event_id': 1, 'reviewer_user_email': 'r2@r.com', 'num_reviews': 10} + header = self.get_auth_header_for('c1@c.com') + + response = self.app.post('/api/v1/reviewassignment', headers=header, data=params) + + self.assertEqual(response.status_code, FORBIDDEN[1]) + + def test_reviewer_not_found(self): + self.seed_static_data() + params = {'event_id': 1, 'reviewer_user_email': 'non_existent@user.com', 'num_reviews': 10} + header = self.get_auth_header_for('sa@sa.com') + + response = self.app.post('/api/v1/reviewassignment', headers=header, data=params) + + self.assertEqual(response.status_code, USER_NOT_FOUND[1]) + + def test_add_reviewer_with_no_roles(self): + self.seed_static_data() + params = {'event_id': 1, 'reviewer_user_email': 'r1@r.com', 'num_reviews': 10} + header = self.get_auth_header_for('ea@ea.com') + + response = self.app.post('/api/v1/reviewassignment', headers=header, data=params) + + event_roles = db.session.query(EventRole).filter_by(user_id=1, event_id=1).all() + self.assertEqual(len(event_roles), 1) + self.assertEqual(event_roles[0].role, 'reviewer') + + def test_add_reviewer_with_a_role(self): + self.seed_static_data() + params = {'event_id': 1, 'reviewer_user_email': 'ea@ea.com', 'num_reviews': 10} + header = self.get_auth_header_for('sa@sa.com') + + response = self.app.post('/api/v1/reviewassignment', headers=header, data=params) + + event_roles = db.session.query(EventRole).filter_by(user_id=10, event_id=1).order_by(EventRole.id).all() + self.assertEqual(len(event_roles), 2) + self.assertEqual(event_roles[0].role, 'admin') + self.assertEqual(event_roles[1].role, 'reviewer') + + def setup_responses_without_reviewers(self): + self.add_response(1, 5, is_submitted=True) + self.add_response(1, 6, is_submitted=True) + self.add_response(1, 7, is_submitted=True) + self.add_response(1, 8, is_submitted=True) + + def test_adding_first_reviewer(self): + self.seed_static_data() + self.setup_responses_without_reviewers() + params = {'event_id': 1, 'reviewer_user_email': 'r3@r.com', 'num_reviews': 4} + header = self.get_auth_header_for('ea@ea.com') + + response = self.app.post('/api/v1/reviewassignment', headers=header, data=params) + response_reviewers = db.session.query(ResponseReviewer).filter_by(reviewer_user_id=3).all() + self.assertEqual(response.status_code, 201) + self.assertEqual(len(response_reviewers), 4) + + def test_limit_of_num_reviews(self): + self.seed_static_data() + self.setup_responses_without_reviewers() + params = {'event_id': 1, 'reviewer_user_email': 'r3@r.com', 'num_reviews': 3} + header = self.get_auth_header_for('ea@ea.com') + + response = self.app.post('/api/v1/reviewassignment', headers=header, data=params) + + response_reviewers = db.session.query(ResponseReviewer).filter_by(reviewer_user_id=3).all() + self.assertEqual(len(response_reviewers), 3) + + def setup_reviewer_with_own_response(self): + self.add_response(1, 3, is_submitted=True) # reviewer + self.add_response(1, 5, is_submitted=True) # someone else + + def test_reviewer_does_not_get_assigned_to_own_response(self): + self.seed_static_data() + self.setup_reviewer_with_own_response() + params = {'event_id': 1, 'reviewer_user_email': 'r3@r.com', 'num_reviews': 3} + header = self.get_auth_header_for('ea@ea.com') + + response = self.app.post('/api/v1/reviewassignment', headers=header, data=params) + + response_reviewers = db.session.query(ResponseReviewer).filter_by(reviewer_user_id=3).all() + self.assertEqual(len(response_reviewers), 1) + self.assertEqual(response_reviewers[0].response_id, 2) + + def setup_withdrawn_and_unsubmitted_responses(self): + self.add_response(1, 5) + self.add_response(1, 6, is_withdrawn=True) + self.add_response(1, 7, is_submitted=True) + + def test_withdrawn_and_unsubmitted_responses_are_not_assigned_reviewers(self): + self.seed_static_data() + self.setup_withdrawn_and_unsubmitted_responses() + params = {'event_id': 1, 'reviewer_user_email': 'r3@r.com', 'num_reviews': 3} + header = self.get_auth_header_for('ea@ea.com') + + response = self.app.post('/api/v1/reviewassignment', headers=header, data=params) + + response_reviewers = db.session.query(ResponseReviewer).filter_by(reviewer_user_id=3).all() + self.assertEqual(len(response_reviewers), 1) + self.assertEqual(response_reviewers[0].response_id, 3) + + def setup_response_with_three_reviewers(self): + response = self.add_response(1, 5, is_submitted=True) + + response_reviewers = [ + ResponseReviewer(1, 1), + ResponseReviewer(1, 2), + ResponseReviewer(1, 4) + ] + db.session.add_all(response_reviewers) + db.session.commit() + + def test_response_with_three_reviewers_does_not_get_assigned_another_reviewer(self): + self.seed_static_data() + self.setup_response_with_three_reviewers() + params = {'event_id': 1, 'reviewer_user_email': 'r3@r.com', 'num_reviews': 3} + header = self.get_auth_header_for('ea@ea.com') + + response = self.app.post('/api/v1/reviewassignment', headers=header, data=params) + + response_reviewers = db.session.query(ResponseReviewer).filter_by(reviewer_user_id=3).all() + self.assertEqual(len(response_reviewers), 0) + + def setup_responsereview_with_different_reviewer(self): + self.add_response(1, 5, is_submitted=True) + + response_reviewer = ResponseReviewer(1, 1) + db.session.add(response_reviewer) + db.session.commit() + + def test_response_will_get_multiple_reviewers_assigned(self): + self.seed_static_data() + self.setup_responsereview_with_different_reviewer() + params = {'event_id': 1, 'reviewer_user_email': 'r3@r.com', 'num_reviews': 3} + header = self.get_auth_header_for('ea@ea.com') + + response = self.app.post('/api/v1/reviewassignment', headers=header, data=params) + response_reviewers = db.session.query(ResponseReviewer).order_by(ResponseReviewer.reviewer_user_id).all() + + self.assertEqual(len(response_reviewers), 2) + self.assertEqual(response_reviewers[0].reviewer_user_id, 1) + self.assertEqual(response_reviewers[1].reviewer_user_id, 3) + + def setup_reviewer_is_not_assigned_to_response_more_than_once(self): + self.add_response(1, 5, is_submitted=True) + + def setup_count_reviews_allocated_and_completed(self): + db.session.add_all([ + EventRole('reviewer', 1, 1), + EventRole('reviewer', 2, 1), + EventRole('reviewer', 3, 1), + EventRole('reviewer', 4, 1) + ]) + + self.add_response(1, 5, is_submitted=True) #1 + self.add_response(1, 6, is_submitted=True) #2 + self.add_response(1, 7, is_submitted=True) #3 + self.add_response(1, 8, is_submitted=True) #4 + self.add_response(2, 5, is_submitted=True) #5 + self.add_response(2, 6, is_submitted=True) #6 + + response_reviewers = [ + ResponseReviewer(1, 2), + ResponseReviewer(2, 2), + ResponseReviewer(3, 2), + ResponseReviewer(4, 2), + ResponseReviewer(6, 2), + + ResponseReviewer(2, 3), + ResponseReviewer(4, 3), + + ResponseReviewer(3, 4), + + ResponseReviewer(5, 1), + + ] + + + db.session.add_all(response_reviewers) + # review form, review_user_id, response_id + review_responses = [ + ReviewResponse(1, 3, 2, 'en'), + ReviewResponse(1, 3, 4, 'en'), + ReviewResponse(1, 2, 1, 'en'), + ReviewResponse(1, 2, 3, 'en'), + ReviewResponse(1, 2, 4, 'en'), + ReviewResponse(2, 1, 5, 'en'), + ReviewResponse(2, 2, 6, 'en') + ] + db.session.add_all(review_responses) + + db.session.commit() + + # response 1 - 1 review assigned - 1 complete + # response 2 - 2 reviews - 1 complete + # response 3 - 2 reviews - 1 complete + # response 4 - 2 reviews - 1 complete + # response 5 - 1 review - 1 complete + # response 6 - 1 review - 1 complete + + # reviewer 1 - 1 review assigned (1 from event 2) - 1 complete + # reviewer 2 - 5 reviews assigned (1 from event 2)- 3 complete + # reviewer 3 - 2 reviews assigned - 2 complete + # reviewer 4 - 1 review assigned - none complete + + # total assigned reviews: 9 + # total required review = 6*3 = 18 + # total unallocated: 18 - 9 = 9 + # total completed reviews: 6 + + @SkipTest + def test_count_reviews_allocated_and_completed(self): + self.seed_static_data() + self.setup_count_reviews_allocated_and_completed() + header = self.get_auth_header_for('ea@ea.com') + params = {'event_id': 1} + + response = self.app.get('/api/v1/reviewassignment', headers=header, data=params) + + data = json.loads(response.data) + data = sorted(data, key=lambda k: k['email']) + LOGGER.debug(data) + self.assertEqual(len(data),3) + self.assertEqual(data[0]['email'], 'r2@r.com') + self.assertEqual(data[0]['reviews_allocated'], 4) + self.assertEqual(data[0]['reviews_completed'], 3) + self.assertEqual(data[1]['email'], 'r3@r.com') + self.assertEqual(data[1]['reviews_allocated'], 2) + self.assertEqual(data[1]['reviews_completed'], 2) + self.assertEqual(data[2]['email'], 'r4@r.com') + self.assertEqual(data[2]['reviews_allocated'], 1) + self.assertEqual(data[2]['reviews_completed'], 0) + + def test_reviewer_is_not_assigned_to_response_more_than_once(self): + self.seed_static_data() + self.setup_reviewer_is_not_assigned_to_response_more_than_once() + params = {'event_id': 1, 'reviewer_user_email': 'r3@r.com', 'num_reviews': 3} + header = self.get_auth_header_for('ea@ea.com') + + response = self.app.post('/api/v1/reviewassignment', headers=header, data=params) + response2 = self.app.post('/api/v1/reviewassignment', headers=header, data=params) + response_reviewers = db.session.query(ResponseReviewer).all() + + self.assertEqual(len(response_reviewers), 1) + + def setup_reviewer_responses_finalverdict_reviewquestion_reviewresponses_and_scores(self): + second_reviewer = EventRole('reviewer', 2, 1) + db.session.add(second_reviewer) + db.session.commit() + + users_id = [5,6,7] + self.add_response(application_form_id=1, user_id=users_id[0], is_submitted=True) + self.add_response(application_form_id=1, user_id=users_id[1], is_submitted=True) + self.add_response(application_form_id=1, user_id=users_id[2], is_submitted=True) + + final_verdict_options = [ + {'label': 'Yes', 'value': 2}, + {'label': 'No', 'value': 0}, + {'label': 'Maybe', 'value': 1}, + ] + verdict_question = ReviewQuestion(1, None, 'multi-choice', True, 3, 0) + db.session.add(verdict_question) + db.session.commit() + verdict_question_translation = ReviewQuestionTranslation(verdict_question.id, 'en', headline='Final Verdict', options=final_verdict_options) + db.session.add(verdict_question_translation) + db.session.commit() + + review_responses = [ + ReviewResponse(1,3,1, 'en'), + ReviewResponse(1,3,2, 'en'), + ReviewResponse(1,2,1, 'en'), + ReviewResponse(1,2,2, 'en'), + ReviewResponse(1,3,3, 'en') + ] + review_responses[0].review_scores = [ReviewScore(1, '23'), ReviewScore(5, '1')] + review_responses[1].review_scores = [ReviewScore(1, '55'), ReviewScore(5, '2')] + review_responses[2].review_scores = [ReviewScore(1, '45'), ReviewScore(2, '67'), ReviewScore(5, 'No')] + review_responses[3].review_scores = [ReviewScore(1, '220'), ReviewScore(5, '2')] + review_responses[4].review_scores = [ReviewScore(1, '221'), ReviewScore(5, '1')] + db.session.add_all(review_responses) + db.session.commit() + + return users_id + + + def test_review_history_returned(self): + self.seed_static_data() + users_id = self.setup_reviewer_responses_finalverdict_reviewquestion_reviewresponses_and_scores() + + params ={'event_id' : 1, 'page_number' : 0, 'limit' : 10, 'sort_column' : 'review_response_id'} + header = self.get_auth_header_for('r3@r.com') + + response = self.app.get('/api/v1/reviewhistory', headers=header, data=params) + data = json.loads(response.data) + + self.assertEqual(len(data['reviews']), 3) + self.assertEqual(data['num_entries'], 3) + + self.assertEqual(data['reviews'][0]['review_response_id'], 1) + self.assertEqual(data['reviews'][0]['reviewed_user_id'], str(users_id[0])) + + self.assertEqual(data['reviews'][1]['review_response_id'], 2) + self.assertEqual(data['reviews'][1]['reviewed_user_id'], str(users_id[1])) + + self.assertEqual(data['reviews'][2]['review_response_id'], 5) + self.assertEqual(data['reviews'][2]['reviewed_user_id'], str(users_id[2])) + + def test_brings_back_only_logged_in_reviewer_reviewresponses(self): + self.seed_static_data() + self.setup_reviewer_responses_finalverdict_reviewquestion_reviewresponses_and_scores() + + params ={'event_id' : 1, 'page_number' : 0, 'limit' : 10, 'sort_column' : 'review_response_id'} + header = self.get_auth_header_for('r2@r.com') + + response = self.app.get('/api/v1/reviewhistory', headers=header, data=params) + data = json.loads(response.data) + + self.assertEqual(len(data['reviews']), 2) + self.assertEqual(data['reviews'][0]['review_response_id'], 3) + self.assertEqual(data['reviews'][1]['review_response_id'], 4) + + def test_logged_in_user_not_reviewer(self): + self.seed_static_data() + self.setup_reviewer_responses_finalverdict_reviewquestion_reviewresponses_and_scores() + + params ={'event_id' : 1, 'page_number' : 0, 'limit' : 10, 'sort_column' : 'review_response_id'} + header = self.get_auth_header_for('c1@c.com') + + response = self.app.get('/api/v1/reviewhistory', headers=header, data=params) + + self.assertEqual(response.status_code, FORBIDDEN[1]) + + def setup_reviewer_with_no_reviewresponses(self): + reviewer = EventRole('reviewer', 1, 1) + db.session.add(reviewer) + db.session.commit() + + def test_reviewer_with_no_reviewresponses(self): + self.seed_static_data() + self.setup_reviewer_responses_finalverdict_reviewquestion_reviewresponses_and_scores() + self.setup_reviewer_with_no_reviewresponses() + + params ={'event_id' : 1, 'page_number' : 0, 'limit' : 10, 'sort_column' : 'review_response_id'} + header = self.get_auth_header_for('r1@r.com') + + response = self.app.get('/api/v1/reviewhistory', headers=header, data=params) + data = json.loads(response.data) + + self.assertEqual(data['num_entries'], 0) + self.assertEqual(data['reviews'], []) + + def test_order_by_reviewresponseid(self): + self.seed_static_data() + self.setup_reviewer_responses_finalverdict_reviewquestion_reviewresponses_and_scores() + + params ={'event_id' : 1, 'page_number' : 0, 'limit' : 10, 'sort_column' : 'review_response_id'} + header = self.get_auth_header_for('r3@r.com') + + response = self.app.get('/api/v1/reviewhistory', headers=header, data=params) + data = json.loads(response.data) + + self.assertEqual(data['reviews'][0]['review_response_id'], 1) + self.assertEqual(data['reviews'][1]['review_response_id'], 2) + self.assertEqual(data['reviews'][2]['review_response_id'], 5) + + def setup_reviewresponses_with_unordered_timestamps(self): + final_verdict_options = [ + {'label': 'Yes', 'value': 2}, + {'label': 'No', 'value': 0}, + {'label': 'Maybe', 'value': 1}, + ] + + verdict_question = ReviewQuestion(1, None, 'multi-choice', True, 3, 0) + db.session.add(verdict_question) + db.session.commit() + verdict_question_translation = ReviewQuestionTranslation(verdict_question.id, 'en', headline='Final Verdict', options=final_verdict_options) + db.session.add(verdict_question_translation) + db.session.commit() + + self.add_response(1, 5, is_submitted=True) + self.add_response(1, 6, is_submitted=True) + self.add_response(1, 7, is_submitted=True) + + review_response_1 = ReviewResponse(1,3,1, 'en') + review_response_2 = ReviewResponse(1,3,2, 'en') + review_response_3 = ReviewResponse(1,3,3, 'en') + review_response_1.submitted_timestamp = datetime(2019, 1, 1) + review_response_2.submitted_timestamp = datetime(2018, 1, 1) + review_response_3.submitted_timestamp = datetime(2018, 6, 6) + review_responses = [review_response_1, review_response_2, review_response_3] + review_responses[0].review_scores = [ReviewScore(1, '67'), ReviewScore(5, 'Yes')] + review_responses[1].review_scores = [ReviewScore(1, '23'), ReviewScore(5, 'Yes')] + review_responses[2].review_scores = [ReviewScore(1, '53'), ReviewScore(5, 'Yes')] + db.session.add_all(review_responses) + db.session.commit() + + def test_order_by_submittedtimestamp(self): + self.seed_static_data() + self.setup_reviewresponses_with_unordered_timestamps() + + params ={'event_id' : 1, 'page_number' : 0, 'limit' : 10, 'sort_column' : 'submitted_timestamp'} + header = self.get_auth_header_for('r3@r.com') + + response = self.app.get('/api/v1/reviewhistory', headers=header, data=params) + data = json.loads(response.data) + LOGGER.debug(data) + self.assertEqual(data['reviews'][0]['submitted_timestamp'], '2018-01-01T00:00:00') + self.assertEqual(data['reviews'][1]['submitted_timestamp'], '2018-06-06T00:00:00') + self.assertEqual(data['reviews'][2]['submitted_timestamp'], '2019-01-01T00:00:00') + + # TODO re-add these tests once we can get the info outside of AppUser + @SkipTest + def test_order_by_nationalitycountry(self): + self.seed_static_data() + self.setup_reviewer_responses_finalverdict_reviewquestion_reviewresponses_and_scores() + + params ={'event_id' : 1, 'page_number' : 0, 'limit' : 10, 'sort_column' : 'nationality_country'} + header = self.get_auth_header_for('r3@r.com') + + response = self.app.get('/api/v1/reviewhistory', headers=header, data=params) + data = json.loads(response.data) + + self.assertEqual(data['reviews'][0]['nationality_country'], 'Botswana') + self.assertEqual(data['reviews'][1]['nationality_country'], 'South Africa') + self.assertEqual(data['reviews'][2]['nationality_country'], 'Zimbabwe') + + # TODO re-add these tests once we can get the info outside of AppUser + @SkipTest + def test_order_by_residencecountry(self): + self.seed_static_data() + self.setup_reviewer_responses_finalverdict_reviewquestion_reviewresponses_and_scores() + + params ={'event_id' : 1, 'page_number' : 0, 'limit' : 10, 'sort_column' : 'residence_country'} + header = self.get_auth_header_for('r3@r.com') + + response = self.app.get('/api/v1/reviewhistory', headers=header, data=params) + data = json.loads(response.data) + + self.assertEqual(data['reviews'][0]['residence_country'], 'Egypt') + self.assertEqual(data['reviews'][1]['residence_country'], 'Mozambique') + self.assertEqual(data['reviews'][2]['residence_country'], 'Namibia') + + # TODO re-add these tests once we can get the info outside of AppUser + @SkipTest + def test_order_by_affiliation(self): + self.seed_static_data() + self.setup_reviewer_responses_finalverdict_reviewquestion_reviewresponses_and_scores() + + params ={'event_id' : 1, 'page_number' : 0, 'limit' : 10, 'sort_column' : 'affiliation'} + header = self.get_auth_header_for('r3@r.com') + + response = self.app.get('/api/v1/reviewhistory', headers=header, data=params) + data = json.loads(response.data) + + self.assertEqual(data['reviews'][0]['affiliation'], 'RU') + self.assertEqual(data['reviews'][1]['affiliation'], 'UFH') + self.assertEqual(data['reviews'][2]['affiliation'], 'UWC') + + # TODO re-add these tests once we can get the info outside of AppUser + @SkipTest + def test_order_by_department(self): + self.seed_static_data() + self.setup_reviewer_responses_finalverdict_reviewquestion_reviewresponses_and_scores() + + params ={'event_id' : 1, 'page_number' : 0, 'limit' : 10, 'sort_column' : 'department'} + header = self.get_auth_header_for('r3@r.com') + + response = self.app.get('/api/v1/reviewhistory', headers=header, data=params) + data = json.loads(response.data) + + self.assertEqual(data['reviews'][0]['department'], 'CS') # ascii ordering orders capital letters before lowercase + self.assertEqual(data['reviews'][1]['department'], 'Chem') + self.assertEqual(data['reviews'][2]['department'], 'Phys') + + @SkipTest + def test_order_by_usercategory(self): + self.seed_static_data() + self.setup_reviewer_responses_finalverdict_reviewquestion_reviewresponses_and_scores() + + params ={'event_id' : 1, 'page_number' : 0, 'limit' : 10, 'sort_column' : 'user_category'} + header = self.get_auth_header_for('r3@r.com') + + response = self.app.get('/api/v1/reviewhistory', headers=header, data=params) + data = json.loads(response.data) + + self.assertEqual(data['reviews'][0]['user_category'], 'Honours') + self.assertEqual(data['reviews'][1]['user_category'], 'MSc') + self.assertEqual(data['reviews'][2]['user_category'], 'Student') + + def setup_two_extra_responses_for_reviewer3(self): + self.add_response(1, 8, is_submitted=True) + self.add_response(1, 1, is_submitted=True) + + review_responses = [ + ReviewResponse(1,3,4, 'en'), + ReviewResponse(1,3,5, 'en') + ] + review_responses[0].review_scores = [ReviewScore(1, '89'), ReviewScore(5, 'Maybe')] + review_responses[1].review_scores = [ReviewScore(1, '75'), ReviewScore(5, 'Yes')] + db.session.add_all(review_responses) + db.session.commit() + + def test_first_page_in_pagination(self): + self.seed_static_data() + self.setup_reviewer_responses_finalverdict_reviewquestion_reviewresponses_and_scores() + self.setup_two_extra_responses_for_reviewer3() + + params ={'event_id' : 1, 'page_number' : 0, 'limit' : 2, 'sort_column' : 'review_response_id'} + header = self.get_auth_header_for('r3@r.com') + + response = self.app.get('/api/v1/reviewhistory', headers=header, data=params) + data = json.loads(response.data) + + self.assertEqual(len(data['reviews']), 2) + self.assertEqual(data['num_entries'], 5) + + self.assertEqual(data['reviews'][0]['review_response_id'], 1) + self.assertEqual(data['reviews'][1]['review_response_id'], 2) + + def test_middle_page_in_pagination(self): + self.seed_static_data() + self.setup_reviewer_responses_finalverdict_reviewquestion_reviewresponses_and_scores() + self.setup_two_extra_responses_for_reviewer3() + + params ={'event_id' : 1, 'page_number' : 1, 'limit' : 2, 'sort_column' : 'review_response_id'} + header = self.get_auth_header_for('r3@r.com') + + response = self.app.get('/api/v1/reviewhistory', headers=header, data=params) + data = json.loads(response.data) + + self.assertEqual(len(data['reviews']), 2) + self.assertEqual(data['num_entries'], 5) + + self.assertEqual(data['reviews'][0]['review_response_id'], 5) + self.assertEqual(data['reviews'][1]['review_response_id'], 6) + + def test_last_page_in_pagination(self): + self.seed_static_data() + self.setup_reviewer_responses_finalverdict_reviewquestion_reviewresponses_and_scores() + self.setup_two_extra_responses_for_reviewer3() + + params ={'event_id' : 1, 'page_number' : 2, 'limit' : 2, 'sort_column' : 'review_response_id'} + header = self.get_auth_header_for('r3@r.com') + + response = self.app.get('/api/v1/reviewhistory', headers=header, data=params) + data = json.loads(response.data) + + self.assertEqual(len(data['reviews']), 1) + self.assertEqual(data['num_entries'], 5) + self.assertEqual(data['reviews'][0]['review_response_id'], 7) + + def test_total_number_of_pages_greater_than_zero(self): + self.seed_static_data() + self.setup_reviewer_responses_finalverdict_reviewquestion_reviewresponses_and_scores() + self.setup_two_extra_responses_for_reviewer3() + + params ={'event_id' : 1, 'page_number' : 2, 'limit' : 2, 'sort_column' : 'review_response_id'} + header = self.get_auth_header_for('r3@r.com') + + response = self.app.get('/api/v1/reviewhistory', headers=header, data=params) + data = json.loads(response.data) + + self.assertEqual(data['total_pages'], 3) + + def test_total_number_of_pages_when_zero(self): + self.seed_static_data() + self.setup_reviewer_with_no_reviewresponses() + + params ={'event_id' : 1, 'page_number' : 2, 'limit' : 2, 'sort_column' : 'review_response_id'} + header = self.get_auth_header_for('r1@r.com') + + response = self.app.get('/api/v1/reviewhistory', headers=header, data=params) + data = json.loads(response.data) + + self.assertEqual(data['total_pages'], 0) + + + def test_review_form_language(self): + """Test that the review form questions are returned in the correct language.""" + self.seed_static_data() + params ={'event_id' : 1, 'language': 'en'} + header = self.get_auth_header_for('r1@r.com') + + response = self.app.get('/api/v1/review', headers=header, data=params) + data = json.loads(response.data) + + print(data['review_form']['review_sections']) + + self.assertEqual(data['review_form']['review_sections'][0]['headline'], 'Review Section 1 English') + self.assertEqual(data['review_form']['review_sections'][0]['description'], 'Review Section 1 Description English') + + self.assertEqual(len(data['review_form']['review_sections'][0]['review_questions']), 2) + + self.assertEqual(data['review_form']['review_sections'][0]['review_questions'][1]['description'], 'English Description') + self.assertEqual(data['review_form']['review_sections'][0]['review_questions'][1]['headline'], 'English Headline') + self.assertEqual(data['review_form']['review_sections'][0]['review_questions'][1]['placeholder'], 'English Placeholder') + self.assertDictEqual(data['review_form']['review_sections'][0]['review_questions'][1]['options'][0], {'label': 'en1', 'value': 'en'}) + self.assertEqual(data['review_form']['review_sections'][0]['review_questions'][1]['validation_regex'], 'EN Regex') + self.assertEqual(data['review_form']['review_sections'][0]['review_questions'][1]['validation_text'], 'EN Validation Message') + + params ={'event_id' : 1, 'language': 'fr'} + + response = self.app.get('/api/v1/review', headers=header, data=params) + data = json.loads(response.data) + + self.assertEqual(data['review_form']['review_sections'][0]['headline'], 'Review Section 1 French') + self.assertEqual(data['review_form']['review_sections'][0]['description'], 'Review Section 1 Description French') + + self.assertEqual(len(data['review_form']['review_sections'][0]['review_questions']), 2) + + self.assertEqual(data['review_form']['review_sections'][0]['review_questions'][1]['description'], 'French Description') + self.assertEqual(data['review_form']['review_sections'][0]['review_questions'][1]['headline'], 'French Headline') + self.assertEqual(data['review_form']['review_sections'][0]['review_questions'][1]['placeholder'], 'French Placeholder') + self.assertDictEqual(data['review_form']['review_sections'][0]['review_questions'][1]['options'][0], {'label': 'fr1', 'value': 'fr'}) + self.assertEqual(data['review_form']['review_sections'][0]['review_questions'][1]['validation_regex'], 'FR Regex') + self.assertEqual(data['review_form']['review_sections'][0]['review_questions'][1]['validation_text'], 'FR Validation Message') + + +class ReviewListAPITest(ApiTestCase): + + def seed_static_data(self): + self.event1 = self.add_event(key='event1') + self.event2 = self.add_event(key='event2') + + self.reviewer1 = self.add_user('reviewer1@mail.com') + self.reviewer2 = self.add_user('reviewer2@mail.com') + self.event1.add_event_role('reviewer', self.reviewer1.id) + self.event2.add_event_role('reviewer', self.reviewer1.id) + self.event1.add_event_role('reviewer', self.reviewer2.id) + self.event2.add_event_role('reviewer', self.reviewer2.id) + + self.user1 = self.add_user('user1@mail.com') + user2 = self.add_user('user2@mail.com') + user3 = self.add_user('user3@mail.com') + user4 = self.add_user('user4@mail.com') + + application_form1 = self.create_application_form(self.event1.id) + section1 = self.add_section(application_form1.id) + question1 = self.add_question(application_form1.id, section1.id) + question1.key = 'review-identifier' + self.add_question_translation(question1.id, 'en', 'Headline 1 EN') + self.add_question_translation(question1.id, 'fr', 'Headline 1 FR') + question2 = self.add_question(application_form1.id, section1.id) + question2.key = 'review-identifier' + self.add_question_translation(question2.id, 'en', 'Headline 2 EN') + self.add_question_translation(question2.id, 'fr', 'Headline 2 FR') + question3 = self.add_question(application_form1.id, section1.id) + self.add_question_translation(question3.id, 'en', 'Headline 3 EN') + self.add_question_translation(question3.id, 'fr', 'Headline 3 FR') + + review_form1 = self.add_review_form(application_form1.id) + + review_form1_section1 = self.add_review_section(review_form1.id) + self.add_review_section_translation(review_form1_section1.id, 'en', 'Review Section 1 en', 'Review Section 1 en') + self.add_review_section_translation(review_form1_section1.id, 'fr', 'Review Section 1 fr', 'Review Section 1 fr') + review_form1_section2 = self.add_review_section(review_form1.id) + self.add_review_section_translation(review_form1_section2.id, 'en', 'Review Section 2 en', 'Review Section 2 en') + self.add_review_section_translation(review_form1_section2.id, 'fr', 'Review Section 2 fr', 'Review Section 2 fr') + + review_form1_section2 = self.add_review_section(review_form1.id) + review_q1 = self.add_review_question(review_form1_section1.id, weight=1) + review_q1_translation_en = self.add_review_question_translation(review_q1.id, 'en', headline='Heading En') + review_q1_translation_fr = self.add_review_question_translation(review_q1.id, 'fr', headline='Heading Fr') + review_q2 = self.add_review_question(review_form1_section1.id, weight=0) + review_q2_translation_en = self.add_review_question_translation(review_q2.id, 'en', headline='Heading En') + review_q2_translation_fr = self.add_review_question_translation(review_q2.id, 'fr', headline='Heading Fr') + review_q3 = self.add_review_question(review_form1_section2.id, weight=1) + review_q3_translation_en = self.add_review_question_translation(review_q3.id, 'en', headline='Heading En') + review_q3_translation_fr = self.add_review_question_translation(review_q3.id, 'fr', headline='Heading Fr') + + application_form2 = self.create_application_form(self.event2.id) + review_form2 = self.add_review_form(application_form2.id) + + event1_response1 = self.add_response(application_form1.id, self.user1.id, is_submitted=True) + self.add_answer(event1_response1.id, question1.id, 'First answer') + self.add_answer(event1_response1.id, question2.id, 'Second answer') + self.add_answer(event1_response1.id, question3.id, 'Third answer') + + event1_response2 = self.add_response(application_form1.id, user2.id, is_submitted=True, language='fr') + self.add_answer(event1_response2.id, question1.id, 'Forth answer') + self.add_answer(event1_response2.id, question2.id, 'Fifth answer') + self.add_answer(event1_response2.id, question3.id, 'Sixth answer') + + event1_response3 = self.add_response(application_form1.id, user3.id, is_submitted=True) + self.add_answer(event1_response3.id, question1.id, 'Seventh answer') + self.add_answer(event1_response3.id, question2.id, 'Eigth answer') + self.add_answer(event1_response3.id, question3.id, 'Ninth answer') + + event1_response4 = self.add_response(application_form1.id, user4.id, is_submitted=True, language='zu') + self.add_answer(event1_response4.id, question1.id, 'Tenth answer') + self.add_answer(event1_response4.id, question2.id, 'Eleventh answer') + self.add_answer(event1_response4.id, question3.id, 'Twelfth answer') + + event2_response1 = self.add_response(application_form2.id, self.user1.id, is_submitted=True) + event2_response2 = self.add_response(application_form2.id, user2.id, is_submitted=True, language='fr') + event2_response3 = self.add_response(application_form2.id, user3.id, is_submitted=True) + + # Review 1 completed review + self.add_response_reviewer(event1_response1.id, self.reviewer1.id) + review_response1 = self.add_review_response(self.reviewer1.id, event1_response1.id, review_form1.id) + review_response1.submit() + self.add_review_score(review_response1.id, review_q1.id, 10.5) + self.add_review_score(review_response1.id, review_q2.id, 100) + self.add_review_score(review_response1.id, review_q3.id, 'Hello world') + + # Reviewer 1 incomplete review + self.review_response1_submitted = review_response1.submitted_timestamp.isoformat() + self.add_response_reviewer(event1_response2.id, self.reviewer1.id) + review_response2 = self.add_review_response(self.reviewer1.id, event1_response2.id, review_form1.id) + self.add_review_score(review_response2.id, review_q1.id, 13) + + # Reviewer 1 not started review + self.add_response_reviewer(event1_response3.id, self.reviewer1.id) + + # Confounders + self.add_response_reviewer(event1_response2.id, self.reviewer2.id) + self.add_response_reviewer(event1_response3.id, self.reviewer2.id) + self.add_response_reviewer(event2_response1.id, self.reviewer1.id) + self.add_response_reviewer(event2_response2.id, self.reviewer2.id) + + def test_review_list(self): + self.seed_static_data() + params ={'event_id' : 1, 'language': 'en'} + + response = self.app.get( + '/api/v1/reviewlist', + headers=self.get_auth_header_for('reviewer1@mail.com'), + data=params) + + data = json.loads(response.data) + + self.assertEqual(len(data), 3) + + self.assertEqual(data[0]['response_id'], 1) + self.assertEqual(data[0]['language'], 'en') + self.assertEqual(len(data[0]['information']), 2) + self.assertEqual(data[0]['information'][0]['headline'], 'Headline 1 EN') + self.assertEqual(data[0]['information'][0]['value'], 'First answer') + self.assertEqual(data[0]['information'][1]['headline'], 'Headline 2 EN') + self.assertEqual(data[0]['information'][1]['value'], 'Second answer') + self.assertTrue(data[0]['started']) + self.assertEqual(data[0]['submitted'], self.review_response1_submitted) + self.assertEqual(data[0]['total_score'], 10.5) + + self.assertEqual(data[1]['response_id'], 2) + self.assertEqual(data[1]['language'], 'fr') + self.assertEqual(len(data[1]['information']), 2) + self.assertEqual(data[1]['information'][0]['headline'], 'Headline 1 EN') + self.assertEqual(data[1]['information'][0]['value'], 'Forth answer') + self.assertEqual(data[1]['information'][1]['headline'], 'Headline 2 EN') + self.assertEqual(data[1]['information'][1]['value'], 'Fifth answer') + self.assertTrue(data[1]['started']) + self.assertIsNone(data[1]['submitted']) + self.assertEqual(data[1]['total_score'], 13) + + self.assertEqual(data[2]['response_id'], 3) + self.assertEqual(data[2]['language'], 'en') + self.assertEqual(len(data[2]['information']), 2) + self.assertEqual(data[2]['information'][0]['headline'], 'Headline 1 EN') + self.assertEqual(data[2]['information'][0]['value'], 'Seventh answer') + self.assertEqual(data[2]['information'][1]['headline'], 'Headline 2 EN') + self.assertEqual(data[2]['information'][1]['value'], 'Eigth answer') + self.assertFalse(data[2]['started']) + self.assertIsNone(data[2]['submitted']) + self.assertEqual(data[2]['total_score'], 0.0) + +class ResponseReviewerAssignmentApiTest(ApiTestCase): + def seed_static_data(self): + self.event = self.add_event(key='event1') + self.event2 = self.add_event(key='event2') + self.event_admin = self.add_user('eventadmin@mail.com') + self.reviewer = self.add_user('reviewer@mail.com') + self.reviewer_user_id = self.reviewer.id + + self.user1 = self.add_user('user1@mail.com') + self.user2 = self.add_user('user2@mail.com') + self.user3 = self.add_user('user3@mail.com') + + self.event.add_event_role('admin', self.event_admin.id) + + application_form = self.create_application_form(self.event.id) + application_form2 = self.create_application_form(self.event2.id) + self.response1 = self.add_response(application_form.id, self.user1.id, is_submitted=True) + self.response2 = self.add_response(application_form.id, self.user2.id, is_submitted=True) + self.response3 = self.add_response(application_form.id, self.user3.id, is_submitted=True) + + self.add_review_form(application_form.id) + self.add_review_form(application_form2.id) + + self.event2_response_id = self.add_response(application_form2.id, self.user1.id, is_submitted=True).id + + self.add_email_template('reviews-assigned') + + def test_responses_assigned(self): + self.seed_static_data() + + params = {'event_id' : 1, 'response_ids': [1, 2], 'reviewer_email': 'reviewer@mail.com'} + + response = self.app.post( + '/api/v1/assignresponsereviewer', + headers=self.get_auth_header_for('eventadmin@mail.com'), + data=params) + + self.assertEqual(response.status_code, 201) + + response_reviewers = (db.session.query(ResponseReviewer) + .join(Response, ResponseReviewer.response_id == Response.id) + .filter_by(application_form_id=1).all()) + + self.assertEqual(len(response_reviewers), 2) + + for rr in response_reviewers: + self.assertEqual(rr.reviewer_user_id, self.reviewer_user_id) + + def test_response_for_different_event_forbidden(self): + self.seed_static_data() + + params = {'event_id' : 1, 'response_ids': [1, 2, self.event2_response_id], 'reviewer_email': 'reviewer@mail.com'} + + response = self.app.post( + '/api/v1/assignresponsereviewer', + headers=self.get_auth_header_for('eventadmin@mail.com'), + data=params) + + self.assertEqual(response.status_code, 403) + + def test_delete(self): + """Test that a review assignment can be deleted.""" + self.seed_static_data() + + # Assign a reviewer + response_id = self.response1.id + self.add_response_reviewer(response_id, self.reviewer_user_id) + + params = {'event_id' : 1, 'response_id': self.response1.id, 'reviewer_user_id': self.reviewer_user_id} + + response = self.app.delete( + '/api/v1/assignresponsereviewer', + headers=self.get_auth_header_for('eventadmin@mail.com'), + data=params) + + self.assertEqual(response.status_code, 200) + + # Check that it was actually deleted + response_reviewer = db.session.query(ResponseReviewer).filter_by(response_id=response_id, reviewer_user_id=self.reviewer_user_id).first() + self.assertIsNone(response_reviewer) + + def test_delete_not_allowed_if_completed(self): + """Test that a review assignment can't be deleted if the review has been completed.""" + self.seed_static_data() + + # Assign a reviewer and create a review resposne + response_id = self.response1.id + self.add_response_reviewer(response_id, self.reviewer_user_id) + self.add_review_response(self.reviewer_user_id, response_id) + + params = {'event_id' : 1, 'response_id': self.response1.id, 'reviewer_user_id': self.reviewer_user_id} + + response = self.app.delete( + '/api/v1/assignresponsereviewer', + headers=self.get_auth_header_for('eventadmin@mail.com'), + data=params) + + self.assertEqual(response.status_code, 400) + + +class ReferenceReviewRequest(ApiTestCase): + def static_seed_data(self): + # User, country and organisation is set up by ApiTestCase + self.first_user = self.add_user('firstuser@mail.com', 'First', 'User', 'Mx') + + self.event = self.add_event() + + self.system_admin = self.add_user('sa@sa.com', is_admin=True) + non_admin_users = self.add_n_users(3) + users = non_admin_users.append(self.system_admin) + self.event.add_event_role('admin', 4), + self.event.add_event_role('reviewer', 1), + + db.session.commit() + + application_form = [ + self.create_application_form(1, True, False), + ] + + closed_review = ReviewForm(1, datetime(2018, 4, 30), 1, True) + inactive_review = ReviewForm(1, datetime(2018, 4, 30), 2, False) + closed_review.close() + review_forms = [ + ReviewForm(1, datetime(2019, 4, 30), 1, True), + ReviewForm(1, datetime(2019, 4, 30), 2, False), + closed_review, + inactive_review + ] + db.session.add_all(review_forms) + db.session.commit() + + self.test_response = self.add_response(1, self.first_user.id, language='en', is_submitted=True) + + self.response_review = self.add_response_reviewer(self.test_response.id, self.first_user.id) + + self.first_headers = self.get_auth_header_for("firstuser@mail.com") + + db.session.flush() + + def test_get_reference_request_by_event_id(self): + self.static_seed_data() + reference_req = ReferenceRequest(1, 'Mr', 'John', 'Snow', 'Supervisor', 'common@email.com') + reference_request_repository.create(reference_req) + REFERENCE_DETAIL = { + 'token': reference_req.token, + 'uploaded_document': 'DOCT-UPLOAD-78999', + } + + response = self.app.post( + '/api/v1/reference', data=REFERENCE_DETAIL, headers=self.first_headers) + self.assertEqual(response.status_code, 201) + + params = {'event_id': 1, 'language': 'en'} + response = self.app.get('/api/v1/review', headers=self.get_auth_header_for('firstuser@mail.com'), data=params) + + data = json.loads(response.data) + + self.assertEqual(response.status_code, 200) + self.assertEqual(len(data), 8) + self.assertEqual(len(data['references']), 1) + self.assertDictEqual(data['references'][0], { + u'title': u'Mr', + u'firstname': u'John', + u'lastname': u'Snow', + u'relation': u'Supervisor', + u'uploaded_document': u'DOCT-UPLOAD-78999', + }) + + def test_get_reference_request_with_two_references(self): + """ + In this test, there are two references requested and submitted + """ + self.static_seed_data() + reference_req = ReferenceRequest(1, 'Mr', 'John', 'Snow', 'Supervisor', 'common@email.com') + reference_req2 = ReferenceRequest(1, 'Mrs', 'Jane', 'Jones', 'Manager', 'jane@email.com') + reference_request_repository.create(reference_req) + reference_request_repository.create(reference_req2) + + REFERENCE_DETAIL = { + 'token': reference_req.token, + 'uploaded_document': 'DOCT-UPLOAD-78999', + } + REFERENCE_DETAIL_2 = { + 'token': reference_req2.token, + 'uploaded_document': 'DOCT-UPLOAD-78979', + } + response = self.app.post( + '/api/v1/reference', data=REFERENCE_DETAIL, headers=self.first_headers) + self.assertEqual(response.status_code, 201) + + response = self.app.post( + '/api/v1/reference', data=REFERENCE_DETAIL_2, headers=self.first_headers) + self.assertEqual(response.status_code, 201) + + params = {'event_id': 1, 'language': 'en'} + response = self.app.get('/api/v1/review', headers=self.get_auth_header_for('firstuser@mail.com'), data=params) + + data = json.loads(response.data) + + self.assertEqual(response.status_code, 200) + self.assertEqual(len(data), 8) + self.assertEqual(len(data['references']), 2) + self.assertDictEqual(data['references'][0], { + u'title': u'Mr', + u'firstname': u'John', + u'lastname': u'Snow', + u'relation': u'Supervisor', + u'uploaded_document': u'DOCT-UPLOAD-78999', + }) + + self.assertDictEqual(data['references'][1], { + u'title': u'Mrs', + u'firstname': u'Jane', + u'lastname': u'Jones', + u'relation': u'Manager', + u'uploaded_document': u'DOCT-UPLOAD-78979', + }) + + def test_get_reference_not_yet_submitted(self): + """ + In this test, a reference has been requested, but not yet submitted by the referee (id exists, but no reference yet) + """ + self.static_seed_data() + reference_req = ReferenceRequest(1, 'Mr', 'John', 'Snow', 'Supervisor', 'common@email.com') + reference_request_repository.create(reference_req) + + params = {'event_id': 1, 'language': 'en'} + response = self.app.get('/api/v1/review', headers=self.get_auth_header_for('firstuser@mail.com'), data=params) + + data = json.loads(response.data) + + self.assertEqual(response.status_code, 200) + self.assertEqual(len(data), 8) + self.assertEqual(data.get('references', None), None) + + def test_get_reference_submitted_later(self): + """ + In this test, a reference has been requested, but the reference is only submitted after the second check + """ + self.static_seed_data() + reference_req = ReferenceRequest(1, 'Mr', 'John', 'Snow', 'Supervisor', 'common@email.com') + reference_request_repository.create(reference_req) + + params = {'event_id': 1, 'language': 'en'} + + REFERENCE_DETAIL = { + 'token': reference_req.token, + 'uploaded_document': 'DOCT-UPLOAD-78999', + } + + response = self.app.get('/api/v1/review', headers=self.get_auth_header_for('firstuser@mail.com'), + data=params) + + data = json.loads(response.data) + + self.assertEqual(response.status_code, 200) + self.assertEqual(len(data), 8) + self.assertEqual(data.get('references', None), None) + + response = self.app.post( + '/api/v1/reference', data=REFERENCE_DETAIL, headers=self.first_headers) + self.assertEqual(response.status_code, 201) + + params = {'event_id': 1, 'language': 'en'} + response = self.app.get('/api/v1/review', headers=self.get_auth_header_for('firstuser@mail.com'), data=params) + + data = json.loads(response.data) + + self.assertEqual(response.status_code, 200) + self.assertEqual(len(data), 8) + self.assertEqual(len(data['references']), 1) + self.assertDictEqual(data['references'][0], { + u'title': u'Mr', + u'firstname': u'John', + u'lastname': u'Snow', + u'relation': u'Supervisor', + u'uploaded_document': u'DOCT-UPLOAD-78999', + }) + + def test_two_references_only_one_submitted(self): + """ + In this test, two references have been requested, but only one submitted (two ids exists, one reference submitted) + """ + self.static_seed_data() + reference_req = ReferenceRequest(1, 'Mr', 'John', 'Snow', 'Supervisor', 'common@email.com') + reference_req2 = ReferenceRequest(1, 'Mrs', 'Jane', 'Jones', 'Manager', 'jane@email.com') + reference_request_repository.create(reference_req) + reference_request_repository.create(reference_req2) + + REFERENCE_DETAIL_2 = { + 'token': reference_req2.token, + 'uploaded_document': 'DOCT-UPLOAD-78979', + } + + response = self.app.post( + '/api/v1/reference', data=REFERENCE_DETAIL_2, headers=self.first_headers) + self.assertEqual(response.status_code, 201) + + params = {'event_id': 1, 'language': 'en'} + response = self.app.get('/api/v1/review', headers=self.get_auth_header_for('firstuser@mail.com'), data=params) + + data = json.loads(response.data) + + self.assertEqual(response.status_code, 200) + self.assertEqual(len(data), 8) + self.assertEqual(len(data['references']), 1) + + self.assertDictEqual(data['references'][0], { + u'title': u'Mrs', + u'firstname': u'Jane', + u'lastname': u'Jones', + u'relation': u'Manager', + u'uploaded_document': u'DOCT-UPLOAD-78979', + }) + + def test_get_review_no_reference(self): + """ + In this test, there is no reference attached to the application at all for review (i.e. no reference request) + """ + self.static_seed_data() + + params = {'event_id': 1, 'language': 'en'} + response = self.app.get('/api/v1/review', headers=self.get_auth_header_for('firstuser@mail.com'), data=params) + + data = json.loads(response.data) + self.assertEqual(response.status_code, 200) + self.assertEqual(len(data), 8) + self.assertEqual(data.get('references', None), None) + + +class ReviewResponseDetailListApiTest(ApiTestCase): + def seed_static_data(self): + self.user1 = self.add_user('user1@mail.com', 'Jane', 'Bloggs', 'Ms') + self.user2 = self.add_user('user2@mail.com', 'Alex', 'Person', 'Dr') + self.reviewer = self.add_user('reviewer@mail.com', 'Joe', 'Soap', 'Mr') + self.reviewer2 = self.add_user('reviewer2@mail.com', 'Jenny', 'Sharp', 'Ms') + self.event_admin = self.add_user('event_admin@mail.com') + + self.event = self.add_event() + self.event2 = self.add_event(key='Empty') + self.event.add_event_role('admin', self.event_admin.id) + self.event2.add_event_role('admin', self.event_admin.id) + + self.application_form = self.create_application_form(self.event.id) + self.section = self.add_section(self.application_form.id) + self.question1 = self.add_question( + self.application_form.id, + self.section.id, + order=1, + key='review-identifier') + self.question_translation1 = self.add_question_translation( + self.question1.id, + 'en', + 'Organisation') + self.question2 = self.add_question( + self.application_form.id, + self.section.id, + order=2, + key='review-identifier') + self.question_translation2 = self.add_question_translation( + self.question2.id, + 'en', + 'Country') + self.question3 = self.add_question( + self.application_form.id, + self.section.id, + order=3, + ) + self.question_translation3 = self.add_question_translation( + self.question3.id, + 'en', + 'Non-review question' + ) + + self.response1 = self.add_response( + self.application_form.id, + self.user1.id, + is_submitted=True) + self.answer1 = self.add_answer(self.response1.id, self.question1.id, 'Pets R Us') + self.answer2 = self.add_answer(self.response1.id, self.question2.id, 'Nigeria') + self.answer3 = self.add_answer(self.response1.id, self.question3.id, 'Non-review answer') + + self.response2 = self.add_response( + self.application_form.id, + self.user2.id, + is_submitted=True) + self.answer4 = self.add_answer(self.response2.id, self.question1.id, 'Nokia') + self.answer5 = self.add_answer(self.response2.id, self.question2.id, 'South Africa') + self.answer6 = self.add_answer(self.response2.id, self.question3.id, 'Non-review answer 2') + + self.review_form = self.add_review_form(self.application_form.id) + self.review_section1 = self.add_review_section(self.review_form.id) + self.review_section1_translation1 = self.add_review_section_translation( + self.review_section1.id, + 'en') + self.review_section2 = self.add_review_section(self.review_form.id) + self.review_section2_translation1 = self.add_review_section_translation( + self.review_section2.id, + 'en' + 'Review Section 2', + 'Review Section 2 Description') + self.review_question1 = self.add_review_question(self.review_section1.id, type='short-text', weight=1) + self.review_question_translation1 = self.add_review_question_translation( + self.review_question1.id, + 'en', + headline='Ethical Considerations', + description="How ethical is the candidate's proposal from 1 to 5?") + self.review_question2 = self.add_review_question(self.review_section1.id, type='long-text', weight=0) + self.review_question_translation2 = self.add_review_question_translation( + self.review_question2.id, + 'en', + headline=None, + description='Comments for the candidate' + ) + self.review_question3 = self.add_review_question(self.review_section2.id, type='multi-choice', weight=2) + self.review_question_translation3 = self.add_review_question_translation( + self.review_question3.id, + 'en', + headline=None, + description='What is your overall rating?' + ) + self.review_question4 = self.add_review_question(self.review_section2.id, type='checkbox', weight=0) + self.review_question_translation4 = self.add_review_question_translation( + self.review_question4.id, + 'en', + headline='Yes/No Question' + ) + + self.review_response1 = self.add_review_response( + self.reviewer.id, + self.response1.id, + self.review_form.id, + is_submitted=True) + self.review_score1 = self.add_review_score(self.review_response1.id, self.review_question1.id, '4') + self.review_score2 = self.add_review_score(self.review_response1.id, self.review_question2.id, 'This is a very good proposal') + self.review_score3 = self.add_review_score(self.review_response1.id, self.review_question3.id, '5') + self.review_score4 = self.add_review_score(self.review_response1.id, self.review_question4.id, 'Yes') + + self.review_response2 = self.add_review_response( + self.reviewer2.id, + self.response2.id, + self.review_form.id, + is_submitted=True) + self.review_score5 = self.add_review_score(self.review_response2.id, self.review_question1.id, '3') + self.review_score6 = self.add_review_score(self.review_response2.id, self.review_question2.id, 'Not bad!') + self.review_score7 = self.add_review_score(self.review_response2.id, self.review_question3.id, '3') + self.review_score8 = self.add_review_score(self.review_response2.id, self.review_question4.id, 'No') + + def test_not_authed(self): + response = self.app.get('/api/v1/reviewresponsedetaillist') + self.assertEqual(response.status_code, 401) + + def test_not_event_admin(self): + self.seed_static_data() + + response = self.app.get( + '/api/v1/reviewresponsedetaillist', + headers=self.get_auth_header_for('user1@mail.com'), + data={'event_id': 1} + ) + + self.assertEqual(response.status_code, 403) + + def test_no_review_responses(self): + self.seed_static_data() + + response = self.app.get( + '/api/v1/reviewresponsedetaillist', + headers=self.get_auth_header_for('event_admin@mail.com'), + data={'event_id': 2, 'language': 'en'} + ) + data = json.loads(response.data) + + print(data) + + self.assertEqual(data, []) + + def test_review_responses_for_event(self): + self.seed_static_data() + + response = self.app.get( + '/api/v1/reviewresponsedetaillist', + headers=self.get_auth_header_for('event_admin@mail.com'), + data={'event_id': 1, 'language': 'en'} + ) + + data = json.loads(response.data) + + self.assertEqual(len(data), 2) + + self.assertEqual(data[0]['review_response_id'], 1) + self.assertEqual(data[0]['response_id'], 1) + + self.assertEqual(data[0]['reviewer_user_title'], 'Mr') + self.assertEqual(data[0]['reviewer_user_firstname'], 'Joe') + self.assertEqual(data[0]['reviewer_user_lastname'], 'Soap') + + self.assertEqual(data[0]['response_user_title'], 'Ms') + self.assertEqual(data[0]['response_user_firstname'], 'Jane') + self.assertEqual(data[0]['response_user_lastname'], 'Bloggs') + + self.assertEqual(len(data[0]['identifiers']), 2) + self.assertEqual(data[0]['identifiers'][0]['headline'], 'Organisation') + self.assertEqual(data[0]['identifiers'][0]['value'], 'Pets R Us') + self.assertEqual(data[0]['identifiers'][1]['headline'], 'Country') + self.assertEqual(data[0]['identifiers'][1]['value'], 'Nigeria') + + self.assertEqual(len(data[0]['scores']), 3) + 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]['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]['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]['weight'], 2) + + self.assertEqual(data[0]['total'], 14) + + + self.assertEqual(data[1]['review_response_id'], 2) + self.assertEqual(data[1]['response_id'], 2) + + self.assertEqual(data[1]['reviewer_user_title'], 'Ms') + self.assertEqual(data[1]['reviewer_user_firstname'], 'Jenny') + self.assertEqual(data[1]['reviewer_user_lastname'], 'Sharp') + + self.assertEqual(data[1]['response_user_title'], 'Dr') + self.assertEqual(data[1]['response_user_firstname'], 'Alex') + self.assertEqual(data[1]['response_user_lastname'], 'Person') + + self.assertEqual(len(data[1]['identifiers']), 2) + self.assertEqual(data[1]['identifiers'][0]['headline'], 'Organisation') + self.assertEqual(data[1]['identifiers'][0]['value'], 'Nokia') + self.assertEqual(data[1]['identifiers'][1]['headline'], 'Country') + self.assertEqual(data[1]['identifiers'][1]['value'], 'South Africa') + + self.assertEqual(len(data[1]['scores']), 3) + 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]['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]['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]['weight'], 2) + + self.assertEqual(data[1]['total'], 9) + + +class ReviewResponseSummaryListApiTest(ApiTestCase): + def seed_static_data(self): + self.user1 = self.add_user('user1@mail.com', 'Jane', 'Bloggs', 'Ms') + self.user2 = self.add_user('user2@mail.com', 'Alex', 'Person', 'Dr') + self.reviewer = self.add_user('reviewer@mail.com', 'Joe', 'Soap', 'Mr') + self.reviewer2 = self.add_user('reviewer2@mail.com', 'Jenny', 'Sharp', 'Ms') + self.event_admin = self.add_user('event_admin@mail.com') + + self.event = self.add_event() + self.event2 = self.add_event(key='Empty') + self.event.add_event_role('admin', self.event_admin.id) + self.event2.add_event_role('admin', self.event_admin.id) + + self.application_form = self.create_application_form(self.event.id) + self.section = self.add_section(self.application_form.id) + self.question1 = self.add_question( + self.application_form.id, + self.section.id, + order=1, + key='review-identifier') + self.question_translation1 = self.add_question_translation( + self.question1.id, + 'en', + 'Organisation') + self.question2 = self.add_question( + self.application_form.id, + self.section.id, + order=2, + key='review-identifier') + self.question_translation2 = self.add_question_translation( + self.question2.id, + 'en', + 'Country') + self.question3 = self.add_question( + self.application_form.id, + self.section.id, + order=3, + ) + self.question_translation3 = self.add_question_translation( + self.question3.id, + 'en', + 'Non-review question' + ) + + self.response1 = self.add_response( + self.application_form.id, + self.user1.id, + is_submitted=True) + self.answer1 = self.add_answer(self.response1.id, self.question1.id, 'Pets R Us') + self.answer2 = self.add_answer(self.response1.id, self.question2.id, 'Nigeria') + self.answer3 = self.add_answer(self.response1.id, self.question3.id, 'Non-review answer') + + self.response2 = self.add_response( + self.application_form.id, + self.user2.id, + is_submitted=True) + self.answer4 = self.add_answer(self.response2.id, self.question1.id, 'Nokia') + self.answer5 = self.add_answer(self.response2.id, self.question2.id, 'South Africa') + self.answer6 = self.add_answer(self.response2.id, self.question3.id, 'Non-review answer 2') + + self.review_form = self.add_review_form(self.application_form.id) + self.review_section1 = self.add_review_section(self.review_form.id) + self.review_section1_translation1 = self.add_review_section_translation( + self.review_section1.id, + 'en') + self.review_section2 = self.add_review_section(self.review_form.id) + self.review_section2_translation1 = self.add_review_section_translation( + self.review_section2.id, + 'en' + 'Review Section 2', + 'Review Section 2 Description') + self.review_question1 = self.add_review_question(self.review_section1.id, type='short-text', weight=1) + self.review_question_translation1 = self.add_review_question_translation( + self.review_question1.id, + 'en', + headline='Ethical Considerations', + description="How ethical is the candidate's proposal from 1 to 5?") + self.review_question2 = self.add_review_question(self.review_section1.id, type='long-text', weight=0) + self.review_question_translation2 = self.add_review_question_translation( + self.review_question2.id, + 'en', + headline=None, + description='Comments for the candidate' + ) + self.review_question3 = self.add_review_question(self.review_section2.id, type='multi-choice', weight=2) + self.review_question_translation3 = self.add_review_question_translation( + self.review_question3.id, + 'en', + headline=None, + description='What is your overall rating?' + ) + self.review_question4 = self.add_review_question(self.review_section2.id, type='checkbox', weight=0) + self.review_question_translation4 = self.add_review_question_translation( + self.review_question4.id, + 'en', + headline='Yes/No Question' + ) + + # reviewer 1 for first response + self.review_response1 = self.add_review_response( + self.reviewer.id, + self.response1.id, + self.review_form.id, + is_submitted=True) + self.review_score1 = self.add_review_score(self.review_response1.id, self.review_question1.id, '4') + self.review_score2 = self.add_review_score(self.review_response1.id, self.review_question2.id, 'This is a very good proposal') + self.review_score3 = self.add_review_score(self.review_response1.id, self.review_question3.id, '5') + self.review_score4 = self.add_review_score(self.review_response1.id, self.review_question4.id, 'Yes') + + # reviewer 2 for first response + self.review_response3 = self.add_review_response( + self.reviewer2.id, + self.response1.id, + self.review_form.id, + is_submitted=True) + self.review_score9 = self.add_review_score(self.review_response3.id, self.review_question1.id, '3') + self.review_score10 = self.add_review_score(self.review_response3.id, self.review_question2.id, 'This is a good proposal') + self.review_score11 = self.add_review_score(self.review_response3.id, self.review_question3.id, '4') + self.review_score12 = self.add_review_score(self.review_response3.id, self.review_question4.id, 'Yes') + + # reviewer 2 for second response + self.review_response2 = self.add_review_response( + self.reviewer2.id, + self.response2.id, + self.review_form.id, + is_submitted=True) + self.review_score5 = self.add_review_score(self.review_response2.id, self.review_question1.id, '3') + self.review_score6 = self.add_review_score(self.review_response2.id, self.review_question2.id, 'Close to bad!') + self.review_score7 = self.add_review_score(self.review_response2.id, self.review_question3.id, '2') + self.review_score8 = self.add_review_score(self.review_response2.id, self.review_question4.id, 'No') + + # reviewer 1 for second response + self.review_response4 = self.add_review_response( + self.reviewer.id, + self.response2.id, + self.review_form.id, + is_submitted=True) + self.review_score13 = self.add_review_score(self.review_response4.id, self.review_question1.id, '2') + self.review_score14 = self.add_review_score(self.review_response4.id, self.review_question2.id, 'Bad!') + self.review_score15 = self.add_review_score(self.review_response4.id, self.review_question3.id, '1') + self.review_score16 = self.add_review_score(self.review_response4.id, self.review_question4.id, 'No') + + def test_not_authed(self): + response = self.app.get('/api/v1/reviewresponsesummarylist') + self.assertEqual(response.status_code, 401) + + def test_not_event_admin(self): + self.seed_static_data() + + response = self.app.get( + '/api/v1/reviewresponsesummarylist', + headers=self.get_auth_header_for('user1@mail.com'), + data={'event_id': 1} + ) + + self.assertEqual(response.status_code, 403) + + def test_review_response_summary_for_event(self): + self.seed_static_data() + + response = self.app.get( + '/api/v1/reviewresponsesummarylist', + headers=self.get_auth_header_for('event_admin@mail.com'), + data={'event_id': 1, 'language': 'en'} + ) + + data = json.loads(response.data) + + self.assertEqual(len(data), 2) + + self.assertEqual(data[0]['response_id'], 1) + self.assertEqual(data[0]['response_user_title'], 'Ms') + self.assertEqual(data[0]['response_user_firstname'], 'Jane') + self.assertEqual(data[0]['response_user_lastname'], 'Bloggs') + + self.assertEqual(len(data[0]['identifiers']), 2) + self.assertEqual(data[0]['identifiers'][0]['headline'], 'Organisation') + self.assertEqual(data[0]['identifiers'][0]['value'], 'Pets R Us') + self.assertEqual(data[0]['identifiers'][1]['headline'], 'Country') + self.assertEqual(data[0]['identifiers'][1]['value'], 'Nigeria') + + self.assertEqual(len(data[0]['scores']), 2) + 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'], 3.5) + self.assertEqual(data[0]['scores'][0]['weight'], 1) + + self.assertEqual(data[0]['scores'][1]['headline'], None) + self.assertEqual(data[0]['scores'][1]['description'], 'What is your overall rating?') + self.assertEqual(data[0]['scores'][1]['type'], 'multi-choice') + self.assertEqual(data[0]['scores'][1]['score'], 4.5) + self.assertEqual(data[0]['scores'][1]['weight'], 2) + + self.assertEqual(data[0]['total'], 12.5) + + + self.assertEqual(data[1]['response_id'], 2) + self.assertEqual(data[1]['response_user_title'], 'Dr') + self.assertEqual(data[1]['response_user_firstname'], 'Alex') + self.assertEqual(data[1]['response_user_lastname'], 'Person') + + self.assertEqual(len(data[1]['identifiers']), 2) + self.assertEqual(data[1]['identifiers'][0]['headline'], 'Organisation') + self.assertEqual(data[1]['identifiers'][0]['value'], 'Nokia') + self.assertEqual(data[1]['identifiers'][1]['headline'], 'Country') + self.assertEqual(data[1]['identifiers'][1]['value'], 'South Africa') + + self.assertEqual(len(data[1]['scores']), 2) + 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'], 2.5) + self.assertEqual(data[1]['scores'][0]['weight'], 1) + + self.assertEqual(data[1]['scores'][1]['headline'], None) + self.assertEqual(data[1]['scores'][1]['description'], 'What is your overall rating?') + self.assertEqual(data[1]['scores'][1]['type'], 'multi-choice') + self.assertEqual(data[1]['scores'][1]['score'], 1.5) + self.assertEqual(data[1]['scores'][1]['weight'], 2) + + self.assertEqual(data[1]['total'], 5.5) + + +class ReviewStageAPITest(ApiTestCase): + def seed_static_data(self, activate_second_stage=True): + first_event = self.add_event() + self.create_application_form() + + event_admin = self.add_user("event@admin.com") + first_event.add_event_role('admin', event_admin.id) + + self.add_review_form(stage=1, active=False) + self.add_review_form(stage=2, active=activate_second_stage) + self.add_review_form(stage=3, active=False) + + # An event with no review form + no_form_event = self.add_event(key="no_form_event") + self.no_form_event_id = no_form_event.id + no_form_event.add_event_role('admin', event_admin.id) + + db.session.commit() + self.event_admin_headers = self.get_auth_header_for("event@admin.com") + + def test_get(self): + """Test that the get method returns the correct current stage and number of stages.""" + self.seed_static_data() + + response = self.app.get( + '/api/v1/reviewstage', + headers=self.event_admin_headers, + data={'event_id': 1} + ) + + data = json.loads(response.data) + + self.assertEqual(data['current_stage'], 2) + self.assertEqual(data['total_stages'], 3) + + def test_get_no_active(self): + """Test that the get method returns a 404 when ther is no active review form.""" + self.seed_static_data(activate_second_stage=False) + + response = self.app.get( + '/api/v1/reviewstage', + headers=self.event_admin_headers, + data={'event_id': 1} + ) + + self.assertEqual(response.status_code, 404) + + def test_get_no_form(self): + """Test that the get method returns a 404 when there are no review forms for the event.""" + self.seed_static_data() + + response = self.app.get( + '/api/v1/reviewstage', + headers=self.event_admin_headers, + data={'event_id': self.no_form_event_id} + ) + + self.assertEqual(response.status_code, 404) + + def test_post(self): + """Test that the review stage can be updated.""" + self.seed_static_data() + + response = self.app.post( + '/api/v1/reviewstage', + headers=self.event_admin_headers, + data={'event_id': 1, 'stage': 3} + ) + + self.assertEqual(response.status_code, 201) + + response = self.app.get( + '/api/v1/reviewstage', + headers=self.event_admin_headers, + data={'event_id': 1} + ) + + data = json.loads(response.data) + + self.assertEqual(data['current_stage'], 3) + self.assertEqual(data['total_stages'], 3) + + def test_post_no_form(self): + """Test that the post method returns a 404 when there are no review forms for the event.""" + self.seed_static_data() + + response = self.app.post( + '/api/v1/reviewstage', + headers=self.event_admin_headers, + data={'event_id': self.no_form_event_id, 'stage': 1} + ) + + self.assertEqual(response.status_code, 404) + + def test_post_no_form(self): + """Test that the post method returns a 404 when trying to activate a stage that doesn't exist.""" + self.seed_static_data() + + response = self.app.post( + '/api/v1/reviewstage', + headers=self.event_admin_headers, + data={'event_id': 1, 'stage': 4} + ) + + self.assertEqual(response.status_code, 404) + + +REVIEW_FORM_POST = { + "event_id": 1, + "application_form_id": 1, + "stage": 2, + "is_open": True, + "deadline": datetime(2024, 6, 1).strftime('%Y-%m-%dT%H:%M:%S'), + "active": True, + "sections": [ + { + "order": 1, + "name": { + "en": "Section 1", + "fr": "Section 1" + }, + "description": { + "en": "This is the first section", + "fr": "Ceci est la première section" + }, + "questions": [ + { + "type": "short-text", + "is_required": False, + "order": 1, + "weight": 0.0, + "question_id": 0, + "headline": { + "en": "Question 1", + "fr": "Question 1" + }, + "description": { + "en": "This is the first question", + "fr": "C'est la première question" + }, + "placeholder": { + "en": "Enter something interesting", + "fr": "Entrez quelque chose d'intéressant" + }, + "options": { + "en": None, + "fr": None + }, + "validation_regex": { + "en": "(a|b|c|d)", + "fr": "(a|b|c|d)" + }, + "validation_text": { + "en": "You must enter a, b, c or d", + "fr": "Vous devez entrer a, b, c ou d" + } + }, + { + "type": "radio", + "is_required": True, + "order": 2, + "weight": 1.5, + "question_id": 0, + "headline": { + "en": "Question 2", + "fr": "Question 2" + }, + "description": { + "en": "This is the second question", + "fr": "C'est la deuxième question" + }, + "placeholder": { + "en": None, + "fr": None + }, + "options": { + "en": [ + {"value": "0", "label": "Bad"}, + {"value": "1", "label": "Average"}, + {"value": "2", "label": "Good"}, + ], + "fr": [ + {"value": "0", "label": "Mal"}, + {"value": "1", "label": "Moyenne"}, + {"value": "2", "label": "Bien"}, + ], + }, + "validation_regex": { + "en": None, + "fr": None + }, + "validation_text": { + "en": None, + "fr": None + } + } + ] + }, + { + "order": 2, + "name": { + "en": "Section 2", + "fr": "Section 2" + }, + "description": { + "en": "This is the first section", + "fr": "Ceci est la deuxième section" + }, + "questions": [ + { + "type": "information", + "is_required": False, + "order": 1, + "weight": 0.0, + "question_id": 1, + "headline": { + "en": "Question 1 from application form", + "fr": "Question 1 from application form" + }, + "description": { + "en": "This is the first question from the application form", + "fr": "C'est la première question du formulaire de candidature" + }, + "placeholder": { + "en": None, + "fr": None + }, + "options": { + "en": None, + "fr": None + }, + "validation_regex": { + "en": None, + "fr": None + }, + "validation_text": { + "en": None, + "fr": None + } + } + ] + } + ] +} + + +REVIEW_FORM_PUT = { + "id": 1, + "event_id": 1, + "application_form_id": 1, + "stage": 3, # Updated + "is_open": True, + "deadline": datetime(2023, 2, 2).strftime('%Y-%m-%dT%H:%M:%S'), # Updated + "active": True, + "sections": [ + { # Updated first section + "id": 1, + "order": 2, # Updated + "name": { + "en": "Section 1 Updated", + "fr": "Section 1 Updated" + }, + "description": { + "en": "This is the first section updated", + "fr": "Ceci est la première section updated" + }, + "questions": [ + { + "id": 1, # Updated + "type": "long-text", # Updated + "is_required": True, + "order": 10, + "weight": 1.0, + "question_id": 0, + "headline": { + "en": "Question 1 Updated", + "fr": "Question 1 Updated" + }, + "description": { + "en": "This is the first question Updated", + "fr": "C'est la première question Updated" + }, + "placeholder": { + "en": "Enter something interesting updated", + "fr": "Entrez quelque chose d'intéressant updated" + }, + "options": { + "en": {"blah": "blah"}, + "fr": {"blah": "blah"} + }, + "validation_regex": { + "en": "(a|b|c|d|e)", + "fr": "(a|b|c|d|e)" + }, + "validation_text": { + "en": "You must enter a, b, c or d, or e", + "fr": "Vous devez entrer a, b, c, d, ou e" + } + }, + # Deleted question 2 + { # New question + "type": "radio", + "is_required": True, + "order": 20, + "weight": 2.5, + "question_id": 0, + "headline": { + "en": "Question 3", + "fr": "Question 3" + }, + "description": { + "en": "This is the third question", + "fr": "C'est la deuxième question" + }, + "placeholder": { + "en": "hello", + "fr": "world" + }, + "options": { + "en": [ + {"value": "0", "label": "Bad"}, + {"value": "1", "label": "Average"}, + {"value": "2", "label": "Good"}, + ], + "fr": [ + {"value": "0", "label": "Mal"}, + {"value": "1", "label": "Moyenne"}, + {"value": "2", "label": "Bien"}, + ], + }, + "validation_regex": { + "en": None, + "fr": None + }, + "validation_text": { + "en": None, + "fr": None + } + } + ] + }, + # Deleted section 2 + { # New section + "order": 3, + "name": { + "en": "Section 2 New", + "fr": "Section 2 New" + }, + "description": { + "en": "This is the new second section", + "fr": "Ceci est la neue deuxième section" + }, + "questions": [ + { + "type": "information", + "is_required": False, + "order": 1, + "weight": 0.0, + "question_id": 1, + "headline": { + "en": "Question 1 from application form", + "fr": "Question 1 from application form" + }, + "description": { + "en": "This is the first question from the application form", + "fr": "C'est la première question du formulaire de candidature" + }, + "placeholder": { + "en": None, + "fr": None + }, + "options": { + "en": None, + "fr": None + }, + "validation_regex": { + "en": None, + "fr": None + }, + "validation_text": { + "en": None, + "fr": None + } + } + ] + } + ] +} + +class ReviewFormDetailAPITest(ApiTestCase): + + def seed_static_data(self): + first_event = self.add_event() + self.first_event_id = first_event.id + + second_event = self.add_event(key="event2") + self.no_form_event_id = second_event.id + + app_form = self.create_application_form() + section = self.add_section(app_form.id) + self.question = self.add_question(app_form.id, section.id) + + event_admin = self.add_user("event@admin.com") + first_event.add_event_role('admin', event_admin.id) + second_event.add_event_role('admin', event_admin.id) + db.session.commit() + + def add_review_forms(self): + # First stage review form + form1 = self.add_review_form(stage=1, active=False, deadline=datetime(2021, 6, 1)) + form1_section1 = self.add_review_section(form1.id) + self.add_review_section_translation(form1_section1.id, 'en', 'Form 1 Section 1 EN', 'Form 1 Section 1 Description EN') + self.add_review_section_translation(form1_section1.id, 'fr', 'Form 1 Section 1 FR', 'Form 1 Section 1 Description FR') + + form1_section1_question1 = self.add_review_question(form1_section1.id, weight=1, type='numeric-text') + self.add_review_question_translation( + form1_section1_question1.id, + language='en', + headline='Review Question 1 EN', + description='Review Question 1 Description EN', + placeholder='Placeholder Question 1 EN', + validation_regex='(a|b|c|d)', + validation_text='Validation text question 1 EN') + self.add_review_question_translation( + form1_section1_question1.id, + language='fr', + headline='Review Question 1 FR', + description='Review Question 1 Description FR', + placeholder='Placeholder Question 1 FR', + validation_regex='(a|b|c|d)', + validation_text='Validation text question 1 FR') + + form1_section1_question2 = self.add_review_question(form1_section1.id, type='information', order=2, question_id=self.question.id) + self.add_review_question_translation( + form1_section1_question2.id, + language='en', + headline='Review Question 2 EN', + description='Review Question 2 Description EN') + self.add_review_question_translation( + form1_section1_question2.id, + language='fr', + headline='Review Question 2 FR', + description='Review Question 2 Description FR') + + form1_section2 = self.add_review_section(form1.id, order=2) + self.add_review_section_translation(form1_section2.id, 'en', 'Form 1 Section 2 EN', 'Form 1 Section 2 Description EN') + self.add_review_section_translation(form1_section2.id, 'fr', 'Form 1 Section 2 FR', 'Form 1 Section 2 Description FR') + + form1_section2_question1 = self.add_review_question(form1_section2.id, weight=1, type='radio') + self.add_review_question_translation( + form1_section2_question1.id, + language='en', + headline='Review Question 3 EN', + description='Review Question 3 Description EN', + options=[ + {'value': '0', 'label': 'Bad EN'}, + {'value': '1', 'label': 'Average EN'}, + {'value': '2', 'label': 'Good EN'} + ]) + self.add_review_question_translation( + form1_section2_question1.id, + language='fr', + headline='Review Question 3 FR', + description='Review Question 3 Description FR', + options=[ + {'value': '0', 'label': 'Bad FR'}, + {'value': '1', 'label': 'Average FR'}, + {'value': '2', 'label': 'Good FR'} + ]) + + # Second stage review form + form2 = self.add_review_form(stage=2, active=True, deadline=datetime(2021, 8, 1)) + + form2_section1 = self.add_review_section(form2.id) + self.add_review_section_translation(form2_section1.id, 'en', 'Form 2 Section 1 EN', 'Form 2 Section 1 Description EN') + self.add_review_section_translation(form2_section1.id, 'fr', 'Form 2 Section 1 FR', 'Form 2 Section 1 Description FR') + + form2_section1_question1 = self.add_review_question(form2_section1.id, type='information', question_id=self.question.id) + self.add_review_question_translation( + form2_section1_question1.id, + language='en', + headline='Stage 2 Review Question 1 EN', + description='Stage 2 Review Question 1 Description EN') + self.add_review_question_translation( + form2_section1_question1.id, + language='fr', + headline='Stage 2 Review Question 1 FR', + description='Stage 2 Review Question 1 Description FR') + + db.session.commit() + + def test_get_without_stage(self): + """Test that the get method returns the correct stage when a stage parameter isn't provided.""" + self.seed_static_data() + self.add_review_forms() + + response = self.app.get( + '/api/v1/review-form-detail', + headers=self.get_auth_header_for("event@admin.com"), + data={'event_id': self.first_event_id} + ) + + data = json.loads(response.data) + + self.assertEqual(data['stage'], 2) + + def test_get_first_stage(self): + """Check that the get method returns the form for stage 1.""" + self.seed_static_data() + self.add_review_forms() + + response = self.app.get( + '/api/v1/review-form-detail', + headers=self.get_auth_header_for("event@admin.com"), + data={'event_id': self.first_event_id, 'stage': 1} + ) + + data = json.loads(response.data) + + self.assertEqual(data['id'], 1) + self.assertEqual(data['event_id'], self.first_event_id) + self.assertEqual(data['application_form_id'], 1) + self.assertEqual(data['is_open'], True) + self.assertEqual(data['deadline'], "2021-06-01T00:00:00") + self.assertEqual(data['active'], False) + self.assertEqual(data['stage'], 1) + self.assertEqual(len(data['sections']), 2) + + section1 = data['sections'][0] + self.assertEqual(section1['order'], 1) + self.assertEqual(section1['name']['en'], 'Form 1 Section 1 EN') + self.assertEqual(section1['name']['fr'], 'Form 1 Section 1 FR') + self.assertEqual(section1['description']['en'], 'Form 1 Section 1 Description EN') + self.assertEqual(section1['description']['fr'], 'Form 1 Section 1 Description FR') + self.assertEqual(len(section1['questions']), 2) + + question1 = section1['questions'][0] + self.assertEqual(question1['order'], 1) + self.assertEqual(question1['weight'], 1) + self.assertEqual(question1['type'], 'numeric-text') + self.assertEqual(question1['question_id'], 0) + self.assertEqual(question1['headline']['en'], 'Review Question 1 EN') + self.assertEqual(question1['headline']['fr'], 'Review Question 1 FR') + self.assertEqual(question1['description']['en'], 'Review Question 1 Description EN') + self.assertEqual(question1['description']['fr'], 'Review Question 1 Description FR') + self.assertEqual(question1['placeholder']['en'], 'Placeholder Question 1 EN') + self.assertEqual(question1['placeholder']['fr'], 'Placeholder Question 1 FR') + self.assertEqual(question1['validation_regex']['en'], '(a|b|c|d)') + self.assertEqual(question1['validation_regex']['fr'], '(a|b|c|d)') + self.assertEqual(question1['validation_text']['en'], 'Validation text question 1 EN') + self.assertEqual(question1['validation_text']['fr'], 'Validation text question 1 FR') + self.assertEqual(question1['options']['en'], None) + self.assertEqual(question1['options']['fr'], None) + + question2 = section1['questions'][1] + self.assertEqual(question2['order'], 2) + self.assertEqual(question2['weight'], 0) + self.assertEqual(question2['type'], 'information') + self.assertEqual(question2['question_id'], 1) + self.assertEqual(question2['headline']['en'], 'Review Question 2 EN') + self.assertEqual(question2['headline']['fr'], 'Review Question 2 FR') + self.assertEqual(question2['description']['en'], 'Review Question 2 Description EN') + self.assertEqual(question2['description']['fr'], 'Review Question 2 Description FR') + self.assertEqual(question2['placeholder']['en'], None) + self.assertEqual(question2['placeholder']['fr'], None) + self.assertEqual(question2['validation_regex']['en'], None) + self.assertEqual(question2['validation_regex']['fr'], None) + self.assertEqual(question2['validation_text']['en'], None) + self.assertEqual(question2['validation_text']['fr'], None) + self.assertEqual(question2['options']['en'], None) + self.assertEqual(question2['options']['fr'], None) + + section2 = data['sections'][1] + self.assertEqual(section2['order'], 2) + self.assertEqual(section2['name']['en'], 'Form 1 Section 2 EN') + self.assertEqual(section2['name']['fr'], 'Form 1 Section 2 FR') + self.assertEqual(section2['description']['en'], 'Form 1 Section 2 Description EN') + self.assertEqual(section2['description']['fr'], 'Form 1 Section 2 Description FR') + self.assertEqual(len(section2['questions']), 1) + + question3 = section2['questions'][0] + self.assertEqual(question3['order'], 1) + self.assertEqual(question3['weight'], 1) + self.assertEqual(question3['type'], 'radio') + self.assertEqual(question3['question_id'], 0) + self.assertEqual(question3['headline']['en'], 'Review Question 3 EN') + self.assertEqual(question3['headline']['fr'], 'Review Question 3 FR') + self.assertEqual(question3['description']['en'], 'Review Question 3 Description EN') + self.assertEqual(question3['description']['fr'], 'Review Question 3 Description FR') + self.assertEqual(question3['placeholder']['en'], None) + self.assertEqual(question3['placeholder']['fr'], None) + self.assertEqual(question3['validation_regex']['en'], None) + self.assertEqual(question3['validation_regex']['fr'], None) + self.assertEqual(question3['validation_text']['en'], None) + self.assertEqual(question3['validation_text']['fr'], None) + self.assertEqual(len(question3['options']['en']), 3) + self.assertEqual(len(question3['options']['fr']), 3) + + def test_get_second_stage(self): + """Check that the get method returns the form for stage 2.""" + self.seed_static_data() + self.add_review_forms() + + response = self.app.get( + '/api/v1/review-form-detail', + headers=self.get_auth_header_for("event@admin.com"), + data={'event_id': self.first_event_id, 'stage': 2} + ) + + data = json.loads(response.data) + + self.assertEqual(data['id'], 2) + self.assertEqual(data['event_id'], self.first_event_id) + self.assertEqual(data['application_form_id'], 1) + self.assertEqual(data['is_open'], True) + self.assertEqual(data['deadline'], "2021-08-01T00:00:00") + self.assertEqual(data['active'], True) + self.assertEqual(data['stage'], 2) + self.assertEqual(len(data['sections']), 1) + + section1 = data['sections'][0] + self.assertEqual(section1['order'], 1) + self.assertEqual(section1['name']['en'], 'Form 2 Section 1 EN') + self.assertEqual(section1['name']['fr'], 'Form 2 Section 1 FR') + self.assertEqual(section1['description']['en'], 'Form 2 Section 1 Description EN') + self.assertEqual(section1['description']['fr'], 'Form 2 Section 1 Description FR') + self.assertEqual(len(section1['questions']), 1) + + question1 = section1['questions'][0] + self.assertEqual(question1['order'], 1) + self.assertEqual(question1['weight'], 0) + self.assertEqual(question1['type'], 'information') + self.assertEqual(question1['question_id'], 1) + self.assertEqual(question1['headline']['en'], 'Stage 2 Review Question 1 EN') + self.assertEqual(question1['headline']['fr'], 'Stage 2 Review Question 1 FR') + self.assertEqual(question1['description']['en'], 'Stage 2 Review Question 1 Description EN') + self.assertEqual(question1['description']['fr'], 'Stage 2 Review Question 1 Description FR') + self.assertEqual(question1['placeholder']['en'], None) + self.assertEqual(question1['placeholder']['fr'], None) + self.assertEqual(question1['validation_regex']['en'], None) + self.assertEqual(question1['validation_regex']['fr'], None) + self.assertEqual(question1['validation_text']['en'], None) + self.assertEqual(question1['validation_text']['fr'], None) + self.assertEqual(question1['options']['en'], None) + self.assertEqual(question1['options']['fr'], None) + + def test_get_new_stage(self): + """Check that the get method returns not found for a stage that doesn't exist.""" + self.seed_static_data() + self.add_review_forms() + + response = self.app.get( + '/api/v1/review-form-detail', + headers=self.get_auth_header_for("event@admin.com"), + data={'event_id': self.first_event_id, 'stage': 3} + ) + + self.assertEqual(response.status_code, 404) + + def test_get_no_form(self): + """Check that the get method returns not found for an event that has no review form.""" + self.seed_static_data() + self.add_review_forms() + + response = self.app.get( + '/api/v1/review-form-detail', + headers=self.get_auth_header_for("event@admin.com"), + data={'event_id': self.no_form_event_id, 'stage': 1} + ) + + self.assertEqual(response.status_code, 404) + + def test_post(self): + """Check that the post method works.""" + self.seed_static_data() + + response = self.app.post( + '/api/v1/review-form-detail', + headers=self.get_auth_header_for("event@admin.com"), + data=json.dumps(REVIEW_FORM_POST), + content_type='application/json',) + + self.assertEqual(response.status_code, 201) + + response = self.app.get( + '/api/v1/review-form-detail', + headers=self.get_auth_header_for("event@admin.com"), + data={'event_id': REVIEW_FORM_POST["event_id"], 'stage': REVIEW_FORM_POST["stage"]} + ) + + data = json.loads(response.data) + + self.assertTrue("id" in data and data["id"] is not None) + del data["id"] + + self.assertTrue("id" in data["sections"][0] and data["sections"][0]["id"] is not None) + del data["sections"][0]["id"] + + self.assertTrue("id" in data["sections"][1] and data["sections"][1]["id"] is not None) + del data["sections"][1]["id"] + + self.assertTrue("id" in data["sections"][0]["questions"][0] and data["sections"][0]["questions"][0]["id"] is not None) + del data["sections"][0]["questions"][0]["id"] + + self.assertTrue("id" in data["sections"][0]["questions"][1] and data["sections"][0]["questions"][1]["id"] is not None) + del data["sections"][0]["questions"][1]["id"] + + self.assertTrue("id" in data["sections"][1]["questions"][0] and data["sections"][1]["questions"][0]["id"] is not None) + del data["sections"][1]["questions"][0]["id"] + + self.maxDiff = 6000 + self.assertEqual(data, REVIEW_FORM_POST) + + def test_put(self): + self.seed_static_data() + + # First create the review form + response = self.app.post( + '/api/v1/review-form-detail', + headers=self.get_auth_header_for("event@admin.com"), + data=json.dumps(REVIEW_FORM_POST), + content_type='application/json',) + + self.assertEqual(response.status_code, 201) + + # Now update it + response = self.app.put( + '/api/v1/review-form-detail', + headers=self.get_auth_header_for("event@admin.com"), + data=json.dumps(REVIEW_FORM_PUT), + content_type='application/json' + ) + + data = json.loads(response.data) + + del data["sections"][1]["id"] + del data["sections"][0]["questions"][1]["id"] + del data["sections"][1]["questions"][0]["id"] + self.maxDiff = None + self.assertEqual(data, REVIEW_FORM_PUT) diff --git a/api/app/users/repository.py b/api/app/users/repository.py index f2a498a7c..205dcca1f 100644 --- a/api/app/users/repository.py +++ b/api/app/users/repository.py @@ -1,80 +1,80 @@ -from app import db -from app.applicationModel.models import ApplicationForm -from app.events.models import EventRole -from app.responses.models import Response -from app.users.models import AppUser -from app.invitedGuest.models import InvitedGuest -from app.organisation.models import Organisation -from sqlalchemy import func -from sqlalchemy import or_ - -class UserRepository(): - - @staticmethod - def get_by_id(user_id): - return db.session.query(AppUser).get(user_id) - - @staticmethod - def get_by_id_with_response(user_id): - return db.session.query(AppUser, Response)\ - .filter_by(id=user_id)\ - .join(Response, Response.user_id==AppUser.id)\ - .first() - - @staticmethod - def get_by_email(email, organisation_id): - return db.session.query(AppUser)\ - .filter(func.lower(AppUser.email) == func.lower(email))\ - .filter_by(organisation_id=organisation_id).first() - - @staticmethod - def get_by_event_admin(user_id, event_admin_user_id): - return db.session.query(AppUser, Response)\ - .filter_by(id=user_id)\ - .join(Response, Response.user_id==AppUser.id)\ - .join(ApplicationForm, ApplicationForm.id==Response.application_form_id)\ - .join(EventRole, EventRole.event_id==ApplicationForm.event_id)\ - .filter_by(user_id=event_admin_user_id, role='admin')\ - .first() - - @staticmethod - def get_all_with_unsubmitted_response(): - # TODO: Filter this to event - return db.session.query(AppUser)\ - .filter_by(active=True, is_deleted=False)\ - .join(Response, Response.user_id==AppUser.id)\ - .filter_by(is_submitted=False, is_withdrawn=False)\ - .all() - - @staticmethod - def get_all_without_responses(): - # TODO: Filter this to event - return db.session.query(AppUser)\ - .filter_by(active=True, is_deleted=False)\ - .outerjoin(Response, Response.user_id==AppUser.id)\ - .filter_by(id=None)\ - .all() - - @staticmethod - def get_all_with_responses_for(event_id): - return db.session.query(AppUser, Response)\ - .filter_by(active=True, is_deleted=False)\ - .join(Response, Response.user_id==AppUser.id)\ - .join(ApplicationForm, ApplicationForm.id==Response.application_form_id)\ - .filter_by(event_id=event_id)\ - .all() - - @staticmethod - def get_all_with_responses_or_invited_guests_for(event_id): - return ( - db.session.query(AppUser, Response, InvitedGuest) - .filter_by(active=True, is_deleted=False) - .join(Response, Response.user_id==AppUser.id, isouter=True) - .join(InvitedGuest, InvitedGuest.user_id==AppUser.id, isouter=True) - .filter(or_(InvitedGuest != None, Response != None)) - .join(ApplicationForm, ApplicationForm.id==Response.application_form_id, isouter=True) - - # Since both InvitedGuest and ApplicationForm have an event_id we need to include - # both in the filter. - .filter(or_(InvitedGuest.event_id==event_id, ApplicationForm.event_id==event_id)) - ).all() +from app import db +from app.applicationModel.models import ApplicationForm +from app.events.models import EventRole +from app.responses.models import Response +from app.users.models import AppUser +from app.invitedGuest.models import InvitedGuest +from app.organisation.models import Organisation +from sqlalchemy import func +from sqlalchemy import or_ + +class UserRepository(): + + @staticmethod + def get_by_id(user_id): + return db.session.query(AppUser).get(user_id) + + @staticmethod + def get_by_id_with_response(user_id): + return db.session.query(AppUser, Response)\ + .filter_by(id=user_id)\ + .join(Response, Response.user_id==AppUser.id)\ + .first() + + @staticmethod + def get_by_email(email, organisation_id): + return db.session.query(AppUser)\ + .filter(func.lower(AppUser.email) == func.lower(email))\ + .filter_by(organisation_id=organisation_id).first() + + @staticmethod + def get_by_event_admin(user_id, event_admin_user_id): + return db.session.query(AppUser, Response)\ + .filter_by(id=user_id)\ + .join(Response, Response.user_id==AppUser.id)\ + .join(ApplicationForm, ApplicationForm.id==Response.application_form_id)\ + .join(EventRole, EventRole.event_id==ApplicationForm.event_id)\ + .filter_by(user_id=event_admin_user_id, role='admin')\ + .first() + + @staticmethod + def get_all_with_unsubmitted_response(): + # TODO: Filter this to event + return db.session.query(AppUser)\ + .filter_by(active=True, is_deleted=False)\ + .join(Response, Response.user_id==AppUser.id)\ + .filter_by(is_submitted=False, is_withdrawn=False)\ + .all() + + @staticmethod + def get_all_without_responses(): + # TODO: Filter this to event + return db.session.query(AppUser)\ + .filter_by(active=True, is_deleted=False)\ + .outerjoin(Response, Response.user_id==AppUser.id)\ + .filter_by(id=None)\ + .all() + + @staticmethod + def get_all_with_responses_for(event_id): + return db.session.query(AppUser, Response)\ + .filter_by(active=True, is_deleted=False)\ + .join(Response, Response.user_id==AppUser.id)\ + .join(ApplicationForm, ApplicationForm.id==Response.application_form_id)\ + .filter_by(event_id=event_id)\ + .all() + + @staticmethod + def get_all_with_responses_or_invited_guests_for(event_id): + return ( + db.session.query(AppUser, Response, InvitedGuest) + .filter_by(active=True, is_deleted=False) + .join(Response, Response.user_id==AppUser.id, isouter=True) + .join(InvitedGuest, InvitedGuest.user_id==AppUser.id, isouter=True) + .filter(or_(InvitedGuest != None, Response != None)) + .join(ApplicationForm, ApplicationForm.id==Response.application_form_id, isouter=True) + + # Since both InvitedGuest and ApplicationForm have an event_id we need to include + # both in the filter. + .filter(or_(InvitedGuest.event_id==event_id, ApplicationForm.event_id==event_id)) + ).all() From e08e1bc6b8a8b91038921d81843fc3bbf63aa35d Mon Sep 17 00:00:00 2001 From: Amrit Purshotam Date: Thu, 17 Mar 2022 20:33:50 +0100 Subject: [PATCH 3/8] Tell editor to use LF line endings --- .editorconfig | 4 ++++ .gitignore | 4 ---- .vscode/extensions.json | 5 +++++ 3 files changed, 9 insertions(+), 4 deletions(-) create mode 100644 .editorconfig create mode 100644 .vscode/extensions.json diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..006bc2fc7 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,4 @@ +root = true + +[*] +end_of_line = lf diff --git a/.gitignore b/.gitignore index a7c533b1a..306aeef57 100644 --- a/.gitignore +++ b/.gitignore @@ -103,10 +103,6 @@ venv.bak/ .spyderproject .spyproject -# VSCode project settings -.vscode - - # IntelliJ/Pycharm/Webstorm/etc. project settings .iml diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 000000000..189adec04 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "editorconfig.editorconfig" + ] +} \ No newline at end of file From 1c0ab5a43dee16639e1873be600e5422a17a5648 Mon Sep 17 00:00:00 2001 From: Amrit Purshotam Date: Thu, 17 Mar 2022 21:09:14 +0100 Subject: [PATCH 4/8] Add black and isort formatting settings --- .circleci/config.yml | 6 ++++++ .vscode/extensions.json | 3 ++- .vscode/settings.json | 10 ++++++++++ api/Dockerfile | 7 ++++--- api/requirements-dev.txt | 2 ++ api/setup.cfg | 2 ++ 6 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 api/requirements-dev.txt create mode 100644 api/setup.cfg diff --git a/.circleci/config.yml b/.circleci/config.yml index 15dd8a01b..5f216c067 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -14,6 +14,12 @@ jobs: gcloud --quiet config set compute/zone ${GOOGLE_COMPUTE_ZONE} openssl aes256 -md md5 -d -in docker-compose.circleci.yaml.enc -out docker-compose.yml -pass pass:$CONFIG_PASSWORD docker-compose up -d + - run: + name: Format with black + command: docker-compose run web black --check api + - run: + name: Format with isort + command: docker-compose run web isort --check --settings-path=api/setup.cfg api - run: name: Run migrations command: | diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 189adec04..d2d1709fb 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,5 +1,6 @@ { "recommendations": [ - "editorconfig.editorconfig" + "editorconfig.editorconfig", + "ms-python.python" ] } \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..a99b1761d --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,10 @@ +{ + "python.formatting.provider": "black", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports": true + }, + "python.sortImports.args": [ + "--settings-path=${workspaceFolder}/api/setup.cfg" + ] +} \ No newline at end of file diff --git a/api/Dockerfile b/api/Dockerfile index 29470489b..efb56830a 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -9,14 +9,15 @@ ENV PATH /env/bin:$PATH RUN apt-get update -qq \ && apt-get install -y software-properties-common \ - && apt-get install -y libreoffice + && apt-get install -y libreoffice \ + && python3 -m pip install --upgrade pip # Upgrade pip RUN python -m pip install --upgrade pip # Add the application source code. -ADD requirements.txt /code/requirements.txt -RUN pip3 install -r /code/requirements.txt +COPY requirements.txt requirements-dev.txt /code/ +RUN pip3 install -r /code/requirements.txt -r /code/requirements-dev.txt ADD . /code/ WORKDIR /code diff --git a/api/requirements-dev.txt b/api/requirements-dev.txt new file mode 100644 index 000000000..1f1de0656 --- /dev/null +++ b/api/requirements-dev.txt @@ -0,0 +1,2 @@ +black==22.1.0 +isort==5.10.1 \ No newline at end of file diff --git a/api/setup.cfg b/api/setup.cfg new file mode 100644 index 000000000..841770380 --- /dev/null +++ b/api/setup.cfg @@ -0,0 +1,2 @@ +[isort] +profile = black \ No newline at end of file From 284a7e72c6c6f793af0442c96d604ac4e82ae7ee Mon Sep 17 00:00:00 2001 From: Amrit Purshotam Date: Thu, 17 Mar 2022 21:15:21 +0100 Subject: [PATCH 5/8] Apply black and isort changes --- api/app/__init__.py | 123 +- api/app/applicationModel/api.py | 438 +- api/app/applicationModel/models.py | 124 +- api/app/applicationModel/repository.py | 39 +- api/app/applicationModel/tests.py | 1111 ++--- api/app/attendance/api.py | 135 +- api/app/attendance/mixins.py | 5 +- api/app/attendance/models.py | 22 +- api/app/attendance/repository.py | 21 +- api/app/attendance/tests.py | 235 +- api/app/content/api.py | 58 +- api/app/email_template/models.py | 14 +- api/app/email_template/repository.py | 23 +- api/app/events/api.py | 500 ++- api/app/events/mixins.py | 52 +- api/app/events/models.py | 93 +- api/app/events/repository.py | 73 +- api/app/events/status.py | 99 +- api/app/events/tests.py | 707 ++-- api/app/files/api.py | 45 +- api/app/files/mixins.py | 3 +- api/app/guestRegistrations/api.py | 404 +- api/app/guestRegistrations/mixins.py | 8 +- api/app/guestRegistrations/repository.py | 127 +- api/app/guestRegistrations/tests.py | 235 +- api/app/integration_tests/api.py | 56 +- api/app/integration_tests/mixins.py | 3 +- api/app/invitationletter/api.py | 232 +- api/app/invitationletter/generator.py | 212 +- api/app/invitationletter/mixins.py | 24 +- api/app/invitationletter/models.py | 8 +- api/app/invitationletter/tests.py | 283 +- api/app/invitedGuest/api.py | 154 +- api/app/invitedGuest/mixins.py | 8 +- api/app/invitedGuest/models.py | 25 +- api/app/invitedGuest/repository.py | 28 +- api/app/invitedGuest/tests.py | 138 +- api/app/organisation/api.py | 30 +- api/app/organisation/models.py | 18 +- api/app/organisation/repository.py | 9 +- api/app/organisation/resolver.py | 31 +- api/app/organisation/tests.py | 59 +- api/app/outcome/api.py | 95 +- api/app/outcome/models.py | 40 +- api/app/outcome/repository.py | 29 +- api/app/outcome/tests.py | 171 +- api/app/references/api.py | 244 +- api/app/references/mixins.py | 31 +- api/app/references/models.py | 30 +- api/app/references/repository.py | 57 +- api/app/references/tests.py | 257 +- api/app/registration/api.py | 526 ++- api/app/registration/mixins.py | 38 +- api/app/registration/models.py | 97 +- api/app/registration/repository.py | 85 +- api/app/registration/tests.py | 216 +- api/app/registrationResponse/api.py | 472 ++- api/app/registrationResponse/mixins.py | 19 +- api/app/registrationResponse/repository.py | 90 +- api/app/registrationResponse/tests.py | 309 +- api/app/responses/api.py | 533 ++- api/app/responses/mixins.py | 35 +- api/app/responses/models.py | 72 +- api/app/responses/repository.py | 179 +- api/app/responses/tests.py | 1355 +++--- api/app/reviews/api.py | 1005 +++-- api/app/reviews/mixins.py | 45 +- api/app/reviews/models.py | 190 +- api/app/reviews/repository.py | 412 +- api/app/reviews/tests.py | 3675 ++++++++++------- api/app/routes.py | 253 +- api/app/tags/api.py | 56 +- api/app/tags/models.py | 18 +- api/app/tags/repository.py | 2 +- api/app/tags/tests.py | 204 +- api/app/tests/test_email.py | 16 +- api/app/tests/test_logger.py | 9 +- api/app/users/api.py | 407 +- api/app/users/mixins.py | 39 +- api/app/users/models.py | 113 +- api/app/users/repository.py | 113 +- api/app/users/tests.py | 987 +++-- api/app/utils/auth.py | 38 +- api/app/utils/emailer.py | 146 +- api/app/utils/errors.py | 268 +- api/app/utils/logger.py | 41 +- api/app/utils/misc.py | 1 + api/app/utils/pdfconvertor.py | 78 +- api/app/utils/rate_limit.py | 14 +- api/app/utils/storage.py | 41 +- api/app/utils/strings.py | 106 +- api/app/utils/testing.py | 441 +- api/app/utils/tests.py | 249 +- api/app/utils/zipping.py | 2 +- api/config.py | 43 +- api/gunicorn.config.py | 4 +- api/main.py | 3 +- api/migrations/env.py | 27 +- api/migrations/versions/0023aeda53b0_.py | 22 +- .../versions/016571f41a20_add_organisation.py | 74 +- .../versions/02242641e122_add_local_event.py | 173 +- .../versions/039addb92a03_ai4d_call_launch.py | 151 +- api/migrations/versions/04188674173e_.py | 38 +- .../07d536b58953_review_form_stages.py | 18 +- api/migrations/versions/09b2cc2105be_.py | 48 +- api/migrations/versions/0ab2d6bca67d_.py | 20 +- api/migrations/versions/0e13fd6f0d2a_.py | 28 +- .../10b4b888c16b_ai4d_first_call_updates.py | 1617 ++++---- .../111c4f9eab84_add_ai4d_prc_keys.py | 159 +- api/migrations/versions/122dd6a5c035_.py | 19 +- api/migrations/versions/1349a2c924f4_.py | 436 +- api/migrations/versions/14d53fa0c9c8_.py | 443 +- api/migrations/versions/1993beca93d7_.py | 26 +- .../1afd8dfc8d05_add_neuro_imbizo_org.py | 56 +- api/migrations/versions/1c3fda18bad6_.py | 844 +++- .../1e05a293f402_eeml_registration_form.py | 523 ++- .../versions/20a94c6b2952_update_eeml_form.py | 22 +- ...241922a3305_redo_french_email_templates.py | 326 +- .../2279e1fa2e49_eeml_review_update2.py | 290 +- api/migrations/versions/245d12695c69_.py | 9 +- ...ef90a874_add_review_configuration_table.py | 35 +- api/migrations/versions/2873f4f08a4b_.py | 38 +- api/migrations/versions/297276d57414_.py | 6 +- api/migrations/versions/29dd5b8bc663_.py | 6 +- .../versions/2dfc0d119bc2_review_sections.py | 115 +- .../2e81ebc8d90a_response_reviewer_active.py | 14 +- .../303140b3cefb_ai4d_scholarship_review1.py | 827 ++-- api/migrations/versions/32c1ae6dd23a_.py | 6 +- .../33b563f7be6b_review_response_language.py | 14 +- api/migrations/versions/33e175de61ca_.py | 18 +- ...454400e3738_review_question_translation.py | 100 +- .../347a14922cff_ai4d_prc_review_form.py | 522 ++- .../34a5a1fce672_ai4d_prc_review_updates.py | 542 ++- api/migrations/versions/351c26992f23_.py | 50 +- .../versions/358d0e0282bb_add_user_comment.py | 40 +- .../35cdc3c51184_add_validation_text.py | 54 +- api/migrations/versions/36235e184d09_.py | 54 +- api/migrations/versions/3745cf17152b_.py | 31 +- api/migrations/versions/37fbee2da7b7_.py | 20 +- api/migrations/versions/38dde1052c5e_.py | 65 +- .../3ab678fc66cd_populate_reg_form.py | 734 ++-- ...c5355f159c_add_org_url_and_event_fields.py | 68 +- api/migrations/versions/3dcef2e3c442_.py | 14 +- .../3e7bdff8af80_eeml_form_updates.py | 73 +- api/migrations/versions/3ec3ed0eb3c6_.py | 6 +- ...d_add_section_and_question_translations.py | 195 +- api/migrations/versions/405d106c8292_.py | 6 +- api/migrations/versions/40ee6f91f6f3_.py | 6 +- .../versions/43597f3ea193_redo_ai4d_call.py | 1484 ++++--- api/migrations/versions/438ca9e1c2e8_.py | 6 +- api/migrations/versions/44df297ad858_.py | 34 +- .../versions/4605cce59919_add_ai4d.py | 270 +- api/migrations/versions/472bf293e0a1_.py | 6 +- api/migrations/versions/492546998537_.py | 6 +- .../49d55031108e_ai4d_scholarships.py | 1518 ++++--- ...d67699684_ai4d_scholarship_call_updates.py | 190 +- .../4b881c1c6dc2_add_email_templates.py | 215 +- .../4d8b254d7e7e_link_user_to_organisation.py | 27 +- .../4ec21502ae82_add_call_event_type.py | 12 +- api/migrations/versions/539d0567cd6c_.py | 10 +- .../57e55dc645c1_set_travel_accomm_columns.py | 26 +- .../613df2d7a759_update_email_template.py | 14 +- api/migrations/versions/62352569fb5d_.py | 6 +- .../62c7711123a8_french_email_templates.py | 299 +- api/migrations/versions/65f15544a656_.py | 58 +- .../6b64b8037b7b_prc_review_update2.py | 1425 ++++--- .../versions/6d34d1ab6864_eeml_review_form.py | 309 +- api/migrations/versions/6fa47605c615_.py | 53 +- api/migrations/versions/72548fc390d4_.py | 12 +- api/migrations/versions/742a2a556eb9_.py | 16 +- .../versions/74f1f1476873_update_eeml_form.py | 54 +- ...26545bb_update_review_assigned_template.py | 36 +- .../78f4d11203c5_ai4d_more_changes.py | 192 +- api/migrations/versions/794506c08e10_.py | 53 +- api/migrations/versions/79c61673a487_.py | 18 +- api/migrations/versions/7e8bffa88454_.py | 19 +- .../versions/7f37bf248c81_add_org_icon.py | 32 +- api/migrations/versions/7fced3d22209_.py | 96 +- .../8339a70d7345_small_ai4d_update.py | 25 +- ...9a618_add_integration_test_organization.py | 86 +- api/migrations/versions/85ce7069fb96_.py | 64 +- api/migrations/versions/85d13062a5e7_.py | 32 +- api/migrations/versions/865b2e2d02ff_.py | 6 +- api/migrations/versions/866611669529_.py | 6 +- .../87213d612eaf_eeml_application_form.py | 468 ++- api/migrations/versions/878f67285c72_.py | 56 +- api/migrations/versions/88199ece9b26_.py | 6 +- .../versions/88cc4397bdf5_ai4d_org.py | 51 +- api/migrations/versions/8a1468838847_.py | 45 +- api/migrations/versions/8ba06d0a54ee_.py | 6 +- api/migrations/versions/910e74b87f06_.py | 33 +- api/migrations/versions/91468f804113_.py | 15 +- api/migrations/versions/91a7bc19a5ec_.py | 6 +- .../9630ab6b0437_alter_rejection_reason.py | 10 +- .../versions/964ead196cb9_add_miniconf_url.py | 26 +- .../984cb348ec98_add_response_language.py | 12 +- api/migrations/versions/99cfdad9869c_.py | 34 +- api/migrations/versions/99f650dc0924_.py | 149 +- api/migrations/versions/9b181cdd3fa1_merge.py | 6 +- .../a4662031beca_add_outcome_model.py | 50 +- .../a5480ca6646b_indaba2019_reg_form.py | 36 +- api/migrations/versions/a5876b93baee_.py | 19 +- api/migrations/versions/a714c69d1a6c_.py | 12 +- api/migrations/versions/aa890a74c772_.py | 85 +- api/migrations/versions/ab8b7ff61eee_.py | 68 +- api/migrations/versions/ae94d067a229_.py | 65 +- api/migrations/versions/af9c317d2c92_.py | 404 +- ...2d1fc607635_add_organisation_system_url.py | 24 +- api/migrations/versions/b3f7a73c98f3_.py | 656 +-- .../b4c1dfa70233_add_ethnicity_question.py | 126 +- api/migrations/versions/b6a2a69fbb13_.py | 33 +- api/migrations/versions/b7006cbdd923_.py | 74 +- api/migrations/versions/c06c236ad4c5_.py | 39 +- api/migrations/versions/c0885e531d29_.py | 86 +- .../c3b4ca7ceef9_add_org_privacy_policy.py | 24 +- api/migrations/versions/c3c5ef958ca1_.py | 427 +- api/migrations/versions/c795c179c2b2_.py | 337 +- api/migrations/versions/c8d762cf6784_.py | 730 ++-- .../ca538998737f_add_ai4d_first_call.py | 1506 ++++--- api/migrations/versions/cb36953371ab_.py | 6 +- api/migrations/versions/cb391b878586_.py | 35 +- .../versions/cb503c2afd08_ai4d_fixes.py | 184 +- ...62bfc5be5_add_email_verification_fields.py | 18 +- .../versions/d7dfd413e768_add_tag_models.py | 67 +- api/migrations/versions/d7f3adff2c9f_.py | 6 +- api/migrations/versions/d80784e674a8_.py | 6 +- api/migrations/versions/d8d17ed47c53_.py | 13 +- .../versions/de12a6f382c1_ai4d_review_form.py | 419 +- ...034c22_add_submitted_to_review_response.py | 45 +- .../e3e47585f336_section_question_key.py | 42 +- api/migrations/versions/e622c3deea5d_.py | 36 +- .../e8d1ee2746e0_event_travel_grant.py | 20 +- .../versions/ea987b257ee0_ai4d_updates.py | 283 +- api/migrations/versions/eac3fd9d5ae8_.py | 61 +- .../eb6f96db82e8_eeml_review_form_update1.py | 30 +- api/migrations/versions/efc34996e565_.py | 82 +- api/migrations/versions/f2cefa124236_.py | 25 +- api/migrations/versions/f349debf9e15_.py | 13 +- api/migrations/versions/f4bc3108c330_.py | 9 +- api/migrations/versions/f8ab033698a2_.py | 14 +- api/migrations/versions/fbd283484655_.py | 123 +- ...c8e54fd7_emailtemplate_language_subject.py | 62 +- api/run.py | 6 +- 243 files changed, 25805 insertions(+), 17278 deletions(-) diff --git a/api/app/__init__.py b/api/app/__init__.py index 9443ba919..65fa46004 100644 --- a/api/app/__init__.py +++ b/api/app/__init__.py @@ -1,23 +1,24 @@ -from flask import Flask, g, url_for, redirect, render_template, request, flash -from flask_cors import CORS +import flask_login as login import flask_restful as restful -from flask_sqlalchemy import SQLAlchemy +import tldextract +from flask import Flask, flash, g, redirect, render_template, request, url_for +from flask_admin import Admin, AdminIndexView, expose, helpers +from flask_admin.contrib.sqla import ModelView from flask_bcrypt import Bcrypt +from flask_cors import CORS from flask_migrate import Migrate, MigrateCommand -from flask_script import Manager from flask_redis import FlaskRedis +from flask_script import Manager +from flask_sqlalchemy import SQLAlchemy +from werkzeug.security import check_password_hash, generate_password_hash +from wtforms import fields, form, validators + from .utils.logger import Logger -from flask_admin import Admin, AdminIndexView, helpers, expose -from flask_admin.contrib.sqla import ModelView -import flask_login as login -from wtforms import form, fields, validators -from werkzeug.security import generate_password_hash, check_password_hash -import tldextract app = Flask(__name__) -app.config.from_object('config') +app.config.from_object("config") CORS(app, resources={r"/api/*": {"origins": "*"}}, supports_credentials=True) -print((app.config['SQLALCHEMY_DATABASE_URI'])) +print((app.config["SQLALCHEMY_DATABASE_URI"])) rest_api = restful.Api(app) db = SQLAlchemy(app) bcrypt = Bcrypt(app) @@ -29,49 +30,66 @@ migrate = Migrate(app, db) manager = Manager(app) -manager.add_command('db', MigrateCommand) +manager.add_command("db", MigrateCommand) from .organisation.resolver import OrganisationResolver + def get_domain(): # TODO: Remove this test-related hack! - if app.config['TESTING'] and 'HTTP_ORIGIN' not in request.environ and 'HTTP_REFERER' not in request.environ: - return 'org' - - origin = request.environ.get('HTTP_ORIGIN', '') + if ( + app.config["TESTING"] + and "HTTP_ORIGIN" not in request.environ + and "HTTP_REFERER" not in request.environ + ): + return "org" + + origin = request.environ.get("HTTP_ORIGIN", "") if not origin: # Try to get from Referer header - origin = request.environ.get('HTTP_REFERER', '') - LOGGER.debug('No ORIGIN header, falling back to Referer: {}'.format(origin)) - + origin = request.environ.get("HTTP_REFERER", "") + LOGGER.debug("No ORIGIN header, falling back to Referer: {}".format(origin)) + if origin: domain = tldextract.extract(origin).domain else: - LOGGER.warning('Could not determine origin domain') - domain = '' - + LOGGER.warning("Could not determine origin domain") + domain = "" + return domain + @app.before_request def populate_organisation(): domain = get_domain() - LOGGER.info('Origin Domain: {}'.format(domain)) # TODO: Remove this after testing + LOGGER.info("Origin Domain: {}".format(domain)) # TODO: Remove this after testing g.organisation = OrganisationResolver.resolve_from_domain(domain) + ## Flask Admin Config # set optional bootswatch theme -app.config['FLASK_ADMIN_SWATCH'] = 'darkly' +app.config["FLASK_ADMIN_SWATCH"] = "darkly" +from app.utils.auth import admin_required, auth_required, generate_token +from app.utils.errors import FORBIDDEN, UNAUTHORIZED + from .applicationModel.models import Question, Section -from .responses.models import Response, Answer, ResponseReviewer -from .users.models import UserCategory, AppUser, UserComment from .email_template.models import EmailTemplate -from .outcome.models import Outcome from .events.models import Event, EventRole -from app.utils.auth import auth_required, admin_required, generate_token -from app.utils.errors import UNAUTHORIZED, FORBIDDEN +from .invitationletter.models import InvitationLetterRequest, InvitationTemplate +from .outcome.models import Outcome +from .registration.models import ( + Offer, + Registration, + RegistrationAnswer, + RegistrationForm, + RegistrationQuestion, + RegistrationSection, +) +from .responses.models import Answer, Response, ResponseReviewer from .reviews.models import ReviewForm, ReviewQuestion -from .registration.models import Offer, RegistrationForm, RegistrationSection, RegistrationQuestion, Registration, RegistrationAnswer -from .invitationletter.models import InvitationTemplate, InvitationLetterRequest +from .users.models import AppUser, UserCategory, UserComment + + # Define login and registration forms (for flask-login) class LoginForm(form.Form): email = fields.TextField(validators=[validators.required()]) @@ -81,11 +99,10 @@ def validate(self): user = self.get_user() if user is None: - raise validators.ValidationError('Invalid user') - + raise validators.ValidationError("Invalid user") if not bcrypt.check_password_hash(user.password, self.password.data): - raise validators.ValidationError('Invalid password') + raise validators.ValidationError("Invalid password") if not user.is_admin: raise validators.ValidationError("Adminstrator rights required") @@ -95,7 +112,10 @@ def validate(self): def get_user(self): # TODO: What organisation should we use to query here? - return db.session.query(AppUser).filter(AppUser.email==self.email.data).first() + return ( + db.session.query(AppUser).filter(AppUser.email == self.email.data).first() + ) + # Initialize flask-login def init_login(): @@ -108,45 +128,48 @@ def load_user(user_id): return db.session.query(AppUser).get(user_id) - # Create customized index view class that handles login & registration class BaobabAdminIndexView(AdminIndexView): - - @expose('/') + @expose("/") def index(self): if not login.current_user.is_authenticated: - return redirect(url_for('.login_view')) + return redirect(url_for(".login_view")) return super(BaobabAdminIndexView, self).index() - @expose('/login/', methods=('GET', 'POST')) + @expose("/login/", methods=("GET", "POST")) def login_view(self): try: # handle user login form = LoginForm(request.form) - if request.method == 'POST': + if request.method == "POST": if form.validate(): user = form.get_user() login.login_user(user) if login.current_user.is_authenticated: - return redirect(url_for('.index')) + return redirect(url_for(".index")) except validators.ValidationError as error: flash(str(error)) - self._template_args['form'] = form + self._template_args["form"] = form return super(BaobabAdminIndexView, self).index() - - @expose('/logout/') + @expose("/logout/") def logout_view(self): login.logout_user() - return redirect(url_for('.index')) + return redirect(url_for(".index")) + # Initialize flask-login init_login() -admin = Admin(app, name='Deep Learning Indaba Admin Portal', index_view=BaobabAdminIndexView(), template_mode='bootstrap3') +admin = Admin( + app, + name="Deep Learning Indaba Admin Portal", + index_view=BaobabAdminIndexView(), + template_mode="bootstrap3", +) class BaobabModelView(ModelView): @@ -155,7 +178,8 @@ def is_accessible(self): def inaccessible_callback(self, name, **kwargs): # redirect to login page if user doesn't have access - return redirect(url_for('admin.login_view', next=request.url)) + return redirect(url_for("admin.login_view", next=request.url)) + admin.add_view(BaobabModelView(Question, db.session)) admin.add_view(BaobabModelView(Section, db.session)) @@ -181,4 +205,3 @@ def inaccessible_callback(self, name, **kwargs): admin.add_view(BaobabModelView(InvitationTemplate, db.session)) admin.add_view(BaobabModelView(InvitationLetterRequest, db.session)) admin.add_view(BaobabModelView(EmailTemplate, db.session)) - diff --git a/api/app/applicationModel/api.py b/api/app/applicationModel/api.py index 6226102be..a13c7cd62 100644 --- a/api/app/applicationModel/api.py +++ b/api/app/applicationModel/api.py @@ -1,23 +1,36 @@ -from datetime import datetime import traceback +from datetime import datetime +from typing import Sequence import flask_restful as restful - -from flask_restful import reqparse, fields, marshal_with, marshal -from sqlalchemy.exc import SQLAlchemyError from flask import g +from flask_restful import fields, marshal, marshal_with, reqparse +from sqlalchemy.exc import SQLAlchemyError -from app.applicationModel.models import ApplicationForm, Question, Section, SectionTranslation, QuestionTranslation +from app import LOGGER, bcrypt, db +from app.applicationModel.models import ( + ApplicationForm, + Question, + QuestionTranslation, + Section, + SectionTranslation, +) +from app.applicationModel.repository import ( + ApplicationFormRepository as application_form_repository, +) from app.events.repository import EventRepository as event_repository -from app.applicationModel.repository import ApplicationFormRepository as application_form_repository from app.users.repository import UserRepository as user_repository from app.utils.auth import auth_required, event_admin_required -from app.utils.errors import APPLICATION_FORM_EXISTS, EVENT_NOT_FOUND, QUESTION_NOT_FOUND, SECTION_NOT_FOUND, DB_NOT_AVAILABLE, FORM_NOT_FOUND, APPLICATIONS_CLOSED +from app.utils.errors import ( + APPLICATION_FORM_EXISTS, + APPLICATIONS_CLOSED, + DB_NOT_AVAILABLE, + EVENT_NOT_FOUND, + FORM_NOT_FOUND, + QUESTION_NOT_FOUND, + SECTION_NOT_FOUND, +) -from app import db, bcrypt -from app import LOGGER - -from typing import Sequence def get_form_fields(form, language): section_fields = [] @@ -26,102 +39,110 @@ def get_form_fields(form, language): for question in section.questions: question_translation = question.get_translation(language) question_field = { - 'id': question.id, - 'type': question.type, - 'description': question_translation.description, - 'headline': question_translation.headline, - 'order': question.order, - 'options': question_translation.options, - 'placeholder': question_translation.placeholder, - 'validation_regex': question_translation.validation_regex, - 'validation_text': question_translation.validation_text, - 'is_required': question.is_required, - 'depends_on_question_id': question.depends_on_question_id, - 'show_for_values': question_translation.show_for_values, - 'key': question.key + "id": question.id, + "type": question.type, + "description": question_translation.description, + "headline": question_translation.headline, + "order": question.order, + "options": question_translation.options, + "placeholder": question_translation.placeholder, + "validation_regex": question_translation.validation_regex, + "validation_text": question_translation.validation_text, + "is_required": question.is_required, + "depends_on_question_id": question.depends_on_question_id, + "show_for_values": question_translation.show_for_values, + "key": question.key, } question_fields.append(question_field) section_translation = section.get_translation(language) section_field = { - 'id': section.id, - 'name': section_translation.name, - 'description': section_translation.description, - 'order': section.order, - 'depends_on_question_id': section.depends_on_question_id, - 'show_for_values': section_translation.show_for_values, - 'questions': question_fields + "id": section.id, + "name": section_translation.name, + "description": section_translation.description, + "order": section.order, + "depends_on_question_id": section.depends_on_question_id, + "show_for_values": section_translation.show_for_values, + "questions": question_fields, } section_fields.append(section_field) form_fields = { - 'id': form.id, - 'event_id': form.event_id, - 'is_open': form.is_open, - 'nominations': form.nominations, - 'sections': section_fields + "id": form.id, + "event_id": form.event_id, + "is_open": form.is_open, + "nominations": form.nominations, + "sections": section_fields, } return form_fields + question_detail_fields = { - 'id': fields.Integer, - 'type': fields.String, - 'description': fields.Raw(attribute=lambda q: q.description_translations), - 'headline': fields.Raw(attribute=lambda q: q.headline_translations), - 'order': fields.Integer, - 'options': fields.Raw(attribute=lambda q: q.options_translations), - 'placeholder': fields.Raw(attribute=lambda q: q.placeholder_translations), - 'validation_regex': fields.Raw(attribute=lambda q: q.validation_regex_translations), - 'validation_text': fields.Raw(attribute=lambda q: q.validation_text_translations), - 'is_required': fields.Boolean, - 'depends_on_question_id': fields.Integer, - 'show_for_values': fields.Raw(attribute=lambda q: q.show_for_values_translations), - 'key': fields.String + "id": fields.Integer, + "type": fields.String, + "description": fields.Raw(attribute=lambda q: q.description_translations), + "headline": fields.Raw(attribute=lambda q: q.headline_translations), + "order": fields.Integer, + "options": fields.Raw(attribute=lambda q: q.options_translations), + "placeholder": fields.Raw(attribute=lambda q: q.placeholder_translations), + "validation_regex": fields.Raw(attribute=lambda q: q.validation_regex_translations), + "validation_text": fields.Raw(attribute=lambda q: q.validation_text_translations), + "is_required": fields.Boolean, + "depends_on_question_id": fields.Integer, + "show_for_values": fields.Raw(attribute=lambda q: q.show_for_values_translations), + "key": fields.String, } section_detail_fields = { - 'id': fields.Integer, - 'name': fields.Raw(attribute=lambda s: s.name_translations), - 'description': fields.Raw(attribute=lambda s: s.description_translations), - 'order': fields.Integer, - 'depends_on_question_id': fields.Integer, - 'show_for_values': fields.Raw(attribute=lambda s: s.show_for_values_translations), - 'questions': fields.List(fields.Nested(question_detail_fields)), - 'key': fields.String + "id": fields.Integer, + "name": fields.Raw(attribute=lambda s: s.name_translations), + "description": fields.Raw(attribute=lambda s: s.description_translations), + "order": fields.Integer, + "depends_on_question_id": fields.Integer, + "show_for_values": fields.Raw(attribute=lambda s: s.show_for_values_translations), + "questions": fields.List(fields.Nested(question_detail_fields)), + "key": fields.String, } application_form_detail_fields = { - 'id': fields.Integer, - 'event_id': fields.Integer, - 'is_open': fields.Boolean, - 'nominations': fields.Boolean, - 'sections': fields.List(fields.Nested(section_detail_fields)) + "id": fields.Integer, + "event_id": fields.Integer, + "is_open": fields.Boolean, + "nominations": fields.Boolean, + "sections": fields.List(fields.Nested(section_detail_fields)), } -class ApplicationFormAPI(restful.Resource): +class ApplicationFormAPI(restful.Resource): @auth_required def get(self): req_parser = reqparse.RequestParser() - req_parser.add_argument('event_id', type=int, required=True, help='Invalid event_id requested. Event_id\'s should be of type int.') - req_parser.add_argument('language', type=str, required=True) + req_parser.add_argument( + "event_id", + type=int, + required=True, + help="Invalid event_id requested. Event_id's should be of type int.", + ) + req_parser.add_argument("language", type=str, required=True) args = req_parser.parse_args() - language = args['language'] + language = args["language"] try: - form = application_form_repository.get_by_event_id(args['event_id']) - event = event_repository.get_by_id(args['event_id']) + form = application_form_repository.get_by_event_id(args["event_id"]) + event = event_repository.get_by_id(args["event_id"]) if not form: return FORM_NOT_FOUND - if not event.is_application_open and not user_repository.get_by_id(g.current_user['id']).is_event_admin(args['event_id']): + if not event.is_application_open and not user_repository.get_by_id( + g.current_user["id"] + ).is_event_admin(args["event_id"]): return APPLICATIONS_CLOSED - + if not form.sections: return SECTION_NOT_FOUND - + if not form.questions: return QUESTION_NOT_FOUND @@ -134,8 +155,8 @@ def get(self): LOGGER.error("Encountered unknown error: {}".format(traceback.format_exc())) return DB_NOT_AVAILABLE -class ApplicationFormDetailAPI(restful.Resource): +class ApplicationFormDetailAPI(restful.Resource): @event_admin_required @marshal_with(application_form_detail_fields) def get(self, event_id): @@ -147,24 +168,21 @@ def get(self, event_id): @marshal_with(application_form_detail_fields) def post(self, event_id): req_parser = reqparse.RequestParser() - req_parser.add_argument('is_open', type=bool, required=True) - req_parser.add_argument('nominations', type=bool, required=True) - req_parser.add_argument('sections', type=dict, required=True, action='append') + req_parser.add_argument("is_open", type=bool, required=True) + req_parser.add_argument("nominations", type=bool, required=True) + req_parser.add_argument("sections", type=dict, required=True, action="append") args = req_parser.parse_args() app_form = application_form_repository.get_by_event_id(event_id) if app_form: return APPLICATION_FORM_EXISTS - is_open = args['is_open'] - nominations = args['nominations'] + is_open = args["is_open"] + nominations = args["nominations"] - app_form = ApplicationForm( - event_id, - is_open, - nominations) + app_form = ApplicationForm(event_id, is_open, nominations) application_form_repository.add(app_form) - sections_data = args['sections'] + sections_data = args["sections"] # Keep track of which objects match with which incoming data for populating dependencies later. section_data_map = {} @@ -173,31 +191,30 @@ def post(self, event_id): for section_data in sections_data: section = Section( - app_form.id, - section_data['order'], - key=section_data['key'] + app_form.id, section_data["order"], key=section_data["key"] ) application_form_repository.add(section) section_data_map[section] = section_data - languages = section_data['name'].keys() + languages = section_data["name"].keys() for language in languages: section_translation = SectionTranslation( - section.id, - language, - section_data['name'][language], - section_data['description'][language], - section_data['show_for_values'][language]) + section.id, + language, + section_data["name"][language], + section_data["description"][language], + section_data["show_for_values"][language], + ) application_form_repository.add(section_translation) - for question_data in section_data['questions']: + for question_data in section_data["questions"]: # application_form_id, section_id, order, questionType, is_required=True question = Question( app_form.id, section.id, - question_data['order'], - question_data['type'], - question_data['is_required'] + question_data["order"], + question_data["type"], + question_data["is_required"], ) application_form_repository.add(question) question_data_map[question] = question_data @@ -209,23 +226,28 @@ def post(self, event_id): question_translation = QuestionTranslation( question_id=question.id, language=language, - headline=question_data['headline'][language], - description=question_data['description'][language], - placeholder=question_data['placeholder'][language], - validation_regex=question_data['validation_regex'][language], - validation_text=question_data['validation_text'][language], - options=question_data['options'][language], - show_for_values=question_data['show_for_values'][language]) + headline=question_data["headline"][language], + description=question_data["description"][language], + placeholder=question_data["placeholder"][language], + validation_regex=question_data["validation_regex"][language], + validation_text=question_data["validation_text"][language], + options=question_data["options"][language], + show_for_values=question_data["show_for_values"][language], + ) application_form_repository.add(question_translation) # Now that all the questions have been created, we can populate the dependencies for section, section_data in section_data_map.items(): - if section_data['depends_on_question_id']: - section.depends_on_question_id = question_id_map[section_data['depends_on_question_id']] - + if section_data["depends_on_question_id"]: + section.depends_on_question_id = question_id_map[ + section_data["depends_on_question_id"] + ] + for question, question_data in question_data_map.items(): - if question_data['depends_on_question_id']: - question.depends_on_question_id = question_id_map[question_data['depends_on_question_id']] + if question_data["depends_on_question_id"]: + question.depends_on_question_id = question_id_map[ + question_data["depends_on_question_id"] + ] app_form = application_form_repository.get_by_id(app_form.id) return app_form, 201 @@ -234,87 +256,102 @@ def post(self, event_id): @marshal_with(application_form_detail_fields) def put(self, event_id): req_parser = reqparse.RequestParser() - req_parser.add_argument('is_open', type=bool, required=True) - req_parser.add_argument('nominations', type=bool, required=True) - req_parser.add_argument('id', type=int, required=True) - req_parser.add_argument('sections', type=dict, required=True, action='append') + req_parser.add_argument("is_open", type=bool, required=True) + req_parser.add_argument("nominations", type=bool, required=True) + req_parser.add_argument("id", type=int, required=True) + req_parser.add_argument("sections", type=dict, required=True, action="append") args = req_parser.parse_args() - user_id = g.current_user['id'] - application_form_id = args['id'] + user_id = g.current_user["id"] + application_form_id = args["id"] - app_form = application_form_repository.get_by_id(application_form_id) # type: ApplicationForm + app_form = application_form_repository.get_by_id( + application_form_id + ) # type: ApplicationForm if not app_form: return FORM_NOT_FOUND_BY_ID if event_id != app_form.event_id: return UPDATE_CONFLICT - app_form.is_open = args['is_open'] - app_form.nominations = args['nominations'] + app_form.is_open = args["is_open"] + app_form.nominations = args["nominations"] current_sections = app_form.sections - incoming_sections = args['sections'] + incoming_sections = args["sections"] # Delete questions in the application form that no longer exist - all_question_ids = [q['id'] for s in incoming_sections for q in s['questions'] if 'id' in q] + all_question_ids = [ + q["id"] for s in incoming_sections for q in s["questions"] if "id" in q + ] print("all_question_ids:", all_question_ids) for question in app_form.questions: if question.id not in all_question_ids: print("DELETING QUESTION ID ", question.id) application_form_repository.delete_question(question) - all_section_ids = [s['id'] for s in incoming_sections if 'id' in s] + all_section_ids = [s["id"] for s in incoming_sections if "id" in s] print("all_section_ids:", all_section_ids) for section in app_form.sections: if section.id not in all_section_ids: print("DELETING SECTION ID ", section.id) application_form_repository.delete_section(section) - + # Keep track of which objects match with which incoming data for populating dependencies later. section_data_map = {} question_data_map = {} question_id_map = {} for section_data in incoming_sections: - if 'id' in section_data: + if "id" in section_data: # If ID is populated, then update the existing section - section = next((s for s in current_sections if s.id == section_data['id']), None) # type: Section + section = next( + (s for s in current_sections if s.id == section_data["id"]), None + ) # type: Section if not section: return SECTION_NOT_FOUND - current_translations = section.section_translations # type: Sequence[SectionTranslation] + current_translations = ( + section.section_translations + ) # type: Sequence[SectionTranslation] for current_translation in current_translations: - current_translation.description = section_data['description'][current_translation.language] - current_translation.name = section_data['name'][current_translation.language] - current_translation.show_for_values = section_data['show_for_values'][current_translation.language] + current_translation.description = section_data["description"][ + current_translation.language + ] + current_translation.name = section_data["name"][ + current_translation.language + ] + current_translation.show_for_values = section_data[ + "show_for_values" + ][current_translation.language] + + section.key = section_data["key"] + section.order = section_data["order"] - section.key = section_data['key'] - section.order = section_data['order'] - db.session.commit() else: # if not populated, then add new section section = Section( - app_form.id, - section_data['order'], - key=section_data['key'] + app_form.id, section_data["order"], key=section_data["key"] ) application_form_repository.add(section) - languages = section_data['name'].keys() + languages = section_data["name"].keys() for language in languages: section_translation = SectionTranslation( - section.id, - language, - section_data['name'][language], - section_data['description'][language], - section_data['show_for_values'][language]) + section.id, + language, + section_data["name"][language], + section_data["description"][language], + section_data["show_for_values"][language], + ) application_form_repository.add(section_translation) section_data_map[section] = section_data try: - question_map, question_ids = _process_questions(section_data['questions'], app_form, section.id) + question_map, question_ids = _process_questions( + section_data["questions"], app_form, section.id + ) question_data_map.update(question_map) question_id_map.update(question_ids) except ValueError: @@ -324,12 +361,16 @@ def put(self, event_id): # Now that all the questions have been created, we can populate the dependencies for section, section_data in section_data_map.items(): - if section_data['depends_on_question_id']: - section.depends_on_question_id = question_id_map[section_data['depends_on_question_id']] - + if section_data["depends_on_question_id"]: + section.depends_on_question_id = question_id_map[ + section_data["depends_on_question_id"] + ] + for question, question_data in question_data_map.items(): - if question_data['depends_on_question_id']: - question.depends_on_question_id = question_id_map[question_data['depends_on_question_id']] + if question_data["depends_on_question_id"]: + question.depends_on_question_id = question_id_map[ + question_data["depends_on_question_id"] + ] app_form = application_form_repository.get_by_id(app_form.id) @@ -341,77 +382,100 @@ def _process_questions(questions, application_form, section_id): question_data_map = {} question_ids = {} for question_data in questions: - if 'id' in question_data: - current_question = next((s for s in current_questions if s.id == question_data['id']), None) # type: Question + if "id" in question_data: + current_question = next( + (s for s in current_questions if s.id == question_data["id"]), None + ) # type: Question if not current_question: raise ValueError(f'Question with id {question_data["id"]} not found') - - current_question.order = question_data['order'] + + current_question.order = question_data["order"] current_question.section_id = section_id - current_question.is_required = question_data['is_required'] - current_question.key = question_data['key'] - current_question.type = question_data['type'] - - current_translations = current_question.question_translations # type: Sequence[QuestionTranslation] + current_question.is_required = question_data["is_required"] + current_question.key = question_data["key"] + current_question.type = question_data["type"] + + current_translations = ( + current_question.question_translations + ) # type: Sequence[QuestionTranslation] for current_translation in current_translations: - current_translation.description = question_data['description'][current_translation.language] - current_translation.headline = question_data['headline'][current_translation.language] - current_translation.options = question_data['options'][current_translation.language] - current_translation.placeholder = question_data['placeholder'][current_translation.language] - current_translation.show_for_values = question_data['show_for_values'][current_translation.language] - current_translation.validation_regex = question_data['validation_regex'][current_translation.language] - current_translation.validation_text = question_data['validation_text'][current_translation.language] - - question_ids[question_data['id']] = question_data['id'] + current_translation.description = question_data["description"][ + current_translation.language + ] + current_translation.headline = question_data["headline"][ + current_translation.language + ] + current_translation.options = question_data["options"][ + current_translation.language + ] + current_translation.placeholder = question_data["placeholder"][ + current_translation.language + ] + current_translation.show_for_values = question_data["show_for_values"][ + current_translation.language + ] + current_translation.validation_regex = question_data[ + "validation_regex" + ][current_translation.language] + current_translation.validation_text = question_data["validation_text"][ + current_translation.language + ] + + question_ids[question_data["id"]] = question_data["id"] question_data_map[current_question] = question_data else: question = Question( - application_form_id=application_form.id, - section_id=section_id, - order=question_data['order'], - questionType=question_data['type'], - is_required=question_data['is_required']) - question.key = question_data['key'] + application_form_id=application_form.id, + section_id=section_id, + order=question_data["order"], + questionType=question_data["type"], + is_required=question_data["is_required"], + ) + question.key = question_data["key"] application_form_repository.add(question) - for language in question_data['headline'].keys(): + for language in question_data["headline"].keys(): translation = QuestionTranslation( question.id, language, - headline=question_data['headline'][language], - description=question_data['description'][language], - placeholder=question_data['placeholder'][language], - validation_regex=question_data['validation_regex'][language], - validation_text=question_data['validation_text'][language], - options=question_data['options'][language], - show_for_values=question_data['show_for_values'][language]) + headline=question_data["headline"][language], + description=question_data["description"][language], + placeholder=question_data["placeholder"][language], + validation_regex=question_data["validation_regex"][language], + validation_text=question_data["validation_text"][language], + options=question_data["options"][language], + show_for_values=question_data["show_for_values"][language], + ) application_form_repository.add(translation) - if 'surrogate_id' in question_data: - question_ids[question_data['surrogate_id']] = question.id + if "surrogate_id" in question_data: + question_ids[question_data["surrogate_id"]] = question.id question_data_map[question] = question_data - + return question_data_map, question_ids + def _serialize_question(question, language): translation = question.get_translation(language) if not translation: - LOGGER.warn('Could not find {} translation for question id {}'.format(language, question.id)) - translation = question.get_translation('en') + LOGGER.warn( + "Could not find {} translation for question id {}".format( + language, question.id + ) + ) + translation = question.get_translation("en") return dict( - question_id=question.id, - headline=translation.headline, - type=question.type + question_id=question.id, headline=translation.headline, type=question.type ) -class QuestionListApi(restful.Resource): +class QuestionListApi(restful.Resource): @event_admin_required def get(self, event_id): req_parser = reqparse.RequestParser() - req_parser.add_argument('language', type=str, required=True) + req_parser.add_argument("language", type=str, required=True) args = req_parser.parse_args() - language = args['language'] + language = args["language"] questions = application_form_repository.get_questions_for_event(event_id) return [_serialize_question(q, language) for q in questions] diff --git a/api/app/applicationModel/models.py b/api/app/applicationModel/models.py index 0211b77be..ce1efc016 100644 --- a/api/app/applicationModel/models.py +++ b/api/app/applicationModel/models.py @@ -1,57 +1,77 @@ -from app import db, bcrypt -import app from typing import Any, Callable, Mapping +import app +from app import bcrypt, db + class ApplicationForm(db.Model): - __tablename__ = 'application_form' + __tablename__ = "application_form" id = db.Column(db.Integer(), primary_key=True) - event_id = db.Column(db.Integer(), db.ForeignKey('event.id'), nullable=False) + event_id = db.Column(db.Integer(), db.ForeignKey("event.id"), nullable=False) is_open = db.Column(db.Boolean(), nullable=False) nominations = db.Column(db.Boolean(), nullable=False) - event = db.relationship('Event', foreign_keys=[event_id]) - sections = db.relationship('Section', order_by='Section.order') - questions = db.relationship('Question') + event = db.relationship("Event", foreign_keys=[event_id]) + sections = db.relationship("Section", order_by="Section.order") + questions = db.relationship("Question") def __init__(self, event_id, is_open, nominations): self.event_id = event_id self.is_open = is_open self.nominations = nominations + class Question(db.Model): - __tablename__ = 'question' + __tablename__ = "question" id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) - section_id = db.Column(db.Integer(), db.ForeignKey('section.id'), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) + section_id = db.Column(db.Integer(), db.ForeignKey("section.id"), nullable=False) type = db.Column(db.String(), nullable=False) order = db.Column(db.Integer(), nullable=False) is_required = db.Column(db.Boolean(), nullable=False) - depends_on_question_id = db.Column(db.Integer(), db.ForeignKey('question.id'), nullable=True) + depends_on_question_id = db.Column( + db.Integer(), db.ForeignKey("question.id"), nullable=True + ) key = db.Column(db.String(255), nullable=True) - application_form = db.relationship('ApplicationForm', foreign_keys=[application_form_id]) - section = db.relationship('Section', foreign_keys=[section_id]) - question_translations = db.relationship('QuestionTranslation', lazy='dynamic') + application_form = db.relationship( + "ApplicationForm", foreign_keys=[application_form_id] + ) + section = db.relationship("Section", foreign_keys=[section_id]) + question_translations = db.relationship("QuestionTranslation", lazy="dynamic") - def __init__(self, application_form_id, section_id, order, questionType, is_required=True, key=None): + def __init__( + self, + application_form_id, + section_id, + order, + questionType, + is_required=True, + key=None, + ): self.application_form_id = application_form_id self.section_id = section_id self.order = order self.type = questionType self.is_required = is_required self.key = key - - def get_translation(self, language: str) -> 'QuestionTranslation': - question_translation = self.question_translations.filter_by(language=language).first() + + def get_translation(self, language: str) -> "QuestionTranslation": + question_translation = self.question_translations.filter_by( + language=language + ).first() return question_translation - + def is_review_identifier(self): - return self.key == 'review-identifier' + return self.key == "review-identifier" - def _translations_for_field(self, accessor_fn: Callable[['QuestionTranslation'], Any]) -> Mapping[str, Any]: + def _translations_for_field( + self, accessor_fn: Callable[["QuestionTranslation"], Any] + ) -> Mapping[str, Any]: translations = {} for translation in self.question_translations: translations[translation.language] = accessor_fn(translation) @@ -85,30 +105,45 @@ def validation_text_translations(self): def show_for_values_translations(self): return self._translations_for_field(lambda q: q.show_for_values) + class Section(db.Model): - __tablename__ = 'section' + __tablename__ = "section" id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) order = db.Column(db.Integer(), nullable=False) - depends_on_question_id = db.Column(db.Integer(), db.ForeignKey('question.id', use_alter=True), nullable=True) + depends_on_question_id = db.Column( + db.Integer(), db.ForeignKey("question.id", use_alter=True), nullable=True + ) key = db.Column(db.String(255), nullable=True) - application_form = db.relationship('ApplicationForm', foreign_keys=[application_form_id]) - section_translations = db.relationship('SectionTranslation', lazy='dynamic') - questions = db.relationship('Question', primaryjoin=id==Question.section_id, order_by='Question.order') + application_form = db.relationship( + "ApplicationForm", foreign_keys=[application_form_id] + ) + section_translations = db.relationship("SectionTranslation", lazy="dynamic") + questions = db.relationship( + "Question", primaryjoin=id == Question.section_id, order_by="Question.order" + ) - def __init__(self, application_form_id, order, depends_on_question_id=None, key=None): + def __init__( + self, application_form_id, order, depends_on_question_id=None, key=None + ): self.application_form_id = application_form_id self.order = order self.depends_on_question_id = depends_on_question_id self.key = key - def get_translation(self, language: str) -> 'SectionTranslation': - section_translation = self.section_translations.filter_by(language=language).first() + def get_translation(self, language: str) -> "SectionTranslation": + section_translation = self.section_translations.filter_by( + language=language + ).first() return section_translation - def _translations_for_field(self, accessor_fn: Callable[['SectionTranslation'], Any]) -> Mapping[str, Any]: + def _translations_for_field( + self, accessor_fn: Callable[["SectionTranslation"], Any] + ) -> Mapping[str, Any]: translations = {} for translation in self.section_translations: translations[translation.language] = accessor_fn(translation) @@ -126,18 +161,21 @@ def description_translations(self): def show_for_values_translations(self): return self._translations_for_field(lambda t: t.show_for_values) + class SectionTranslation(db.Model): - __tablename__ = 'section_translation' - __table_args__ = tuple([db.UniqueConstraint('section_id', 'language', name='uq_section_id_language')]) + __tablename__ = "section_translation" + __table_args__ = tuple( + [db.UniqueConstraint("section_id", "language", name="uq_section_id_language")] + ) id = db.Column(db.Integer(), primary_key=True) - section_id = db.Column(db.Integer(), db.ForeignKey('section.id'), nullable=False) + section_id = db.Column(db.Integer(), db.ForeignKey("section.id"), nullable=False) language = db.Column(db.String(2), nullable=False) name = db.Column(db.String(255), nullable=False) description = db.Column(db.String(), nullable=False) show_for_values = db.Column(db.JSON(), nullable=True) - section = db.relationship('Section', foreign_keys=[section_id]) + section = db.relationship("Section", foreign_keys=[section_id]) def __init__(self, section_id, language, name, description, show_for_values=None): self.section_id = section_id @@ -145,24 +183,26 @@ def __init__(self, section_id, language, name, description, show_for_values=None self.name = name self.description = description self.show_for_values = show_for_values - + class QuestionTranslation(db.Model): - __tablename__ = 'question_translation' - __table_args__ = tuple([db.UniqueConstraint('question_id', 'language', name='uq_question_id_language')]) + __tablename__ = "question_translation" + __table_args__ = tuple( + [db.UniqueConstraint("question_id", "language", name="uq_question_id_language")] + ) id = db.Column(db.Integer(), primary_key=True) - question_id = db.Column(db.Integer(), db.ForeignKey('question.id'), nullable=False) + question_id = db.Column(db.Integer(), db.ForeignKey("question.id"), nullable=False) language = db.Column(db.String(2), nullable=False) headline = db.Column(db.String(), nullable=False) description = db.Column(db.String(), nullable=True) - placeholder = db.Column(db.String(), nullable=True ) + placeholder = db.Column(db.String(), nullable=True) validation_regex = db.Column(db.String(), nullable=True) validation_text = db.Column(db.String(), nullable=True) options = db.Column(db.JSON(), nullable=True) show_for_values = db.Column(db.JSON(), nullable=True) - question = db.relationship('Question', foreign_keys=[question_id]) + question = db.relationship("Question", foreign_keys=[question_id]) def __init__( self, @@ -174,7 +214,7 @@ def __init__( validation_regex=None, validation_text=None, options=None, - show_for_values=None + show_for_values=None, ): self.question_id = question_id self.language = language diff --git a/api/app/applicationModel/repository.py b/api/app/applicationModel/repository.py index 5c458ccd0..da4e34f92 100644 --- a/api/app/applicationModel/repository.py +++ b/api/app/applicationModel/repository.py @@ -1,19 +1,22 @@ from app import db -from app.applicationModel.models import ApplicationForm, Question, QuestionTranslation, Section, SectionTranslation +from app.applicationModel.models import ( + ApplicationForm, + Question, + QuestionTranslation, + Section, + SectionTranslation, +) from app.events.models import Event -class ApplicationFormRepository(): - +class ApplicationFormRepository: @staticmethod def get_by_id(id): return db.session.query(ApplicationForm).get(id) @staticmethod def get_by_event_id(event_id): - return db.session.query(ApplicationForm)\ - .filter_by(event_id=event_id)\ - .first() + return db.session.query(ApplicationForm).filter_by(event_id=event_id).first() def add(obj): db.session.add(obj) @@ -22,12 +25,14 @@ def add(obj): @staticmethod def get_questions_for_event(event_id): - return (db.session.query(Question) - .join(ApplicationForm, Question.application_form_id == ApplicationForm.id) - .filter_by(event_id=event_id) - .join(Section, Question.section_id == Section.id) - .order_by(Section.order, Question.order) - .all()) + return ( + db.session.query(Question) + .join(ApplicationForm, Question.application_form_id == ApplicationForm.id) + .filter_by(event_id=event_id) + .join(Section, Question.section_id == Section.id) + .order_by(Section.order, Question.order) + .all() + ) @staticmethod def delete_question(question_to_delete: Question): @@ -40,9 +45,11 @@ def delete_question(question_to_delete: Question): section.depends_on_question_id = None db.session.commit() - + # Delete question and translations - db.session.query(QuestionTranslation).filter_by(question_id=question_to_delete.id).delete() + db.session.query(QuestionTranslation).filter_by( + question_id=question_to_delete.id + ).delete() db.session.query(Question).filter_by(id=question_to_delete.id).delete() db.session.commit() @@ -50,7 +57,9 @@ def delete_question(question_to_delete: Question): def delete_section(section): db.session.query(SectionTranslation).filter_by(section_id=section.id).delete() - for question in db.session.query(Question).filter_by(section_id=section.id).all(): + for question in ( + db.session.query(Question).filter_by(section_id=section.id).all() + ): ApplicationFormRepository.delete_question(question) db.session.query(Section).filter_by(id=section.id).delete() diff --git a/api/app/applicationModel/tests.py b/api/app/applicationModel/tests.py index 0e5d75dc6..4c039f52b 100644 --- a/api/app/applicationModel/tests.py +++ b/api/app/applicationModel/tests.py @@ -1,305 +1,323 @@ # -*- coding: utf-8 -*- import json -from datetime import datetime, timedelta import pprint +from datetime import datetime, timedelta from app import LOGGER, db from app.applicationModel.models import ApplicationForm, Question, Section -from app.applicationModel.repository import ApplicationFormRepository as application_form_repository +from app.applicationModel.repository import ( + ApplicationFormRepository as application_form_repository, +) from app.events.models import Event, EventType from app.organisation.models import Organisation from app.utils.testing import ApiTestCase class ApplicationFormApiTest(ApiTestCase): - def setUp(self): super(ApplicationFormApiTest, self).setUp() self._seed_data() def _seed_data(self): - self.test_user = self.add_user('something@email.com') + self.test_user = self.add_user("something@email.com") - self.add_organisation('IndabaX') + self.add_organisation("IndabaX") start_time = datetime.now() + timedelta(days=30) end_time = datetime.now() + timedelta(days=60) - self.event = self.add_event({'en': 'Test Event'}, {'en': 'Event Description'}, start_time, end_time) + self.event = self.add_event( + {"en": "Test Event"}, {"en": "Event Description"}, start_time, end_time + ) self.form = self.create_application_form(self.event.id, True, False) self.section = self.add_section(self.form.id, 1) - self.section_translation_en = self.add_section_translation(self.section.id, 'en', 'Test Section', 'Test Description', ['no']) - self.section_translation_fr = self.add_section_translation(self.section.id, 'fr', 'Section francaise', 'Description du test', ['non']) - self.question = self.add_question(self.form.id, self.section.id, question_type='multi-choice') + self.section_translation_en = self.add_section_translation( + self.section.id, "en", "Test Section", "Test Description", ["no"] + ) + self.section_translation_fr = self.add_section_translation( + self.section.id, "fr", "Section francaise", "Description du test", ["non"] + ) + self.question = self.add_question( + self.form.id, self.section.id, question_type="multi-choice" + ) self.question_translation_en = self.add_question_translation( question_id=self.question.id, - language='en', - headline='English Headline', - description='English Question Description', - placeholder='English Placeholder', - validation_regex='^\\W*(\\w+(\\W+|$)){0,150}$', - validation_text='Enter a maximum of 150 words', - options=[{'label': 'Yes', 'value': 'yes'}, {'label': 'No', 'value': 'no'}], - show_for_values=['yes'] + language="en", + headline="English Headline", + description="English Question Description", + placeholder="English Placeholder", + validation_regex="^\\W*(\\w+(\\W+|$)){0,150}$", + validation_text="Enter a maximum of 150 words", + options=[{"label": "Yes", "value": "yes"}, {"label": "No", "value": "no"}], + show_for_values=["yes"], ) self.question_translation_fr = self.add_question_translation( question_id=self.question.id, - language='fr', - headline='Titre francais', - description='Description de la question en Francais', - placeholder='Espace reserve Francais', - validation_regex='^\\W*(\\w+(\\W+|$)){0,200}$', - validation_text='Entrez un maximum de 200 mots', - options=[{'label': 'Oui', 'value': 'oui'}, {'label': 'Non', 'value': 'non'}], - show_for_values=['oui'] + language="fr", + headline="Titre francais", + description="Description de la question en Francais", + placeholder="Espace reserve Francais", + validation_regex="^\\W*(\\w+(\\W+|$)){0,200}$", + validation_text="Entrez un maximum de 200 mots", + options=[ + {"label": "Oui", "value": "oui"}, + {"label": "Non", "value": "non"}, + ], + show_for_values=["oui"], ) self.section2 = self.add_section(self.form.id, 2) - self.section_translation2_en = self.add_section_translation(self.section2.id, 'en', 'Test Section 2', 'Test Description 2') - self.section_translation2_fr = self.add_section_translation(self.section2.id, 'fr', 'Section francaise 2', 'Description du test 2') - self.question2 = self.add_question(self.form.id, self.section2.id, question_type='text') - self.question_translation2_en = self.add_question_translation(self.question2.id, 'en', 'English Headline 2') - self.question_translation2_fr = self.add_question_translation(self.question2.id, 'fr', 'Titre francais 2') + self.section_translation2_en = self.add_section_translation( + self.section2.id, "en", "Test Section 2", "Test Description 2" + ) + self.section_translation2_fr = self.add_section_translation( + self.section2.id, "fr", "Section francaise 2", "Description du test 2" + ) + self.question2 = self.add_question( + self.form.id, self.section2.id, question_type="text" + ) + self.question_translation2_en = self.add_question_translation( + self.question2.id, "en", "English Headline 2" + ) + self.question_translation2_fr = self.add_question_translation( + self.question2.id, "fr", "Titre francais 2" + ) def test_get_form_in_english(self): header = self.get_auth_header_for(self.test_user.email) response = self.app.get( - '/api/v1/application-form', + "/api/v1/application-form", headers=header, - query_string={'event_id': 1, 'language': 'en'} + query_string={"event_id": 1, "language": "en"}, ) data = json.loads(response.data) - self.assertEqual(data['event_id'], 1) - self.assertEqual(data['is_open'], True) - self.assertEqual(data['nominations'], False) - - self.assertEqual(len(data['sections']), 2) - self.assertEqual(data['sections'][0]['order'], 1) - self.assertEqual(data['sections'][0]['depends_on_question_id'], None) - - self.assertEqual(data['sections'][0]['name'], 'Test Section') - self.assertEqual(data['sections'][0]['description'], 'Test Description') - self.assertEqual(data['sections'][0]['show_for_values'], ['no']) - - self.assertEqual(data['sections'][0]['questions'][0]['type'], 'multi-choice') - self.assertEqual(data['sections'][0]['questions'][0]['order'], 1) - self.assertEqual(data['sections'][0]['questions'][0]['is_required'], True) - self.assertEqual(data['sections'][0]['questions'][0]['depends_on_question_id'], None) - self.assertEqual(data['sections'][0]['questions'][0]['key'], None) - - self.assertEqual(data['sections'][0]['questions'][0]['description'], 'English Question Description') - self.assertEqual(data['sections'][0]['questions'][0]['headline'], 'English Headline') - self.assertEqual(data['sections'][0]['questions'][0]['options'], [{'label': 'Yes', 'value': 'yes'}, {'label': 'No', 'value': 'no'}]) - self.assertEqual(data['sections'][0]['questions'][0]['placeholder'], 'English Placeholder') - self.assertEqual(data['sections'][0]['questions'][0]['validation_regex'], '^\\W*(\\w+(\\W+|$)){0,150}$') - self.assertEqual(data['sections'][0]['questions'][0]['validation_text'], 'Enter a maximum of 150 words') - self.assertEqual(data['sections'][0]['questions'][0]['show_for_values'], ['yes']) - - + self.assertEqual(data["event_id"], 1) + self.assertEqual(data["is_open"], True) + self.assertEqual(data["nominations"], False) + + self.assertEqual(len(data["sections"]), 2) + self.assertEqual(data["sections"][0]["order"], 1) + self.assertEqual(data["sections"][0]["depends_on_question_id"], None) + + self.assertEqual(data["sections"][0]["name"], "Test Section") + self.assertEqual(data["sections"][0]["description"], "Test Description") + self.assertEqual(data["sections"][0]["show_for_values"], ["no"]) + + self.assertEqual(data["sections"][0]["questions"][0]["type"], "multi-choice") + self.assertEqual(data["sections"][0]["questions"][0]["order"], 1) + self.assertEqual(data["sections"][0]["questions"][0]["is_required"], True) + self.assertEqual( + data["sections"][0]["questions"][0]["depends_on_question_id"], None + ) + self.assertEqual(data["sections"][0]["questions"][0]["key"], None) + + self.assertEqual( + data["sections"][0]["questions"][0]["description"], + "English Question Description", + ) + self.assertEqual( + data["sections"][0]["questions"][0]["headline"], "English Headline" + ) + self.assertEqual( + data["sections"][0]["questions"][0]["options"], + [{"label": "Yes", "value": "yes"}, {"label": "No", "value": "no"}], + ) + self.assertEqual( + data["sections"][0]["questions"][0]["placeholder"], "English Placeholder" + ) + self.assertEqual( + data["sections"][0]["questions"][0]["validation_regex"], + "^\\W*(\\w+(\\W+|$)){0,150}$", + ) + self.assertEqual( + data["sections"][0]["questions"][0]["validation_text"], + "Enter a maximum of 150 words", + ) + self.assertEqual( + data["sections"][0]["questions"][0]["show_for_values"], ["yes"] + ) + def test_get_form_in_french(self): header = self.get_auth_header_for(self.test_user.email) response = self.app.get( - '/api/v1/application-form', + "/api/v1/application-form", headers=header, - query_string={'event_id': 1, 'language': 'fr'} + query_string={"event_id": 1, "language": "fr"}, ) data = json.loads(response.data) - self.assertEqual(data['event_id'], 1) - self.assertEqual(data['is_open'], True) - self.assertEqual(data['nominations'], False) + self.assertEqual(data["event_id"], 1) + self.assertEqual(data["is_open"], True) + self.assertEqual(data["nominations"], False) - self.assertEqual(len(data['sections']), 2) - self.assertEqual(data['sections'][0]['order'], 1) - self.assertEqual(data['sections'][0]['depends_on_question_id'], None) + self.assertEqual(len(data["sections"]), 2) + self.assertEqual(data["sections"][0]["order"], 1) + self.assertEqual(data["sections"][0]["depends_on_question_id"], None) - self.assertEqual(data['sections'][0]['name'], 'Section francaise') - self.assertEqual(data['sections'][0]['description'], 'Description du test') - self.assertEqual(data['sections'][0]['show_for_values'], ['non']) + self.assertEqual(data["sections"][0]["name"], "Section francaise") + self.assertEqual(data["sections"][0]["description"], "Description du test") + self.assertEqual(data["sections"][0]["show_for_values"], ["non"]) - self.assertEqual(data['sections'][0]['questions'][0]['type'], 'multi-choice') - self.assertEqual(data['sections'][0]['questions'][0]['order'], 1) - self.assertEqual(data['sections'][0]['questions'][0]['is_required'], True) - self.assertEqual(data['sections'][0]['questions'][0]['depends_on_question_id'], None) - self.assertEqual(data['sections'][0]['questions'][0]['key'], None) + self.assertEqual(data["sections"][0]["questions"][0]["type"], "multi-choice") + self.assertEqual(data["sections"][0]["questions"][0]["order"], 1) + self.assertEqual(data["sections"][0]["questions"][0]["is_required"], True) + self.assertEqual( + data["sections"][0]["questions"][0]["depends_on_question_id"], None + ) + self.assertEqual(data["sections"][0]["questions"][0]["key"], None) - self.assertEqual(data['sections'][0]['questions'][0]['description'], 'Description de la question en Francais') - self.assertEqual(data['sections'][0]['questions'][0]['headline'], 'Titre francais') - self.assertEqual(data['sections'][0]['questions'][0]['options'], [{'label': 'Oui', 'value': 'oui'}, {'label': 'Non', 'value': 'non'}]) - self.assertEqual(data['sections'][0]['questions'][0]['placeholder'], 'Espace reserve Francais') - self.assertEqual(data['sections'][0]['questions'][0]['validation_regex'], '^\\W*(\\w+(\\W+|$)){0,200}$') - self.assertEqual(data['sections'][0]['questions'][0]['validation_text'], 'Entrez un maximum de 200 mots') - self.assertEqual(data['sections'][0]['questions'][0]['show_for_values'], ['oui']) + self.assertEqual( + data["sections"][0]["questions"][0]["description"], + "Description de la question en Francais", + ) + self.assertEqual( + data["sections"][0]["questions"][0]["headline"], "Titre francais" + ) + self.assertEqual( + data["sections"][0]["questions"][0]["options"], + [{"label": "Oui", "value": "oui"}, {"label": "Non", "value": "non"}], + ) + self.assertEqual( + data["sections"][0]["questions"][0]["placeholder"], + "Espace reserve Francais", + ) + self.assertEqual( + data["sections"][0]["questions"][0]["validation_regex"], + "^\\W*(\\w+(\\W+|$)){0,200}$", + ) + self.assertEqual( + data["sections"][0]["questions"][0]["validation_text"], + "Entrez un maximum de 200 mots", + ) + self.assertEqual( + data["sections"][0]["questions"][0]["show_for_values"], ["oui"] + ) APPLICATION_FORM_POST_DATA = { - "event_id": 1, - "is_open": True, - "nominations": False, - "sections": [ - { - "name": { - "en": "Section 1", - "fr": "Section 1" - }, - "description": { - "en": "Description of the section", - "fr": "Description de la section" - }, - "order": 1, - "depends_on_question_id": None, - "show_for_values": { - "en": None, - "fr": None - }, - "key": "candidate_information", - "questions": [ - { - "surrogate_id": 1, - "validation_regex": { - "en": None, - "fr": None - }, - "options": { - "en": [ - {"value": "undergrad", "label": "An undergraduate student"}, - {"value": "masters", "label": "A masters student"}, - ], - "fr": [ - {"value": "undergrad", "label": "un étudiant de premier cycle"}, - {"value": "masters", "label": "un étudiant en maîtrise"}, - ], - }, - "description": { - "en": "Question 1 en", - "fr": "Question 1 fr" - }, - "headline": { - "en": "Question 1 en", - "fr": "Question 1 fr" - }, - "placeholder": { - "en": "Select an Option...", - "fr": "Sélectionnez une option" - }, - "is_required": True, - "type": "multi-choice", - "validation_text": { - "en": None, - "fr": None - }, - "order": 1, - "depends_on_question_id": 3, - "key": None, - "show_for_values": { - "en": ["the-matrix", "terminator"], - "fr": ["the-matrix", "terminator"] - } - }, + "event_id": 1, + "is_open": True, + "nominations": False, + "sections": [ { - "surrogate_id": 2, - "validation_regex": { - "en": "^Moo$", - "fr": "^moo$" - }, - "options": { - "en": None, - "fr": None, - }, - "description": { - "en": "Question 2 en", - "fr": "Question 2 fr" - }, - "headline": { - "en": "Question 2 en", - "fr": "Question 2 fr" - }, - "placeholder": { - "en": "Enter some text...", - "fr": "Entrez du texte ..." - }, - "is_required": False, - "type": "short-text", - "validation_text": { - "en": "You must enter the word Moo", - "fr": "Vous devez entrer le mot Moo" - }, - "order": 2, - "depends_on_question_id": None, - "key": None, - "show_for_values": { - "en": None, - "fr": None - } - } - ], - }, # End section 1 - { - "name": { - "en": "Section 2", - "fr": "Section 2 fr" - }, - "description": { - "en": "Description of the section 2", - "fr": "Description de la section 2" - }, - "order": 2, - "depends_on_question_id": 1, - "show_for_values": { - "en": ["masters"], - "fr": ["masters"], - }, - "key": None, - "questions": [ + "name": {"en": "Section 1", "fr": "Section 1"}, + "description": { + "en": "Description of the section", + "fr": "Description de la section", + }, + "order": 1, + "depends_on_question_id": None, + "show_for_values": {"en": None, "fr": None}, + "key": "candidate_information", + "questions": [ + { + "surrogate_id": 1, + "validation_regex": {"en": None, "fr": None}, + "options": { + "en": [ + {"value": "undergrad", "label": "An undergraduate student"}, + {"value": "masters", "label": "A masters student"}, + ], + "fr": [ + { + "value": "undergrad", + "label": "un étudiant de premier cycle", + }, + {"value": "masters", "label": "un étudiant en maîtrise"}, + ], + }, + "description": {"en": "Question 1 en", "fr": "Question 1 fr"}, + "headline": {"en": "Question 1 en", "fr": "Question 1 fr"}, + "placeholder": { + "en": "Select an Option...", + "fr": "Sélectionnez une option", + }, + "is_required": True, + "type": "multi-choice", + "validation_text": {"en": None, "fr": None}, + "order": 1, + "depends_on_question_id": 3, + "key": None, + "show_for_values": { + "en": ["the-matrix", "terminator"], + "fr": ["the-matrix", "terminator"], + }, + }, + { + "surrogate_id": 2, + "validation_regex": {"en": "^Moo$", "fr": "^moo$"}, + "options": { + "en": None, + "fr": None, + }, + "description": {"en": "Question 2 en", "fr": "Question 2 fr"}, + "headline": {"en": "Question 2 en", "fr": "Question 2 fr"}, + "placeholder": { + "en": "Enter some text...", + "fr": "Entrez du texte ...", + }, + "is_required": False, + "type": "short-text", + "validation_text": { + "en": "You must enter the word Moo", + "fr": "Vous devez entrer le mot Moo", + }, + "order": 2, + "depends_on_question_id": None, + "key": None, + "show_for_values": {"en": None, "fr": None}, + }, + ], + }, # End section 1 { - "surrogate_id": 3, - "validation_regex": { - "en": None, - "fr": None - }, - "options": { - "en": [ - {"value": "the-matrix", "label": "The Matrix"}, - {"value": "terminator", "label": "Terminator"}, - ], - "fr": [ - {"value": "the-matrix", "label": "La matrice"}, - {"value": "terminator", "label": "Terminator"}, - ], - }, - "description": { - "en": "Question 3 en", - "fr": "Question 3 fr" - }, - "headline": { - "en": "Question 3 en", - "fr": "Question 3 fr" - }, - "placeholder": { - "en": "Select an Option...", - "fr": "Sélectionnez une option" - }, - "is_required": True, - "type": "multi-choice", - "validation_text": { - "en": None, - "fr": None - }, - "order": 1, - "depends_on_question_id": None, - "key": "review-identifier", - "show_for_values": { - "en": None, - "fr": None - } - }, - ], - }, # End section 2 - ] + "name": {"en": "Section 2", "fr": "Section 2 fr"}, + "description": { + "en": "Description of the section 2", + "fr": "Description de la section 2", + }, + "order": 2, + "depends_on_question_id": 1, + "show_for_values": { + "en": ["masters"], + "fr": ["masters"], + }, + "key": None, + "questions": [ + { + "surrogate_id": 3, + "validation_regex": {"en": None, "fr": None}, + "options": { + "en": [ + {"value": "the-matrix", "label": "The Matrix"}, + {"value": "terminator", "label": "Terminator"}, + ], + "fr": [ + {"value": "the-matrix", "label": "La matrice"}, + {"value": "terminator", "label": "Terminator"}, + ], + }, + "description": {"en": "Question 3 en", "fr": "Question 3 fr"}, + "headline": {"en": "Question 3 en", "fr": "Question 3 fr"}, + "placeholder": { + "en": "Select an Option...", + "fr": "Sélectionnez une option", + }, + "is_required": True, + "type": "multi-choice", + "validation_text": {"en": None, "fr": None}, + "order": 1, + "depends_on_question_id": None, + "key": "review-identifier", + "show_for_values": {"en": None, "fr": None}, + }, + ], + }, # End section 2 + ], } + class ApplicationFormCreateTest(ApiTestCase): """ Test the Application Form Detail API's POST method @@ -308,9 +326,9 @@ class ApplicationFormCreateTest(ApiTestCase): def _seed_data_create(self): self.event = self.add_event() self.system_admin = self.add_user(is_admin=True) - self.event_admin = self.add_user(email='user2@user.com') - self.normal_user = self.add_user(email='user3@user.com') - self.event.add_event_role('admin', self.event_admin.id) + self.event_admin = self.add_user(email="user2@user.com") + self.normal_user = self.add_user(email="user3@user.com") + self.event.add_event_role("admin", self.event_admin.id) db.session.commit() def test_event_admin_permission(self): @@ -320,10 +338,10 @@ def test_event_admin_permission(self): self._seed_data_create() response = self.app.post( - '/api/v1/application-form-detail', + "/api/v1/application-form-detail", data=json.dumps(APPLICATION_FORM_POST_DATA), - content_type='application/json', - headers=self.get_auth_header_for(self.event_admin.email) + content_type="application/json", + headers=self.get_auth_header_for(self.event_admin.email), ) self.assertEqual(response.status_code, 201) @@ -335,10 +353,10 @@ def test_non_event_admin_permission(self): self._seed_data_create() response = self.app.post( - '/api/v1/application-form-detail', + "/api/v1/application-form-detail", data=json.dumps(APPLICATION_FORM_PUT_DATA), - content_type='application/json', - headers=self.get_auth_header_for(self.normal_user.email) + content_type="application/json", + headers=self.get_auth_header_for(self.normal_user.email), ) self.assertEqual(response.status_code, 403) @@ -350,220 +368,221 @@ def test_app_form_is_created(self): self._seed_data_create() response = self.app.post( - '/api/v1/application-form-detail', + "/api/v1/application-form-detail", data=json.dumps(APPLICATION_FORM_POST_DATA), - content_type='application/json', - headers=self.get_auth_header_for(self.system_admin.email) + content_type="application/json", + headers=self.get_auth_header_for(self.system_admin.email), ) response_data = json.loads(response.data) - print('Response Data:', response_data) + print("Response Data:", response_data) # Check that the ID is populated - self.assertIsNotNone(response_data['id']) + self.assertIsNotNone(response_data["id"]) # Check that the fields are populated correctly in the response - self.assertEqual(response_data['event_id'], APPLICATION_FORM_POST_DATA['event_id']) - self.assertEqual(response_data['is_open'], APPLICATION_FORM_POST_DATA['is_open']) - self.assertEqual(response_data['nominations'], APPLICATION_FORM_POST_DATA['nominations']) + self.assertEqual( + response_data["event_id"], APPLICATION_FORM_POST_DATA["event_id"] + ) + self.assertEqual( + response_data["is_open"], APPLICATION_FORM_POST_DATA["is_open"] + ) + self.assertEqual( + response_data["nominations"], APPLICATION_FORM_POST_DATA["nominations"] + ) - self.assertEqual(len(response_data['sections']), len(APPLICATION_FORM_POST_DATA['sections'])) + self.assertEqual( + len(response_data["sections"]), len(APPLICATION_FORM_POST_DATA["sections"]) + ) # Check a few fields in the sections themselves - section0 = response_data['sections'][0] - self.assertIsNotNone(section0['id']) - self.assertDictEqual(section0['name'], APPLICATION_FORM_POST_DATA['sections'][0]['name']) - self.assertEqual(len(section0['questions']), len(APPLICATION_FORM_POST_DATA['sections'][0]['questions'])) + section0 = response_data["sections"][0] + self.assertIsNotNone(section0["id"]) + self.assertDictEqual( + section0["name"], APPLICATION_FORM_POST_DATA["sections"][0]["name"] + ) + self.assertEqual( + len(section0["questions"]), + len(APPLICATION_FORM_POST_DATA["sections"][0]["questions"]), + ) - section0_question0 = section0['questions'][0] - self.assertIsNotNone(section0_question0['id']) - self.assertDictEqual(section0_question0['headline'], - APPLICATION_FORM_POST_DATA['sections'][0]['questions'][0]['headline']) + section0_question0 = section0["questions"][0] + self.assertIsNotNone(section0_question0["id"]) + self.assertDictEqual( + section0_question0["headline"], + APPLICATION_FORM_POST_DATA["sections"][0]["questions"][0]["headline"], + ) - section0_question1 = section0['questions'][1] - self.assertIsNotNone(section0_question1['id']) - self.assertDictEqual(section0_question1['headline'], - APPLICATION_FORM_POST_DATA['sections'][0]['questions'][1]['headline']) + section0_question1 = section0["questions"][1] + self.assertIsNotNone(section0_question1["id"]) + self.assertDictEqual( + section0_question1["headline"], + APPLICATION_FORM_POST_DATA["sections"][0]["questions"][1]["headline"], + ) - section1 = response_data['sections'][1] - self.assertIsNotNone(section1['id']) - self.assertDictEqual(section1['name'], APPLICATION_FORM_POST_DATA['sections'][1]['name']) - self.assertEqual(len(section1['questions']), len(APPLICATION_FORM_POST_DATA['sections'][1]['questions'])) + section1 = response_data["sections"][1] + self.assertIsNotNone(section1["id"]) + self.assertDictEqual( + section1["name"], APPLICATION_FORM_POST_DATA["sections"][1]["name"] + ) + self.assertEqual( + len(section1["questions"]), + len(APPLICATION_FORM_POST_DATA["sections"][1]["questions"]), + ) - section1_question0 = section1['questions'][0] - self.assertIsNotNone(section1_question0['id']) - self.assertDictEqual(section1_question0['headline'], - APPLICATION_FORM_POST_DATA['sections'][1]['questions'][0]['headline']) + section1_question0 = section1["questions"][0] + self.assertIsNotNone(section1_question0["id"]) + self.assertDictEqual( + section1_question0["headline"], + APPLICATION_FORM_POST_DATA["sections"][1]["questions"][0]["headline"], + ) APPLICATION_FORM_PUT_DATA = { - "id": 1, - "event_id": 1, - "is_open": False, # Updated - "nominations": True, # Updated - "sections": [ - { - "id": 1, - "name": { - "en": "Section 1 en updated", # Updated - "fr": "Section 1" - }, - "description": { - "en": "Description of the section", - "fr": "Description de la section updated" # Updated - }, - "order": 1, - "depends_on_question_id": 1, # Updated - "show_for_values": { # Updated - "en": ["value1", "value2"], - "fr": ["value1", "value2"], - }, - "key": None, # Updated - "questions": [ - { - "id": 1, - "validation_regex": { - "en": None, - "fr": None - }, - "options": { # Updated - "en": [ - {"value": "undergrad", "label": "An undergraduate student updated"}, - {"value": "masters", "label": "A masters student updated"}, - ], - "fr": [ - {"value": "undergrad", "label": "un étudiant de premier cycle updated"}, - {"value": "masters", "label": "un étudiant en maîtrise updated"}, - ], - }, - "description": { # Updated - "en": "Question 1 en updated", - "fr": "Question 1 fr updated" - }, - "headline": { - "en": "Question 1 en", - "fr": "Question 1 fr updated" # Updated - }, - "placeholder": { - "en": "Select an Option... updated", # Updated - "fr": "Sélectionnez une option" - }, - "is_required": False, # Updated - "type": "multi-choice", - "validation_text": { - "en": None, - "fr": None - }, - "order": 2, # Updated - "depends_on_question_id": 100, - "key": "my_key", # Updated - "show_for_values": { # Updated - "en": ["the-matrix"], - "fr": ["the-matrix"] - } - }, - # DELETED QUESTION 2 - { # NEW QUESTION - "surrogate_id": 100, - "validation_regex": { - "en": "blah", - "fr": "bleh" - }, - "options": { - "en": None, - "fr": None, - }, - "description": { - "en": "Question 2 NEW en", - "fr": "Question 2 NEW fr" - }, - "headline": { - "en": "Question 2 NEW en", - "fr": "Question 2 NEW fr" - }, - "placeholder": { - "en": "Enter some text...", - "fr": "Entrez du texte ..." - }, - "is_required": True, - "type": "long-text", - "validation_text": { - "en": "You must enter the word blah", - "fr": "Vous devez entrer le mot bleh" - }, - "order": 3, - "depends_on_question_id": None, - "key": "my_key", - "show_for_values": { - "en": None, - "fr": None - } - } - ], - }, # End section 1 - # DELETED SECTION 2 - { # New Section 3 - "name": { - "en": "Section 3", - "fr": "Section 3 fr" - }, - "description": { - "en": "Description of the section 3", - "fr": "Description de la section 3" - }, - "order": 3, - "depends_on_question_id": 1, - "show_for_values": { - "en": ["masters"], - "fr": ["masters"], - }, - "key": "my_section", - "questions": [ + "id": 1, + "event_id": 1, + "is_open": False, # Updated + "nominations": True, # Updated + "sections": [ { - "surrogate_id": 101, - "validation_regex": { - "en": None, - "fr": None - }, - "options": { - "en": [ - {"value": "the-matrix", "label": "The Matrix"}, - {"value": "terminator", "label": "Terminator"}, - ], - "fr": [ - {"value": "the-matrix", "label": "La matrice"}, - {"value": "terminator", "label": "Terminator"}, - ], - }, - "description": { - "en": "Question 3 NEW en", - "fr": "Question 3 NEW fr" - }, - "headline": { - "en": "Question 3 NEW en", - "fr": "Question 3 NEW fr" - }, - "placeholder": { - "en": "Select an Option...", - "fr": "Sélectionnez une option" - }, - "is_required": False, - "type": "multi-choice", - "validation_text": { - "en": None, - "fr": None - }, - "order": 1, - "depends_on_question_id": None, - "key": "review-identifier", - "show_for_values": { - "en": None, - "fr": None - } - }, - ], - }, # End section 3 - ] + "id": 1, + "name": {"en": "Section 1 en updated", "fr": "Section 1"}, # Updated + "description": { + "en": "Description of the section", + "fr": "Description de la section updated", # Updated + }, + "order": 1, + "depends_on_question_id": 1, # Updated + "show_for_values": { # Updated + "en": ["value1", "value2"], + "fr": ["value1", "value2"], + }, + "key": None, # Updated + "questions": [ + { + "id": 1, + "validation_regex": {"en": None, "fr": None}, + "options": { # Updated + "en": [ + { + "value": "undergrad", + "label": "An undergraduate student updated", + }, + {"value": "masters", "label": "A masters student updated"}, + ], + "fr": [ + { + "value": "undergrad", + "label": "un étudiant de premier cycle updated", + }, + { + "value": "masters", + "label": "un étudiant en maîtrise updated", + }, + ], + }, + "description": { # Updated + "en": "Question 1 en updated", + "fr": "Question 1 fr updated", + }, + "headline": { + "en": "Question 1 en", + "fr": "Question 1 fr updated", # Updated + }, + "placeholder": { + "en": "Select an Option... updated", # Updated + "fr": "Sélectionnez une option", + }, + "is_required": False, # Updated + "type": "multi-choice", + "validation_text": {"en": None, "fr": None}, + "order": 2, # Updated + "depends_on_question_id": 100, + "key": "my_key", # Updated + "show_for_values": { # Updated + "en": ["the-matrix"], + "fr": ["the-matrix"], + }, + }, + # DELETED QUESTION 2 + { # NEW QUESTION + "surrogate_id": 100, + "validation_regex": {"en": "blah", "fr": "bleh"}, + "options": { + "en": None, + "fr": None, + }, + "description": { + "en": "Question 2 NEW en", + "fr": "Question 2 NEW fr", + }, + "headline": {"en": "Question 2 NEW en", "fr": "Question 2 NEW fr"}, + "placeholder": { + "en": "Enter some text...", + "fr": "Entrez du texte ...", + }, + "is_required": True, + "type": "long-text", + "validation_text": { + "en": "You must enter the word blah", + "fr": "Vous devez entrer le mot bleh", + }, + "order": 3, + "depends_on_question_id": None, + "key": "my_key", + "show_for_values": {"en": None, "fr": None}, + }, + ], + }, # End section 1 + # DELETED SECTION 2 + { # New Section 3 + "name": {"en": "Section 3", "fr": "Section 3 fr"}, + "description": { + "en": "Description of the section 3", + "fr": "Description de la section 3", + }, + "order": 3, + "depends_on_question_id": 1, + "show_for_values": { + "en": ["masters"], + "fr": ["masters"], + }, + "key": "my_section", + "questions": [ + { + "surrogate_id": 101, + "validation_regex": {"en": None, "fr": None}, + "options": { + "en": [ + {"value": "the-matrix", "label": "The Matrix"}, + {"value": "terminator", "label": "Terminator"}, + ], + "fr": [ + {"value": "the-matrix", "label": "La matrice"}, + {"value": "terminator", "label": "Terminator"}, + ], + }, + "description": { + "en": "Question 3 NEW en", + "fr": "Question 3 NEW fr", + }, + "headline": {"en": "Question 3 NEW en", "fr": "Question 3 NEW fr"}, + "placeholder": { + "en": "Select an Option...", + "fr": "Sélectionnez une option", + }, + "is_required": False, + "type": "multi-choice", + "validation_text": {"en": None, "fr": None}, + "order": 1, + "depends_on_question_id": None, + "key": "review-identifier", + "show_for_values": {"en": None, "fr": None}, + }, + ], + }, # End section 3 + ], } + class ApplicationFormUpdateTest(ApiTestCase): """ Test that an application form's sections and question are updated by an admin with the correct permissions @@ -571,21 +590,22 @@ class ApplicationFormUpdateTest(ApiTestCase): def _seed_data_update(self): self.event = self.add_event() - self.system_admin = self.add_user(email='system@admin.com', is_admin=True) - self.event_admin = self.add_user(email='user2@user.com') - self.normal_user = self.add_user(email='user3@user.com') - self.event.add_event_role('admin', self.event_admin.id) + self.system_admin = self.add_user(email="system@admin.com", is_admin=True) + self.event_admin = self.add_user(email="user2@user.com") + self.normal_user = self.add_user(email="user3@user.com") + self.event.add_event_role("admin", self.event_admin.id) db.session.commit() - self.normal_user_headers = self.get_auth_header_for('user3@user.com') - self.event_admin_headers = self.get_auth_header_for('user2@user.com') - self.system_admin_headers = self.get_auth_header_for('system@admin.com') + self.normal_user_headers = self.get_auth_header_for("user3@user.com") + self.event_admin_headers = self.get_auth_header_for("user2@user.com") + self.system_admin_headers = self.get_auth_header_for("system@admin.com") self.app.post( - '/api/v1/application-form-detail', + "/api/v1/application-form-detail", data=json.dumps(APPLICATION_FORM_POST_DATA), - content_type='application/json', - headers=self.event_admin_headers) + content_type="application/json", + headers=self.event_admin_headers, + ) def test_event_admin_permission(self): """ @@ -594,10 +614,11 @@ def test_event_admin_permission(self): self._seed_data_update() response = self.app.put( - '/api/v1/application-form-detail', + "/api/v1/application-form-detail", data=json.dumps(APPLICATION_FORM_PUT_DATA), - content_type='application/json', - headers=self.event_admin_headers) + content_type="application/json", + headers=self.event_admin_headers, + ) self.assertEqual(response.status_code, 200) @@ -608,10 +629,11 @@ def test_non_event_admin_permission(self): self._seed_data_update() response = self.app.put( - '/api/v1/application-form-detail', + "/api/v1/application-form-detail", data=json.dumps(APPLICATION_FORM_PUT_DATA), - content_type='application/json', - headers=self.normal_user_headers) + content_type="application/json", + headers=self.normal_user_headers, + ) self.assertEqual(response.status_code, 403) @@ -619,10 +641,11 @@ def test_app_form_updated(self): self._seed_data_update() response = self.app.put( - '/api/v1/application-form-detail', + "/api/v1/application-form-detail", data=json.dumps(APPLICATION_FORM_PUT_DATA), - content_type='application/json', - headers=self.system_admin_headers) + content_type="application/json", + headers=self.system_admin_headers, + ) self.assertEqual(response.status_code, 200) @@ -631,67 +654,87 @@ def test_app_form_updated(self): pp = pprint.PrettyPrinter(indent=2) pp.pprint(response_data) - self.assertEqual(response_data['id'], 1) - self.assertEqual(response_data['event_id'], 1) - self.assertFalse(response_data['is_open']) - self.assertTrue(response_data['nominations']) - self.assertEqual(len(response_data['sections']), 2) + self.assertEqual(response_data["id"], 1) + self.assertEqual(response_data["event_id"], 1) + self.assertFalse(response_data["is_open"]) + self.assertTrue(response_data["nominations"]) + self.assertEqual(len(response_data["sections"]), 2) # Check section 1 (updated) - section1_actual = response_data['sections'][0] - section1_expected = APPLICATION_FORM_PUT_DATA['sections'][0] + section1_actual = response_data["sections"][0] + section1_expected = APPLICATION_FORM_PUT_DATA["sections"][0] for key in section1_expected.keys(): - if key == 'questions': + if key == "questions": continue - self.assertEqual(section1_actual[key], section1_expected[key], - '{}: actual "{}" vs expected "{}"'.format(key, section1_actual[key], section1_expected[key])) + self.assertEqual( + section1_actual[key], + section1_expected[key], + '{}: actual "{}" vs expected "{}"'.format( + key, section1_actual[key], section1_expected[key] + ), + ) - self.assertEqual(len(section1_actual['questions']), 2) + self.assertEqual(len(section1_actual["questions"]), 2) # Check question 1 in section 1 (updated) - section1_question1_actual = response_data['sections'][0]['questions'][0] - section1_question1_expected = APPLICATION_FORM_PUT_DATA['sections'][0]['questions'][0] + section1_question1_actual = response_data["sections"][0]["questions"][0] + section1_question1_expected = APPLICATION_FORM_PUT_DATA["sections"][0][ + "questions" + ][0] for key in section1_question1_expected.keys(): - if key == 'depends_on_question_id' or key == 'surrogate_id': + if key == "depends_on_question_id" or key == "surrogate_id": continue - self.assertEqual(section1_question1_actual[key], section1_question1_expected[key], key) + self.assertEqual( + section1_question1_actual[key], section1_question1_expected[key], key + ) # Check question 2 in section 1 (new) - section1_question2_actual = response_data['sections'][0]['questions'][1] - section1_question2_expected = APPLICATION_FORM_PUT_DATA['sections'][0]['questions'][1] + section1_question2_actual = response_data["sections"][0]["questions"][1] + section1_question2_expected = APPLICATION_FORM_PUT_DATA["sections"][0][ + "questions" + ][1] for key in section1_question2_expected.keys(): - if key == 'depends_on_question_id' or key == 'surrogate_id': + if key == "depends_on_question_id" or key == "surrogate_id": continue - self.assertEqual(section1_question2_actual[key], section1_question2_expected[key], key) + self.assertEqual( + section1_question2_actual[key], section1_question2_expected[key], key + ) # Check section 3 (new) - section3_actual = response_data['sections'][1] - section3_expected = APPLICATION_FORM_PUT_DATA['sections'][1] + section3_actual = response_data["sections"][1] + section3_expected = APPLICATION_FORM_PUT_DATA["sections"][1] for key in section3_expected.keys(): - if key == 'questions': + if key == "questions": continue self.assertEqual(section3_actual[key], section3_expected[key]) - self.assertEqual(len(section3_actual['questions']), len(section3_expected['questions'])) + self.assertEqual( + len(section3_actual["questions"]), len(section3_expected["questions"]) + ) # Check question 1 in section 2 (NEW) - section2_question1_actual = response_data['sections'][1]['questions'][0] - section2_question1_expected = APPLICATION_FORM_PUT_DATA['sections'][1]['questions'][0] + section2_question1_actual = response_data["sections"][1]["questions"][0] + section2_question1_expected = APPLICATION_FORM_PUT_DATA["sections"][1][ + "questions" + ][0] for key in section2_question1_expected.keys(): - if key == 'depends_on_question_id' or key == 'surrogate_id': + if key == "depends_on_question_id" or key == "surrogate_id": continue - self.assertEqual(section2_question1_actual[key], section2_question1_expected[key], key) + self.assertEqual( + section2_question1_actual[key], section2_question1_expected[key], key + ) + class QuestionListApiTest(ApiTestCase): def _seed_static_data(self): - self.user1 = self.add_user('user1@mail.com') - self.user2 = self.add_user('user2@mail.com') + self.user1 = self.add_user("user1@mail.com") + self.user2 = self.add_user("user2@mail.com") self.event = self.add_event() - self.event.add_event_role('admin', self.user1.id) + self.event.add_event_role("admin", self.user1.id) db.session.commit() - self.user1header = self.get_auth_header_for('user1@mail.com') - self.user2header = self.get_auth_header_for('user2@mail.com') + self.user1header = self.get_auth_header_for("user1@mail.com") + self.user2header = self.get_auth_header_for("user2@mail.com") self.application_form = self.create_application_form() @@ -699,24 +742,32 @@ def _seed_static_data(self): self.section1 = self.add_section(self.application_form.id, order=2) self.section2 = self.add_section(self.application_form.id, order=1) - self.section1question1 = self.add_question(self.application_form.id, self.section1.id, order=2) + self.section1question1 = self.add_question( + self.application_form.id, self.section1.id, order=2 + ) self.add_question_translation( - self.section1question1.id, 'en', headline='English Section 1 Question 1' + self.section1question1.id, "en", headline="English Section 1 Question 1" ) self.add_question_translation( - self.section1question1.id, 'fr', headline='French Section 1 Question 1' + self.section1question1.id, "fr", headline="French Section 1 Question 1" ) - self.section1question2 = self.add_question(self.application_form.id, self.section1.id, order=1) + self.section1question2 = self.add_question( + self.application_form.id, self.section1.id, order=1 + ) self.add_question_translation( - self.section1question2.id, 'en', headline='English Section 1 Question 2' + self.section1question2.id, "en", headline="English Section 1 Question 2" ) self.add_question_translation( - self.section1question2.id, 'fr', headline='French Section 1 Question 2' + self.section1question2.id, "fr", headline="French Section 1 Question 2" ) - self.section2question1 = self.add_question(self.application_form.id, self.section2.id, order=1) - self.add_question_translation(self.section2question1.id, 'en', 'English Section 2 Question 1') + self.section2question1 = self.add_question( + self.application_form.id, self.section2.id, order=1 + ) + self.add_question_translation( + self.section2question1.id, "en", "English Section 2 Question 1" + ) # Deliberately not adding a French translation to test defaulting behaviour def test_get(self): @@ -724,59 +775,59 @@ def test_get(self): self._seed_static_data() response = self.app.get( - '/api/v1/questions', + "/api/v1/questions", headers=self.user1header, - query_string={'event_id': 1, 'language': 'en'} + query_string={"event_id": 1, "language": "en"}, ) data = json.loads(response.data) self.assertEqual(len(data), 3) - self.assertEqual(data[0]['question_id'], 3) - self.assertEqual(data[0]['headline'], 'English Section 2 Question 1') - self.assertEqual(data[0]['type'], 'short-text') + self.assertEqual(data[0]["question_id"], 3) + self.assertEqual(data[0]["headline"], "English Section 2 Question 1") + self.assertEqual(data[0]["type"], "short-text") - self.assertEqual(data[1]['question_id'], 2) - self.assertEqual(data[1]['headline'], 'English Section 1 Question 2') - self.assertEqual(data[1]['type'], 'short-text') + self.assertEqual(data[1]["question_id"], 2) + self.assertEqual(data[1]["headline"], "English Section 1 Question 2") + self.assertEqual(data[1]["type"], "short-text") - self.assertEqual(data[2]['question_id'], 1) - self.assertEqual(data[2]['headline'], 'English Section 1 Question 1') - self.assertEqual(data[2]['type'], 'short-text') + self.assertEqual(data[2]["question_id"], 1) + self.assertEqual(data[2]["headline"], "English Section 1 Question 1") + self.assertEqual(data[2]["type"], "short-text") def test_get_language(self): """Test get in a specified language and defaulting.""" self._seed_static_data() response = self.app.get( - '/api/v1/questions', + "/api/v1/questions", headers=self.user1header, - query_string={'event_id': 1, 'language': 'fr'} + query_string={"event_id": 1, "language": "fr"}, ) data = json.loads(response.data) self.assertEqual(len(data), 3) - self.assertEqual(data[0]['question_id'], 3) - self.assertEqual(data[0]['headline'], 'English Section 2 Question 1') - self.assertEqual(data[0]['type'], 'short-text') + self.assertEqual(data[0]["question_id"], 3) + self.assertEqual(data[0]["headline"], "English Section 2 Question 1") + self.assertEqual(data[0]["type"], "short-text") - self.assertEqual(data[1]['question_id'], 2) - self.assertEqual(data[1]['headline'], 'French Section 1 Question 2') - self.assertEqual(data[1]['type'], 'short-text') + self.assertEqual(data[1]["question_id"], 2) + self.assertEqual(data[1]["headline"], "French Section 1 Question 2") + self.assertEqual(data[1]["type"], "short-text") - self.assertEqual(data[2]['question_id'], 1) - self.assertEqual(data[2]['headline'], 'French Section 1 Question 1') - self.assertEqual(data[2]['type'], 'short-text') + self.assertEqual(data[2]["question_id"], 1) + self.assertEqual(data[2]["headline"], "French Section 1 Question 1") + self.assertEqual(data[2]["type"], "short-text") def test_event_admin(self): """Test that a non event admin is blocked from accessing the data.""" self._seed_static_data() response = self.app.get( - '/api/v1/questions', + "/api/v1/questions", headers=self.user2header, # Not an event admin - query_string={'event_id': 1, 'language': 'fr'} + query_string={"event_id": 1, "language": "fr"}, ) self.assertEqual(response.status_code, 403) diff --git a/api/app/attendance/api.py b/api/app/attendance/api.py index eb7ed830a..da3898a7a 100644 --- a/api/app/attendance/api.py +++ b/api/app/attendance/api.py @@ -1,41 +1,59 @@ -from flask import g import flask_restful as restful -from flask_restful import reqparse, fields, marshal_with +from flask import g +from flask_restful import fields, marshal_with, reqparse + from app import db from app.attendance.mixins import AttendanceMixin from app.attendance.models import Attendance from app.attendance.repository import AttendanceRepository as attendance_repository +from app.events.models import EventRole from app.events.repository import EventRepository as event_repository +from app.invitedGuest.models import GuestRegistration, InvitedGuest +from app.registration.models import ( + Offer, + Registration, + RegistrationForm, + RegistrationQuestion, + get_registration_answer_based_headline, +) from app.users.repository import UserRepository as user_repository from app.utils.auth import auth_required from app.utils.emailer import email_user -from app.utils.errors import ATTENDANCE_ALREADY_CONFIRMED, ATTENDANCE_NOT_FOUND, EVENT_NOT_FOUND, FORBIDDEN, USER_NOT_FOUND, OFFER_NOT_FOUND, REGISTRATION_NOT_FOUND -from app.registration.models import RegistrationQuestion -from app.registration.models import Offer -from app.invitedGuest.models import GuestRegistration -from app.registration.models import RegistrationForm -from app.registration.models import Registration -from app.registration.models import get_registration_answer_based_headline -from app.events.models import EventRole -from app.invitedGuest.models import InvitedGuest +from app.utils.errors import ( + ATTENDANCE_ALREADY_CONFIRMED, + ATTENDANCE_NOT_FOUND, + EVENT_NOT_FOUND, + FORBIDDEN, + OFFER_NOT_FOUND, + REGISTRATION_NOT_FOUND, + USER_NOT_FOUND, +) attendance_fields = { - 'id': fields.Integer, - 'event_id': fields.Integer, - 'user_id': fields.Integer, - 'timestamp': fields.DateTime('iso8601'), - 'updated_by_user_id': fields.Integer, - 'accommodation_award': fields.Boolean, - 'shirt_size': fields.String, - 'is_invitedguest': fields.Boolean, - 'bringing_poster': fields.Boolean, - 'message': fields.String, - 'invitedguest_role': fields.String, + "id": fields.Integer, + "event_id": fields.Integer, + "user_id": fields.Integer, + "timestamp": fields.DateTime("iso8601"), + "updated_by_user_id": fields.Integer, + "accommodation_award": fields.Boolean, + "shirt_size": fields.String, + "is_invitedguest": fields.Boolean, + "bringing_poster": fields.Boolean, + "message": fields.String, + "invitedguest_role": fields.String, } -class AttendanceUser(): - def __init__(self, attendance, accommodation_award, shirt_size, is_invitedguest, bringing_poster, invitedguest_role): +class AttendanceUser: + def __init__( + self, + attendance, + accommodation_award, + shirt_size, + is_invitedguest, + bringing_poster, + invitedguest_role, + ): self.id = attendance.id self.event_id = attendance.event_id self.user_id = attendance.user_id @@ -49,14 +67,13 @@ def __init__(self, attendance, accommodation_award, shirt_size, is_invitedguest, class AttendanceAPI(AttendanceMixin, restful.Resource): - @auth_required @marshal_with(attendance_fields) def get(self): args = self.req_parser.parse_args() - event_id = args['event_id'] - user_id = args['user_id'] - registration_user_id = g.current_user['id'] + event_id = args["event_id"] + user_id = args["user_id"] + registration_user_id = g.current_user["id"] registration_user = user_repository.get_by_id(registration_user_id) if not registration_user.is_registration_volunteer(event_id): @@ -80,9 +97,9 @@ def get(self): @marshal_with(attendance_fields) def post(self): args = self.req_parser.parse_args() - event_id = args['event_id'] - user_id = args['user_id'] - registration_user_id = g.current_user['id'] + event_id = args["event_id"] + user_id = args["user_id"] + registration_user_id = g.current_user["id"] registration_user = user_repository.get_by_id(registration_user_id) if not registration_user.is_registration_volunteer(event_id): @@ -102,11 +119,7 @@ def post(self): attendance = Attendance(event_id, user_id, registration_user_id) attendance_repository.create(attendance) - email_user( - 'attendance-confirmation', - event=event, - user=user - ) + email_user("attendance-confirmation", event=event, user=user) # Other Fields unavailable_response = None @@ -114,42 +127,58 @@ def post(self): is_invited_guest = unavailable_response has_accepted_accom_award = unavailable_response - offer = db.session.query(Offer).filter( - Offer.user_id == user_id).filter(Offer.event_id == event_id).first() + offer = ( + db.session.query(Offer) + .filter(Offer.user_id == user_id) + .filter(Offer.event_id == event_id) + .first() + ) if offer: has_accepted_accom_award = ( - offer.accommodation_award and offer.accepted_accommodation_award) - + offer.accommodation_award and offer.accepted_accommodation_award + ) + # Check if invited guest - invited_guest = db.session.query(InvitedGuest).filter( - InvitedGuest.event_id == event_id).filter(InvitedGuest.user_id == user.id).first() - if(invited_guest): + invited_guest = ( + db.session.query(InvitedGuest) + .filter(InvitedGuest.event_id == event_id) + .filter(InvitedGuest.user_id == user.id) + .first() + ) + if invited_guest: is_invited_guest = True invitedguest_role = invited_guest.role - # Shirt Size - shirt_answer = (get_registration_answer_based_headline( - user_id, "T-Shirt Size") and get_registration_answer_based_headline( - user_id, "T-Shirt Size").value) or unavailable_response + shirt_answer = ( + get_registration_answer_based_headline(user_id, "T-Shirt Size") + and get_registration_answer_based_headline(user_id, "T-Shirt Size").value + ) or unavailable_response # Poster registration bringing_poster = False poster_answer = get_registration_answer_based_headline( - user_id, "Will you be bringing a poster?") - if (poster_answer is not None and poster_answer.value == 'yes'): + user_id, "Will you be bringing a poster?" + ) + if poster_answer is not None and poster_answer.value == "yes": bringing_poster = True attendance_user = AttendanceUser( - attendance, has_accepted_accom_award, shirt_answer, is_invited_guest, bringing_poster, invitedguest_role) + attendance, + has_accepted_accom_award, + shirt_answer, + is_invited_guest, + bringing_poster, + invitedguest_role, + ) return attendance_user, 201 @auth_required def delete(self): args = self.req_parser.parse_args() - event_id = args['event_id'] - user_id = args['user_id'] - registration_user_id = g.current_user['id'] + event_id = args["event_id"] + user_id = args["user_id"] + registration_user_id = g.current_user["id"] registration_user = user_repository.get_by_id(registration_user_id) if not registration_user.is_registration_volunteer(event_id): diff --git a/api/app/attendance/mixins.py b/api/app/attendance/mixins.py index 274be156d..10830029e 100644 --- a/api/app/attendance/mixins.py +++ b/api/app/attendance/mixins.py @@ -1,6 +1,7 @@ from flask_restful import reqparse + class AttendanceMixin(object): req_parser = reqparse.RequestParser() - req_parser.add_argument('event_id', type=int, required=True) - req_parser.add_argument('user_id', type=int, required=True) \ No newline at end of file + req_parser.add_argument("event_id", type=int, required=True) + req_parser.add_argument("user_id", type=int, required=True) diff --git a/api/app/attendance/models.py b/api/app/attendance/models.py index 4c29e5bad..007107bee 100644 --- a/api/app/attendance/models.py +++ b/api/app/attendance/models.py @@ -2,22 +2,22 @@ from app import db + class Attendance(db.Model): id = db.Column(db.Integer(), primary_key=True) - event_id = db.Column(db.Integer(), db.ForeignKey('event.id'), nullable=False) - user_id = db.Column(db.Integer(), db.ForeignKey('app_user.id'), nullable=False) + event_id = db.Column(db.Integer(), db.ForeignKey("event.id"), nullable=False) + user_id = db.Column(db.Integer(), db.ForeignKey("app_user.id"), nullable=False) timestamp = db.Column(db.DateTime(), nullable=False) - updated_by_user_id = db.Column(db.Integer(), db.ForeignKey('app_user.id'), nullable=False) + updated_by_user_id = db.Column( + db.Integer(), db.ForeignKey("app_user.id"), nullable=False + ) - event = db.relationship('Event', foreign_keys=[event_id]) - user = db.relationship('AppUser', foreign_keys=[user_id]) - updated_by_user = db.relationship('AppUser', foreign_keys=[updated_by_user_id]) + event = db.relationship("Event", foreign_keys=[event_id]) + user = db.relationship("AppUser", foreign_keys=[user_id]) + updated_by_user = db.relationship("AppUser", foreign_keys=[updated_by_user_id]) - def __init__(self, - event_id, - user_id, - updated_by_user_id): + def __init__(self, event_id, user_id, updated_by_user_id): self.event_id = event_id self.user_id = user_id self.timestamp = datetime.now() - self.updated_by_user_id = updated_by_user_id \ No newline at end of file + self.updated_by_user_id = updated_by_user_id diff --git a/api/app/attendance/repository.py b/api/app/attendance/repository.py index 5da2830ee..bedfb13d9 100644 --- a/api/app/attendance/repository.py +++ b/api/app/attendance/repository.py @@ -1,19 +1,24 @@ from app import db from app.attendance.models import Attendance -class AttendanceRepository(): +class AttendanceRepository: @staticmethod def exists(event_id, user_id): - return db.session.query(Attendance.id)\ - .filter_by(event_id=event_id, user_id=user_id)\ - .first() is not None + return ( + db.session.query(Attendance.id) + .filter_by(event_id=event_id, user_id=user_id) + .first() + is not None + ) @staticmethod def get(event_id, user_id): - return db.session.query(Attendance)\ - .filter_by(event_id=event_id, user_id=user_id)\ - .first() + return ( + db.session.query(Attendance) + .filter_by(event_id=event_id, user_id=user_id) + .first() + ) @staticmethod def create(attendance): @@ -23,4 +28,4 @@ def create(attendance): @staticmethod def delete(attendance): db.session.delete(attendance) - db.session.commit() \ No newline at end of file + db.session.commit() diff --git a/api/app/attendance/tests.py b/api/app/attendance/tests.py index 1d067fddc..1ae95f10e 100644 --- a/api/app/attendance/tests.py +++ b/api/app/attendance/tests.py @@ -1,48 +1,55 @@ -from datetime import datetime import json +from datetime import datetime, timedelta -from app import db +from app import LOGGER, db from app.attendance.models import Attendance from app.attendance.repository import AttendanceRepository as attendance_repository -from app.events.models import Event, EventRole +from app.events.models import Event, EventRole, EventType +from app.invitedGuest.models import InvitedGuest +from app.organisation.models import Organisation +from app.registration.models import ( + Offer, + Registration, + RegistrationAnswer, + RegistrationForm, + RegistrationQuestion, + RegistrationSection, +) +from app.registrationResponse.repository import RegistrationRepository from app.users.models import AppUser, Country, UserCategory from app.utils.errors import ATTENDANCE_ALREADY_CONFIRMED, FORBIDDEN from app.utils.testing import ApiTestCase -from app.registration.models import Offer -from app.registration.models import RegistrationQuestion -from app.registration.models import RegistrationForm -from app.registration.models import Registration -from app.registration.models import RegistrationSection -from app.registration.models import RegistrationAnswer -from app.invitedGuest.models import InvitedGuest -from datetime import datetime, timedelta -from app.registrationResponse.repository import RegistrationRepository -from app import LOGGER -import json -from app.organisation.models import Organisation -from app.events.models import EventType class AttendanceApiTest(ApiTestCase): - def seed_static_data(self): - self.add_organisation('Deep Learning Indaba', 'blah.png', 'blah_big.png', 'deeplearningindaba') - user_category = UserCategory('PhD') + self.add_organisation( + "Deep Learning Indaba", "blah.png", "blah_big.png", "deeplearningindaba" + ) + user_category = UserCategory("PhD") db.session.add(user_category) db.session.commit() - country = Country('South Africa') + country = Country("South Africa") db.session.add(country) - self.attendee = self.add_user(email='attendee@mail.com') + self.attendee = self.add_user(email="attendee@mail.com") - registration_admin = self.add_user('ra@ra.com') + registration_admin = self.add_user("ra@ra.com") - event = self.add_event({'en': 'indaba 2019'}, {'en': 'The Deep Learning Indaba 2019, Kenyatta University, Nairobi, Kenya '}, datetime(2019, 8, 25), datetime(2019, 8, 31),'JOLLOF') + event = self.add_event( + {"en": "indaba 2019"}, + { + "en": "The Deep Learning Indaba 2019, Kenyatta University, Nairobi, Kenya " + }, + datetime(2019, 8, 25), + datetime(2019, 8, 31), + "JOLLOF", + ) self.event = event db.session.add(self.event) - event_role = EventRole('registration-admin', 2, 1) + event_role = EventRole("registration-admin", 2, 1) db.session.add(event_role) db.session.commit() offer = Offer( @@ -54,13 +61,11 @@ def seed_static_data(self): accommodation_award=True, travel_award=True, accepted_accommodation_award=True, - accepted_travel_award=True + accepted_travel_award=True, ) db.session.add_all([offer]) - form = RegistrationForm( - event_id=event.id - ) + form = RegistrationForm(event_id=event.id) db.session.add(form) db.session.commit() self.form = form @@ -85,49 +90,42 @@ def seed_static_data(self): placeholder="the placeholder", headline="Will you be bringing a poster?", validation_regex="[]/", - validation_text=" text" + validation_text=" text", ) db.session.add(rq) db.session.commit() registration = Registration( - offer_id=offer.id, - registration_form_id=form.id, - confirmed=True) + offer_id=offer.id, registration_form_id=form.id, confirmed=True + ) db.session.add_all([registration]) db.session.commit() ra = RegistrationAnswer( - registration_id=registration.id, - registration_question_id=rq.id, - value="yes" + registration_id=registration.id, registration_question_id=rq.id, value="yes" ) db.session.add_all([ra]) db.session.commit() - self.add_email_template('attendance-confirmation') + self.add_email_template("attendance-confirmation") def get_auth_header_for(self, email): - body = { - 'email': email, - 'password': 'abc' - } - response = self.app.post('api/v1/authenticate', data=body) + body = {"email": email, "password": "abc"} + response = self.app.post("api/v1/authenticate", data=body) data = json.loads(response.data) - header = {'Authorization': data['token']} + header = {"Authorization": data["token"]} return header def test_non_admin_cannot_get_post_delete(self): self.seed_static_data() - header = self.get_auth_header_for('attendee@mail.com') - params = {'user_id': 2, 'event_id': 1} + header = self.get_auth_header_for("attendee@mail.com") + params = {"user_id": 2, "event_id": 1} - response_get = self.app.get( - '/api/v1/attendance', headers=header, data=params) - response_post = self.app.post( - '/api/v1/attendance', headers=header, data=params) + response_get = self.app.get("/api/v1/attendance", headers=header, data=params) + response_post = self.app.post("/api/v1/attendance", headers=header, data=params) response_delete = self.app.delete( - '/api/v1/attendance', headers=header, data=params) + "/api/v1/attendance", headers=header, data=params + ) self.assertEqual(response_get.status_code, FORBIDDEN[1]) self.assertEqual(response_post.status_code, FORBIDDEN[1]) @@ -140,36 +138,34 @@ def setup_get_attendance(self): def test_get_attendance(self): self.seed_static_data() self.setup_get_attendance() - header = self.get_auth_header_for('ra@ra.com') - params = {'user_id': 1, 'event_id': 1} + header = self.get_auth_header_for("ra@ra.com") + params = {"user_id": 1, "event_id": 1} - response = self.app.get('/api/v1/attendance', - headers=header, data=params) + response = self.app.get("/api/v1/attendance", headers=header, data=params) data = json.loads(response.data) - self.assertEqual(data['user_id'], 1) - self.assertEqual(data['event_id'], 1) - self.assertEqual(data['updated_by_user_id'], 2) + self.assertEqual(data["user_id"], 1) + self.assertEqual(data["event_id"], 1) + self.assertEqual(data["updated_by_user_id"], 2) def test_post_attendance(self): self.seed_static_data() - header = self.get_auth_header_for('ra@ra.com') - params = {'user_id': 1, 'event_id': 1} + header = self.get_auth_header_for("ra@ra.com") + params = {"user_id": 1, "event_id": 1} - response = self.app.post('/api/v1/attendance', - headers=header, data=params) + response = self.app.post("/api/v1/attendance", headers=header, data=params) data = json.loads(response.data) - self.assertEqual(data['user_id'], 1) - self.assertEqual(data['bringing_poster'], True) - self.assertEqual(data['updated_by_user_id'], 2) - self.assertEqual(data['accommodation_award'], True) + self.assertEqual(data["user_id"], 1) + self.assertEqual(data["bringing_poster"], True) + self.assertEqual(data["updated_by_user_id"], 2) + self.assertEqual(data["accommodation_award"], True) # Normal Attendance def test_get_attendance_list(self): self.seed_static_data() - attendee2 = self.add_user('attendee2@mail.com') + attendee2 = self.add_user("attendee2@mail.com") db.session.commit() offer2 = Offer( @@ -181,123 +177,129 @@ def test_get_attendance_list(self): accommodation_award=True, travel_award=True, accepted_accommodation_award=True, - accepted_travel_award=True + accepted_travel_award=True, ) db.session.add(offer2) db.session.commit() registration2 = Registration( - offer_id=offer2.id, - registration_form_id=self.form.id, - confirmed=False) + offer_id=offer2.id, registration_form_id=self.form.id, confirmed=False + ) db.session.add(registration2) db.session.commit() - header = self.get_auth_header_for('ra@ra.com') + header = self.get_auth_header_for("ra@ra.com") user_id = 1 - params = {'event_id': 1} + params = {"event_id": 1} result = self.app.get( - '/api/v1/registration/confirmed', headers=header, data=params) + "/api/v1/registration/confirmed", headers=header, data=params + ) data = json.loads(result.data) self.assertEqual(len(data), 2) - self.assertEqual(data[0]['user_id'], user_id) + self.assertEqual(data[0]["user_id"], user_id) - params = {'user_id': user_id, 'event_id': 1} - self.app.post('/api/v1/attendance', - headers=header, data=params) + params = {"user_id": user_id, "event_id": 1} + self.app.post("/api/v1/attendance", headers=header, data=params) # Exclude signed in - params = { 'event_id': 1, - 'exclude_already_signed_in': True} + params = {"event_id": 1, "exclude_already_signed_in": True} result2 = self.app.get( - '/api/v1/registration/confirmed', headers=header, data=params) + "/api/v1/registration/confirmed", headers=header, data=params + ) data2 = json.loads(result2.data) self.assertEqual(len(data2), 1) # Include signed in - possible to undo - params = {'exclude_already_signed_in': 'false','event_id': 1} + params = {"exclude_already_signed_in": "false", "event_id": 1} LOGGER.debug(params) result2 = self.app.get( - '/api/v1/registration/confirmed', headers=header, data=params) + "/api/v1/registration/confirmed", headers=header, data=params + ) data2 = json.loads(result2.data) self.assertEqual(len(data2), 2) # Invited Guests attendance def test_get_attendance_list_2(self): self.seed_static_data() - mrObama = self.add_user('obama@mail.com', 'Barack', 'Obama', 'Mr') + mrObama = self.add_user("obama@mail.com", "Barack", "Obama", "Mr") db.session.add(mrObama) db.session.commit() invited_guest_id = mrObama.id role = "EveryRole" - mrObamaInvitedGuest = InvitedGuest(event_id=1, user_id=invited_guest_id, role=role) + mrObamaInvitedGuest = InvitedGuest( + event_id=1, user_id=invited_guest_id, role=role + ) db.session.add(mrObamaInvitedGuest) db.session.commit() - header = self.get_auth_header_for('ra@ra.com') + header = self.get_auth_header_for("ra@ra.com") # Invited Guests always get returned - params = { 'event_id': 1} + params = {"event_id": 1} result = self.app.get( - '/api/v1/registration/confirmed', headers=header, data=params) + "/api/v1/registration/confirmed", headers=header, data=params + ) data = json.loads(result.data) - self.assertGreaterEqual(len(data),1) + self.assertGreaterEqual(len(data), 1) is_invited_guest_returned = False for attendee in data: - if(attendee['user_id'] == invited_guest_id): + if attendee["user_id"] == invited_guest_id: is_invited_guest_returned = True self.assertTrue(is_invited_guest_returned) # Confirm Attendance of Invited Guest - params = {'user_id': invited_guest_id, 'event_id': 1} - attendance_response = self.app.post('/api/v1/attendance', - headers=header, data=params) - + params = {"user_id": invited_guest_id, "event_id": 1} + attendance_response = self.app.post( + "/api/v1/attendance", headers=header, data=params + ) + response = json.loads(attendance_response.data) - self.assertEqual(response['is_invitedguest'],True) - self.assertEqual(response['invitedguest_role'],role) + self.assertEqual(response["is_invitedguest"], True) + self.assertEqual(response["invitedguest_role"], role) # No Invited Guest since he/she has already been signed in. - params = { 'event_id': 1, - 'exclude_already_signed_in': True} + params = {"event_id": 1, "exclude_already_signed_in": True} result2 = self.app.get( - '/api/v1/registration/confirmed', headers=header, data=params) + "/api/v1/registration/confirmed", headers=header, data=params + ) data2 = json.loads(result2.data) is_invited_guest_returned = False for attendee in data2: - if(attendee['user_id'] == invited_guest_id): + if attendee["user_id"] == invited_guest_id: is_invited_guest_returned = True - + self.assertFalse(is_invited_guest_returned) - # No duplicate attendances + # No duplicate attendances def test_get_attendance_list_3(self): self.seed_static_data() # Add attendee as Invited Guest as well invited_guest_id = self.attendee.id - dupl_attendee = InvitedGuest(event_id=1, user_id=invited_guest_id, role='EveryRole') + dupl_attendee = InvitedGuest( + event_id=1, user_id=invited_guest_id, role="EveryRole" + ) db.session.add(dupl_attendee) db.session.commit() - header = self.get_auth_header_for('ra@ra.com') - params = { 'event_id': 1} + header = self.get_auth_header_for("ra@ra.com") + params = {"event_id": 1} result = self.app.get( - '/api/v1/registration/confirmed', headers=header, data=params) + "/api/v1/registration/confirmed", headers=header, data=params + ) data = json.loads(result.data) - occurences_in_attendance_list = [att for att in data if att['user_id'] == invited_guest_id] + occurences_in_attendance_list = [ + att for att in data if att["user_id"] == invited_guest_id + ] num_occurences_in_attendance_list = len(occurences_in_attendance_list) - self.assertEqual(num_occurences_in_attendance_list,1) - + self.assertEqual(num_occurences_in_attendance_list, 1) def test_cannot_register_attendance_twice(self): self.seed_static_data() - header = self.get_auth_header_for('ra@ra.com') - params = {'user_id': 1, 'event_id': 1} + header = self.get_auth_header_for("ra@ra.com") + params = {"user_id": 1, "event_id": 1} - response = self.app.post('/api/v1/attendance', - headers=header, data=params) - response = self.app.post('/api/v1/attendance', - headers=header, data=params) + response = self.app.post("/api/v1/attendance", headers=header, data=params) + response = self.app.post("/api/v1/attendance", headers=header, data=params) self.assertEqual(response.status_code, ATTENDANCE_ALREADY_CONFIRMED[1]) @@ -308,11 +310,10 @@ def setup_delete_attendance(self): def test_delete_attendance(self): self.seed_static_data() self.setup_delete_attendance() - header = self.get_auth_header_for('ra@ra.com') - params = {'user_id': 1, 'event_id': 1} + header = self.get_auth_header_for("ra@ra.com") + params = {"user_id": 1, "event_id": 1} - response = self.app.delete( - '/api/v1/attendance', headers=header, data=params) + response = self.app.delete("/api/v1/attendance", headers=header, data=params) attendance = attendance_repository.get(1, 1) self.assertEqual(response.status_code, 200) diff --git a/api/app/content/api.py b/api/app/content/api.py index 3e899ce4a..43e365587 100644 --- a/api/app/content/api.py +++ b/api/app/content/api.py @@ -3,20 +3,19 @@ from datetime import datetime import flask_restful as restful -from flask_restful import reqparse, fields, marshal_with +from flask_restful import fields, marshal_with, reqparse -from app.users.models import Country, UserCategory from app import db - +from app.users.models import Country, UserCategory country_fields = { - 'value': fields.Integer(attribute='id'), - 'label': fields.String(attribute='name') + "value": fields.Integer(attribute="id"), + "label": fields.String(attribute="name"), } category_fields = { - 'value': fields.Integer(attribute='id'), - 'label': fields.String(attribute='name') + "value": fields.Integer(attribute="id"), + "label": fields.String(attribute="name"), } @@ -32,8 +31,7 @@ class CategoryContentAPI(restful.Resource): # DEPRECATED - TODO: REMOVE @marshal_with(category_fields) def get(self): - countries = db.session.query( - UserCategory).order_by(UserCategory.id).all() + countries = db.session.query(UserCategory).order_by(UserCategory.id).all() return countries @@ -41,28 +39,28 @@ class EthnicityContentAPI(restful.Resource): # DEPRECATED - TODO: REMOVE def get(self): return [ - {"label": "Black", "value": 'black'}, - {"label": "Coloured / Mixed Descent", "value": 'coloured/mixed'}, - {"label": "White", "value": 'white'}, - {"label": "Indian Descent", "value": 'indian'}, - {"label": "Other", "value": "other"} + {"label": "Black", "value": "black"}, + {"label": "Coloured / Mixed Descent", "value": "coloured/mixed"}, + {"label": "White", "value": "white"}, + {"label": "Indian Descent", "value": "indian"}, + {"label": "Other", "value": "other"}, ] class TitleContentAPI(restful.Resource): def get(self): req_parser = reqparse.RequestParser() - req_parser.add_argument('language', type=str, required=True) + req_parser.add_argument("language", type=str, required=True) args = req_parser.parse_args() - language = args['language'] + language = args["language"] - if language == 'fr': + if language == "fr": return [ {"value": "M.", "label": "M."}, {"value": "Mme", "label": "Mme"}, {"value": "Mlle", "label": "Mlle"}, {"value": "Dr", "label": "Dr"}, - {"value": "Pr", "label": "Pr"} + {"value": "Pr", "label": "Pr"}, ] # Default to English if not another known language @@ -73,18 +71,18 @@ def get(self): {"value": "Hon", "label": "Hon"}, {"value": "Prof", "label": "Prof"}, {"value": "Dr", "label": "Dr"}, - {"value": "Mx", "label": "Mx"} + {"value": "Mx", "label": "Mx"}, ] class GenderContentAPI(restful.Resource): def get(self): req_parser = reqparse.RequestParser() - req_parser.add_argument('language', type=str, required=True) + req_parser.add_argument("language", type=str, required=True) args = req_parser.parse_args() - language = args['language'] + language = args["language"] - if language == 'fr': + if language == "fr": {"value": "male", "label": "Homme"}, {"value": "female", "label": "Femme"}, {"value": "other", "label": "Autre"}, @@ -95,22 +93,26 @@ def get(self): {"value": "male", "label": "Male"}, {"value": "female", "label": "Female"}, {"value": "other", "label": "Other"}, - {"value": "prefer_not_to_say", "label": "Prefer not to say"} + {"value": "prefer_not_to_say", "label": "Prefer not to say"}, ] class DisabilityContentAPI(restful.Resource): # DEPRECATED - TODO: REMOVE def get(self): - return[ + return [ {"label": "No disabilities", "value": "none"}, {"label": "Sight disability", "value": "sight"}, {"label": "Hearing disability", "value": "hearing"}, {"label": "Communication disability", "value": "communication"}, - {"label": "Physical disability(e.g. difficulty in walking)", - "value": "physical"}, - {"label": "Mental disability(e.g. difficulty in remembering or concentrating)", - "value": "mental"}, + { + "label": "Physical disability(e.g. difficulty in walking)", + "value": "physical", + }, + { + "label": "Mental disability(e.g. difficulty in remembering or concentrating)", + "value": "mental", + }, {"label": "Difficulty in self-care", "value": "self-care"}, {"label": "Other", "value": "other"}, ] diff --git a/api/app/email_template/models.py b/api/app/email_template/models.py index 6a32b3fc2..c7a4f7223 100644 --- a/api/app/email_template/models.py +++ b/api/app/email_template/models.py @@ -3,17 +3,23 @@ class EmailTemplate(db.Model): - __tablename__ = 'email_template' - __table_args__ = tuple([db.UniqueConstraint('key', 'event_id', 'language', name='uq_email_template_key_event_id')]) + __tablename__ = "email_template" + __table_args__ = tuple( + [ + db.UniqueConstraint( + "key", "event_id", "language", name="uq_email_template_key_event_id" + ) + ] + ) id = db.Column(db.Integer(), primary_key=True) key = db.Column(db.String(50), nullable=False) - event_id = db.Column(db.Integer(), db.ForeignKey('event.id'), nullable=True) + event_id = db.Column(db.Integer(), db.ForeignKey("event.id"), nullable=True) language = db.Column(db.String(2), nullable=False) template = db.Column(db.String(), nullable=False) subject = db.Column(db.String(), nullable=False) - event = db.relationship('Event', foreign_keys=[event_id]) + event = db.relationship("Event", foreign_keys=[event_id]) def __init__(self, key, event_id, subject, template, language): self.key = key diff --git a/api/app/email_template/repository.py b/api/app/email_template/repository.py index d32ef6eb6..a7ba2ad0f 100644 --- a/api/app/email_template/repository.py +++ b/api/app/email_template/repository.py @@ -1,14 +1,25 @@ from app import db - from app.email_template.models import EmailTemplate -class EmailRepository(): +class EmailRepository: @staticmethod def get(event_id, key, language): - email_template = db.session.query(EmailTemplate).filter_by(event_id=event_id, key=key, language=language).first() + email_template = ( + db.session.query(EmailTemplate) + .filter_by(event_id=event_id, key=key, language=language) + .first() + ) if email_template is None: - email_template = db.session.query(EmailTemplate).filter_by(event_id=None, key=key, language=language).first() + email_template = ( + db.session.query(EmailTemplate) + .filter_by(event_id=None, key=key, language=language) + .first() + ) if email_template is None: - email_template = db.session.query(EmailTemplate).filter_by(event_id=None, key=key, language='en').first() - return email_template \ No newline at end of file + email_template = ( + db.session.query(EmailTemplate) + .filter_by(event_id=None, key=key, language="en") + .first() + ) + return email_template diff --git a/api/app/events/api.py b/api/app/events/api.py index 69ef260d6..ca90a83a1 100644 --- a/api/app/events/api.py +++ b/api/app/events/api.py @@ -1,158 +1,179 @@ -from datetime import datetime -import traceback import itertools -from flask import g, request +import traceback +from datetime import datetime + import flask_restful as restful -from flask_restful import reqparse, fields, marshal_with -from sqlalchemy.exc import SQLAlchemyError, IntegrityError +from flask import g, request +from flask_restful import fields, marshal_with, reqparse +from sqlalchemy.exc import IntegrityError, SQLAlchemyError -from app.email_template.repository import EmailRepository as email_repository -from app.events.models import Event, EventRole -from app.events.mixins import EventsMixin, EventsKeyMixin, EventMixin -from app.users.models import AppUser -from app.users.repository import UserRepository as user_repository +import app.events.status as event_status +from app import LOGGER, bcrypt, db from app.applicationModel.models import ApplicationForm +from app.email_template.repository import EmailRepository as email_repository +from app.events.mixins import EventMixin, EventsKeyMixin, EventsMixin +from app.events.models import Event, EventRole, EventType +from app.events.repository import EventRepository as event_repository +from app.guestRegistrations.repository import ( + GuestRegistrationRepository as guest_registration_repository, +) +from app.organisation.models import Organisation +from app.registration.repository import OfferRepository as offer_repository +from app.registration.repository import ( + RegistrationRepository as registration_repository, +) from app.responses.models import Response from app.responses.repository import ResponseRepository as response_repository -from app.registration.repository import OfferRepository as offer_repository -from app.registration.repository import RegistrationRepository as registration_repository -from app.guestRegistrations.repository import GuestRegistrationRepository as guest_registration_repository - -from app import db, bcrypt, LOGGER +from app.reviews.repository import ( + ReviewConfigurationRepository as review_config_repository, +) +from app.reviews.repository import ReviewRepository as review_repository +from app.users.models import AppUser +from app.users.repository import UserRepository as user_repository +from app.utils.auth import auth_optional, auth_required, event_admin_required +from app.utils.emailer import email_user from app.utils.errors import ( + EVENT_KEY_IN_USE, + EVENT_MUST_CONTAIN_TRANSLATION, EVENT_NOT_FOUND, - FORBIDDEN, + EVENT_TRANSLATION_MISMATCH, EVENT_WITH_KEY_NOT_FOUND, - EVENT_KEY_IN_USE, EVENT_WITH_TRANSLATION_NOT_FOUND, - EVENT_MUST_CONTAIN_TRANSLATION, - EVENT_TRANSLATION_MISMATCH + FORBIDDEN, ) -from app.utils.auth import auth_optional, auth_required, event_admin_required -from app.utils.emailer import email_user -from app.events.repository import EventRepository as event_repository -from app.organisation.models import Organisation -from app.events.models import EventType -import app.events.status as event_status -from app.reviews.repository import ReviewRepository as review_repository -from app.reviews.repository import ReviewConfigurationRepository as review_config_repository def status_info(status): if status is None: return None return { - 'invited_guest': status.invited_guest, - 'application_status': status.application_status, - 'registration_status': status.registration_status, - 'offer_status': status.offer_status, - 'outcome_status': status.outcome_status, - 'is_event_attendee': status.is_event_attendee + "invited_guest": status.invited_guest, + "application_status": status.application_status, + "registration_status": status.registration_status, + "offer_status": status.offer_status, + "outcome_status": status.outcome_status, + "is_event_attendee": status.is_event_attendee, } + def event_info(user_id, event, status, language): return { - 'id': event.id, - 'name': event.get_name(language), - 'description': event.get_description(language), - 'key': event.key, - 'start_date': event.start_date.strftime("%d %B %Y"), - 'end_date': event.end_date.strftime("%d %B %Y"), - 'status': status_info(status), - 'email_from': event.email_from, - 'organisation_name': event.organisation.name, - 'organisation_id': event.organisation.id, - 'url': event.url, - 'event_type': event.event_type.value.upper(), - 'is_application_open': event.is_application_open, - 'is_application_opening': event.is_application_opening, - 'is_review_open': event.is_review_open, - 'is_review_opening': event.is_review_opening, - 'is_selection_open': event.is_selection_open, - 'is_selection_opening': event.is_selection_opening, - 'is_offer_open': event.is_offer_open, - 'is_offer_opening': event.is_offer_opening, - 'is_registration_open': event.is_registration_open, - 'is_registration_opening': event.is_registration_opening, - 'is_event_open': event.is_event_open, - 'is_event_opening': event.is_event_opening, - 'travel_grant': event.travel_grant, - "miniconf_url": event.miniconf_url + "id": event.id, + "name": event.get_name(language), + "description": event.get_description(language), + "key": event.key, + "start_date": event.start_date.strftime("%d %B %Y"), + "end_date": event.end_date.strftime("%d %B %Y"), + "status": status_info(status), + "email_from": event.email_from, + "organisation_name": event.organisation.name, + "organisation_id": event.organisation.id, + "url": event.url, + "event_type": event.event_type.value.upper(), + "is_application_open": event.is_application_open, + "is_application_opening": event.is_application_opening, + "is_review_open": event.is_review_open, + "is_review_opening": event.is_review_opening, + "is_selection_open": event.is_selection_open, + "is_selection_opening": event.is_selection_opening, + "is_offer_open": event.is_offer_open, + "is_offer_opening": event.is_offer_opening, + "is_registration_open": event.is_registration_open, + "is_registration_opening": event.is_registration_opening, + "is_event_open": event.is_event_open, + "is_event_opening": event.is_event_opening, + "travel_grant": event.travel_grant, + "miniconf_url": event.miniconf_url, } event_fields = { - 'id': fields.Integer, - 'name': fields.Raw(attribute=lambda event: event.get_all_name_translations() if isinstance(event, Event) else {}), - 'description': fields.Raw(attribute=lambda event: event.get_all_description_translations() if isinstance(event, Event) else {}), - 'key': fields.String, - 'start_date': fields.DateTime(dt_format='iso8601'), - 'end_date': fields.DateTime(dt_format='iso8601'), - 'email_from': fields.String, - 'organisation_name': fields.String(attribute='organisation.name'), - 'organisation_id': fields.String(attribute='organisation.id'), - 'url': fields.String, - 'application_open': fields.DateTime(dt_format='iso8601'), - 'application_close': fields.DateTime(dt_format='iso8601'), - 'review_open': fields.DateTime(dt_format='iso8601'), - 'review_close': fields.DateTime(dt_format='iso8601'), - 'selection_open': fields.DateTime(dt_format='iso8601'), - 'selection_close': fields.DateTime(dt_format='iso8601'), - 'offer_open': fields.DateTime(dt_format='iso8601'), - 'offer_close': fields.DateTime(dt_format='iso8601'), - 'registration_open': fields.DateTime(dt_format='iso8601'), - 'registration_close': fields.DateTime(dt_format='iso8601'), - 'travel_grant': fields.Boolean, - 'miniconf_url': fields.String + "id": fields.Integer, + "name": fields.Raw( + attribute=lambda event: event.get_all_name_translations() + if isinstance(event, Event) + else {} + ), + "description": fields.Raw( + attribute=lambda event: event.get_all_description_translations() + if isinstance(event, Event) + else {} + ), + "key": fields.String, + "start_date": fields.DateTime(dt_format="iso8601"), + "end_date": fields.DateTime(dt_format="iso8601"), + "email_from": fields.String, + "organisation_name": fields.String(attribute="organisation.name"), + "organisation_id": fields.String(attribute="organisation.id"), + "url": fields.String, + "application_open": fields.DateTime(dt_format="iso8601"), + "application_close": fields.DateTime(dt_format="iso8601"), + "review_open": fields.DateTime(dt_format="iso8601"), + "review_close": fields.DateTime(dt_format="iso8601"), + "selection_open": fields.DateTime(dt_format="iso8601"), + "selection_close": fields.DateTime(dt_format="iso8601"), + "offer_open": fields.DateTime(dt_format="iso8601"), + "offer_close": fields.DateTime(dt_format="iso8601"), + "registration_open": fields.DateTime(dt_format="iso8601"), + "registration_close": fields.DateTime(dt_format="iso8601"), + "travel_grant": fields.Boolean, + "miniconf_url": fields.String, } def get_user_event_response_status(user_id, event_id): - def _log_application_status(context): - LOGGER.debug("Application {} for user_id: {}, event_id: {}".format( - context, user_id, event_id)) + LOGGER.debug( + "Application {} for user_id: {}, event_id: {}".format( + context, user_id, event_id + ) + ) try: - applicationForm = db.session.query(ApplicationForm).filter( - ApplicationForm.event_id == event_id).first() + applicationForm = ( + db.session.query(ApplicationForm) + .filter(ApplicationForm.event_id == event_id) + .first() + ) if applicationForm: if applicationForm.event.application_close < datetime.now(): - _log_application_status('closed, deadline passed') + _log_application_status("closed, deadline passed") return "Application closed" elif user_id: - response = db.session.query(Response).filter( - Response.application_form_id == applicationForm.id).filter( - Response.user_id == user_id - ).order_by(Response.started_timestamp.desc()).first() + response = ( + db.session.query(Response) + .filter(Response.application_form_id == applicationForm.id) + .filter(Response.user_id == user_id) + .order_by(Response.started_timestamp.desc()) + .first() + ) if response: if response.is_withdrawn: - _log_application_status('withdrawn') + _log_application_status("withdrawn") return "Application withdrawn" elif response.is_submitted: - _log_application_status('submitted') + _log_application_status("submitted") return "Applied" else: - _log_application_status('continue') + _log_application_status("continue") return "Continue application" else: - _log_application_status('open') + _log_application_status("open") return "Apply now" else: - _log_application_status('open') + _log_application_status("open") return "Apply now" except SQLAlchemyError as e: LOGGER.error("Database error encountered: {}".format(e)) except: - LOGGER.error("Encountered unknown error: {}".format( - traceback.format_exc())) + LOGGER.error("Encountered unknown error: {}".format(traceback.format_exc())) - _log_application_status('not available') + _log_application_status("not available") return "Application not available" @@ -160,7 +181,7 @@ class EventAPI(EventMixin, restful.Resource): @auth_required @marshal_with(event_fields) def get(self): - event_id = request.args['id'] + event_id = request.args["id"] event = event_repository.get_by_id(event_id) if not event: return EVENT_NOT_FOUND @@ -176,40 +197,40 @@ def post(self): if not current_user.is_admin: return FORBIDDEN - if event_repository.exists_by_key(args['key']): + if event_repository.exists_by_key(args["key"]): return EVENT_KEY_IN_USE - - if len(args['name']) == 0 or len(args['description']) == 0: + + if len(args["name"]) == 0 or len(args["description"]) == 0: return EVENT_MUST_CONTAIN_TRANSLATION - - if set(args['name']) != set(args['description']): + + if set(args["name"]) != set(args["description"]): return EVENT_TRANSLATION_MISMATCH event = Event( - args['name'], - args['description'], - args['start_date'], - args['end_date'], - args['key'], - args['organisation_id'], - args['email_from'], - args['url'], - args['application_open'], - args['application_close'], - args['review_open'], - args['review_close'], - args['selection_open'], - args['selection_close'], - args['offer_open'], - args['offer_close'], - args['registration_open'], - args['registration_close'], - EventType[args['event_type'].upper()], - args['travel_grant'], - args['miniconf_url'] + args["name"], + args["description"], + args["start_date"], + args["end_date"], + args["key"], + args["organisation_id"], + args["email_from"], + args["url"], + args["application_open"], + args["application_close"], + args["review_open"], + args["review_close"], + args["selection_open"], + args["selection_close"], + args["offer_open"], + args["offer_close"], + args["registration_open"], + args["registration_close"], + EventType[args["event_type"].upper()], + args["travel_grant"], + args["miniconf_url"], ) - event.add_event_role('admin', user_id) + event.add_event_role("admin", user_id) event = event_repository.add(event) event = event_repository.get_by_id(event.id) @@ -220,17 +241,17 @@ def post(self): def put(self): args = self.req_parser.parse_args() - if len(args['name']) == 0 or len(args['description']) == 0: + if len(args["name"]) == 0 or len(args["description"]) == 0: return EVENT_MUST_CONTAIN_TRANSLATION - - if set(args['name']) != set(args['description']): + + if set(args["name"]) != set(args["description"]): return EVENT_TRANSLATION_MISMATCH - event = event_repository.get_by_id(args['id']) + event = event_repository.get_by_id(args["id"]) if not event: return EVENT_NOT_FOUND - if event_repository.exists_by_key(args['key']) and args['key'] != event.key: + if event_repository.exists_by_key(args["key"]) and args["key"] != event.key: return EVENT_KEY_IN_USE user_id = g.current_user["id"] @@ -239,26 +260,26 @@ def put(self): return FORBIDDEN event.update( - args['name'], - args['description'], - args['start_date'], - args['end_date'], - args['key'], - args['organisation_id'], - args['email_from'], - args['url'], - args['application_open'], - args['application_close'], - args['review_open'], - args['review_close'], - args['selection_open'], - args['selection_close'], - args['offer_open'], - args['offer_close'], - args['registration_open'], - args['registration_close'], - args['travel_grant'], - args['miniconf_url'] + args["name"], + args["description"], + args["start_date"], + args["end_date"], + args["key"], + args["organisation_id"], + args["email_from"], + args["url"], + args["application_open"], + args["application_close"], + args["review_open"], + args["review_close"], + args["selection_open"], + args["selection_close"], + args["offer_open"], + args["offer_close"], + args["registration_open"], + args["registration_close"], + args["travel_grant"], + args["miniconf_url"], ) db.session.commit() @@ -267,36 +288,43 @@ def put(self): class EventsAPI(restful.Resource): - @auth_required def get(self): user_id = g.current_user["id"] - language = request.args['language'] - default_language = 'en' + language = request.args["language"] + default_language = "en" - upcoming_events = event_repository.get_upcoming_for_organisation(g.organisation.id) - attended_events = event_repository.get_attended_by_user_for_organisation(g.organisation.id, user_id) + upcoming_events = event_repository.get_upcoming_for_organisation( + g.organisation.id + ) + attended_events = event_repository.get_attended_by_user_for_organisation( + g.organisation.id, user_id + ) returnEvents = [] for event in itertools.chain(upcoming_events, attended_events): if not event.has_specific_translation(language): - LOGGER.error('Missing {} translation for event {}.'.format(language, event.id)) + LOGGER.error( + "Missing {} translation for event {}.".format(language, event.id) + ) language = default_language - status = None if user_id == 0 else event_status.get_event_status(event.id, user_id) + status = ( + None + if user_id == 0 + else event_status.get_event_status(event.id, user_id) + ) returnEvents.append(event_info(user_id, event, status, language)) - language = request.args['language'] + language = request.args["language"] return returnEvents, 200 def _process_timeseries(timeseries): - timeseries = [ - (d.strftime('%Y-%m-%d'), c) for (d, c) in timeseries - if d is not None - ] + timeseries = [(d.strftime("%Y-%m-%d"), c) for (d, c) in timeseries if d is not None] return timeseries + def _combine_timeseries(first_timeseries, second_timeseries): output = [] i, j = 0, 0 @@ -325,7 +353,6 @@ def _combine_timeseries(first_timeseries, second_timeseries): class EventStatsAPI(EventsMixin, restful.Resource): - @event_admin_required def get(self, event_id): event = event_repository.get_by_id(event_id) @@ -333,84 +360,111 @@ def get(self, event_id): return EVENT_NOT_FOUND num_responses = response_repository.get_total_count_by_event(event_id) - num_submitted_respones = response_repository.get_submitted_count_by_event(event_id) - num_withdrawn_responses = response_repository.get_withdrawn_count_by_event(event_id) - submitted_response_timeseries = _process_timeseries(response_repository.get_submitted_timeseries_by_event(event_id)) + num_submitted_respones = response_repository.get_submitted_count_by_event( + event_id + ) + num_withdrawn_responses = response_repository.get_withdrawn_count_by_event( + event_id + ) + submitted_response_timeseries = _process_timeseries( + response_repository.get_submitted_timeseries_by_event(event_id) + ) review_config = review_config_repository.get_configuration_for_event(event_id) - required_reviews = 1 if review_config is None else review_config.num_reviews_required - reviews_completed = review_repository.get_count_reviews_completed_for_event(event_id) - reviews_incomplete = review_repository.get_count_reviews_incomplete_for_event(event_id) - reviews_unallocated = review_repository.count_unassigned_reviews(event_id, required_reviews) - reviews_timeseries = _process_timeseries(review_repository.get_review_complete_timeseries_by_event(event_id)) + required_reviews = ( + 1 if review_config is None else review_config.num_reviews_required + ) + reviews_completed = review_repository.get_count_reviews_completed_for_event( + event_id + ) + reviews_incomplete = review_repository.get_count_reviews_incomplete_for_event( + event_id + ) + reviews_unallocated = review_repository.count_unassigned_reviews( + event_id, required_reviews + ) + reviews_timeseries = _process_timeseries( + review_repository.get_review_complete_timeseries_by_event(event_id) + ) offers_allocated = offer_repository.count_offers_allocated(event_id) offers_accepted = offer_repository.count_offers_accepted(event_id) offers_rejected = offer_repository.count_offers_rejected(event_id) - offers_timeseries = _process_timeseries(offer_repository.timeseries_offers_accepted(event_id)) + offers_timeseries = _process_timeseries( + offer_repository.timeseries_offers_accepted(event_id) + ) num_registrations = registration_repository.count_registrations(event_id) num_guests = guest_registration_repository.count_guests(event_id) - num_registered_guests = guest_registration_repository.count_registered_guests(event_id) - registration_timeseries = _process_timeseries(registration_repository.timeseries_registrations(event_id)) - guest_registration_timeseries = _process_timeseries(guest_registration_repository.timeseries_guest_registrations(event_id)) - registration_timeseries = _combine_timeseries(registration_timeseries, guest_registration_timeseries) + num_registered_guests = guest_registration_repository.count_registered_guests( + event_id + ) + registration_timeseries = _process_timeseries( + registration_repository.timeseries_registrations(event_id) + ) + guest_registration_timeseries = _process_timeseries( + guest_registration_repository.timeseries_guest_registrations(event_id) + ) + registration_timeseries = _combine_timeseries( + registration_timeseries, guest_registration_timeseries + ) return { - 'num_responses': num_responses, - 'num_submitted_responses': num_submitted_respones, - 'num_withdrawn_responses': num_withdrawn_responses, - 'submitted_timeseries': submitted_response_timeseries, - 'reviews_completed': reviews_completed, - 'review_incomplete': reviews_incomplete, - 'reviews_unallocated': reviews_unallocated, - 'reviews_complete_timeseries': reviews_timeseries, - 'offers_allocated': offers_allocated, - 'offers_accepted': offers_accepted, - 'offers_rejected': offers_rejected, - 'offers_accepted_timeseries': offers_timeseries, - 'num_registrations': num_registrations, - 'num_guests': num_guests, - 'num_registered_guests': num_registered_guests, - 'registration_timeseries': registration_timeseries + "num_responses": num_responses, + "num_submitted_responses": num_submitted_respones, + "num_withdrawn_responses": num_withdrawn_responses, + "submitted_timeseries": submitted_response_timeseries, + "reviews_completed": reviews_completed, + "review_incomplete": reviews_incomplete, + "reviews_unallocated": reviews_unallocated, + "reviews_complete_timeseries": reviews_timeseries, + "offers_allocated": offers_allocated, + "offers_accepted": offers_accepted, + "offers_rejected": offers_rejected, + "offers_accepted_timeseries": offers_timeseries, + "num_registrations": num_registrations, + "num_guests": num_guests, + "num_registered_guests": num_registered_guests, + "registration_timeseries": registration_timeseries, }, 200 class EventsByKeyAPI(EventsKeyMixin, restful.Resource): - @auth_required def get(self): args = self.req_parser.parse_args() - user_id = g.current_user['id'] - language = args['language'] + user_id = g.current_user["id"] + language = args["language"] if language is None or len(language) > 2: - LOGGER.warning("Missing or invalid language parameter for EventsByKeyAPI. Defaulting to 'en'") - default_language = 'en' + LOGGER.warning( + "Missing or invalid language parameter for EventsByKeyAPI. Defaulting to 'en'" + ) + default_language = "en" language = default_language - event = event_repository.get_by_key(args['event_key']) + event = event_repository.get_by_key(args["event_key"]) if not event: return EVENT_WITH_KEY_NOT_FOUND - + if not event.has_specific_translation(language): return EVENT_WITH_TRANSLATION_NOT_FOUND - + info = event_info( - g.current_user['id'], - event, + g.current_user["id"], + event, event_status.get_event_status(event.id, user_id), - language) + language, + ) return info, 200 class NotSubmittedReminderAPI(EventsMixin, restful.Resource): - @auth_required def post(self): args = self.req_parser.parse_args() - event_id = args['event_id'] - user_id = g.current_user['id'] + event_id = args["event_id"] + user_id = g.current_user["id"] event = db.session.query(Event).get(event_id) if not event: @@ -423,26 +477,26 @@ def post(self): users = user_repository.get_all_with_unsubmitted_response() for user in users: organisation_name = event.organisation.name - deadline = event.application_close.strftime('%A %-d %B %Y') + deadline = event.application_close.strftime("%A %-d %B %Y") email_user( - 'application-not-submitted', + "application-not-submitted", template_parameters=dict( - organisation_name=organisation_name, - deadline=deadline), + organisation_name=organisation_name, deadline=deadline + ), event=event, - user=user) + user=user, + ) - return {'unsubmitted_responses': len(users)}, 201 + return {"unsubmitted_responses": len(users)}, 201 class NotStartedReminderAPI(EventsMixin, restful.Resource): - @auth_required def post(self): args = self.req_parser.parse_args() - event_id = args['event_id'] - user_id = g.current_user['id'] + event_id = args["event_id"] + user_id = g.current_user["id"] event = db.session.query(Event).get(event_id) if not event: @@ -454,21 +508,21 @@ def post(self): users = user_repository.get_all_without_responses() for user in users: - event_name = event.get_name('en') + event_name = event.get_name("en") organisation_name = event.organisation.name system_name = event.organisation.system_name - deadline = event.application_close.strftime('%A %-d %B %Y') + deadline = event.application_close.strftime("%A %-d %B %Y") email_user( - 'application-not-started', + "application-not-started", template_parameters=dict( event=event_name, organisation_name=organisation_name, system_name=system_name, - deadline=deadline + deadline=deadline, ), event=event, - user=user + user=user, ) - return {'not_started_responses': len(users)}, 201 + return {"not_started_responses": len(users)}, 201 diff --git a/api/app/events/mixins.py b/api/app/events/mixins.py index 260ec1cb6..0a7eee90d 100644 --- a/api/app/events/mixins.py +++ b/api/app/events/mixins.py @@ -4,39 +4,39 @@ class EventMixin(object): - dt_format = lambda x: datetime.strptime(x, '%Y-%m-%dT%H:%M:%SZ') + dt_format = lambda x: datetime.strptime(x, "%Y-%m-%dT%H:%M:%SZ") req_parser = reqparse.RequestParser() - req_parser.add_argument('id', type=int, required=False) - req_parser.add_argument('name', type=dict, required=True, location='json') - req_parser.add_argument('description', type=dict, required=True, location='json') - req_parser.add_argument('start_date', type=dt_format, required=True) - req_parser.add_argument('end_date', type=dt_format, required=True) - req_parser.add_argument('key', type=str, required=True) - req_parser.add_argument('organisation_id', type=int, required=True) - req_parser.add_argument('email_from', type=str, required=True) - req_parser.add_argument('url', type=str, required=True) - req_parser.add_argument('application_open', type=dt_format, required=True) - req_parser.add_argument('application_close', type=dt_format, required=True) - req_parser.add_argument('review_open', type=dt_format, required=True) - req_parser.add_argument('review_close', type=dt_format, required=True) - req_parser.add_argument('selection_open', type=dt_format, required=True) - req_parser.add_argument('selection_close', type=dt_format, required=True) - req_parser.add_argument('offer_open', type=dt_format, required=True) - req_parser.add_argument('offer_close', type=dt_format, required=True) - req_parser.add_argument('registration_open', type=dt_format, required=True) - req_parser.add_argument('registration_close', type=dt_format, required=True) - req_parser.add_argument('event_type', type=str, required=True) - req_parser.add_argument('travel_grant', type=bool, required=True) - req_parser.add_argument('miniconf_url', type=str, required=False) + req_parser.add_argument("id", type=int, required=False) + req_parser.add_argument("name", type=dict, required=True, location="json") + req_parser.add_argument("description", type=dict, required=True, location="json") + req_parser.add_argument("start_date", type=dt_format, required=True) + req_parser.add_argument("end_date", type=dt_format, required=True) + req_parser.add_argument("key", type=str, required=True) + req_parser.add_argument("organisation_id", type=int, required=True) + req_parser.add_argument("email_from", type=str, required=True) + req_parser.add_argument("url", type=str, required=True) + req_parser.add_argument("application_open", type=dt_format, required=True) + req_parser.add_argument("application_close", type=dt_format, required=True) + req_parser.add_argument("review_open", type=dt_format, required=True) + req_parser.add_argument("review_close", type=dt_format, required=True) + req_parser.add_argument("selection_open", type=dt_format, required=True) + req_parser.add_argument("selection_close", type=dt_format, required=True) + req_parser.add_argument("offer_open", type=dt_format, required=True) + req_parser.add_argument("offer_close", type=dt_format, required=True) + req_parser.add_argument("registration_open", type=dt_format, required=True) + req_parser.add_argument("registration_close", type=dt_format, required=True) + req_parser.add_argument("event_type", type=str, required=True) + req_parser.add_argument("travel_grant", type=bool, required=True) + req_parser.add_argument("miniconf_url", type=str, required=False) class EventsMixin(object): req_parser = reqparse.RequestParser() - req_parser.add_argument('event_id', type=int, required=True) + req_parser.add_argument("event_id", type=int, required=True) class EventsKeyMixin(object): req_parser = reqparse.RequestParser() - req_parser.add_argument('event_key', type=str, required=True) - req_parser.add_argument('language', type=str, required=False) + req_parser.add_argument("event_key", type=str, required=True) + req_parser.add_argument("language", type=str, required=False) diff --git a/api/app/events/models.py b/api/app/events/models.py index df5178d1b..33b30d303 100644 --- a/api/app/events/models.py +++ b/api/app/events/models.py @@ -1,13 +1,13 @@ - from datetime import datetime +from enum import Enum from app import db -from enum import Enum + class EventType(Enum): - EVENT = 'event' - AWARD = 'award' - CALL = 'call' + EVENT = "event" + AWARD = "award" + CALL = "call" class Event(db.Model): @@ -18,7 +18,9 @@ class Event(db.Model): start_date = db.Column(db.DateTime(), nullable=False) end_date = db.Column(db.DateTime(), nullable=False) key = db.Column(db.String(255), nullable=False, unique=True) - organisation_id = db.Column(db.Integer(), db.ForeignKey('organisation.id'), nullable=False) + organisation_id = db.Column( + db.Integer(), db.ForeignKey("organisation.id"), nullable=False + ) email_from = db.Column(db.String(255), nullable=False) url = db.Column(db.String(255), nullable=False) application_open = db.Column(db.DateTime(), nullable=False) @@ -35,11 +37,11 @@ class Event(db.Model): travel_grant = db.Column(db.Boolean(), nullable=False) miniconf_url = db.Column(db.String(100), nullable=True) - organisation = db.relationship('Organisation', foreign_keys=[organisation_id]) - application_forms = db.relationship('ApplicationForm') - email_templates = db.relationship('EmailTemplate') - event_roles = db.relationship('EventRole') - event_translations = db.relationship('EventTranslation', lazy='dynamic') + organisation = db.relationship("Organisation", foreign_keys=[organisation_id]) + application_forms = db.relationship("ApplicationForm") + email_templates = db.relationship("EmailTemplate") + event_roles = db.relationship("EventRole") + event_translations = db.relationship("EventTranslation", lazy="dynamic") def __init__( self, @@ -63,7 +65,7 @@ def __init__( registration_close, event_type, travel_grant, - miniconf_url=None + miniconf_url=None, ): self.start_date = start_date self.end_date = end_date @@ -148,7 +150,7 @@ def get_description(self, language): if event_translation is not None: return event_translation.description return None - + def get_all_name_translations(self): name_translation_map = {} for event_translation in self.event_translations.all(): @@ -158,7 +160,9 @@ def get_all_name_translations(self): def get_all_description_translations(self): description_translation_map = {} for event_translation in self.event_translations.all(): - description_translation_map[event_translation.language] = event_translation.description + description_translation_map[ + event_translation.language + ] = event_translation.description return description_translation_map def add_event_translations(self, names, descriptions): @@ -171,27 +175,29 @@ def add_event_translations(self, names, descriptions): def has_specific_translation(self, language): return self.event_translations.filter_by(language=language).count() == 1 - def update(self, - names, - descriptions, - start_date, - end_date, - key, - organisation_id, - email_from, - url, - application_open, - application_close, - review_open, - review_close, - selection_open, - selection_close, - offer_open, - offer_close, - registration_open, - registration_close, - travel_grant, - miniconf_url=None): + def update( + self, + names, + descriptions, + start_date, + end_date, + key, + organisation_id, + email_from, + url, + application_open, + application_close, + review_open, + review_close, + selection_open, + selection_close, + offer_open, + offer_close, + registration_open, + registration_close, + travel_grant, + miniconf_url=None, + ): self.start_date = start_date self.end_date = end_date self.key = key @@ -278,7 +284,9 @@ def is_event_opening(self): class EventTranslation(db.Model): __tablename__ = "event_translation" - __table_args__ = tuple([db.UniqueConstraint('event_id', 'language', name='uq_event_id_language')]) + __table_args__ = tuple( + [db.UniqueConstraint("event_id", "language", name="uq_event_id_language")] + ) id = db.Column(db.Integer(), primary_key=True) event_id = db.Column(db.Integer(), db.ForeignKey("event.id"), nullable=False) @@ -286,26 +294,25 @@ class EventTranslation(db.Model): description = db.Column(db.String(500), nullable=False) language = db.Column(db.String(2)) - event = db.relationship('Event', foreign_keys=[event_id]) + event = db.relationship("Event", foreign_keys=[event_id]) def __init__(self, name, description, language): self.name = name self.description = description self.language = language + class EventRole(db.Model): __tablename__ = "event_role" id = db.Column(db.Integer(), primary_key=True) - event_id = db.Column(db.Integer(), db.ForeignKey( - "event.id"), nullable=False) - user_id = db.Column(db.Integer(), db.ForeignKey( - "app_user.id"), nullable=False) + event_id = db.Column(db.Integer(), db.ForeignKey("event.id"), nullable=False) + user_id = db.Column(db.Integer(), db.ForeignKey("app_user.id"), nullable=False) role = db.Column(db.String(50), nullable=False) - user = db.relationship('AppUser', foreign_keys=[user_id]) - event = db.relationship('Event', foreign_keys=[event_id]) + user = db.relationship("AppUser", foreign_keys=[user_id]) + event = db.relationship("Event", foreign_keys=[event_id]) def __init__(self, role, user_id, event_id): self.role = role diff --git a/api/app/events/repository.py b/api/app/events/repository.py index b592849bd..9aec6ed84 100644 --- a/api/app/events/repository.py +++ b/api/app/events/repository.py @@ -1,57 +1,74 @@ from datetime import datetime + +from sqlalchemy import and_, or_ + from app import db +from app.applicationModel.models import ApplicationForm from app.events.models import Event +from app.invitedGuest.models import InvitedGuest from app.organisation.models import Organisation -from app.responses.models import Response -from app.applicationModel.models import ApplicationForm from app.registration.models import Offer -from app.invitedGuest.models import InvitedGuest -from sqlalchemy import and_, or_ - +from app.responses.models import Response -class EventRepository(): +class EventRepository: @staticmethod def get_by_id(event_id): return db.session.query(Event).get(event_id) @staticmethod def exists_by_key(event_key): - return db.session.query(Event.id)\ - .filter_by(key=event_key)\ - .first() is not None + return db.session.query(Event.id).filter_by(key=event_key).first() is not None @staticmethod def get_by_key(event_key): - return db.session.query(Event)\ - .filter_by(key=event_key)\ - .first() + return db.session.query(Event).filter_by(key=event_key).first() @staticmethod def get_event_by_response_id(response_id): - result = db.session.query(Response.application_form_id, ApplicationForm.event_id, Event)\ - .filter_by(id=response_id)\ - .join(ApplicationForm, ApplicationForm.id == Response.application_form_id)\ - .join(Event, Event.id == ApplicationForm.event_id)\ - .first() + result = ( + db.session.query( + Response.application_form_id, ApplicationForm.event_id, Event + ) + .filter_by(id=response_id) + .join(ApplicationForm, ApplicationForm.id == Response.application_form_id) + .join(Event, Event.id == ApplicationForm.event_id) + .first() + ) return result.Event if result else None @staticmethod def get_upcoming_for_organisation(organisation_id): - return db.session.query(Event)\ - .filter(Event.end_date >= datetime.now())\ - .filter_by(organisation_id=organisation_id)\ - .all() + return ( + db.session.query(Event) + .filter(Event.end_date >= datetime.now()) + .filter_by(organisation_id=organisation_id) + .all() + ) @staticmethod def get_attended_by_user_for_organisation(organisation_id, user_id): - return db.session.query(Event)\ - .filter(Event.end_date < datetime.now())\ - .filter_by(organisation_id=organisation_id)\ - .outerjoin(Offer, and_(Event.id == Offer.event_id, Offer.user_id == user_id, Offer.candidate_response == True))\ - .outerjoin(InvitedGuest, and_(Event.id == InvitedGuest.event_id, InvitedGuest.user_id == user_id))\ - .filter(or_(Offer.id != None, InvitedGuest.id != None))\ - .all() + return ( + db.session.query(Event) + .filter(Event.end_date < datetime.now()) + .filter_by(organisation_id=organisation_id) + .outerjoin( + Offer, + and_( + Event.id == Offer.event_id, + Offer.user_id == user_id, + Offer.candidate_response == True, + ), + ) + .outerjoin( + InvitedGuest, + and_( + Event.id == InvitedGuest.event_id, InvitedGuest.user_id == user_id + ), + ) + .filter(or_(Offer.id != None, InvitedGuest.id != None)) + .all() + ) @staticmethod def add(event): diff --git a/api/app/events/status.py b/api/app/events/status.py index 8519b7d83..345b0d985 100644 --- a/api/app/events/status.py +++ b/api/app/events/status.py @@ -1,31 +1,48 @@ from app.events.models import Event -from app.users.models import AppUser -from app.invitedGuest.repository import InvitedGuestRepository as invited_guest_repository -from app.responses.repository import ResponseRepository as response_repository -from app.registrationResponse.repository import RegistrationRepository as registration_repository -from app.registration.repository import OfferRepository as offer_repository +from app.invitedGuest.repository import ( + InvitedGuestRepository as invited_guest_repository, +) from app.outcome.repository import OutcomeRepository as outcome_repository +from app.registration.repository import OfferRepository as offer_repository +from app.registrationResponse.repository import ( + RegistrationRepository as registration_repository, +) +from app.responses.repository import ResponseRepository as response_repository +from app.users.models import AppUser -class EventStatus(): +class EventStatus: """Represents the status of a user at an event.""" - def __init__(self, - invited_guest=None, - application_status=None, - outcome_status=None, - offer_status=None, - registration_status=None): - if invited_guest is None and registration_status is not None and offer_status is None: - raise ValueError('Non-invited guest must have an offer to have a registration status') - - if invited_guest is None and outcome_status is not None and application_status is None: - raise ValueError('User must have applied to have an outcome') - self.invited_guest=invited_guest + def __init__( + self, + invited_guest=None, + application_status=None, + outcome_status=None, + offer_status=None, + registration_status=None, + ): + if ( + invited_guest is None + and registration_status is not None + and offer_status is None + ): + raise ValueError( + "Non-invited guest must have an offer to have a registration status" + ) + + if ( + invited_guest is None + and outcome_status is not None + and application_status is None + ): + raise ValueError("User must have applied to have an outcome") + + self.invited_guest = invited_guest self.application_status = application_status - self.registration_status=registration_status - self.offer_status=offer_status - self.outcome_status=outcome_status + self.registration_status = registration_status + self.offer_status = offer_status + self.outcome_status = outcome_status @property def is_event_attendee(self): @@ -36,28 +53,32 @@ def _get_registration_status(registration): if registration is None: return None if registration.confirmed: - return 'Confirmed' + return "Confirmed" else: - return 'Not Confirmed' + return "Not Confirmed" def get_event_status(event_id, user_id): invited_guest = invited_guest_repository.get_for_event_and_user(event_id, user_id) if invited_guest: - registration = invited_guest_repository.get_registration_for_event_and_user(event_id, user_id) + registration = invited_guest_repository.get_registration_for_event_and_user( + event_id, user_id + ) # If they're an invited guest, we don't bother with whether they applied or not - return EventStatus(invited_guest=invited_guest.role, - registration_status=_get_registration_status(registration)) - + return EventStatus( + invited_guest=invited_guest.role, + registration_status=_get_registration_status(registration), + ) + response = response_repository.get_by_user_id_for_event(user_id, event_id) if response is None: application_status = None elif response.is_submitted: - application_status = 'Submitted' + application_status = "Submitted" elif response.is_withdrawn: - application_status = 'Withdrawn' + application_status = "Withdrawn" else: - application_status = 'Not Submitted' + application_status = "Not Submitted" outcome = outcome_repository.get_latest_by_user_for_event(user_id, event_id) if outcome is None: @@ -69,18 +90,20 @@ def get_event_status(event_id, user_id): if offer is None: offer_status = None elif offer.candidate_response: - offer_status = 'Accepted' + offer_status = "Accepted" elif offer.candidate_response == False: - offer_status = 'Rejected' + offer_status = "Rejected" elif offer.is_expired(): - offer_status = 'Expired' + offer_status = "Expired" else: - offer_status = 'Pending' + offer_status = "Pending" registration = registration_repository.get_by_user_id(user_id, event_id) registration_status = _get_registration_status(registration) - return EventStatus(application_status=application_status, - outcome_status=outcome_status, - offer_status=offer_status, - registration_status=registration_status) + return EventStatus( + application_status=application_status, + outcome_status=outcome_status, + offer_status=offer_status, + registration_status=registration_status, + ) diff --git a/api/app/events/tests.py b/api/app/events/tests.py index d092cbf7a..866298de6 100644 --- a/api/app/events/tests.py +++ b/api/app/events/tests.py @@ -1,34 +1,33 @@ import json +from datetime import date, datetime, timedelta -from datetime import datetime, date, timedelta -from app import app, db, LOGGER -from app.events.models import Event -from app.utils.testing import ApiTestCase -from app.responses.models import Response, Answer +import app.events.status as event_status +from app import LOGGER, app, db +from app.applicationModel.models import ApplicationForm, Question, Section from app.email_template.models import EmailTemplate -from app.events.models import Event, EventRole -from app.users.models import AppUser, Country, UserCategory -from app.applicationModel.models import ApplicationForm, Section, Question -from app.utils.errors import FORBIDDEN +from app.events.models import Event, EventRole, EventType +from app.invitedGuest.models import GuestRegistration, InvitedGuest from app.organisation.models import Organisation -from app.events.models import EventType from app.outcome.models import Outcome from app.outcome.models import Status as OutcomeStatus -from app.invitedGuest.models import InvitedGuest, GuestRegistration -import app.events.status as event_status -from app.registration.models import RegistrationForm, Offer, Registration +from app.registration.models import Offer, Registration, RegistrationForm +from app.responses.models import Answer, Response +from app.users.models import AppUser, Country, UserCategory +from app.utils.errors import FORBIDDEN +from app.utils.testing import ApiTestCase -class EventsAPITest(ApiTestCase): +class EventsAPITest(ApiTestCase): def seed_static_data(self): - self.test_user = self.add_user('something@email.com') - self.add_organisation('Deep Learning Indaba', - 'blah.png', 'blah_big.png', 'deeplearningindaba') - test_country = Country('Indaba Land') + self.test_user = self.add_user("something@email.com") + self.add_organisation( + "Deep Learning Indaba", "blah.png", "blah_big.png", "deeplearningindaba" + ) + test_country = Country("Indaba Land") db.session.add(test_country) db.session.commit() - test_category = UserCategory('Category1') + test_category = UserCategory("Category1") db.session.add(test_category) db.session.commit() @@ -36,14 +35,19 @@ def seed_static_data(self): db.session.add(self.test_user) db.session.commit() - test_event = self.add_event({'en': 'Test Event'}, {'en': 'Event Description'}, - datetime.now() + timedelta(days=30), datetime.now() + timedelta(days=60), - 'SPEEDNET', 1, 'abx@indaba.deeplearning') + test_event = self.add_event( + {"en": "Test Event"}, + {"en": "Event Description"}, + datetime.now() + timedelta(days=30), + datetime.now() + timedelta(days=60), + "SPEEDNET", + 1, + "abx@indaba.deeplearning", + ) db.session.add(test_event) db.session.commit() - self.test_form = self.create_application_form( - test_event.id, True, False) + self.test_form = self.create_application_form(test_event.id, True, False) db.session.add(self.test_form) db.session.commit() @@ -52,219 +56,243 @@ def test_get_events_applied(self): self.add_response(self.test_form.id, self.test_user.id, is_submitted=True) - response = self.app.post('/api/v1/authenticate', data={ - 'email': 'something@email.com', - 'password': 'abc' - }) + response = self.app.post( + "/api/v1/authenticate", + data={"email": "something@email.com", "password": "abc"}, + ) self.assertEqual(response.status_code, 200) data = json.loads(response.data) response = self.app.get( - '/api/v1/events', - headers={'Authorization': data["token"]}, - query_string={'language': 'en'} + "/api/v1/events", + headers={"Authorization": data["token"]}, + query_string={"language": "en"}, ) data = json.loads(response.data) self.assertEqual(len(data), 1) self.assertEqual(data[0]["id"], 1) - self.assertEqual(data[0]["description"], 'Event Description') - self.assertEqual(data[0]["status"]['application_status'], 'Submitted') + self.assertEqual(data[0]["description"], "Event Description") + self.assertEqual(data[0]["status"]["application_status"], "Submitted") def test_get_events_withdrawn(self): self.seed_static_data() self.add_response(self.test_form.id, self.test_user.id, is_withdrawn=True) - response = self.app.post('/api/v1/authenticate', data={ - 'email': 'something@email.com', - 'password': 'abc' - }) + response = self.app.post( + "/api/v1/authenticate", + data={"email": "something@email.com", "password": "abc"}, + ) self.assertEqual(response.status_code, 200) data = json.loads(response.data) response = self.app.get( - '/api/v1/events', - headers={'Authorization': data["token"]}, - query_string={'language': 'en'} + "/api/v1/events", + headers={"Authorization": data["token"]}, + query_string={"language": "en"}, ) data = json.loads(response.data) self.assertEqual(len(data), 1) self.assertEqual(data[0]["id"], 1) - self.assertEqual(data[0]["description"], 'Event Description') - self.assertEqual(data[0]["status"]['application_status'], 'Withdrawn') + self.assertEqual(data[0]["description"], "Event Description") + self.assertEqual(data[0]["status"]["application_status"], "Withdrawn") def test_unsubmitted_response(self): self.seed_static_data() self.add_response(self.test_form.id, self.test_user.id) - response = self.app.post('/api/v1/authenticate', data={ - 'email': 'something@email.com', - 'password': 'abc' - }) + response = self.app.post( + "/api/v1/authenticate", + data={"email": "something@email.com", "password": "abc"}, + ) self.assertEqual(response.status_code, 200) data = json.loads(response.data) response = self.app.get( - '/api/v1/events', - headers={'Authorization': data["token"]}, - query_string={'language': 'en'}) + "/api/v1/events", + headers={"Authorization": data["token"]}, + query_string={"language": "en"}, + ) data = json.loads(response.data) self.assertEqual(len(data), 1) self.assertEqual(data[0]["id"], 1) - self.assertEqual(data[0]["description"], 'Event Description') - self.assertEqual(data[0]["status"]['application_status'], 'Not Submitted') + self.assertEqual(data[0]["description"], "Event Description") + self.assertEqual(data[0]["status"]["application_status"], "Not Submitted") def test_get_event_type(self): self.seed_static_data() db.session.delete(self.test_form) db.session.commit() - response = self.app.get('/api/v1/events') + response = self.app.get("/api/v1/events") self.assertEqual(response.status_code, 401) # Unauthorized def test_past_event_offer_accepted(self): """API should return past events that user had an accepted offer for.""" self.seed_static_data() - past_event = self.add_event(start_date=datetime.now() - timedelta(days=30), - end_date=datetime.now() - timedelta(days=30), - key='PAST12') + past_event = self.add_event( + start_date=datetime.now() - timedelta(days=30), + end_date=datetime.now() - timedelta(days=30), + key="PAST12", + ) self.add_offer(self.test_user.id, past_event.id, candidate_response=True) - + response = self.app.get( - '/api/v1/events', - headers=self.get_auth_header_for('something@email.com'), - query_string={'language': 'en'}) + "/api/v1/events", + headers=self.get_auth_header_for("something@email.com"), + query_string={"language": "en"}, + ) data = json.loads(response.data) self.assertEqual(len(data), 2) - self.assertEqual(data[0]["event_type"], 'EVENT') - self.assertTrue(data[1]["status"]['is_event_attendee']) - + self.assertEqual(data[0]["event_type"], "EVENT") + self.assertTrue(data[1]["status"]["is_event_attendee"]) + def test_past_event_offer_rejected(self): """API should not return past events where the user rejected an offer.""" self.seed_static_data() - past_event = self.add_event(start_date=datetime.now() - timedelta(days=30), - end_date=datetime.now() - timedelta(days=30), - key='PAST12') + past_event = self.add_event( + start_date=datetime.now() - timedelta(days=30), + end_date=datetime.now() - timedelta(days=30), + key="PAST12", + ) self.add_offer(self.test_user.id, past_event.id, candidate_response=False) - + response = self.app.get( - '/api/v1/events', - headers=self.get_auth_header_for('something@email.com'), - query_string={'language': 'en'}) + "/api/v1/events", + headers=self.get_auth_header_for("something@email.com"), + query_string={"language": "en"}, + ) data = json.loads(response.data) self.assertEqual(len(data), 1) - self.assertEqual(data[0]["event_type"], 'EVENT') + self.assertEqual(data[0]["event_type"], "EVENT") def test_past_event_guest(self): """API should return past events where user was an invited guest.""" self.seed_static_data() - past_event = self.add_event(start_date=datetime.now() - timedelta(days=30), - end_date=datetime.now() - timedelta(days=30), - key='PAST12') + past_event = self.add_event( + start_date=datetime.now() - timedelta(days=30), + end_date=datetime.now() - timedelta(days=30), + key="PAST12", + ) self.add_invited_guest(self.test_user.id, past_event.id) - + response = self.app.get( - '/api/v1/events', - headers=self.get_auth_header_for('something@email.com'), - query_string={'language': 'en'}) + "/api/v1/events", + headers=self.get_auth_header_for("something@email.com"), + query_string={"language": "en"}, + ) data = json.loads(response.data) self.assertEqual(len(data), 2) - self.assertEqual(data[0]["event_type"], 'EVENT') - self.assertTrue(data[1]["status"]['is_event_attendee']) + self.assertEqual(data[0]["event_type"], "EVENT") + self.assertTrue(data[1]["status"]["is_event_attendee"]) def test_past_event_non_attendee(self): """API should not return past events where the user was not an attendee.""" self.seed_static_data() - past_event1 = self.add_event(start_date=datetime.now() - timedelta(days=30), - end_date=datetime.now() - timedelta(days=30), - key='PAST1') + past_event1 = self.add_event( + start_date=datetime.now() - timedelta(days=30), + end_date=datetime.now() - timedelta(days=30), + key="PAST1", + ) - past_event2 = self.add_event(start_date=datetime.now() - timedelta(days=30), - end_date=datetime.now() - timedelta(days=30), - key='PAST2') + past_event2 = self.add_event( + start_date=datetime.now() - timedelta(days=30), + end_date=datetime.now() - timedelta(days=30), + key="PAST2", + ) # Add a different user who attended these events - other_user = self.add_user(email='other@user.com') + other_user = self.add_user(email="other@user.com") self.add_invited_guest(other_user.id, event_id=past_event1.id) self.add_offer(other_user.id, event_id=past_event2.id, candidate_response=True) - + response = self.app.get( - '/api/v1/events', - headers=self.get_auth_header_for('something@email.com'), - query_string={'language': 'en'}) + "/api/v1/events", + headers=self.get_auth_header_for("something@email.com"), + query_string={"language": "en"}, + ) data = json.loads(response.data) self.assertEqual(len(data), 1) - self.assertEqual(data[0]["event_type"], 'EVENT') + self.assertEqual(data[0]["event_type"], "EVENT") class EventsStatsAPITest(ApiTestCase): user_data_dict = { - 'email': 'something@email.com', - 'firstname': 'Some', - 'lastname': 'Thing', - 'user_title': 'Mr', - 'password': 'abc', - 'policy_agreed': True, - 'language': 'en' + "email": "something@email.com", + "firstname": "Some", + "lastname": "Thing", + "user_title": "Mr", + "password": "abc", + "policy_agreed": True, + "language": "en", } def seed_static_data(self): - self.add_organisation('Deep Learning Indaba', - 'blah.png', 'blah_big.png', 'deeplearningindaba') + self.add_organisation( + "Deep Learning Indaba", "blah.png", "blah_big.png", "deeplearningindaba" + ) - test_country = Country('Indaba Land') + test_country = Country("Indaba Land") db.session.add(test_country) db.session.commit() - test_category = UserCategory('Category1') + test_category = UserCategory("Category1") db.session.add(test_category) db.session.commit() - self.add_email_template('verify-email') + self.add_email_template("verify-email") - response = self.app.post('/api/v1/user', data=self.user_data_dict) + response = self.app.post("/api/v1/user", data=self.user_data_dict) self.test_user1 = json.loads(response.data) other_user_data = self.user_data_dict.copy() - other_user_data['email'] = 'other@user.com' - response = self.app.post('/api/v1/user', data=other_user_data) + other_user_data["email"] = "other@user.com" + response = self.app.post("/api/v1/user", data=other_user_data) self.test_user2 = json.loads(response.data) - self.test_event = self.add_event({'en': 'Test Event'}, {'en': 'Event Description'}, - datetime.now() + timedelta(days=30), datetime.now() + timedelta(days=60), - 'KONNET', 1, 'abx@indaba.deeplearning', 'indaba.deeplearning', - datetime.now(), datetime.now() + timedelta(days=30)) + self.test_event = self.add_event( + {"en": "Test Event"}, + {"en": "Event Description"}, + datetime.now() + timedelta(days=30), + datetime.now() + timedelta(days=60), + "KONNET", + 1, + "abx@indaba.deeplearning", + "indaba.deeplearning", + datetime.now(), + datetime.now() + timedelta(days=30), + ) db.session.add(self.test_event) db.session.commit() - self.test_form = self.create_application_form( - self.test_event.id, True, False) + self.test_form = self.create_application_form(self.test_event.id, True, False) db.session.add(self.test_form) db.session.commit() - self.test_response = self.add_response(self.test_form.id, self.test_user1['id']) - self.test_response2 = self.add_response(self.test_form.id, self.test_user2['id'], is_submitted=True) + self.test_response = self.add_response(self.test_form.id, self.test_user1["id"]) + self.test_response2 = self.add_response( + self.test_form.id, self.test_user2["id"], is_submitted=True + ) - self.user_role1 = EventRole( - 'admin', self.test_user1['id'], self.test_event.id) + self.user_role1 = EventRole("admin", self.test_user1["id"], self.test_event.id) db.session.add(self.user_role1) db.session.commit() @@ -272,112 +300,132 @@ def seed_static_data(self): def test_get_stats_forbidden(self): self.seed_static_data() - response = self.app.get('/api/v1/eventstats', - headers={ - 'Authorization': self.test_user2['token']}, - query_string={'event_id': self.test_event.id}) + response = self.app.get( + "/api/v1/eventstats", + headers={"Authorization": self.test_user2["token"]}, + query_string={"event_id": self.test_event.id}, + ) self.assertEqual(response.status_code, 403) def test_event_id_required(self): self.seed_static_data() - response = self.app.get('/api/v1/eventstats', - headers={ - 'Authorization': self.test_user2['token']}, - query_string={'someparam': self.test_event.id}) + response = self.app.get( + "/api/v1/eventstats", + headers={"Authorization": self.test_user2["token"]}, + query_string={"someparam": self.test_event.id}, + ) self.assertEqual(response.status_code, 400) class RemindersAPITest(ApiTestCase): def seed_static_data(self): inactive_user = self.add_user( - 'inactive@mail.co.za', 'inactive', post_create_fn=lambda u: u.deactivate()) + "inactive@mail.co.za", "inactive", post_create_fn=lambda u: u.deactivate() + ) deleted_user = self.add_user( - 'deleted@mail.co.za', 'deleted', post_create_fn=lambda u: u.delete()) + "deleted@mail.co.za", "deleted", post_create_fn=lambda u: u.delete() + ) - event_admin = self.add_user('event@admin.co.za', 'event', 'admin') - self.add_user('applicant@mail.co.za', 'applicant') - self.add_user('notstarted@mail.co.za', 'notstarted') - self.add_user('applicant2@mail.co.za', 'applicant') + event_admin = self.add_user("event@admin.co.za", "event", "admin") + self.add_user("applicant@mail.co.za", "applicant") + self.add_user("notstarted@mail.co.za", "notstarted") + self.add_user("applicant2@mail.co.za", "applicant") db.session.commit() - self.add_organisation('Deep Learning Indaba', - 'blah.png', 'blah_big.png', 'deeplearningindaba') - country = Country('South Africa') + self.add_organisation( + "Deep Learning Indaba", "blah.png", "blah_big.png", "deeplearningindaba" + ) + country = Country("South Africa") db.session.add(country) - user_category = UserCategory('Post Doc') + user_category = UserCategory("Post Doc") db.session.add(user_category) event = self.add_event( - {'en': 'Indaba 2019'}, {'en': 'Deep Learning Indaba'}, - datetime(2019, 8, 25), datetime(2019, 8, 31), - 'COOLER', 1, 'abx@indaba.deeplearning', 'indaba.deeplearning' + {"en": "Indaba 2019"}, + {"en": "Deep Learning Indaba"}, + datetime(2019, 8, 25), + datetime(2019, 8, 31), + "COOLER", + 1, + "abx@indaba.deeplearning", + "indaba.deeplearning", ) db.session.add(event) db.session.commit() email_templates = [ - EmailTemplate('application-not-submitted', None, 'Application Not Submitted', '', 'en'), - EmailTemplate('application-not-started', None, 'Application Not Started', '', 'en')] + EmailTemplate( + "application-not-submitted", None, "Application Not Submitted", "", "en" + ), + EmailTemplate( + "application-not-started", None, "Application Not Started", "", "en" + ), + ] db.session.add_all(email_templates) db.session.commit() - event_role = EventRole('admin', event_admin.id, event.id) + event_role = EventRole("admin", event_admin.id, event.id) db.session.add(event_role) application_form = self.create_application_form(1, True, False) db.session.add(application_form) db.session.commit() - submitted_response = self.add_response(application_form.id, self.test_users[0].id, is_submitted=True) - withdrawn_response = self.add_response(application_form.id, self.test_users[3].id, is_withdrawn=True) + submitted_response = self.add_response( + application_form.id, self.test_users[0].id, is_submitted=True + ) + withdrawn_response = self.add_response( + application_form.id, self.test_users[3].id, is_withdrawn=True + ) self.add_response(application_form.id, self.test_users[1].id) self.add_response(application_form.id, self.test_users[4].id) - self.add_email_template('application-not-submitted') - self.add_email_template('application-not-started') + self.add_email_template("application-not-submitted") + self.add_email_template("application-not-started") def get_auth_header_for(self, email): - body = { - 'email': email, - 'password': 'abc' - } - response = self.app.post('api/v1/authenticate', data=body) + body = {"email": email, "password": "abc"} + response = self.app.post("api/v1/authenticate", data=body) data = json.loads(response.data) - header = {'Authorization': data['token']} + header = {"Authorization": data["token"]} return header def test_not_submitted_reminder(self): self.seed_static_data() - header = self.get_auth_header_for('event@admin.co.za') - params = {'event_id': 1} + header = self.get_auth_header_for("event@admin.co.za") + params = {"event_id": 1} response = self.app.post( - '/api/v1/reminder-unsubmitted', headers=header, data=params) + "/api/v1/reminder-unsubmitted", headers=header, data=params + ) data = json.loads(response.data) LOGGER.warning(data) - self.assertEqual(data['unsubmitted_responses'], 1) + self.assertEqual(data["unsubmitted_responses"], 1) def test_not_started_reminder(self): self.seed_static_data() - header = self.get_auth_header_for('event@admin.co.za') - params = {'event_id': 1} + header = self.get_auth_header_for("event@admin.co.za") + params = {"event_id": 1} response = self.app.post( - '/api/v1/reminder-not-started', headers=header, data=params) + "/api/v1/reminder-not-started", headers=header, data=params + ) data = json.loads(response.data) LOGGER.warning(data) - self.assertEqual(data['not_started_responses'], 2) + self.assertEqual(data["not_started_responses"], 2) def test_non_event_admin_blocked_from_sending_reminders(self): self.seed_static_data() - header = self.get_auth_header_for('applicant@mail.co.za') - params = {'event_id': 1} + header = self.get_auth_header_for("applicant@mail.co.za") + params = {"event_id": 1} response_unsubmitted = self.app.post( - '/api/v1/reminder-unsubmitted', headers=header, data=params) + "/api/v1/reminder-unsubmitted", headers=header, data=params + ) response_not_started = self.app.post( - '/api/v1/reminder-not-started', headers=header, data=params) + "/api/v1/reminder-not-started", headers=header, data=params + ) self.assertEqual(response_unsubmitted.status_code, FORBIDDEN[1]) self.assertEqual(response_not_started.status_code, FORBIDDEN[1]) @@ -386,156 +434,174 @@ def test_non_event_admin_blocked_from_sending_reminders(self): class EventAPITest(ApiTestCase): test_event_data_dict = { - 'name': { - 'en': 'Test Event', - 'fr': 'evenement de test' + "name": {"en": "Test Event", "fr": "evenement de test"}, + "description": { + "en": "Test Event Description", + "fr": "Description de l'evenement de test", }, - 'description': { - 'en': 'Test Event Description', - 'fr': "Description de l'evenement de test" - }, - 'start_date': datetime(2020, 6, 1).strftime('%Y-%m-%dT%H:%M:%SZ'), - 'end_date': datetime(2020, 6, 6).strftime('%Y-%m-%dT%H:%M:%SZ'), - 'key': 'testevent', - 'organisation_id': 1, - 'email_from': 'test@testindaba.com', - 'url': 'testindaba.com', - 'application_open': datetime(2020, 1, 1).strftime('%Y-%m-%dT%H:%M:%SZ'), - 'application_close': datetime(2020, 2, 1).strftime('%Y-%m-%dT%H:%M:%SZ'), - 'review_open': datetime(2020, 2, 1).strftime('%Y-%m-%dT%H:%M:%SZ'), - 'review_close': datetime(2020, 3, 1).strftime('%Y-%m-%dT%H:%M:%SZ'), - 'selection_open': datetime(2020, 3, 1).strftime('%Y-%m-%dT%H:%M:%SZ'), - 'selection_close': datetime(2020, 5, 1).strftime('%Y-%m-%dT%H:%M:%SZ'), - 'offer_open': datetime(2020, 5, 1).strftime('%Y-%m-%dT%H:%M:%SZ'), - 'offer_close': datetime(2020, 5, 30).strftime('%Y-%m-%dT%H:%M:%SZ'), - 'registration_open': datetime(2020, 5, 30).strftime('%Y-%m-%dT%H:%M:%SZ'), - 'registration_close': datetime(2020, 6, 1).strftime('%Y-%m-%dT%H:%M:%SZ'), - 'event_type':'EVENT', - 'travel_grant': False + "start_date": datetime(2020, 6, 1).strftime("%Y-%m-%dT%H:%M:%SZ"), + "end_date": datetime(2020, 6, 6).strftime("%Y-%m-%dT%H:%M:%SZ"), + "key": "testevent", + "organisation_id": 1, + "email_from": "test@testindaba.com", + "url": "testindaba.com", + "application_open": datetime(2020, 1, 1).strftime("%Y-%m-%dT%H:%M:%SZ"), + "application_close": datetime(2020, 2, 1).strftime("%Y-%m-%dT%H:%M:%SZ"), + "review_open": datetime(2020, 2, 1).strftime("%Y-%m-%dT%H:%M:%SZ"), + "review_close": datetime(2020, 3, 1).strftime("%Y-%m-%dT%H:%M:%SZ"), + "selection_open": datetime(2020, 3, 1).strftime("%Y-%m-%dT%H:%M:%SZ"), + "selection_close": datetime(2020, 5, 1).strftime("%Y-%m-%dT%H:%M:%SZ"), + "offer_open": datetime(2020, 5, 1).strftime("%Y-%m-%dT%H:%M:%SZ"), + "offer_close": datetime(2020, 5, 30).strftime("%Y-%m-%dT%H:%M:%SZ"), + "registration_open": datetime(2020, 5, 30).strftime("%Y-%m-%dT%H:%M:%SZ"), + "registration_close": datetime(2020, 6, 1).strftime("%Y-%m-%dT%H:%M:%SZ"), + "event_type": "EVENT", + "travel_grant": False, } def seed_static_data(self): - self.add_organisation('Test Indaba', 'blah.png', - 'blah_big.png', 'testindaba') + self.add_organisation("Test Indaba", "blah.png", "blah_big.png", "testindaba") - test_country = Country('Test Land') + test_country = Country("Test Land") db.session.add(test_country) db.session.commit() - test_category = UserCategory('TestYear') + test_category = UserCategory("TestYear") db.session.add(test_category) db.session.commit() - self.test_admin_user = AppUser(email='someoneadmin@email.com', firstname='Some', - lastname='One', user_title='Mr', password='abc', organisation_id=1, is_admin=True) + self.test_admin_user = AppUser( + email="someoneadmin@email.com", + firstname="Some", + lastname="One", + user_title="Mr", + password="abc", + organisation_id=1, + is_admin=True, + ) self.test_admin_user.verify() db.session.add(self.test_admin_user) db.session.commit() - self.test_user = AppUser(email='someone@email.com', firstname='Some', - lastname='One', user_title='Mr', password='abc', organisation_id=1) + self.test_user = AppUser( + email="someone@email.com", + firstname="Some", + lastname="One", + user_title="Mr", + password="abc", + organisation_id=1, + ) self.test_user.verify() db.session.add(self.test_user) db.session.commit() - event = self.add_event({'en': 'Indaba 2019'}, {'en': 'Deep Learning Indaba'}, datetime(2019, 8, 25), datetime(2019, 8, 31), 'COOLER') + event = self.add_event( + {"en": "Indaba 2019"}, + {"en": "Deep Learning Indaba"}, + datetime(2019, 8, 25), + datetime(2019, 8, 31), + "COOLER", + ) db.session.commit() db.session.flush() def get_auth_header_for(self, email): - body = { - 'email': email, - 'password': 'abc' - } - response = self.app.post('api/v1/authenticate', data=body) + body = {"email": email, "password": "abc"} + response = self.app.post("api/v1/authenticate", data=body) data = json.loads(response.data) - header = {'Authorization': data['token']} + header = {"Authorization": data["token"]} return header def test_post_event_unauthed(self): self.seed_static_data() response = self.app.post( - 'api/v1/event', + "api/v1/event", data=json.dumps(self.test_event_data_dict), - content_type='application/json') + content_type="application/json", + ) self.assertEqual(response.status_code, 401) def test_put_event_unauthed(self): self.seed_static_data() response = self.app.put( - 'api/v1/event', + "api/v1/event", data=json.dumps(self.test_event_data_dict), - content_type='application/json') + content_type="application/json", + ) self.assertEqual(response.status_code, 401) def test_post_event_not_admin(self): self.seed_static_data() header = self.get_auth_header_for(self.test_user.email) response = self.app.post( - 'api/v1/event', + "api/v1/event", headers=header, data=json.dumps(self.test_event_data_dict), - content_type='application/json') + content_type="application/json", + ) self.assertEqual(response.status_code, 403) def test_post_event_is_admin(self): self.seed_static_data() header = self.get_auth_header_for(self.test_admin_user.email) response = self.app.post( - 'api/v1/event', - headers=header, + "api/v1/event", + headers=header, data=json.dumps(self.test_event_data_dict), - content_type='application/json') + content_type="application/json", + ) data = json.loads(response.data) self.assertEqual(response.status_code, 201) - self.assertEqual(len(data['name']), 2) - self.assertEqual(len(data['description']), 2) - self.assertEqual(data['name']['en'], 'Test Event') - self.assertEqual(data['name']['fr'], 'evenement de test') - self.assertEqual(data['description']['en'], 'Test Event Description') - self.assertEqual(data['description']['fr'], "Description de l'evenement de test") - + self.assertEqual(len(data["name"]), 2) + self.assertEqual(len(data["description"]), 2) + self.assertEqual(data["name"]["en"], "Test Event") + self.assertEqual(data["name"]["fr"], "evenement de test") + self.assertEqual(data["description"]["en"], "Test Event Description") + self.assertEqual( + data["description"]["fr"], "Description de l'evenement de test" + ) def test_post_event_eventrole_added(self): self.seed_static_data() header = self.get_auth_header_for(self.test_admin_user.email) event_response = self.app.post( - 'api/v1/event', + "api/v1/event", headers=header, data=json.dumps(self.test_event_data_dict), - content_type='application/json') + content_type="application/json", + ) event_data = json.loads(event_response.data) self.assertEqual(event_response.status_code, 201) - body = { - 'email': self.test_admin_user.email, - 'password': 'abc' - } - response = self.app.post('api/v1/authenticate', data=body) + body = {"email": self.test_admin_user.email, "password": "abc"} + response = self.app.post("api/v1/authenticate", data=body) data = json.loads(response.data) - self.assertEqual(len(data['roles']), 1) - for event_role in data['roles']: - if event_role['event_id'] == event_data['id']: - self.assertEqual(data['roles'][0]['role'], 'admin') + self.assertEqual(len(data["roles"]), 1) + for event_role in data["roles"]: + if event_role["event_id"] == event_data["id"]: + self.assertEqual(data["roles"][0]["role"], "admin") def test_put_event_is_admin(self): self.seed_static_data() header = self.get_auth_header_for(self.test_admin_user.email) # update(put) event - self.test_event_data_dict['id'] = 1 - self.test_event_data_dict['name'] = {'en': 'Test Event Name Updated'} - self.test_event_data_dict['description'] = {'en': 'Test Event Description Updated'} + self.test_event_data_dict["id"] = 1 + self.test_event_data_dict["name"] = {"en": "Test Event Name Updated"} + self.test_event_data_dict["description"] = { + "en": "Test Event Description Updated" + } response = self.app.put( - 'api/v1/event', + "api/v1/event", headers=header, data=json.dumps(self.test_event_data_dict), - content_type='application/json') + content_type="application/json", + ) data = json.loads(response.data) self.assertEqual(response.status_code, 200) - self.assertEqual(len(data['name']), 1) - self.assertEqual(len(data['description']), 1) - self.assertEqual(data['name']['en'], 'Test Event Name Updated') - self.assertEqual(data['description']['en'], 'Test Event Description Updated') + self.assertEqual(len(data["name"]), 1) + self.assertEqual(len(data["description"]), 1) + self.assertEqual(data["name"]["en"], "Test Event Name Updated") + self.assertEqual(data["description"]["en"], "Test Event Description Updated") def test_put_event_not_admin(self): self.seed_static_data() @@ -544,75 +610,78 @@ def test_put_event_not_admin(self): # get auth header for non admin user header = self.get_auth_header_for(self.test_user.email) - self.test_event_data_dict['id'] = 1 - self.test_event_data_dict['name'] = {'en': 'Test Event Updated'} - self.test_event_data_dict['description'] = {'en': 'Test Event Description Updated'} + self.test_event_data_dict["id"] = 1 + self.test_event_data_dict["name"] = {"en": "Test Event Updated"} + self.test_event_data_dict["description"] = { + "en": "Test Event Description Updated" + } response = self.app.put( - 'api/v1/event', + "api/v1/event", headers=header, data=json.dumps(self.test_event_data_dict), - content_type='application/json') + content_type="application/json", + ) self.assertEqual(response.status_code, 403) - + def test_post_event_must_contain_translation(self): self.seed_static_data() header = self.get_auth_header_for(self.test_admin_user.email) - self.test_event_data_dict['name'] = {} - self.test_event_data_dict['description'] = {} + self.test_event_data_dict["name"] = {} + self.test_event_data_dict["description"] = {} response = self.app.post( - 'api/v1/event', + "api/v1/event", headers=header, data=json.dumps(self.test_event_data_dict), - content_type='application/json' + content_type="application/json", ) - + self.assertEqual(response.status_code, 400) - + def test_post_event_translation_mismatch(self): self.seed_static_data() header = self.get_auth_header_for(self.test_admin_user.email) - self.test_event_data_dict['name'] = {'en': 'English Translation'} - self.test_event_data_dict['description'] = {'fr': 'French Translation'} + self.test_event_data_dict["name"] = {"en": "English Translation"} + self.test_event_data_dict["description"] = {"fr": "French Translation"} response = self.app.post( - 'api/v1/event', + "api/v1/event", headers=header, data=json.dumps(self.test_event_data_dict), - content_type='application/json' + content_type="application/json", ) - + self.assertEqual(response.status_code, 400) - + def test_put_event_must_contain_translation(self): self.seed_static_data() header = self.get_auth_header_for(self.test_admin_user.email) - self.test_event_data_dict['id'] = 1 - self.test_event_data_dict['name'] = {} - self.test_event_data_dict['description'] = {} + self.test_event_data_dict["id"] = 1 + self.test_event_data_dict["name"] = {} + self.test_event_data_dict["description"] = {} response = self.app.put( - 'api/v1/event', + "api/v1/event", headers=header, data=json.dumps(self.test_event_data_dict), - content_type='application/json' + content_type="application/json", ) - + self.assertEqual(response.status_code, 400) - + def test_put_event_must_contain_translation(self): self.seed_static_data() header = self.get_auth_header_for(self.test_admin_user.email) - self.test_event_data_dict['id'] = 1 - self.test_event_data_dict['name'] = {'en': 'English Translation'} - self.test_event_data_dict['description'] = {'fr': 'French Translation'} + self.test_event_data_dict["id"] = 1 + self.test_event_data_dict["name"] = {"en": "English Translation"} + self.test_event_data_dict["description"] = {"fr": "French Translation"} response = self.app.put( - 'api/v1/event', + "api/v1/event", headers=header, data=json.dumps(self.test_event_data_dict), - content_type='application/json' + content_type="application/json", ) self.assertEqual(response.status_code, 400) @@ -620,28 +689,29 @@ def test_put_event_must_contain_translation(self): class EventStatusTest(ApiTestCase): def seed_static_data(self): - self.event_admin = self.add_user('event@admin.co.za', 'event', 'admin') - self.user1 = self.add_user('applicant@mail.co.za', 'applicant') - self.user2 = self.add_user('applicant2@mail.co.za', 'applicant2') + self.event_admin = self.add_user("event@admin.co.za", "event", "admin") + self.user1 = self.add_user("applicant@mail.co.za", "applicant") + self.user2 = self.add_user("applicant2@mail.co.za", "applicant2") self.event = self.add_event() - self.event2 = self.add_event({'en': 'Second event'}, key='second_event') + self.event2 = self.add_event({"en": "Second event"}, key="second_event") self.application_form = self.create_application_form() - self.registration_form = self.create_registration_form(self.event.id) + self.registration_form = self.create_registration_form(self.event.id) db.session.add(self.registration_form) db.session.commit() - def test_invited_guest_unregistered(self): """Check that un-registred invited guest statuses are correct.""" self.seed_static_data() - invited = InvitedGuest(event_id=self.event.id, user_id=self.user1.id, role='Mentor') + invited = InvitedGuest( + event_id=self.event.id, user_id=self.user1.id, role="Mentor" + ) db.session.add(invited) db.session.commit() status = event_status.get_event_status(self.event.id, self.user1.id) - self.assertEqual(status.invited_guest, 'Mentor') + self.assertEqual(status.invited_guest, "Mentor") self.assertIsNone(status.registration_status) self.assertIsNone(status.application_status) self.assertIsNone(status.outcome_status) @@ -657,21 +727,24 @@ def test_invited_guest_unregistered(self): def test_registered_invited_guest(self): """Check that statuses are correct for a registered invited guest.""" self.seed_static_data() - invited = InvitedGuest(event_id=self.event.id, user_id=self.user1.id, role='Mentor') + invited = InvitedGuest( + event_id=self.event.id, user_id=self.user1.id, role="Mentor" + ) db.session.add(invited) # Check un-confirmed guest registration guest_registration = GuestRegistration( - user_id=self.user1.id, + user_id=self.user1.id, registration_form_id=self.registration_form.id, - confirmed=False) + confirmed=False, + ) db.session.add(guest_registration) db.session.commit() status = event_status.get_event_status(self.event.id, self.user1.id) - self.assertEqual(status.invited_guest, 'Mentor') - self.assertEqual(status.registration_status, 'Not Confirmed') + self.assertEqual(status.invited_guest, "Mentor") + self.assertEqual(status.registration_status, "Not Confirmed") self.assertIsNone(status.application_status) self.assertIsNone(status.outcome_status) self.assertIsNone(status.offer_status) @@ -681,8 +754,8 @@ def test_registered_invited_guest(self): db.session.commit() status = event_status.get_event_status(self.event.id, self.user1.id) - self.assertEqual(status.invited_guest, 'Mentor') - self.assertEqual(status.registration_status, 'Confirmed') + self.assertEqual(status.invited_guest, "Mentor") + self.assertEqual(status.registration_status, "Confirmed") self.assertIsNone(status.application_status) self.assertIsNone(status.outcome_status) self.assertIsNone(status.offer_status) @@ -690,14 +763,16 @@ def test_registered_invited_guest(self): def test_invited_guest_and_applied(self): """Check invited guest status when the user also applied.""" self.seed_static_data() - invited = InvitedGuest(event_id=self.event.id, user_id=self.user1.id, role='Mentor') + invited = InvitedGuest( + event_id=self.event.id, user_id=self.user1.id, role="Mentor" + ) db.session.add(invited) self.add_response(self.application_form.id, self.user1.id) status = event_status.get_event_status(self.event.id, self.user1.id) - self.assertEqual(status.invited_guest, 'Mentor') + self.assertEqual(status.invited_guest, "Mentor") self.assertIsNone(status.registration_status) self.assertIsNone(status.application_status) self.assertIsNone(status.outcome_status) @@ -713,7 +788,7 @@ def test_applied(self): self.assertIsNone(status.invited_guest) self.assertIsNone(status.registration_status) - self.assertEqual(status.application_status, 'Not Submitted') + self.assertEqual(status.application_status, "Not Submitted") self.assertIsNone(status.outcome_status) self.assertIsNone(status.offer_status) @@ -725,7 +800,7 @@ def test_applied(self): self.assertIsNone(status.invited_guest) self.assertIsNone(status.registration_status) - self.assertEqual(status.application_status, 'Submitted') + self.assertEqual(status.application_status, "Submitted") self.assertIsNone(status.outcome_status) self.assertIsNone(status.offer_status) @@ -737,7 +812,7 @@ def test_applied(self): self.assertIsNone(status.invited_guest) self.assertIsNone(status.registration_status) - self.assertEqual(status.application_status, 'Withdrawn') + self.assertEqual(status.application_status, "Withdrawn") self.assertIsNone(status.outcome_status) self.assertIsNone(status.offer_status) @@ -760,19 +835,21 @@ def test_outcome(self): self.seed_static_data() # Check that status is invalid if there is an outcome with not application (response) - outcome = Outcome(self.event.id, self.user1.id, OutcomeStatus.ACCEPTED, self.user2.id) + outcome = Outcome( + self.event.id, self.user1.id, OutcomeStatus.ACCEPTED, self.user2.id + ) db.session.add(outcome) with self.assertRaises(ValueError): event_status.get_event_status(self.event.id, self.user1.id) - + # Check status when user has applied and has an outcome self.add_response(self.application_form.id, self.user1.id, is_submitted=True) status = event_status.get_event_status(self.event.id, self.user1.id) self.assertIsNone(status.invited_guest) self.assertIsNone(status.registration_status) - self.assertEqual(status.application_status, 'Submitted') + self.assertEqual(status.application_status, "Submitted") self.assertEqual(status.outcome_status, outcome.status.name) self.assertIsNone(status.offer_status) @@ -780,29 +857,32 @@ def test_offer(self): """Test statuses when offer.""" self.seed_static_data() - outcome = Outcome(self.event.id, self.user1.id, OutcomeStatus.ACCEPTED, self.user2.id) + outcome = Outcome( + self.event.id, self.user1.id, OutcomeStatus.ACCEPTED, self.user2.id + ) db.session.add(outcome) self.add_response(self.application_form.id, self.user1.id, is_submitted=True) # Test pending offer offer = Offer( - user_id=self.user1.id, - event_id=self.event.id, - offer_date=date.today(), + user_id=self.user1.id, + event_id=self.event.id, + offer_date=date.today(), expiry_date=date.today() + timedelta(days=1), payment_required=False, travel_award=False, - accommodation_award=False) + accommodation_award=False, + ) db.session.add(offer) db.session.commit() status = event_status.get_event_status(self.event.id, self.user1.id) self.assertIsNone(status.invited_guest) self.assertIsNone(status.registration_status) - self.assertEqual(status.application_status, 'Submitted') + self.assertEqual(status.application_status, "Submitted") self.assertEqual(status.outcome_status, outcome.status.name) - self.assertEqual(status.offer_status, 'Pending') + self.assertEqual(status.offer_status, "Pending") # Test accepted offer offer.candidate_response = True @@ -810,46 +890,49 @@ def test_offer(self): status = event_status.get_event_status(self.event.id, self.user1.id) self.assertIsNone(status.invited_guest) self.assertIsNone(status.registration_status) - self.assertEqual(status.application_status, 'Submitted') + self.assertEqual(status.application_status, "Submitted") self.assertEqual(status.outcome_status, outcome.status.name) - self.assertEqual(status.offer_status, 'Accepted') + self.assertEqual(status.offer_status, "Accepted") # Should still be accepted when past the expiry date offer.expiry_date = date.today() - timedelta(days=2) db.session.commit() status = event_status.get_event_status(self.event.id, self.user1.id) - self.assertEqual(status.offer_status, 'Accepted') + self.assertEqual(status.offer_status, "Accepted") # Test expired offer offer.candidate_response = None db.session.commit() status = event_status.get_event_status(self.event.id, self.user1.id) - self.assertEqual(status.offer_status, 'Expired') + self.assertEqual(status.offer_status, "Expired") # Test rejected offer offer.candidate_response = False db.session.commit() status = event_status.get_event_status(self.event.id, self.user1.id) - self.assertEqual(status.offer_status, 'Rejected') + self.assertEqual(status.offer_status, "Rejected") def test_registered(self): """Test statusess for registered attendees.""" self.seed_static_data() - outcome = Outcome(self.event.id, self.user1.id, OutcomeStatus.ACCEPTED, self.user2.id) + outcome = Outcome( + self.event.id, self.user1.id, OutcomeStatus.ACCEPTED, self.user2.id + ) db.session.add(outcome) self.add_response(self.application_form.id, self.user1.id, is_submitted=True) offer = Offer( - user_id=self.user1.id, - event_id=self.event.id, - offer_date=date.today(), + user_id=self.user1.id, + event_id=self.event.id, + offer_date=date.today(), expiry_date=date.today() + timedelta(days=1), payment_required=False, travel_award=False, accommodation_award=False, - candidate_response=True) + candidate_response=True, + ) db.session.add(offer) db.session.commit() @@ -859,13 +942,13 @@ def test_registered(self): status = event_status.get_event_status(self.event.id, self.user1.id) self.assertIsNone(status.invited_guest) - self.assertEqual(status.registration_status, 'Not Confirmed') - self.assertEqual(status.application_status, 'Submitted') + self.assertEqual(status.registration_status, "Not Confirmed") + self.assertEqual(status.application_status, "Submitted") self.assertEqual(status.outcome_status, outcome.status.name) - self.assertEqual(status.offer_status, 'Accepted') + self.assertEqual(status.offer_status, "Accepted") registration.confirm() db.session.commit() status = event_status.get_event_status(self.event.id, self.user1.id) - self.assertEqual(status.registration_status, 'Confirmed') + self.assertEqual(status.registration_status, "Confirmed") diff --git a/api/app/files/api.py b/api/app/files/api.py index e8bc09f6f..a4d0b9bad 100644 --- a/api/app/files/api.py +++ b/api/app/files/api.py @@ -1,59 +1,60 @@ +import tempfile import uuid import flask_restful as restful -from flask_restful import reqparse from flask import send_file -from config import FILE_SIZE_LIMIT -from app.utils.errors import FILE_SIZE_EXCEEDED -from app.files.mixins import FileUploadMixin -from app.utils.auth import auth_required - - -import tempfile +from flask_restful import reqparse from app import LOGGER +from app.files.mixins import FileUploadMixin from app.utils import storage +from app.utils.auth import auth_required +from app.utils.errors import FILE_SIZE_EXCEEDED +from config import FILE_SIZE_LIMIT class FileUploadAPI(FileUploadMixin, restful.Resource): - def get(self): req_parser = reqparse.RequestParser() - req_parser.add_argument('filename', type=str, required=True) - req_parser.add_argument('rename', type=str, required=False) + req_parser.add_argument("filename", type=str, required=True) + req_parser.add_argument("rename", type=str, required=False) args = req_parser.parse_args() bucket = storage.get_storage_bucket() - blob = bucket.blob(args['filename']) + blob = bucket.blob(args["filename"]) with tempfile.NamedTemporaryFile() as temp: blob.download_to_filename(temp.name) - renamed = args['rename'] or args['filename'] + renamed = args["rename"] or args["filename"] return send_file( - temp.name, - as_attachment=True, - attachment_filename=args['rename'] or args['filename']) - + temp.name, + as_attachment=True, + attachment_filename=args["rename"] or args["filename"], + ) def post(self): args = self.req_parser.parse_args() bucket = storage.get_storage_bucket() - + unique_name = str(uuid.uuid4().hex) blob = bucket.blob(unique_name) - file = args['file'] + file = args["file"] bytes_file = file.read() content_type = file.content_type - file_size = len(bytes_file) + file_size = len(bytes_file) if file_size > FILE_SIZE_LIMIT: - LOGGER.debug('File size of {} exceeds limit of {}'.format(file_size, FILE_SIZE_EXCEEDED)) + LOGGER.debug( + "File size of {} exceeds limit of {}".format( + file_size, FILE_SIZE_EXCEEDED + ) + ) return FILE_SIZE_EXCEEDED blob.upload_from_string(bytes_file, content_type=content_type) return { - 'file_id': unique_name, + "file_id": unique_name, }, 201 diff --git a/api/app/files/mixins.py b/api/app/files/mixins.py index 596d0f4ea..0903eec66 100644 --- a/api/app/files/mixins.py +++ b/api/app/files/mixins.py @@ -1,6 +1,7 @@ from flask_restful import reqparse from werkzeug.datastructures import FileStorage + class FileUploadMixin(object): req_parser = reqparse.RequestParser() - req_parser.add_argument('file', location='files', type=FileStorage, required=True) \ No newline at end of file + req_parser.add_argument("file", location="files", type=FileStorage, required=True) diff --git a/api/app/guestRegistrations/api.py b/api/app/guestRegistrations/api.py index bb8f0ca0a..381ce1ca5 100644 --- a/api/app/guestRegistrations/api.py +++ b/api/app/guestRegistrations/api.py @@ -1,70 +1,71 @@ -from datetime import date import traceback -from flask_restful import reqparse +from datetime import date + import flask_restful as restful from flask import request +from flask_restful import fields, marshal, marshal_with, reqparse from sqlalchemy.exc import SQLAlchemyError -from app.utils.auth import verify_token -from app.guestRegistrations.mixins import GuestRegistrationMixin, GuestRegistrationFormMixin + +from app import LOGGER, db +from app.events.models import Event +from app.events.repository import EventRepository as event_repository +from app.guestRegistrations.mixins import ( + GuestRegistrationFormMixin, + GuestRegistrationMixin, +) from app.invitedGuest.models import GuestRegistration, GuestRegistrationAnswer -from flask_restful import fields, marshal_with, marshal -from app.registration.models import RegistrationSection -from app.registration.models import RegistrationQuestion -from app.registration.models import RegistrationForm +from app.registration.models import ( + RegistrationForm, + RegistrationQuestion, + RegistrationSection, +) from app.users.models import AppUser -from app.events.models import Event -from app.utils.auth import auth_required -from app.utils import errors, emailer, strings from app.users.repository import UserRepository as user_repository -from app.events.repository import EventRepository as event_repository -from app import LOGGER - -from app import db +from app.utils import emailer, errors, strings +from app.utils.auth import auth_required, verify_token def _get_answer_value(answer, question): - if question.type == 'multi-choice' and question.options is not None: - value = [o for o in question.options if o['value'] == answer.value] + if question.type == "multi-choice" and question.options is not None: + value = [o for o in question.options if o["value"] == answer.value] if not value: return answer.value - return value[0]['label'] + return value[0]["label"] - if question.type == 'file' and answer.value: - return 'Uploaded File' + if question.type == "file" and answer.value: + return "Uploaded File" return answer.value class GuestRegistrationApi(GuestRegistrationMixin, restful.Resource): answer_fields = { - 'id': fields.Integer, - 'guest_registration_id': fields.Integer, - 'registration_question_id': fields.Integer, - 'value': fields.String + "id": fields.Integer, + "guest_registration_id": fields.Integer, + "registration_question_id": fields.Integer, + "value": fields.String, } registration_fields = { - 'id': fields.Integer, - 'registration_form_id': fields.Integer, - 'confirmed': fields.Boolean, - 'created_at': fields.DateTime, - 'confirmation_email_sent_at': fields.DateTime, - 'user_id': fields.Integer - + "id": fields.Integer, + "registration_form_id": fields.Integer, + "confirmed": fields.Boolean, + "created_at": fields.DateTime, + "confirmation_email_sent_at": fields.DateTime, + "user_id": fields.Integer, } update_registration_fields = { - 'guest_registration_id': fields.Integer, - 'registration_form_id': fields.Integer, - 'confirmed': fields.Boolean, - 'created_at': fields.DateTime, - 'confirmation_email_sent_at': fields.DateTime - + "guest_registration_id": fields.Integer, + "registration_form_id": fields.Integer, + "confirmed": fields.Boolean, + "created_at": fields.DateTime, + "confirmation_email_sent_at": fields.DateTime, } response_fields = { - 'id': fields.Integer, - 'guest_registration_id': fields.Integer, - 'registration_form_id': fields.Integer, - 'answers': fields.List(fields.Nested(answer_fields)) + "id": fields.Integer, + "guest_registration_id": fields.Integer, + "registration_form_id": fields.Integer, + "answers": fields.List(fields.Nested(answer_fields)), } @marshal_with(response_fields) @@ -72,25 +73,37 @@ class GuestRegistrationApi(GuestRegistrationMixin, restful.Resource): def get(self): try: - user_id = verify_token(request.headers.get('Authorization'))['id'] + user_id = verify_token(request.headers.get("Authorization"))["id"] - registration = db.session.query(GuestRegistration).filter(GuestRegistration.user_id == user_id).first() + registration = ( + db.session.query(GuestRegistration) + .filter(GuestRegistration.user_id == user_id) + .first() + ) if registration is None: - return 'no Registration', 404 - registration_form = db.session.query(RegistrationForm).filter(RegistrationForm.id == registration. - registration_form_id).first() + return "no Registration", 404 + registration_form = ( + db.session.query(RegistrationForm) + .filter(RegistrationForm.id == registration.registration_form_id) + .first() + ) if registration_form is None: return errors.REGISTRATION_FORM_NOT_FOUND - db_answers = db.session.query(GuestRegistrationAnswer).filter( - GuestRegistrationAnswer.guest_registration_id == - registration.id, GuestRegistrationAnswer.is_active == True).all() + db_answers = ( + db.session.query(GuestRegistrationAnswer) + .filter( + GuestRegistrationAnswer.guest_registration_id == registration.id, + GuestRegistrationAnswer.is_active == True, + ) + .all() + ) response = { - 'guest_registration_id': registration.id, - 'registration_form_id': registration_form.id, - 'answers': db_answers + "guest_registration_id": registration.id, + "registration_form_id": registration_form.id, + "answers": db_answers, } return response @@ -108,45 +121,74 @@ def post(self): try: - user_id = verify_token(request.headers.get('Authorization'))['id'] + user_id = verify_token(request.headers.get("Authorization"))["id"] if not user_id: return errors.USER_NOT_FOUND - current_user = db.session.query(AppUser).filter(AppUser.id == user_id).first() + current_user = ( + db.session.query(AppUser).filter(AppUser.id == user_id).first() + ) - registration_form = db.session.query(RegistrationForm).filter( - RegistrationForm.id == args['registration_form_id']).first() + registration_form = ( + db.session.query(RegistrationForm) + .filter(RegistrationForm.id == args["registration_form_id"]) + .first() + ) if not registration_form: return errors.REGISTRATION_FORM_NOT_FOUND registration = GuestRegistration( - registration_form_id=args['registration_form_id'], + registration_form_id=args["registration_form_id"], user_id=user_id, confirmed=True, created_at=date.today(), - confirmation_email_sent_at=date.today() # None + confirmation_email_sent_at=date.today(), # None ) db.session.add(registration) db.session.commit() event = event_repository.get_by_id(registration_form.event_id) - for answer_args in args['answers']: - if db.session.query(RegistrationQuestion).filter( - RegistrationQuestion.id == answer_args['registration_question_id']).first(): - answer = GuestRegistrationAnswer(guest_registration_id=registration.id, - registration_question_id=answer_args['registration_question_id'], - value=answer_args['value'], is_active=True) + for answer_args in args["answers"]: + if ( + db.session.query(RegistrationQuestion) + .filter( + RegistrationQuestion.id + == answer_args["registration_question_id"] + ) + .first() + ): + answer = GuestRegistrationAnswer( + guest_registration_id=registration.id, + registration_question_id=answer_args[ + "registration_question_id" + ], + value=answer_args["value"], + is_active=True, + ) db.session.add(answer) db.session.commit() - registration_answers = db.session.query(GuestRegistrationAnswer).filter( - GuestRegistrationAnswer.guest_registration_id == registration.id, - GuestRegistrationAnswer.is_active == True).all() - registration_questions = db.session.query(RegistrationQuestion).filter( - RegistrationQuestion.registration_form_id == args['registration_form_id']).all() + registration_answers = ( + db.session.query(GuestRegistrationAnswer) + .filter( + GuestRegistrationAnswer.guest_registration_id == registration.id, + GuestRegistrationAnswer.is_active == True, + ) + .all() + ) + registration_questions = ( + db.session.query(RegistrationQuestion) + .filter( + RegistrationQuestion.registration_form_id + == args["registration_form_id"] + ) + .all() + ) - email_sent = self.send_confirmation(current_user, registration_questions, registration_answers, event) + email_sent = self.send_confirmation( + current_user, registration_questions, registration_answers, event + ) if email_sent: registration.confirmation_email_sent_at = date.today() db.session.commit() @@ -164,69 +206,115 @@ def put(self): # Update an existing response for the logged-in user. args = self.req_parser.parse_args() try: - user_id = verify_token(request.headers.get('Authorization'))['id'] - registration = db.session.query(GuestRegistration).filter( - GuestRegistration.id == args['guest_registration_id']).one_or_none() + user_id = verify_token(request.headers.get("Authorization"))["id"] + registration = ( + db.session.query(GuestRegistration) + .filter(GuestRegistration.id == args["guest_registration_id"]) + .one_or_none() + ) if registration is None: - return 'Registration not found', 404 - registration.registration_form_id = args['registration_form_id'] + return "Registration not found", 404 + registration.registration_form_id = args["registration_form_id"] db.session.commit() - for answer_args in args['answers']: - answer = db.session.query(GuestRegistrationAnswer).filter( - GuestRegistrationAnswer.registration_question_id == answer_args['registration_question_id'], - GuestRegistrationAnswer.guest_registration_id == args['guest_registration_id'], - GuestRegistrationAnswer.is_active == True).one_or_none() + for answer_args in args["answers"]: + answer = ( + db.session.query(GuestRegistrationAnswer) + .filter( + GuestRegistrationAnswer.registration_question_id + == answer_args["registration_question_id"], + GuestRegistrationAnswer.guest_registration_id + == args["guest_registration_id"], + GuestRegistrationAnswer.is_active == True, + ) + .one_or_none() + ) if answer is not None: answer.is_active = False db.session.merge(answer) - new_answer = GuestRegistrationAnswer(guest_registration_id=registration.id, - registration_question_id=answer_args['registration_question_id'], - value=answer_args['value'], - is_active=True) + new_answer = GuestRegistrationAnswer( + guest_registration_id=registration.id, + registration_question_id=answer_args[ + "registration_question_id" + ], + value=answer_args["value"], + is_active=True, + ) db.session.add(new_answer) - elif db.session.query(RegistrationQuestion).filter( - RegistrationQuestion.id == answer_args['registration_question_id']).one(): - - answer = GuestRegistrationAnswer(guest_registration_id=registration.id, - registration_question_id=answer_args['registration_question_id'], - value=answer_args['value'], - is_active=True) + elif ( + db.session.query(RegistrationQuestion) + .filter( + RegistrationQuestion.id + == answer_args["registration_question_id"] + ) + .one() + ): + + answer = GuestRegistrationAnswer( + guest_registration_id=registration.id, + registration_question_id=answer_args[ + "registration_question_id" + ], + value=answer_args["value"], + is_active=True, + ) db.session.add(answer) db.session.commit() current_user = user_repository.get_by_id(user_id) - registration_answers = db.session.query(GuestRegistrationAnswer).filter( - GuestRegistrationAnswer.guest_registration_id == registration.id, - GuestRegistrationAnswer.is_active == True).all() + registration_answers = ( + db.session.query(GuestRegistrationAnswer) + .filter( + GuestRegistrationAnswer.guest_registration_id == registration.id, + GuestRegistrationAnswer.is_active == True, + ) + .all() + ) - registration_questions = db.session.query(RegistrationQuestion).filter( - RegistrationQuestion.registration_form_id == args['registration_form_id']).all() + registration_questions = ( + db.session.query(RegistrationQuestion) + .filter( + RegistrationQuestion.registration_form_id + == args["registration_form_id"] + ) + .all() + ) - registration_form = db.session.query(RegistrationForm).filter( - RegistrationForm.id == args['registration_form_id']).first() + registration_form = ( + db.session.query(RegistrationForm) + .filter(RegistrationForm.id == args["registration_form_id"]) + .first() + ) event = event_repository.get_by_id(registration_form.event_id) - email_sent = self.send_confirmation(current_user, registration_questions, registration_answers, event) + email_sent = self.send_confirmation( + current_user, registration_questions, registration_answers, event + ) if email_sent: registration.confirmation_email_sent_at = date.today() db.session.commit() return 200 except Exception as e: - return 'Could not access DB', 400 + return "Could not access DB", 400 def send_confirmation(self, user, questions, answers, event): if answers is None: LOGGER.warn( - 'Found no answers associated with response with id {response_id}'.format(response_id=user.id)) + "Found no answers associated with response with id {response_id}".format( + response_id=user.id + ) + ) if questions is None: LOGGER.warn( - 'Found no questions associated with application form with id {form_id}'.format(form_id=user.id)) + "Found no questions associated with application form with id {form_id}".format( + form_id=user.id + ) + ) try: # Building the summary, where the summary is a dictionary whose key is the question headline, and the value # is the relevant answer @@ -234,14 +322,19 @@ def send_confirmation(self, user, questions, answers, event): for answer in answers: for question in questions: if answer.registration_question_id == question.id: - summary += "Question:" + question.headline + "\nAnswer:" + _get_answer_value( - answer, question) + "\n" + summary += ( + "Question:" + + question.headline + + "\nAnswer:" + + _get_answer_value(answer, question) + + "\n" + ) if len(summary) <= 0: - summary = '\nNo valid questions were answered' + summary = "\nNo valid questions were answered" emailer.email_user( - 'guest-registration-confirmation', + "guest-registration-confirmation", template_parameters=dict( summary=summary, ), @@ -252,77 +345,95 @@ def send_confirmation(self, user, questions, answers, event): return True except: - LOGGER.error('Could not send confirmation email for response with id : {response_id}'.format( - response_id=user.id)) + LOGGER.error( + "Could not send confirmation email for response with id : {response_id}".format( + response_id=user.id + ) + ) return False class GuestRegistrationFormAPI(GuestRegistrationFormMixin, restful.Resource): - option_fields = { - 'value': fields.String, - 'label': fields.String - } + option_fields = {"value": fields.String, "label": fields.String} registration_question_fields = { - 'id': fields.Integer, - 'description': fields.String, - 'headline': fields.String, - 'placeholder': fields.String, - 'validation_regex': fields.String, - 'validation_text': fields.String, - 'depends_on_question_id': fields.String, - 'hide_for_dependent_value': fields.String, - 'type': fields.String, - 'is_required': fields.Boolean, - 'order': fields.Integer, - 'options': fields.List(fields.Nested(option_fields)) + "id": fields.Integer, + "description": fields.String, + "headline": fields.String, + "placeholder": fields.String, + "validation_regex": fields.String, + "validation_text": fields.String, + "depends_on_question_id": fields.String, + "hide_for_dependent_value": fields.String, + "type": fields.String, + "is_required": fields.Boolean, + "order": fields.Integer, + "options": fields.List(fields.Nested(option_fields)), } registration_section_fields = { - 'id': fields.Integer, - 'name': fields.String, - 'description': fields.String, - 'order': fields.Integer, - 'registration_questions': fields.List(fields.Nested(registration_question_fields)) + "id": fields.Integer, + "name": fields.String, + "description": fields.String, + "order": fields.Integer, + "registration_questions": fields.List( + fields.Nested(registration_question_fields) + ), } registration_form_fields = { - 'id': fields.Integer, - 'event_id': fields.Integer, - 'registration_sections': fields.List(fields.Nested(registration_section_fields)) + "id": fields.Integer, + "event_id": fields.Integer, + "registration_sections": fields.List( + fields.Nested(registration_section_fields) + ), } @auth_required def get(self): args = self.req_parser.parse_args() - event_id = args['event_id'] + event_id = args["event_id"] try: - registration_form = db.session.query(RegistrationForm).filter( - RegistrationForm.event_id == event_id).first() + registration_form = ( + db.session.query(RegistrationForm) + .filter(RegistrationForm.event_id == event_id) + .first() + ) if not registration_form: return errors.REGISTRATION_FORM_NOT_FOUND - sections = (db.session.query(RegistrationSection) - .filter(RegistrationSection.registration_form_id == registration_form.id) - .filter(RegistrationSection.show_for_travel_award == None) - .filter(RegistrationSection.show_for_accommodation_award == None) - .filter(RegistrationSection.show_for_payment_required == None) - .all()) + sections = ( + db.session.query(RegistrationSection) + .filter( + RegistrationSection.registration_form_id == registration_form.id + ) + .filter(RegistrationSection.show_for_travel_award == None) + .filter(RegistrationSection.show_for_accommodation_award == None) + .filter(RegistrationSection.show_for_payment_required == None) + .all() + ) if not sections: LOGGER.warn( - 'Sections not found for event_id: {}'.format(args['event_id'])) + "Sections not found for event_id: {}".format(args["event_id"]) + ) return errors.SECTION_NOT_FOUND registration_form.registration_sections = sections - questions = db.session.query(RegistrationQuestion).filter( - RegistrationQuestion.registration_form_id == registration_form.id).all() + questions = ( + db.session.query(RegistrationQuestion) + .filter( + RegistrationQuestion.registration_form_id == registration_form.id + ) + .all() + ) if not questions: LOGGER.warn( - 'Questions not found for event_id: {}'.format(args['event_id'])) + "Questions not found for event_id: {}".format(args["event_id"]) + ) return errors.QUESTION_NOT_FOUND for s in registration_form.registration_sections: @@ -337,6 +448,5 @@ def get(self): LOGGER.error("Database error encountered: {}".format(e)) return errors.DB_NOT_AVAILABLE except: - LOGGER.error("Encountered unknown error: {}".format( - traceback.format_exc())) + LOGGER.error("Encountered unknown error: {}".format(traceback.format_exc())) return errors.DB_NOT_AVAILABLE diff --git a/api/app/guestRegistrations/mixins.py b/api/app/guestRegistrations/mixins.py index 59302080f..1048bb9fb 100644 --- a/api/app/guestRegistrations/mixins.py +++ b/api/app/guestRegistrations/mixins.py @@ -3,11 +3,11 @@ class GuestRegistrationMixin(object): req_parser = reqparse.RequestParser() - req_parser.add_argument('guest_registration_id', type=int, required=False) - req_parser.add_argument('registration_form_id', type=int, required=True) - req_parser.add_argument('answers', type=list, required=True, location='json') + req_parser.add_argument("guest_registration_id", type=int, required=False) + req_parser.add_argument("registration_form_id", type=int, required=True) + req_parser.add_argument("answers", type=list, required=True, location="json") class GuestRegistrationFormMixin(object): req_parser = reqparse.RequestParser() - req_parser.add_argument('event_id', type=int, required=True) \ No newline at end of file + req_parser.add_argument("event_id", type=int, required=True) diff --git a/api/app/guestRegistrations/repository.py b/api/app/guestRegistrations/repository.py index 2829876e5..90238890a 100644 --- a/api/app/guestRegistrations/repository.py +++ b/api/app/guestRegistrations/repository.py @@ -1,86 +1,101 @@ -from app import db -from app.invitedGuest.models import InvitedGuest, GuestRegistration -from app.users.models import AppUser +from sqlalchemy import Date, and_, cast, func +from sqlalchemy.sql import exists + +from app import LOGGER, db +from app.attendance.models import Attendance from app.events.models import Event +from app.invitedGuest.models import GuestRegistration, InvitedGuest from app.registration.models import RegistrationForm -from app.attendance.models import Attendance -from sqlalchemy.sql import exists -from app import LOGGER -from sqlalchemy import and_, func, cast, Date +from app.users.models import AppUser -class GuestRegistrationRepository(): +class GuestRegistrationRepository: @staticmethod def get_all_unsigned_guests(event_id): """Get all guests who have not signed in""" - stmt = ~ exists().where(Attendance.user_id == AppUser.id) - return db.session.query(InvitedGuest, AppUser,GuestRegistration).join( - AppUser, InvitedGuest.user_id == AppUser.id - ).filter( - InvitedGuest.event_id == event_id - ).outerjoin( - GuestRegistration, InvitedGuest.user_id == GuestRegistration.user_id - ).filter(stmt).all() + stmt = ~exists().where(Attendance.user_id == AppUser.id) + return ( + db.session.query(InvitedGuest, AppUser, GuestRegistration) + .join(AppUser, InvitedGuest.user_id == AppUser.id) + .filter(InvitedGuest.event_id == event_id) + .outerjoin( + GuestRegistration, InvitedGuest.user_id == GuestRegistration.user_id + ) + .filter(stmt) + .all() + ) @staticmethod def get_all_guests(event_id): """Get all guests.""" - return db.session.query(InvitedGuest, AppUser,GuestRegistration).join( - AppUser, InvitedGuest.user_id == AppUser.id - ).filter( - InvitedGuest.event_id == event_id - ).outerjoin( - GuestRegistration, InvitedGuest.user_id == GuestRegistration.user_id - ).all() + return ( + db.session.query(InvitedGuest, AppUser, GuestRegistration) + .join(AppUser, InvitedGuest.user_id == AppUser.id) + .filter(InvitedGuest.event_id == event_id) + .outerjoin( + GuestRegistration, InvitedGuest.user_id == GuestRegistration.user_id + ) + .all() + ) @staticmethod def get_confirmed_guest_for_event(event_id, confirmed): """Get confirmed guests.""" - return db.session.query(GuestRegistration, InvitedGuest, AppUser).filter( - GuestRegistration.confirmed == confirmed - ).join( - InvitedGuest, InvitedGuest.user_id == GuestRegistration.user_id - ).filter( - InvitedGuest.event_id == event_id - ).join( - AppUser, InvitedGuest.user_id == AppUser.id - ).all() + return ( + db.session.query(GuestRegistration, InvitedGuest, AppUser) + .filter(GuestRegistration.confirmed == confirmed) + .join(InvitedGuest, InvitedGuest.user_id == GuestRegistration.user_id) + .filter(InvitedGuest.event_id == event_id) + .join(AppUser, InvitedGuest.user_id == AppUser.id) + .all() + ) @staticmethod def get_unsigned_in_comfirmed_guest_attendees(event_id, confirmed): """Get guests who have confirmed they will attend and have not already signed in.""" - stmt = ~ exists().where(Attendance.user_id == AppUser.user_id) - return db.session.query(GuestRegistration, InvitedGuest, AppUser).filter( - GuestRegistration.confirmed == confirmed - ).join( - InvitedGuest, InvitedGuest.user_id == GuestRegistration.user_id - ).filter( - InvitedGuest.event_id == event_id - ).join( - AppUser, InvitedGuest.user_id == AppUser.id - ).filter(stmt).all() + stmt = ~exists().where(Attendance.user_id == AppUser.user_id) + return ( + db.session.query(GuestRegistration, InvitedGuest, AppUser) + .filter(GuestRegistration.confirmed == confirmed) + .join(InvitedGuest, InvitedGuest.user_id == GuestRegistration.user_id) + .filter(InvitedGuest.event_id == event_id) + .join(AppUser, InvitedGuest.user_id == AppUser.id) + .filter(stmt) + .all() + ) @staticmethod def count_guests(event_id): - count = (db.session.query(InvitedGuest) - .filter_by(event_id=event_id) - .count()) + count = db.session.query(InvitedGuest).filter_by(event_id=event_id).count() return count @staticmethod def count_registered_guests(event_id): - count = (db.session.query(GuestRegistration) - .join(RegistrationForm, GuestRegistration.registration_form_id == RegistrationForm.id) - .filter_by(event_id=event_id) - .count()) + count = ( + db.session.query(GuestRegistration) + .join( + RegistrationForm, + GuestRegistration.registration_form_id == RegistrationForm.id, + ) + .filter_by(event_id=event_id) + .count() + ) return count - + @staticmethod def timeseries_guest_registrations(event_id): - timeseries = (db.session.query(cast(GuestRegistration.created_at, Date), func.count(GuestRegistration.created_at)) - .join(RegistrationForm, GuestRegistration.registration_form_id == RegistrationForm.id) - .filter_by(event_id=event_id) - .group_by(cast(GuestRegistration.created_at, Date)) - .order_by(cast(GuestRegistration.created_at, Date)) - .all()) + timeseries = ( + db.session.query( + cast(GuestRegistration.created_at, Date), + func.count(GuestRegistration.created_at), + ) + .join( + RegistrationForm, + GuestRegistration.registration_form_id == RegistrationForm.id, + ) + .filter_by(event_id=event_id) + .group_by(cast(GuestRegistration.created_at, Date)) + .order_by(cast(GuestRegistration.created_at, Date)) + .all() + ) return timeseries diff --git a/api/app/guestRegistrations/tests.py b/api/app/guestRegistrations/tests.py index 5d00422d4..3e3084573 100644 --- a/api/app/guestRegistrations/tests.py +++ b/api/app/guestRegistrations/tests.py @@ -1,41 +1,41 @@ -from app.registration.models import RegistrationForm -from app.registration.models import RegistrationSection import json from datetime import datetime, timedelta -from app.utils.testing import ApiTestCase -from app.users.models import AppUser, UserCategory, Country -from app.events.models import Event -from app.registration.models import RegistrationQuestion -from app import app, db -from app import db, LOGGER +from app import LOGGER, app, db +from app.events.models import Event from app.organisation.models import Organisation +from app.registration.models import ( + RegistrationForm, + RegistrationQuestion, + RegistrationSection, +) +from app.users.models import AppUser, Country, UserCategory +from app.utils.testing import ApiTestCase class GuestRegistrationApiTest(ApiTestCase): - def seed_static_data(self): - test_user = self.add_user('something@email.com') - event_admin = self.add_user('event_admin@ea.com') - - self.add_organisation('Deep Learning Indaba', 'blah.png', 'blah_big.png', 'deeplearningindaba') - db.session.add(UserCategory('Postdoc')) - db.session.add(Country('South Africa')) + test_user = self.add_user("something@email.com") + event_admin = self.add_user("event_admin@ea.com") + + self.add_organisation( + "Deep Learning Indaba", "blah.png", "blah_big.png", "deeplearningindaba" + ) + db.session.add(UserCategory("Postdoc")) + db.session.add(Country("South Africa")) db.session.commit() event = self.add_event( - name={'en': "Tech Talk"}, - description={'en': "tech talking"}, + name={"en": "Tech Talk"}, + description={"en": "tech talking"}, start_date=datetime(2019, 12, 12, 10, 10, 10), end_date=datetime(2020, 12, 12, 10, 10, 10), - key='REGINAL' + key="REGINAL", ) db.session.commit() - self.form = RegistrationForm( - event_id=event.id - ) + self.form = RegistrationForm(event_id=event.id) db.session.add(self.form) db.session.commit() self.form_id = self.form.id @@ -76,7 +76,7 @@ def seed_static_data(self): placeholder="the placeholder", headline="the headline", validation_regex="[]/", - validation_text=" text" + validation_text=" text", ) db.session.add(self.question) db.session.commit() @@ -92,7 +92,7 @@ def seed_static_data(self): placeholder="the placeholder", headline="the headline", validation_regex="[]/", - validation_text=" text" + validation_text=" text", ) db.session.add(self.question2) db.session.commit() @@ -108,7 +108,7 @@ def seed_static_data(self): placeholder="the placeholder", headline="the headline", validation_regex="[]/", - validation_text=" text" + validation_text=" text", ) db.session.add(self.question3) db.session.commit() @@ -119,146 +119,133 @@ def seed_static_data(self): self.headers = self.get_auth_header_for("something@email.com") self.adminHeaders = self.get_auth_header_for("event_admin@ea.com") - self.add_email_template('guest-registration-confirmation') - self.add_email_template('guest-invitation-with-registration') - self.add_email_template('guest-invitation') - self.add_email_template('new-guest-registration') - self.add_email_template('new-guest-no-registration') + self.add_email_template("guest-registration-confirmation") + self.add_email_template("guest-invitation-with-registration") + self.add_email_template("guest-invitation") + self.add_email_template("new-guest-registration") + self.add_email_template("new-guest-no-registration") def get_auth_header_for(self, email): - body = { - 'email': email, - 'password': 'abc' - } - response = self.app.post('api/v1/authenticate', data=body) + body = {"email": email, "password": "abc"} + response = self.app.post("api/v1/authenticate", data=body) data = json.loads(response.data) - header = {'Authorization': data['token']} + header = {"Authorization": data["token"]} return header def test_create_registration(self): self.seed_static_data() registration_data = { - 'registration_form_id': self.form_id, - 'answers': [ + "registration_form_id": self.form_id, + "answers": [ + {"registration_question_id": self.question_id, "value": "Answer 1"}, { - 'registration_question_id': self.question_id, - 'value': 'Answer 1' + "registration_question_id": self.question2_id, + "value": "Hello world, this is the 2nd answer.", }, { - 'registration_question_id': self.question2_id, - 'value': 'Hello world, this is the 2nd answer.' + "registration_question_id": self.question3_id, + "value": "Hello world, this is the 3rd answer.", }, - { - 'registration_question_id': self.question3_id, - 'value': 'Hello world, this is the 3rd answer.' - } - ] + ], } response = self.app.post( - '/api/v1/guest-registration', + "/api/v1/guest-registration", data=json.dumps(registration_data), - content_type='application/json', - headers=self.headers) + content_type="application/json", + headers=self.headers, + ) self.assertEqual(response.status_code, 201) def test_get_registration(self): self.seed_static_data() registration_data = { - 'registration_form_id': self.form_id, - 'answers': [ + "registration_form_id": self.form_id, + "answers": [ + {"registration_question_id": self.question_id, "value": "Answer 1"}, { - 'registration_question_id': self.question_id, - 'value': 'Answer 1' + "registration_question_id": self.question2_id, + "value": "Hello world, this is the 2nd answer.", }, { - 'registration_question_id': self.question2_id, - 'value': 'Hello world, this is the 2nd answer.' + "registration_question_id": self.question3_id, + "value": "Hello world, this is the 3rd answer.", }, - { - 'registration_question_id': self.question3_id, - 'value': 'Hello world, this is the 3rd answer.' - } - ] + ], } response = self.app.post( - '/api/v1/guest-registration', + "/api/v1/guest-registration", data=json.dumps(registration_data), - content_type='application/json', - headers=self.headers + content_type="application/json", + headers=self.headers, ) LOGGER.debug("hi: {}".format(response.data)) response = self.app.get( - '/api/v1/guest-registration', - content_type='application/json', - headers=self.headers) + "/api/v1/guest-registration", + content_type="application/json", + headers=self.headers, + ) self.assertEqual(response.status_code, 200) def test_update_200(self): """Test if update work""" self.seed_static_data() registration_data = { - 'registration_form_id': self.form_id, - 'answers': [ + "registration_form_id": self.form_id, + "answers": [ + {"registration_question_id": self.question_id, "value": "Answer 1"}, { - 'registration_question_id': self.question_id, - 'value': 'Answer 1' + "registration_question_id": self.question2_id, + "value": "Hello world, this is the 2nd answer.", }, { - 'registration_question_id': self.question2_id, - 'value': 'Hello world, this is the 2nd answer.' + "registration_question_id": self.question3_id, + "value": "Hello world, this is the 3rd answer.", }, - { - 'registration_question_id': self.question3_id, - 'value': 'Hello world, this is the 3rd answer.' - } - ] + ], } response = self.app.post( - '/api/v1/guest-registration', + "/api/v1/guest-registration", data=json.dumps(registration_data), - content_type='application/json', - headers=self.headers + content_type="application/json", + headers=self.headers, ) data = json.loads(response.data) - LOGGER.debug( - "Reg-form: {}".format(data)) + LOGGER.debug("Reg-form: {}".format(data)) put_registration_data = { - 'guest_registration_id': data['id'], - 'registration_form_id': self.form_id, - 'answers': [ + "guest_registration_id": data["id"], + "registration_form_id": self.form_id, + "answers": [ + {"registration_question_id": self.question_id, "value": "Answer Other"}, { - 'registration_question_id': self.question_id, - 'value': 'Answer Other' + "registration_question_id": self.question2_id, + "value": "Hello world, this is the 2nd answer.", }, { - 'registration_question_id': self.question2_id, - 'value': 'Hello world, this is the 2nd answer.' + "registration_question_id": self.question3_id, + "value": "Hello world, this is the 3rd answer.", }, - { - 'registration_question_id': self.question3_id, - 'value': 'Hello world, this is the 3rd answer.' - } - ] + ], } post_response = self.app.put( - '/api/v1/guest-registration', + "/api/v1/guest-registration", data=json.dumps(put_registration_data), - content_type='application/json', - headers=self.headers) + content_type="application/json", + headers=self.headers, + ) - LOGGER.debug( - "put response: {}".format(post_response)) + LOGGER.debug("put response: {}".format(post_response)) self.assertEqual(post_response.status_code, 200) response = self.app.get( - '/api/v1/guest-registration', - content_type='application/json', - headers=self.headers) + "/api/v1/guest-registration", + content_type="application/json", + headers=self.headers, + ) updated_data = json.loads(response.data) - self.assertEqual(updated_data['answers'][0]['value'], "Answer Other") + self.assertEqual(updated_data["answers"][0]["value"], "Answer Other") def test_get_form(self): self.seed_static_data() @@ -269,39 +256,37 @@ def test_get_form(self): if response.status_code == 403: return - LOGGER.debug( - "form: {}".format(json.loads(response.data))) + LOGGER.debug("form: {}".format(json.loads(response.data))) form = json.loads(response.data) - self.assertEqual(form['registration_sections'][0]['registration_questions'][0]['type'], 'short-text') - self.assertEqual(form['registration_sections'][0]['name'], 'Section 1') + self.assertEqual( + form["registration_sections"][0]["registration_questions"][0]["type"], + "short-text", + ) + self.assertEqual(form["registration_sections"][0]["name"], "Section 1") def test_if_user_is_guest(self): self.seed_static_data() - USER_DATA = { - 'email': 'something@email.com', - 'role': 'mentor', - 'event_id': 1 - } + USER_DATA = {"email": "something@email.com", "role": "mentor", "event_id": 1} response = self.app.post( - '/api/v1/invitedGuest', data=USER_DATA, headers=self.headers) + "/api/v1/invitedGuest", data=USER_DATA, headers=self.headers + ) response_guest = self.app.get( - '/api/v1/checkIfInvitedGuest?event_id=%d' % self.event_id, headers=self.headers) - LOGGER.debug( - "guest response: {}".format(response_guest)) + "/api/v1/checkIfInvitedGuest?event_id=%d" % self.event_id, + headers=self.headers, + ) + LOGGER.debug("guest response: {}".format(response_guest)) self.assertEqual(response_guest.status_code, 200) def test_if_user_is_not_guest(self): self.seed_static_data() - USER_DATA = { - 'email': 'some@email.com', - 'role': 'mentor', - 'event_id': 1 - } + USER_DATA = {"email": "some@email.com", "role": "mentor", "event_id": 1} response = self.app.post( - '/api/v1/invitedGuest', data=USER_DATA, headers=self.headers) + "/api/v1/invitedGuest", data=USER_DATA, headers=self.headers + ) response_guest = self.app.get( - '/api/v1/checkIfInvitedGuest?event_id=%d' % self.event_id, headers=self.headers) - LOGGER.debug( - "guest response: {}".format(response_guest)) + "/api/v1/checkIfInvitedGuest?event_id=%d" % self.event_id, + headers=self.headers, + ) + LOGGER.debug("guest response: {}".format(response_guest)) self.assertEqual(response_guest.status_code, 404) diff --git a/api/app/integration_tests/api.py b/api/app/integration_tests/api.py index 5e0009e2d..15232bb87 100644 --- a/api/app/integration_tests/api.py +++ b/api/app/integration_tests/api.py @@ -1,17 +1,23 @@ -from app.users.models import AppUser -from app import db, app, LOGGER import flask_restful as restful -from app.utils.errors import USER_NOT_FOUND,FAILED_CREATE_INTEGRATION_TEST_USER, FAILED_DELETE_INTEGRATION_TEST_USER + +from app import LOGGER, app, db from app.integration_tests.mixins import IntegratoonTestDelete +from app.users.models import AppUser +from app.utils.errors import ( + FAILED_CREATE_INTEGRATION_TEST_USER, + FAILED_DELETE_INTEGRATION_TEST_USER, + USER_NOT_FOUND, +) + def user_info(user): return { - 'id': user.id, + "id": user.id, "email": user.email, - 'firstname': user.firstname, - 'lastname': user.lastname, - 'title': user.user_title, - 'is_admin': user.is_admin, + "firstname": user.firstname, + "lastname": user.lastname, + "title": user.user_title, + "is_admin": user.is_admin, } @@ -20,43 +26,43 @@ class CreateIntegrationUser(restful.Resource): # Create test user for integation tests def get(self): try: - user = AppUser.query.filter_by(email='john@thewall.com').first() - - if(user is None): - user = AppUser(email="john@thewall.com", - firstname="John", - lastname="Snow", - user_title="Mr", - password="whitewalker360", - organisation_id=4, - is_admin=True) + user = AppUser.query.filter_by(email="john@thewall.com").first() + + if user is None: + user = AppUser( + email="john@thewall.com", + firstname="John", + lastname="Snow", + user_title="Mr", + password="whitewalker360", + organisation_id=4, + is_admin=True, + ) user.verify() db.session.add(user) db.session.commit() except Exception as e: - LOGGER.error( - 'Failed to create test user {} due to: {}'.format(user, e)) + LOGGER.error("Failed to create test user {} due to: {}".format(user, e)) return FAILED_CREATE_INTEGRATION_TEST_USER return user_info(user) -class DeleteIntegrationUser(IntegratoonTestDelete,restful.Resource): +class DeleteIntegrationUser(IntegratoonTestDelete, restful.Resource): # Delete test user for integation tests def post(self): try: user = None args = self.req_parser.parse_args() - user = AppUser.query.filter_by(email=args['email']).first() + user = AppUser.query.filter_by(email=args["email"]).first() # Already deleted - if(user is None): + if user is None: return USER_NOT_FOUND db.session.delete(user) db.session.commit() except Exception as e: - LOGGER.error( - 'Failed to delete test user {} due to: {}'.format(user, e)) + LOGGER.error("Failed to delete test user {} due to: {}".format(user, e)) return FAILED_DELETE_INTEGRATION_TEST_USER return 200 diff --git a/api/app/integration_tests/mixins.py b/api/app/integration_tests/mixins.py index 030ce9643..e857fedd2 100644 --- a/api/app/integration_tests/mixins.py +++ b/api/app/integration_tests/mixins.py @@ -1,6 +1,7 @@ from flask_restful import reqparse + class IntegratoonTestDelete(object): req_parser = reqparse.RequestParser() - req_parser.add_argument('email', type=str, required=True) \ No newline at end of file + req_parser.add_argument("email", type=str, required=True) diff --git a/api/app/invitationletter/api.py b/api/app/invitationletter/api.py index a5e8bc726..427abf74c 100644 --- a/api/app/invitationletter/api.py +++ b/api/app/invitationletter/api.py @@ -1,64 +1,82 @@ from datetime import datetime + import flask_restful as restful -from sqlalchemy.exc import IntegrityError -from app.utils.auth import verify_token from flask import g, request -from app.invitationletter.models import InvitationTemplate -from app.registration.models import Offer, Registration, RegistrationAnswer, RegistrationQuestion, Registration, RegistrationForm -from app.invitationletter.mixins import InvitationMixin -from app.invitationletter.models import InvitationLetterRequest +from sqlalchemy.exc import IntegrityError + +from app import LOGGER, db +from app.events.repository import EventRepository from app.invitationletter.generator import generate +from app.invitationletter.mixins import InvitationMixin +from app.invitationletter.models import InvitationLetterRequest, InvitationTemplate +from app.invitedGuest.models import GuestRegistration +from app.registration.models import ( + Offer, + Registration, + RegistrationAnswer, + RegistrationForm, + RegistrationQuestion, +) from app.users.models import AppUser, Country -from app import db, LOGGER from app.utils import errors -from app.utils.auth import auth_required -from app.events.repository import EventRepository -from app.invitedGuest.models import GuestRegistration +from app.utils.auth import auth_required, verify_token def invitation_info(invitation_request): return { - 'invitation_letter_request_id': invitation_request.id, + "invitation_letter_request_id": invitation_request.id, } class InvitationLetterAPI(InvitationMixin, restful.Resource): - @auth_required def post(self): # Process arguments args = self.req_parser.parse_args() - event_id = args['event_id'] - work_address = args['work_address'] if args['work_address'] is not None else ' ' - addressed_to = args['addressed_to'] or 'Whom it May Concern' - residential_address = args['residential_address'] - passport_name = args['passport_name'] - passport_no = args['passport_no'] - passport_issued_by = args['passport_issued_by'] - passport_expiry_date = datetime.strptime((args['passport_expiry_date']), '%Y-%m-%d') + event_id = args["event_id"] + work_address = args["work_address"] if args["work_address"] is not None else " " + addressed_to = args["addressed_to"] or "Whom it May Concern" + residential_address = args["residential_address"] + passport_name = args["passport_name"] + passport_no = args["passport_no"] + passport_issued_by = args["passport_issued_by"] + passport_expiry_date = datetime.strptime( + (args["passport_expiry_date"]), "%Y-%m-%d" + ) registration_event = EventRepository.get_by_id(event_id) - if(registration_event is not None): + if registration_event is not None: to_date = registration_event.end_date from_date = registration_event.start_date else: return errors.EVENT_ID_NOT_FOUND # Finding registation_id for this user at this event - user_id = verify_token(request.headers.get('Authorization'))['id'] - offer = db.session.query(Offer).filter( - Offer.user_id == user_id).filter(Offer.event_id == event_id).first() - registration_form = db.session.query(RegistrationForm).filter( - RegistrationForm.event_id == event_id).first() + user_id = verify_token(request.headers.get("Authorization"))["id"] + offer = ( + db.session.query(Offer) + .filter(Offer.user_id == user_id) + .filter(Offer.event_id == event_id) + .first() + ) + registration_form = ( + db.session.query(RegistrationForm) + .filter(RegistrationForm.event_id == event_id) + .first() + ) if not registration_form: return errors.REGISTRATION_FORM_NOT_FOUND - + # Check if Guest Registration registration = None - registration = db.session.query(GuestRegistration).filter( - GuestRegistration.user_id == user_id).filter(GuestRegistration.registration_form_id == registration_form.id).first() + registration = ( + db.session.query(GuestRegistration) + .filter(GuestRegistration.user_id == user_id) + .filter(GuestRegistration.registration_form_id == registration_form.id) + .first() + ) if registration: is_guest_registration = True else: @@ -66,14 +84,17 @@ def post(self): # Normal Registration if (not registration) and offer: - registration = db.session.query(Registration).filter( - Registration.offer_id == offer.id).first() + registration = ( + db.session.query(Registration) + .filter(Registration.offer_id == offer.id) + .first() + ) if not registration: return errors.REGISTRATION_NOT_FOUND try: - if(is_guest_registration): + if is_guest_registration: invitation_letter_request = InvitationLetterRequest( guest_registration_id=registration.id, event_id=event_id, @@ -85,7 +106,7 @@ def post(self): passport_issued_by=passport_issued_by, passport_expiry_date=passport_expiry_date, to_date=to_date, - from_date=from_date + from_date=from_date, ) else: invitation_letter_request = InvitationLetterRequest( @@ -99,16 +120,20 @@ def post(self): passport_issued_by=passport_issued_by, passport_expiry_date=passport_expiry_date, to_date=to_date, - from_date=from_date + from_date=from_date, ) db.session.add(invitation_letter_request) db.session.commit() except Exception as e: - LOGGER.error('Failed to add invitation letter request for user id {} due to: {}'.format(user_id, e)) + LOGGER.error( + "Failed to add invitation letter request for user id {} due to: {}".format( + user_id, e + ) + ) return errors.ADD_INVITATION_REQUEST_FAILED invitation_template = None - if (is_guest_registration): + if is_guest_registration: invitation_template = ( db.session.query(InvitationTemplate) .filter(InvitationTemplate.send_for_both_travel_accommodation == False) @@ -116,21 +141,34 @@ def post(self): .filter(InvitationTemplate.send_for_accommodation_award_only == False) .first() ) - elif (offer.accommodation_award and offer.accepted_accommodation_award - and offer.travel_award and offer.accepted_travel_award): - invitation_template = db.session.query(InvitationTemplate).filter( - InvitationTemplate.event_id == offer.event_id).filter( - InvitationTemplate.send_for_both_travel_accommodation == True).first() - - elif (offer.travel_award and offer.accepted_travel_award): - invitation_template = db.session.query(InvitationTemplate).filter( - InvitationTemplate.event_id == offer.event_id).filter( - InvitationTemplate.send_for_travel_award_only == True).first() - - elif (offer.accommodation_award and offer.accepted_accommodation_award): - invitation_template = db.session.query(InvitationTemplate).filter( - InvitationTemplate.event_id == offer.event_id).filter( - InvitationTemplate.send_for_accommodation_award_only == True).first() + elif ( + offer.accommodation_award + and offer.accepted_accommodation_award + and offer.travel_award + and offer.accepted_travel_award + ): + invitation_template = ( + db.session.query(InvitationTemplate) + .filter(InvitationTemplate.event_id == offer.event_id) + .filter(InvitationTemplate.send_for_both_travel_accommodation == True) + .first() + ) + + elif offer.travel_award and offer.accepted_travel_award: + invitation_template = ( + db.session.query(InvitationTemplate) + .filter(InvitationTemplate.event_id == offer.event_id) + .filter(InvitationTemplate.send_for_travel_award_only == True) + .first() + ) + + elif offer.accommodation_award and offer.accepted_accommodation_award: + invitation_template = ( + db.session.query(InvitationTemplate) + .filter(InvitationTemplate.event_id == offer.event_id) + .filter(InvitationTemplate.send_for_accommodation_award_only == True) + .first() + ) else: invitation_template = ( @@ -140,12 +178,20 @@ def post(self): .filter(InvitationTemplate.send_for_accommodation_award_only == False) .first() ) - + template_url = invitation_template.template_path - user = db.session.query(AppUser).filter(AppUser.id==user_id).first() - country_of_residence = db.session.query(Country).filter(Country.id == user.residence_country_id).first() - nationality = db.session.query(Country).filter(Country.id == user.nationality_country_id).first() + user = db.session.query(AppUser).filter(AppUser.id == user_id).first() + country_of_residence = ( + db.session.query(Country) + .filter(Country.id == user.residence_country_id) + .first() + ) + nationality = ( + db.session.query(Country) + .filter(Country.id == user.nationality_country_id) + .first() + ) if user.user_dateOfBirth is not None: date_of_birth = user.user_dateOfBirth.strftime("%Y-%m-%d") @@ -154,43 +200,57 @@ def post(self): # Poster registration bringing_poster = "" - poster_registration_question = db.session.query(RegistrationQuestion).filter(RegistrationQuestion.headline == "Will you be bringing a poster?").first() + poster_registration_question = ( + db.session.query(RegistrationQuestion) + .filter(RegistrationQuestion.headline == "Will you be bringing a poster?") + .first() + ) if poster_registration_question is not None: poster_answer = ( db.session.query(RegistrationAnswer) - .join(Registration, RegistrationAnswer.registration_id == Registration.id) + .join( + Registration, RegistrationAnswer.registration_id == Registration.id + ) .join(Offer, Offer.id == Registration.offer_id) .filter(Offer.user_id == user_id) - .filter(RegistrationAnswer.registration_question_id == poster_registration_question.id) + .filter( + RegistrationAnswer.registration_question_id + == poster_registration_question.id + ) .first() ) - if poster_answer is not None and poster_answer.value == 'yes': - bringing_poster = "The participant will be presenting a poster of their research." + if poster_answer is not None and poster_answer.value == "yes": + bringing_poster = ( + "The participant will be presenting a poster of their research." + ) # Handling fields - invitation_letter_request.invitation_letter_sent_at=datetime.now() - is_sent = generate(template_path=template_url, - event_id=event_id, - work_address=work_address, - addressed_to=addressed_to, - residential_address=residential_address, - passport_name=passport_name, - passport_no=passport_no, - passport_issued_by=passport_issued_by, - invitation_letter_sent_at=invitation_letter_request.invitation_letter_sent_at.strftime("%Y-%m-%d"), - expiry_date=passport_expiry_date.strftime("%Y-%m-%d"), - to_date=to_date.strftime("%Y-%m-%d"), - from_date=from_date.strftime("%Y-%m-%d"), - country_of_residence=country_of_residence.name, - nationality=nationality.name, - date_of_birth=date_of_birth, - email=user.email, - user_title=user.user_title, - firstname=user.firstname, - lastname=user.lastname, - bringing_poster=bringing_poster, - user=user - ) + invitation_letter_request.invitation_letter_sent_at = datetime.now() + is_sent = generate( + template_path=template_url, + event_id=event_id, + work_address=work_address, + addressed_to=addressed_to, + residential_address=residential_address, + passport_name=passport_name, + passport_no=passport_no, + passport_issued_by=passport_issued_by, + invitation_letter_sent_at=invitation_letter_request.invitation_letter_sent_at.strftime( + "%Y-%m-%d" + ), + expiry_date=passport_expiry_date.strftime("%Y-%m-%d"), + to_date=to_date.strftime("%Y-%m-%d"), + from_date=from_date.strftime("%Y-%m-%d"), + country_of_residence=country_of_residence.name, + nationality=nationality.name, + date_of_birth=date_of_birth, + email=user.email, + user_title=user.user_title, + firstname=user.firstname, + lastname=user.lastname, + bringing_poster=bringing_poster, + user=user, + ) if not is_sent: return errors.SENDING_INVITATION_FAILED @@ -200,6 +260,8 @@ def post(self): except Exception as e: LOGGER.error( - "Failed to add invitation request for user with email: {} due to {}".format(user.email, e)) + "Failed to add invitation request for user with email: {} due to {}".format( + user.email, e + ) + ) return errors.ADD_INVITATION_REQUEST_FAILED - diff --git a/api/app/invitationletter/generator.py b/api/app/invitationletter/generator.py index aa22e11a7..07f2db075 100644 --- a/api/app/invitationletter/generator.py +++ b/api/app/invitationletter/generator.py @@ -1,83 +1,155 @@ -from mailmerge import MailMerge -from app import LOGGER -from config import GCP_CREDENTIALS_DICT, GCP_PROJECT_NAME, GCP_BUCKET_NAME, FILE_SIZE_LIMIT -from config import GCP_BUCKET_NAME import json -from google.cloud import storage import os -from app.utils import emailer -from app.utils import pdfconvertor -from app.events.models import Event -from app import db -from app.utils import errors +from google.cloud import storage from google.oauth2 import service_account +from mailmerge import MailMerge from six import string_types +from app import LOGGER, db +from app.events.models import Event +from app.utils import emailer, errors, pdfconvertor +from config import ( + FILE_SIZE_LIMIT, + GCP_BUCKET_NAME, + GCP_CREDENTIALS_DICT, + GCP_PROJECT_NAME, +) + def download_blob(bucket_name, source_blob_name, destination_file_name): """Downloads a blob from the bucket.""" - if GCP_CREDENTIALS_DICT['private_key'] == "dummy": - LOGGER.debug('Setting dummy storage client') + if GCP_CREDENTIALS_DICT["private_key"] == "dummy": + LOGGER.debug("Setting dummy storage client") storage_client = storage.Client(project=GCP_PROJECT_NAME) else: - LOGGER.debug('Setting GCP storage client') + LOGGER.debug("Setting GCP storage client") credentials = service_account.Credentials.from_service_account_info( GCP_CREDENTIALS_DICT ) - storage_client = storage.Client(credentials=credentials, project=GCP_PROJECT_NAME) + storage_client = storage.Client( + credentials=credentials, project=GCP_PROJECT_NAME + ) bucket = storage_client.get_bucket(bucket_name) blob = bucket.blob(source_blob_name) blob.download_to_filename(destination_file_name) - print(('Blob {} downloaded to {}.'.format( - source_blob_name, - destination_file_name))) + print(("Blob {} downloaded to {}.".format(source_blob_name, destination_file_name))) return source_blob_name -def check_values(template_path, event_id, work_address, addressed_to, residential_address, passport_name, - passport_no, passport_issued_by, invitation_letter_sent_at, to_date, from_date, country_of_residence, - nationality, date_of_birth, email, user_title, firstname, lastname, bringing_poster, expiry_date): - assert template_path is not None and isinstance(template_path, string_types) - assert event_id is not None and isinstance(event_id, int) - assert work_address is not None and isinstance(work_address, string_types) - assert addressed_to is not None and isinstance(addressed_to, string_types) - assert residential_address is not None and isinstance(residential_address, string_types) - assert passport_name is not None and isinstance(passport_name, string_types) - assert passport_no is not None and isinstance(passport_no, string_types) - assert passport_issued_by is not None and isinstance(passport_issued_by, string_types) - assert invitation_letter_sent_at is not None and isinstance(invitation_letter_sent_at, string_types) - assert to_date is not None and isinstance(to_date, string_types) - assert from_date is not None and isinstance(from_date, string_types) - assert country_of_residence is not None and isinstance(country_of_residence, string_types) - assert nationality is not None and isinstance(nationality, string_types) - assert date_of_birth is not None and isinstance(date_of_birth, string_types) - assert email is not None and isinstance(email, string_types) - assert user_title is not None and isinstance(user_title, string_types) - assert firstname is not None and isinstance(firstname, string_types) - assert lastname is not None and isinstance(lastname, string_types) - assert bringing_poster is not None and isinstance(bringing_poster, string_types) - assert expiry_date is not None and isinstance(expiry_date, string_types) - - -def generate(template_path, event_id, work_address, addressed_to, residential_address, passport_name, - passport_no, passport_issued_by, invitation_letter_sent_at, to_date, from_date, country_of_residence, - nationality, date_of_birth, email, user_title, firstname, lastname, bringing_poster, expiry_date, user): - - check_values(template_path, event_id, work_address, addressed_to, residential_address, passport_name, - passport_no, passport_issued_by, invitation_letter_sent_at, to_date, from_date, country_of_residence, - nationality, date_of_birth, email, user_title, firstname, lastname, bringing_poster, expiry_date) - + +def check_values( + template_path, + event_id, + work_address, + addressed_to, + residential_address, + passport_name, + passport_no, + passport_issued_by, + invitation_letter_sent_at, + to_date, + from_date, + country_of_residence, + nationality, + date_of_birth, + email, + user_title, + firstname, + lastname, + bringing_poster, + expiry_date, +): + assert template_path is not None and isinstance(template_path, string_types) + assert event_id is not None and isinstance(event_id, int) + assert work_address is not None and isinstance(work_address, string_types) + assert addressed_to is not None and isinstance(addressed_to, string_types) + assert residential_address is not None and isinstance( + residential_address, string_types + ) + assert passport_name is not None and isinstance(passport_name, string_types) + assert passport_no is not None and isinstance(passport_no, string_types) + assert passport_issued_by is not None and isinstance( + passport_issued_by, string_types + ) + assert invitation_letter_sent_at is not None and isinstance( + invitation_letter_sent_at, string_types + ) + assert to_date is not None and isinstance(to_date, string_types) + assert from_date is not None and isinstance(from_date, string_types) + assert country_of_residence is not None and isinstance( + country_of_residence, string_types + ) + assert nationality is not None and isinstance(nationality, string_types) + assert date_of_birth is not None and isinstance(date_of_birth, string_types) + assert email is not None and isinstance(email, string_types) + assert user_title is not None and isinstance(user_title, string_types) + assert firstname is not None and isinstance(firstname, string_types) + assert lastname is not None and isinstance(lastname, string_types) + assert bringing_poster is not None and isinstance(bringing_poster, string_types) + assert expiry_date is not None and isinstance(expiry_date, string_types) + + +def generate( + template_path, + event_id, + work_address, + addressed_to, + residential_address, + passport_name, + passport_no, + passport_issued_by, + invitation_letter_sent_at, + to_date, + from_date, + country_of_residence, + nationality, + date_of_birth, + email, + user_title, + firstname, + lastname, + bringing_poster, + expiry_date, + user, +): + + check_values( + template_path, + event_id, + work_address, + addressed_to, + residential_address, + passport_name, + passport_no, + passport_issued_by, + invitation_letter_sent_at, + to_date, + from_date, + country_of_residence, + nationality, + date_of_birth, + email, + user_title, + firstname, + lastname, + bringing_poster, + expiry_date, + ) + # Path to store the template locally of merged and unmerged - template = 'app/invitationletter/template/tmp.docx' - template_merged = 'app/invitationletter/template/template.docx' + template = "app/invitationletter/template/tmp.docx" + template_merged = "app/invitationletter/template/template.docx" - download_blob(bucket_name=GCP_BUCKET_NAME, source_blob_name=template_path, - destination_file_name=template) + download_blob( + bucket_name=GCP_BUCKET_NAME, + source_blob_name=template_path, + destination_file_name=template, + ) if not os.path.exists(template): return errors.TEMPLATE_NOT_FOUND @@ -88,29 +160,33 @@ def generate(template_path, event_id, work_address, addressed_to, residential_ad TITLE=user_title, FIRSTNAME=firstname, LASTNAME=lastname, - WORK_ADDRESS=work_address.decode('utf-8'), - ADDRESSED_TO=addressed_to.decode('utf-8'), - RESIDENTIAL_ADDRESS=residential_address.decode('utf-8'), - PASSPORT_NAME=passport_name.decode('utf-8'), - PASSPORT_NO=passport_no.decode('utf-8'), - ISSUED_BY=passport_issued_by.decode('utf-8'), + WORK_ADDRESS=work_address.decode("utf-8"), + ADDRESSED_TO=addressed_to.decode("utf-8"), + RESIDENTIAL_ADDRESS=residential_address.decode("utf-8"), + PASSPORT_NAME=passport_name.decode("utf-8"), + PASSPORT_NO=passport_no.decode("utf-8"), + ISSUED_BY=passport_issued_by.decode("utf-8"), EXPIRY_DATE=expiry_date, ACCOMODATION_END_DATE=to_date, ACCOMODATION_START_DATE=from_date, - COUNTRY_OF_RESIDENCE=country_of_residence.decode('utf-8'), + COUNTRY_OF_RESIDENCE=country_of_residence.decode("utf-8"), NATIONALITY=nationality, DATE_OF_BIRTH=date_of_birth, INVITATION_LETTER_SENT_AT=invitation_letter_sent_at, - BRINGING_POSTER=bringing_poster + BRINGING_POSTER=bringing_poster, ) document.write(template_merged) # Conversion - template_pdf = 'app/invitationletter/letter/template.pdf' + template_pdf = "app/invitationletter/letter/template.pdf" if os.path.exists(template_pdf): os.remove(template_pdf) - success = pdfconvertor.convert_to(folder='app/invitationletter/letter', source=template_merged, output=template_pdf) + success = pdfconvertor.convert_to( + folder="app/invitationletter/letter", + source=template_merged, + output=template_pdf, + ) if not success: return errors.CREATING_INVITATION_FAILED @@ -120,17 +196,17 @@ def generate(template_path, event_id, work_address, addressed_to, residential_ad try: emailer.email_user( - 'invitation-letter', + "invitation-letter", event=event, user=user, file_name="InvitationLetter.pdf", - file_path=template_pdf + file_path=template_pdf, ) - LOGGER.debug('successfully sent email...') + LOGGER.debug("successfully sent email...") return True except ValueError: - LOGGER.debug('Did not send email...') + LOGGER.debug("Did not send email...") return False return False diff --git a/api/app/invitationletter/mixins.py b/api/app/invitationletter/mixins.py index 8828c4a05..c6b74ebe7 100644 --- a/api/app/invitationletter/mixins.py +++ b/api/app/invitationletter/mixins.py @@ -3,15 +3,15 @@ class InvitationMixin(object): req_parser = reqparse.RequestParser() - req_parser.add_argument('id', type=int, required=False) - req_parser.add_argument('registration_id', type=int, required=False) - req_parser.add_argument('event_id', type=int, required=False) - req_parser.add_argument('work_address', type=str, required=False) - req_parser.add_argument('addressed_to', type=str, required=False) - req_parser.add_argument('residential_address', type=str, required=False) - req_parser.add_argument('passport_name', type=str, required=False) - req_parser.add_argument('passport_no', type=str, required=False) - req_parser.add_argument('passport_issued_by', type=str, required=False) - req_parser.add_argument('passport_expiry_date', type=str, required=False) - req_parser.add_argument('to_date', type=str, required=False) - req_parser.add_argument('from_date', type=str, required=False) + req_parser.add_argument("id", type=int, required=False) + req_parser.add_argument("registration_id", type=int, required=False) + req_parser.add_argument("event_id", type=int, required=False) + req_parser.add_argument("work_address", type=str, required=False) + req_parser.add_argument("addressed_to", type=str, required=False) + req_parser.add_argument("residential_address", type=str, required=False) + req_parser.add_argument("passport_name", type=str, required=False) + req_parser.add_argument("passport_no", type=str, required=False) + req_parser.add_argument("passport_issued_by", type=str, required=False) + req_parser.add_argument("passport_expiry_date", type=str, required=False) + req_parser.add_argument("to_date", type=str, required=False) + req_parser.add_argument("from_date", type=str, required=False) diff --git a/api/app/invitationletter/models.py b/api/app/invitationletter/models.py index 12903f012..0730b4724 100644 --- a/api/app/invitationletter/models.py +++ b/api/app/invitationletter/models.py @@ -18,8 +18,12 @@ class InvitationLetterRequest(db.Model): __tablename__ = "invitation_letter_request" id = db.Column(db.Integer(), primary_key=True) - registration_id = db.Column(db.Integer(), db.ForeignKey("registration.id"), nullable=True) - guest_registration_id = db.Column(db.Integer(), db.ForeignKey("guest_registration.id"), nullable=True) + registration_id = db.Column( + db.Integer(), db.ForeignKey("registration.id"), nullable=True + ) + guest_registration_id = db.Column( + db.Integer(), db.ForeignKey("guest_registration.id"), nullable=True + ) event_id = db.Column(db.Integer(), db.ForeignKey("event.id"), nullable=False) work_address = db.Column(db.String(), nullable=True) addressed_to = db.Column(db.String(), nullable=False) diff --git a/api/app/invitationletter/tests.py b/api/app/invitationletter/tests.py index 1d78e3dfc..f5de2011c 100644 --- a/api/app/invitationletter/tests.py +++ b/api/app/invitationletter/tests.py @@ -1,56 +1,60 @@ -from app.registration.models import RegistrationForm, RegistrationQuestion, RegistrationAnswer, RegistrationSection import json from datetime import datetime, timedelta -from app import db, LOGGER -from app.utils.testing import ApiTestCase -from app.users.models import AppUser, UserCategory, Country + +from nose.tools import nottest + +from app import LOGGER, db from app.events.models import Event -from app.registration.models import Offer -from app.registration.models import Registration -from app.invitationletter.models import InvitationLetterRequest -from app.invitationletter.models import InvitationTemplate -from app.utils.pdfconvertor import convert_to from app.invitationletter.generator import generate -from nose.tools import nottest +from app.invitationletter.models import InvitationLetterRequest, InvitationTemplate from app.organisation.models import Organisation - +from app.registration.models import ( + Offer, + Registration, + RegistrationAnswer, + RegistrationForm, + RegistrationQuestion, + RegistrationSection, +) +from app.users.models import AppUser, Country, UserCategory +from app.utils.pdfconvertor import convert_to +from app.utils.testing import ApiTestCase INVITATION_LETTER = { - 'registration_id': 1, - 'event_id': 1, - 'work_address': "Somewhere over the rainbow", - 'addressed_to': "Sir", - 'residential_address': "Way up high", - 'passport_name': "Jane Do\\u7231", - 'passport_no': "23456565", - 'passport_issued_by': "Neverland", - 'passport_expiry_date': datetime(1984, 12, 12).strftime('%Y-%m-%d'), - 'to_date': datetime(1984, 12, 12).strftime('%Y-%m-%dT%H:%M:%S.%fZ'), - 'from_date': datetime(1984, 12, 12).strftime('%Y-%m-%dT%H:%M:%S.%fZ') + "registration_id": 1, + "event_id": 1, + "work_address": "Somewhere over the rainbow", + "addressed_to": "Sir", + "residential_address": "Way up high", + "passport_name": "Jane Do\\u7231", + "passport_no": "23456565", + "passport_issued_by": "Neverland", + "passport_expiry_date": datetime(1984, 12, 12).strftime("%Y-%m-%d"), + "to_date": datetime(1984, 12, 12).strftime("%Y-%m-%dT%H:%M:%S.%fZ"), + "from_date": datetime(1984, 12, 12).strftime("%Y-%m-%dT%H:%M:%S.%fZ"), } class InvitationLetterTests(ApiTestCase): - def seed_static_data(self): - self.add_organisation('Deep Learning Indaba') - db.session.add(UserCategory('Postdoc')) - db.session.add(Country('South Africa')) + self.add_organisation("Deep Learning Indaba") + db.session.add(UserCategory("Postdoc")) + db.session.add(Country("South Africa")) db.session.commit() - test_user = self.add_user('something@email.com') - test_user_2 = self.add_user('somethingelse@email.com') + test_user = self.add_user("something@email.com") + test_user_2 = self.add_user("somethingelse@email.com") db.session.commit() event = self.add_event( - name={'en': "Tech Talk"}, - description={'en': "tech talking"}, + name={"en": "Tech Talk"}, + description={"en": "tech talking"}, start_date=datetime(2019, 12, 12, 10, 10, 10), end_date=datetime(2020, 12, 12, 10, 10, 10), - key='REGINAL', - organisation_id=1, - email_from='abx@indaba.deeplearning', - url='indaba.deeplearning' + key="REGINAL", + organisation_id=1, + email_from="abx@indaba.deeplearning", + url="indaba.deeplearning", ) db.session.commit() @@ -64,12 +68,11 @@ def seed_static_data(self): accommodation_award=True, travel_award=True, accepted_accommodation_award=True, - accepted_travel_award=True + accepted_travel_award=True, ) db.session.add(offer) db.session.commit() - offer_2 = Offer( user_id=test_user_2.id, event_id=event.id, @@ -84,9 +87,7 @@ def seed_static_data(self): db.session.add(offer_2) db.session.commit() - form = RegistrationForm( - event_id=event.id - ) + form = RegistrationForm(event_id=event.id) db.session.add(form) db.session.commit() @@ -112,31 +113,27 @@ def seed_static_data(self): placeholder="the placeholder", headline="Will you be bringing a poster?", validation_regex="[]/", - validation_text=" text" + validation_text=" text", ) db.session.add(rq) db.session.commit() - registration = Registration( - offer_id=offer.id, - registration_form_id=form.id, - confirmed=True) + offer_id=offer.id, registration_form_id=form.id, confirmed=True + ) db.session.add(registration) db.session.commit() registration_2 = Registration( - offer_id=offer_2.id, - registration_form_id=form.id, - confirmed=True) + offer_id=offer_2.id, registration_form_id=form.id, confirmed=True + ) db.session.add(registration_2) db.session.commit() - ra = RegistrationAnswer( registration_id=registration_2.id, registration_question_id=rq.id, - value="yes" + value="yes", ) db.session.add(ra) db.session.commit() @@ -146,7 +143,8 @@ def seed_static_data(self): template_path="Indaba 2019 - Invitation Letter - General.docx", send_for_travel_award_only=False, send_for_accommodation_award_only=False, - send_for_both_travel_accommodation=True) + send_for_both_travel_accommodation=True, + ) db.session.add(template) template_2 = InvitationTemplate( @@ -154,7 +152,8 @@ def seed_static_data(self): template_path="Indaba 2019 - Invitation Letter - Travel & Accomodation.docx", send_for_travel_award_only=False, send_for_accommodation_award_only=False, - send_for_both_travel_accommodation=False) + send_for_both_travel_accommodation=False, + ) db.session.add(template_2) db.session.commit() @@ -164,28 +163,29 @@ def seed_static_data(self): db.session.flush() def get_auth_header_for(self, email): - body = { - 'email': email, - 'password': '123456' - } - response = self.app.post('api/v1/authenticate', data=body) + body = {"email": email, "password": "123456"} + response = self.app.post("api/v1/authenticate", data=body) data = json.loads(response.data) - header = {'Authorization': data['token']} + header = {"Authorization": data["token"]} return header - + @nottest def test_create_create_invitation_letter(self): self.seed_static_data() response = self.app.post( - '/api/v1/invitation-letter', data=INVITATION_LETTER, headers=self.headers) + "/api/v1/invitation-letter", data=INVITATION_LETTER, headers=self.headers + ) data = json.loads(response.data) LOGGER.debug("invitation letter: {}".format(data)) - letter = db.session.query(InvitationLetterRequest).filter( - InvitationLetterRequest.id == data['invitation_letter_request_id']).first() + letter = ( + db.session.query(InvitationLetterRequest) + .filter(InvitationLetterRequest.id == data["invitation_letter_request_id"]) + .first() + ) assert response.status_code == 201 - assert data['invitation_letter_request_id'] == 1 + assert data["invitation_letter_request_id"] == 1 assert letter.event_id == 1 assert letter.work_address == "Somewhere over the rainbow" assert letter.addressed_to == "Sir" @@ -197,18 +197,22 @@ def test_create_create_invitation_letter(self): @nottest def test_create_create_invitation_letter_no_work(self): self.seed_static_data() - INVITATION_LETTER['work_address'] = None + INVITATION_LETTER["work_address"] = None response = self.app.post( - '/api/v1/invitation-letter', data=INVITATION_LETTER, headers=self.headers) + "/api/v1/invitation-letter", data=INVITATION_LETTER, headers=self.headers + ) data = json.loads(response.data) print(data) LOGGER.debug("invitation letter: {}".format(data)) - letter = db.session.query(InvitationLetterRequest).filter( - InvitationLetterRequest.id == data['invitation_letter_request_id']).first() + letter = ( + db.session.query(InvitationLetterRequest) + .filter(InvitationLetterRequest.id == data["invitation_letter_request_id"]) + .first() + ) assert response.status_code == 201 - assert data['invitation_letter_request_id'] == 1 + assert data["invitation_letter_request_id"] == 1 assert letter.event_id == 1 assert letter.work_address == " " assert letter.addressed_to == "Sir" @@ -217,33 +221,38 @@ def test_create_create_invitation_letter_no_work(self): assert letter.passport_no == "23456565" assert letter.passport_issued_by == "Neverland" - def test_create_create_invitation_letter_correct_template(self): self.seed_static_data() INVITATION_LETTER_2 = { - 'registration_id': 2, - 'event_id': 1, - 'work_address': "Somewhere over the rainbow", - 'addressed_to': "Sir", - 'residential_address': "Way up high", - 'passport_name': "Jane Do\\u7231", - 'passport_no': "23456565", - 'passport_issued_by': "Neverland", - 'passport_expiry_date': datetime(1984, 12, 12).strftime('%Y-%m-%d'), - 'to_date': datetime(1984, 12, 12).strftime('%Y-%m-%dT%H:%M:%S.%fZ'), - 'from_date': datetime(1984, 12, 12).strftime('%Y-%m-%dT%H:%M:%S.%fZ') + "registration_id": 2, + "event_id": 1, + "work_address": "Somewhere over the rainbow", + "addressed_to": "Sir", + "residential_address": "Way up high", + "passport_name": "Jane Do\\u7231", + "passport_no": "23456565", + "passport_issued_by": "Neverland", + "passport_expiry_date": datetime(1984, 12, 12).strftime("%Y-%m-%d"), + "to_date": datetime(1984, 12, 12).strftime("%Y-%m-%dT%H:%M:%S.%fZ"), + "from_date": datetime(1984, 12, 12).strftime("%Y-%m-%dT%H:%M:%S.%fZ"), } response = self.app.post( - '/api/v1/invitation-letter', data=INVITATION_LETTER_2, headers=self.headers_2) + "/api/v1/invitation-letter", + data=INVITATION_LETTER_2, + headers=self.headers_2, + ) data = json.loads(response.data) print(data) LOGGER.debug("invitation letter: {}".format(data)) - letter = db.session.query(InvitationLetterRequest).filter( - InvitationLetterRequest.id == data['invitation_letter_request_id']).first() + letter = ( + db.session.query(InvitationLetterRequest) + .filter(InvitationLetterRequest.id == data["invitation_letter_request_id"]) + .first() + ) assert response.status_code == 201 - assert data['invitation_letter_request_id'] == 1 + assert data["invitation_letter_request_id"] == 1 assert letter.event_id == 1 assert letter.work_address == " " assert letter.addressed_to == "Sir" @@ -252,35 +261,39 @@ def test_create_create_invitation_letter_correct_template(self): assert letter.passport_no == "23456565" assert letter.passport_issued_by == "Neverland" - - @nottest def test_create_create_invitation_letter_correct_template(self): self.seed_static_data() INVITATION_LETTER_2 = { - 'registration_id': 2, - 'event_id': 1, - 'work_address': "Somewhere over the rainbow", - 'addressed_to': "Sir", - 'residential_address': "Way up high", - 'passport_name': "Jane Do\\u7231", - 'passport_no': "23456565", - 'passport_issued_by': "Neverland", - 'passport_expiry_date': datetime(1984, 12, 12).strftime('%Y-%m-%d'), - 'to_date': datetime(1984, 12, 12).strftime('%Y-%m-%dT%H:%M:%S.%fZ'), - 'from_date': datetime(1984, 12, 12).strftime('%Y-%m-%dT%H:%M:%S.%fZ') + "registration_id": 2, + "event_id": 1, + "work_address": "Somewhere over the rainbow", + "addressed_to": "Sir", + "residential_address": "Way up high", + "passport_name": "Jane Do\\u7231", + "passport_no": "23456565", + "passport_issued_by": "Neverland", + "passport_expiry_date": datetime(1984, 12, 12).strftime("%Y-%m-%d"), + "to_date": datetime(1984, 12, 12).strftime("%Y-%m-%dT%H:%M:%S.%fZ"), + "from_date": datetime(1984, 12, 12).strftime("%Y-%m-%dT%H:%M:%S.%fZ"), } response = self.app.post( - '/api/v1/invitation-letter', data=INVITATION_LETTER_2, headers=self.headers_2) + "/api/v1/invitation-letter", + data=INVITATION_LETTER_2, + headers=self.headers_2, + ) data = json.loads(response.data) print(data) LOGGER.debug("invitation letter: {}".format(data)) - letter = db.session.query(InvitationLetterRequest).filter( - InvitationLetterRequest.id == data['invitation_letter_request_id']).first() + letter = ( + db.session.query(InvitationLetterRequest) + .filter(InvitationLetterRequest.id == data["invitation_letter_request_id"]) + .first() + ) assert response.status_code == 201 - assert data['invitation_letter_request_id'] == 1 + assert data["invitation_letter_request_id"] == 1 assert letter.event_id == 1 assert letter.addressed_to == "Sir" assert letter.residential_address == "Way up high" @@ -288,23 +301,21 @@ def test_create_create_invitation_letter_correct_template(self): assert letter.passport_no == "23456565" assert letter.passport_issued_by == "Neverland" - class PDFConverterTest(ApiTestCase): - def seed_static_data(self): - self.test_user = self.add_user('something@email.com') - self.add_organisation('Deep Learning Indaba') - db.session.add(UserCategory('Postdoc')) - db.session.add(Country('South Africa')) + self.test_user = self.add_user("something@email.com") + self.add_organisation("Deep Learning Indaba") + db.session.add(UserCategory("Postdoc")) + db.session.add(Country("South Africa")) db.session.commit() event = self.add_event( - name={'en': "Tech Talk"}, - description={'en': "tech talking"}, + name={"en": "Tech Talk"}, + description={"en": "tech talking"}, start_date=datetime(2019, 12, 12, 10, 10, 10), end_date=datetime(2020, 12, 12, 10, 10, 10), - key='REGINAL' + key="REGINAL", ) db.session.commit() self.event = event @@ -312,31 +323,29 @@ def seed_static_data(self): @nottest def test_generator(self): self.seed_static_data() - self.assertEqual(generate(template_path='Indaba 2019 - Invitation Letter - General.docx', - event_id=self.event.id, - work_address='19 Melle Steet\nRetro Rabbit\nSouth Africa\n1000', - addressed_to='Man with the Visa Power', - residential_address='123 Magic Street\nDance\nMore Dance\n1000', - passport_name='Jeff Jeffdejeff', - passport_no='09876512344', - passport_issued_by='RSA', - invitation_letter_sent_at=datetime(1984, 12, 12).strftime('%Y-%m-%d'), - to_date=datetime(1984, 12, 12).strftime('%Y-%m-%d'), - from_date=datetime(1984, 12, 12).strftime('%Y-%m-%d'), - country_of_residence='South Africa', - nationality='South African', - date_of_birth=datetime(1984, 12, 12).strftime('%Y-%m-%d'), - email='info@gmail.com', - user_title='Mr', - firstname='Jeff', - lastname='Jeffdejeff', - bringing_poster='', - expiry_date=datetime(1984, 12, 12).strftime('%Y-%m-%d')), True, self.test_user) - - - - - - - - + self.assertEqual( + generate( + template_path="Indaba 2019 - Invitation Letter - General.docx", + event_id=self.event.id, + work_address="19 Melle Steet\nRetro Rabbit\nSouth Africa\n1000", + addressed_to="Man with the Visa Power", + residential_address="123 Magic Street\nDance\nMore Dance\n1000", + passport_name="Jeff Jeffdejeff", + passport_no="09876512344", + passport_issued_by="RSA", + invitation_letter_sent_at=datetime(1984, 12, 12).strftime("%Y-%m-%d"), + to_date=datetime(1984, 12, 12).strftime("%Y-%m-%d"), + from_date=datetime(1984, 12, 12).strftime("%Y-%m-%d"), + country_of_residence="South Africa", + nationality="South African", + date_of_birth=datetime(1984, 12, 12).strftime("%Y-%m-%d"), + email="info@gmail.com", + user_title="Mr", + firstname="Jeff", + lastname="Jeffdejeff", + bringing_poster="", + expiry_date=datetime(1984, 12, 12).strftime("%Y-%m-%d"), + ), + True, + self.test_user, + ) diff --git a/api/app/invitedGuest/api.py b/api/app/invitedGuest/api.py index 575ada7b3..0fe4cabc7 100644 --- a/api/app/invitedGuest/api.py +++ b/api/app/invitedGuest/api.py @@ -1,46 +1,50 @@ +import random +import string -from flask_restful import reqparse, fields, marshal_with import flask_restful as restful -from sqlalchemy.exc import IntegrityError - -from app.utils.auth import auth_required -from app import LOGGER -from app import db, bcrypt from flask import g, request -import random -import string +from flask_restful import fields, marshal_with, reqparse +from sqlalchemy import func +from sqlalchemy.exc import IntegrityError -from app.users.models import AppUser, PasswordReset +from app import LOGGER, bcrypt, db +from app.events.repository import EventRepository as event_repository +from app.invitedGuest.mixins import InvitedGuestListMixin, InvitedGuestMixin from app.invitedGuest.models import InvitedGuest -from app.utils.errors import EVENT_NOT_FOUND, USER_NOT_FOUND, ADD_INVITED_GUEST_FAILED, INVITED_GUEST_FOR_EVENT_EXISTS, FORBIDDEN, INVITED_GUEST_EMAIL_FAILED -from app.invitedGuest.mixins import InvitedGuestMixin, InvitedGuestListMixin from app.users import api as UserAPI from app.users.mixins import SignupMixin +from app.users.models import AppUser, PasswordReset from app.users.repository import UserRepository as user_repository -from app.events.repository import EventRepository as event_repository -from sqlalchemy import func from app.utils import misc +from app.utils.auth import auth_required from app.utils.emailer import email_user +from app.utils.errors import ( + ADD_INVITED_GUEST_FAILED, + EVENT_NOT_FOUND, + FORBIDDEN, + INVITED_GUEST_EMAIL_FAILED, + INVITED_GUEST_FOR_EVENT_EXISTS, + USER_NOT_FOUND, +) def invitedGuest_info(invitedGuest, user): return { - 'invitedGuest_id': invitedGuest.id, - 'event_id': invitedGuest.event_id, - 'user_id': invitedGuest.user_id, - 'role': invitedGuest.role, - 'fullname': '{} {} {}'.format(user.user_title, user.firstname, user.lastname) + "invitedGuest_id": invitedGuest.id, + "event_id": invitedGuest.event_id, + "user_id": invitedGuest.user_id, + "role": invitedGuest.role, + "fullname": "{} {} {}".format(user.user_title, user.firstname, user.lastname), } class InvitedGuestAPI(InvitedGuestMixin, restful.Resource): - @auth_required def post(self, send_email=True): args = self.req_parser.parse_args() - event_id = args['event_id'] - email = args['email'] - role = args['role'] + event_id = args["event_id"] + email = args["email"] + role = args["role"] user = user_repository.get_by_email(email, g.organisation.id) @@ -51,49 +55,54 @@ def post(self, send_email=True): if not event: return EVENT_NOT_FOUND - existingInvitedGuest = db.session.query(InvitedGuest).filter( - InvitedGuest.event_id == event_id).filter(InvitedGuest.user_id == user.id).first() + existingInvitedGuest = ( + db.session.query(InvitedGuest) + .filter(InvitedGuest.event_id == event_id) + .filter(InvitedGuest.user_id == user.id) + .first() + ) if existingInvitedGuest: return INVITED_GUEST_FOR_EVENT_EXISTS - invitedGuest = InvitedGuest( - event_id=event_id, - user_id=user.id, - role=role - ) + invitedGuest = InvitedGuest(event_id=event_id, user_id=user.id, role=role) db.session.add(invitedGuest) try: db.session.commit() except IntegrityError: - LOGGER.error( - "Failed to add invited guest: {}".format(email)) + LOGGER.error("Failed to add invited guest: {}".format(email)) return ADD_INVITED_GUEST_FAILED if send_email: try: email_user( - 'guest-invitation-with-registration' if event.is_registration_open else 'guest-invitation', + "guest-invitation-with-registration" + if event.is_registration_open + else "guest-invitation", template_parameters=dict( role=role, system_name=g.organisation.system_name, host=misc.get_baobab_host(), - event_key=event.key + event_key=event.key, ), event=event, - user=user) + user=user, + ) except Exception as e: - LOGGER.error('Failed to send email to invited guest with user Id {}, due to {}'.format(user.id, e)) + LOGGER.error( + "Failed to send email to invited guest with user Id {}, due to {}".format( + user.id, e + ) + ) return INVITED_GUEST_EMAIL_FAILED return invitedGuest_info(invitedGuest, user), 201 class CreateUser(SignupMixin, restful.Resource): - @auth_required def post(self): args = self.req_parser.parse_args() @@ -107,20 +116,22 @@ def post(self): invited_guest_info, status = invited_guest_api.post(send_email=False) if status == 201: - event_id = invited_guest_info['event_id'] - role = invited_guest_info['role'] - user = user_repository.get_by_id(user['id']) + event_id = invited_guest_info["event_id"] + role = invited_guest_info["role"] + user = user_repository.get_by_id(user["id"]) event = event_repository.get_by_id(event_id) - + reset_code = misc.make_code() - password_reset=PasswordReset(user=user) + password_reset = PasswordReset(user=user) db.session.add(password_reset) db.session.commit() try: email_user( - 'new-guest-registration' if event.is_registration_open else 'new-guest-no-registration', + "new-guest-registration" + if event.is_registration_open + else "new-guest-no-registration", template_parameters=dict( event_key=event.key, system_name=g.organisation.system_name, @@ -129,16 +140,20 @@ def post(self): reset_code=password_reset.code, ), event=event, - user=user + user=user, ) except Exception as e: - LOGGER.error('Failed to send email for invited guest with user Id {} due to: {}'.format(user.id, e)) + LOGGER.error( + "Failed to send email for invited guest with user Id {} due to: {}".format( + user.id, e + ) + ) return INVITED_GUEST_EMAIL_FAILED return invited_guest_info, status -class InvitedGuestView(): +class InvitedGuestView: def __init__(self, invitedGuest): self.invited_guest_id = invitedGuest.InvitedGuest.id self.event_id = invitedGuest.InvitedGuest.event_id @@ -163,36 +178,39 @@ def __init__(self, invitedGuest): class InvitedGuestList(InvitedGuestListMixin, restful.Resource): user_profile_list_fields = { - 'user_id': fields.Integer, - 'email': fields.String, - 'firstname': fields.String, - 'lastname': fields.String, - 'user_title': fields.String, + "user_id": fields.Integer, + "email": fields.String, + "firstname": fields.String, + "lastname": fields.String, + "user_title": fields.String, } invited_guest = { - 'invited_guest_id': fields.Integer, - 'event_id': fields.Integer, - 'user': user_profile_list_fields, - 'role': fields.String + "invited_guest_id": fields.Integer, + "event_id": fields.Integer, + "user": user_profile_list_fields, + "role": fields.String, } @marshal_with(invited_guest) @auth_required def get(self): args = self.req_parser.parse_args() - event_id = args['event_id'] - current_user_id = g.current_user['id'] + event_id = args["event_id"] + current_user_id = g.current_user["id"] current_user = user_repository.get_by_id(current_user_id) if not (current_user.is_event_admin(event_id) or current_user.is_admin): return FORBIDDEN - invited_guests = db.session.query(InvitedGuest, AppUser).filter_by(event_id=event_id).join( - AppUser, InvitedGuest.user_id == AppUser.id).all() + invited_guests = ( + db.session.query(InvitedGuest, AppUser) + .filter_by(event_id=event_id) + .join(AppUser, InvitedGuest.user_id == AppUser.id) + .all() + ) - views = [InvitedGuestView(invited_guest) - for invited_guest in invited_guests] + views = [InvitedGuestView(invited_guest) for invited_guest in invited_guests] return views @@ -200,11 +218,15 @@ class CheckIfInvitedGuest(InvitedGuestListMixin, restful.Resource): @auth_required def get(self): args = self.req_parser.parse_args() - event_id = args['event_id'] - current_user_id = g.current_user['id'] - - existing_invited_guest = db.session.query(InvitedGuest).filter( - InvitedGuest.event_id == event_id).filter(InvitedGuest.user_id == current_user_id).first() + event_id = args["event_id"] + current_user_id = g.current_user["id"] + + existing_invited_guest = ( + db.session.query(InvitedGuest) + .filter(InvitedGuest.event_id == event_id) + .filter(InvitedGuest.user_id == current_user_id) + .first() + ) try: @@ -213,4 +235,4 @@ def get(self): else: return "Invited Guest", 200 except Exception as e: - return 'Could not access DB', 400 + return "Could not access DB", 400 diff --git a/api/app/invitedGuest/mixins.py b/api/app/invitedGuest/mixins.py index 16a040364..1c09ff66b 100644 --- a/api/app/invitedGuest/mixins.py +++ b/api/app/invitedGuest/mixins.py @@ -3,11 +3,11 @@ class InvitedGuestMixin(object): req_parser = reqparse.RequestParser() - req_parser.add_argument('email', type=str, required=True) - req_parser.add_argument('event_id', type=int, required=True) - req_parser.add_argument('role', type=str, required=True) + req_parser.add_argument("email", type=str, required=True) + req_parser.add_argument("event_id", type=int, required=True) + req_parser.add_argument("role", type=str, required=True) class InvitedGuestListMixin(object): req_parser = reqparse.RequestParser() - req_parser.add_argument('event_id', type=int, required=True) + req_parser.add_argument("event_id", type=int, required=True) diff --git a/api/app/invitedGuest/models.py b/api/app/invitedGuest/models.py index 79c7c96ef..be3c284dd 100644 --- a/api/app/invitedGuest/models.py +++ b/api/app/invitedGuest/models.py @@ -1,4 +1,3 @@ - from app import db @@ -6,10 +5,8 @@ class InvitedGuest(db.Model): __tablename__ = "invited_guest" id = db.Column(db.Integer(), primary_key=True) - event_id = db.Column(db.Integer(), db.ForeignKey( - "event.id"), nullable=False) - user_id = db.Column(db.Integer(), db.ForeignKey( - "app_user.id"), nullable=False) + event_id = db.Column(db.Integer(), db.ForeignKey("event.id"), nullable=False) + user_id = db.Column(db.Integer(), db.ForeignKey("app_user.id"), nullable=False) role = db.Column(db.String(50), nullable=False) def __init__(self, event_id, user_id, role): @@ -22,10 +19,10 @@ class GuestRegistration(db.Model): __tablename__ = "guest_registration" id = db.Column(db.Integer(), primary_key=True) - user_id = db.Column(db.Integer(), db.ForeignKey( - "app_user.id"), nullable=False) - registration_form_id = db.Column(db.Integer(), db.ForeignKey( - "registration_form.id"), nullable=False) + user_id = db.Column(db.Integer(), db.ForeignKey("app_user.id"), nullable=False) + registration_form_id = db.Column( + db.Integer(), db.ForeignKey("registration_form.id"), nullable=False + ) confirmed = db.Column(db.Boolean(), nullable=False) created_at = db.Column(db.DateTime(), nullable=True) confirmation_email_sent_at = db.Column(db.DateTime(), nullable=True) @@ -39,10 +36,12 @@ class GuestRegistrationAnswer(db.Model): __tablename__ = "guest_registration_answer" id = db.Column(db.Integer(), primary_key=True) - guest_registration_id = db.Column(db.Integer(), db.ForeignKey( - 'guest_registration.id'), nullable=False) - registration_question_id = db.Column(db.Integer(), db.ForeignKey( - 'registration_question.id'), nullable=False) + guest_registration_id = db.Column( + db.Integer(), db.ForeignKey("guest_registration.id"), nullable=False + ) + registration_question_id = db.Column( + db.Integer(), db.ForeignKey("registration_question.id"), nullable=False + ) value = db.Column(db.String(), nullable=False) is_active = db.Column(db.Boolean(), nullable=False) created_on = db.Column(db.DateTime, nullable=False, server_default=db.func.now()) diff --git a/api/app/invitedGuest/repository.py b/api/app/invitedGuest/repository.py index f2200ba97..c42cbf231 100644 --- a/api/app/invitedGuest/repository.py +++ b/api/app/invitedGuest/repository.py @@ -1,20 +1,26 @@ from app import db -from app.invitedGuest.models import InvitedGuest, GuestRegistration +from app.invitedGuest.models import GuestRegistration, InvitedGuest from app.registration.models import RegistrationForm -class InvitedGuestRepository(): +class InvitedGuestRepository: @staticmethod def get_for_event_and_user(event_id, user_id): - return (db.session.query(InvitedGuest) - .filter(InvitedGuest.event_id == event_id, InvitedGuest.user_id == user_id) - .first()) + return ( + db.session.query(InvitedGuest) + .filter(InvitedGuest.event_id == event_id, InvitedGuest.user_id == user_id) + .first() + ) @staticmethod def get_registration_for_event_and_user(event_id, user_id): - return (db.session.query(GuestRegistration) - .filter(GuestRegistration.user_id == user_id) - .join(RegistrationForm, GuestRegistration.registration_form_id == RegistrationForm.id) - .filter(RegistrationForm.event_id == event_id) - .first()) - + return ( + db.session.query(GuestRegistration) + .filter(GuestRegistration.user_id == user_id) + .join( + RegistrationForm, + GuestRegistration.registration_form_id == RegistrationForm.id, + ) + .filter(RegistrationForm.event_id == event_id) + .first() + ) diff --git a/api/app/invitedGuest/tests.py b/api/app/invitedGuest/tests.py index baf389795..195f93326 100644 --- a/api/app/invitedGuest/tests.py +++ b/api/app/invitedGuest/tests.py @@ -1,65 +1,63 @@ import json - from datetime import datetime, timedelta -from app import app, db, LOGGER -from app.utils.testing import ApiTestCase -from app.users.models import AppUser, PasswordReset, UserCategory, Country, UserComment -from app.events.models import Event, EventRole +from app import LOGGER, app, db from app.applicationModel.models import ApplicationForm -from app.responses.models import Response +from app.events.models import Event, EventRole from app.organisation.models import Organisation +from app.responses.models import Response +from app.users.models import AppUser, Country, PasswordReset, UserCategory, UserComment +from app.utils.testing import ApiTestCase +INVITED_GUEST = {"event_id": 1, "email": "something@email.com", "role": "jedi"} -INVITED_GUEST = { - 'event_id': 1, - 'email': 'something@email.com', - 'role': 'jedi' -} - -INVITED_GUEST_2 = { - 'event_id': 1, - 'email': 'something2@email.com', - 'role': 'jedi' -} +INVITED_GUEST_2 = {"event_id": 1, "email": "something2@email.com", "role": "jedi"} -INVITED_GUEST_NEW_USER = { - 'event_id': 1, - 'email': 'new@email.com', - 'role': 'jedi' -} +INVITED_GUEST_NEW_USER = {"event_id": 1, "email": "new@email.com", "role": "jedi"} USER_DATA = { - 'email': 'newsomething@email.com', - 'firstname': 'Some', - 'lastname': 'Thing', - 'user_title': 'Mr', - 'role': 'mentor', - 'event_id': 1, - 'policy_agreed': True, - 'language': 'en' + "email": "newsomething@email.com", + "firstname": "Some", + "lastname": "Thing", + "user_title": "Mr", + "role": "mentor", + "event_id": 1, + "policy_agreed": True, + "language": "en", } class InvitedGuestTest(ApiTestCase): - def seed_static_data(self): - test_user = self.add_user('something@email.com') - test_user2 = self.add_user('something2@email.com') + test_user = self.add_user("something@email.com") + test_user2 = self.add_user("something2@email.com") - event_admin = self.add_user('event_admin@ea.com') + event_admin = self.add_user("event_admin@ea.com") db.session.commit() - self.add_organisation('Deep Learning Indaba') - self.add_organisation('Deep Learning IndabaX') + self.add_organisation("Deep Learning Indaba") + self.add_organisation("Deep Learning IndabaX") db.session.commit() - db.session.add(UserCategory('Postdoc')) - db.session.add(Country('South Africa')) - - self.event1 = self.add_event({'en': 'Indaba'}, {'en': 'Indaba Event'}, datetime.now(), datetime.now(), 'LBFSOLVER') - self.event2 = self.add_event({'en': 'IndabaX'}, {'en': 'IndabaX Sudan'}, datetime.now(), datetime.now(), 'NAGSOLVER', 2) + db.session.add(UserCategory("Postdoc")) + db.session.add(Country("South Africa")) + + self.event1 = self.add_event( + {"en": "Indaba"}, + {"en": "Indaba Event"}, + datetime.now(), + datetime.now(), + "LBFSOLVER", + ) + self.event2 = self.add_event( + {"en": "IndabaX"}, + {"en": "IndabaX Sudan"}, + datetime.now(), + datetime.now(), + "NAGSOLVER", + 2, + ) db.session.commit() - adminRole = EventRole('admin', event_admin.id, self.event1.id) + adminRole = EventRole("admin", event_admin.id, self.event1.id) db.session.add(adminRole) db.session.commit() @@ -68,68 +66,70 @@ def seed_static_data(self): self.headers = self.get_auth_header_for("something@email.com") self.adminHeaders = self.get_auth_header_for("event_admin@ea.com") - self.add_email_template('guest-invitation-with-registration') - self.add_email_template('guest-invitation') - self.add_email_template('new-guest-registration') - self.add_email_template('new-guest-no-registration') + self.add_email_template("guest-invitation-with-registration") + self.add_email_template("guest-invitation") + self.add_email_template("new-guest-registration") + self.add_email_template("new-guest-no-registration") db.session.flush() def get_auth_header_for(self, email): - body = { - 'email': email, - 'password': 'abc' - } - response = self.app.post('api/v1/authenticate', data=body) + body = {"email": email, "password": "abc"} + response = self.app.post("api/v1/authenticate", data=body) data = json.loads(response.data) print(data) - header = {'Authorization': data['token']} + header = {"Authorization": data["token"]} return header def test_create_invitedGuest(self): self.seed_static_data() response = self.app.post( - '/api/v1/invitedGuest', data=INVITED_GUEST, headers=self.headers) + "/api/v1/invitedGuest", data=INVITED_GUEST, headers=self.headers + ) data = json.loads(response.data) assert response.status_code == 201 - assert data['invitedGuest_id'] == 1 + assert data["invitedGuest_id"] == 1 def test_create_invitedGuest_duplicate(self): self.seed_static_data() - self.app.post( - '/api/v1/invitedGuest', data=INVITED_GUEST, headers=self.headers) + self.app.post("/api/v1/invitedGuest", data=INVITED_GUEST, headers=self.headers) response = self.app.post( - '/api/v1/invitedGuest', data=INVITED_GUEST, headers=self.headers) + "/api/v1/invitedGuest", data=INVITED_GUEST, headers=self.headers + ) assert response.status_code == 409 def test_create_invitedGuest_user_not_found(self): self.seed_static_data() response = self.app.post( - '/api/v1/invitedGuest', data=INVITED_GUEST_NEW_USER, headers=self.headers) + "/api/v1/invitedGuest", data=INVITED_GUEST_NEW_USER, headers=self.headers + ) assert response.status_code == 404 def test_create_invitedGuest_create_user(self): self.seed_static_data() response = self.app.post( - '/api/v1/invitedGuest/create', data=USER_DATA, headers=self.headers) + "/api/v1/invitedGuest/create", data=USER_DATA, headers=self.headers + ) data = json.loads(response.data) assert response.status_code == 201 - assert data['fullname'] == '{} {} {}'.format(USER_DATA["user_title"], USER_DATA["firstname"], USER_DATA["lastname"]) + assert data["fullname"] == "{} {} {}".format( + USER_DATA["user_title"], USER_DATA["firstname"], USER_DATA["lastname"] + ) def test_create_invitedGuest_list(self): self.seed_static_data() # Adding some invited guests event_id = 1 - self.app.post( - '/api/v1/invitedGuest', data=INVITED_GUEST, headers=self.headers) + self.app.post("/api/v1/invitedGuest", data=INVITED_GUEST, headers=self.headers) response = self.app.post( - '/api/v1/invitedGuest', data=INVITED_GUEST_2, headers=self.headers) + "/api/v1/invitedGuest", data=INVITED_GUEST_2, headers=self.headers + ) response = self.app.get( - '/api/v1/invitedGuestList?event_id='+str(event_id), headers=self.adminHeaders) + "/api/v1/invitedGuestList?event_id=" + str(event_id), + headers=self.adminHeaders, + ) data = json.loads(response.data) - data = sorted(data, key=lambda k: k['invited_guest_id']) + data = sorted(data, key=lambda k: k["invited_guest_id"]) self.assertEqual(len(data), 2) - self.assertEqual(data[0]['user']['email'], - INVITED_GUEST['email']) - self.assertEqual(data[1]['user']['email'], - INVITED_GUEST_2['email']) + self.assertEqual(data[0]["user"]["email"], INVITED_GUEST["email"]) + self.assertEqual(data[1]["user"]["email"], INVITED_GUEST_2["email"]) diff --git a/api/app/organisation/api.py b/api/app/organisation/api.py index 2672012ee..2a8911e3c 100644 --- a/api/app/organisation/api.py +++ b/api/app/organisation/api.py @@ -1,24 +1,24 @@ -from flask_restful import reqparse import flask_restful as restful -from flask import request, g -from flask_restful import fields, marshal_with, marshal +from flask import g, request +from flask_restful import fields, marshal, marshal_with, reqparse from app import LOGGER + class OrganisationApi(restful.Resource): organisation_fields = { - 'id': fields.Integer, - 'name': fields.String, - 'small_logo': fields.String, - 'large_logo': fields.String, - 'icon_logo': fields.String, - 'domain': fields.String, - 'system_name': fields.String, - 'url': fields.String, - 'email_from': fields.String, - 'system_url': fields.String, - 'privacy_policy': fields.String, - 'languages': fields.Raw + "id": fields.Integer, + "name": fields.String, + "small_logo": fields.String, + "large_logo": fields.String, + "icon_logo": fields.String, + "domain": fields.String, + "system_name": fields.String, + "url": fields.String, + "email_from": fields.String, + "system_url": fields.String, + "privacy_policy": fields.String, + "languages": fields.Raw, } @marshal_with(organisation_fields) diff --git a/api/app/organisation/models.py b/api/app/organisation/models.py index b6798a707..c41799738 100644 --- a/api/app/organisation/models.py +++ b/api/app/organisation/models.py @@ -1,5 +1,6 @@ from app import db + class Organisation(db.Model): __tablename__ = "organisation" @@ -16,9 +17,22 @@ class Organisation(db.Model): system_url = db.Column(db.String(100), nullable=False) privacy_policy = db.Column(db.String(100), nullable=False) languages = db.Column(db.JSON(), nullable=False) - events = db.relationship('Event') + events = db.relationship("Event") - def __init__(self, name, system_name, small_logo, large_logo, icon_logo, domain, url, email_from, system_url, privacy_policy, languages): + def __init__( + self, + name, + system_name, + small_logo, + large_logo, + icon_logo, + domain, + url, + email_from, + system_url, + privacy_policy, + languages, + ): self.name = name self.small_logo = small_logo self.large_logo = large_logo diff --git a/api/app/organisation/repository.py b/api/app/organisation/repository.py index 7e0b64f19..2393d15d4 100644 --- a/api/app/organisation/repository.py +++ b/api/app/organisation/repository.py @@ -2,15 +2,18 @@ from app.organisation.models import Organisation -class OrganisationRepository(): - +class OrganisationRepository: @staticmethod def get_by_id(organisation_id): return db.session.query(Organisation).get(event_id) @staticmethod def get_by_domain(domain): - return db.session.query(Organisation).Filter(Organisation.domain == domain).one_or_none() + return ( + db.session.query(Organisation) + .Filter(Organisation.domain == domain) + .one_or_none() + ) @staticmethod def get_all(): diff --git a/api/app/organisation/resolver.py b/api/app/organisation/resolver.py index 4e72f639f..78bf9af6d 100644 --- a/api/app/organisation/resolver.py +++ b/api/app/organisation/resolver.py @@ -1,14 +1,16 @@ -from app.organisation.repository import OrganisationRepository -from app import LOGGER from flask import request from werkzeug.exceptions import BadRequest -class OrganisationResolver(): +from app import LOGGER +from app.organisation.repository import OrganisationRepository + + +class OrganisationResolver: _cache = None @classmethod def _populate_cache(cls): - LOGGER.info('Populating Organisation Cache') + LOGGER.info("Populating Organisation Cache") organisations = OrganisationRepository.get_all() cls._cache = {} for org in organisations: @@ -18,14 +20,23 @@ def _populate_cache(cls): def resolve_from_domain(cls, domain): if not cls._cache: cls._populate_cache() - + if domain not in cls._cache: # Re-populate the cache and retry cls._populate_cache() - LOGGER.warning('Could not resolve organisation from domain: {}, trying to repopulate cache'.format(domain)) + LOGGER.warning( + "Could not resolve organisation from domain: {}, trying to repopulate cache".format( + domain + ) + ) if domain not in cls._cache: - LOGGER.error('Could not resolve organisation from domain: {}, HTTP Origin: {}, HTTP Referer: {}'.format( - domain, request.environ.get('HTTP_ORIGIN', ''), request.environ.get('HTTP_REFERER', ''))) - raise BadRequest('Could not resolve organisation') - + LOGGER.error( + "Could not resolve organisation from domain: {}, HTTP Origin: {}, HTTP Referer: {}".format( + domain, + request.environ.get("HTTP_ORIGIN", ""), + request.environ.get("HTTP_REFERER", ""), + ) + ) + raise BadRequest("Could not resolve organisation") + return cls._cache[domain] diff --git a/api/app/organisation/tests.py b/api/app/organisation/tests.py index 5f9c3955e..ba391ae20 100644 --- a/api/app/organisation/tests.py +++ b/api/app/organisation/tests.py @@ -1,18 +1,20 @@ import json -from app.utils.testing import ApiTestCase -from app import app, db, LOGGER + +from app import LOGGER, app, db from app.organisation.models import Organisation +from app.utils.testing import ApiTestCase class OrganisationApiTest(ApiTestCase): def seed_static_data(self): self.add_organisation( - name='Deep Learning Indaba', - system_name='Baobab', - small_logo='blah.png', - large_logo='blah_big.png', - domain='deeplearningindaba', - languages=[{"code": "en", "description": "English"}]) + name="Deep Learning Indaba", + system_name="Baobab", + small_logo="blah.png", + large_logo="blah_big.png", + domain="deeplearningindaba", + languages=[{"code": "en", "description": "English"}], + ) db.session.flush() def setUp(self): @@ -22,36 +24,41 @@ def setUp(self): def test_organisation_from_origin(self): with app.app_context(): response = self.app.get( - '/api/v1/organisation', - headers={'Origin': 'https://baobab.deeplearningindaba.com/apply'}) + "/api/v1/organisation", + headers={"Origin": "https://baobab.deeplearningindaba.com/apply"}, + ) self.assertEqual(response.status_code, 200) data = json.loads(response.data) - self.assertEqual(data['name'], 'Deep Learning Indaba') - self.assertEqual(data['small_logo'], 'blah.png') - self.assertEqual(data['large_logo'], 'blah_big.png') - self.assertEqual(data['domain'], 'deeplearningindaba') - self.assertEqual(data['languages'][0]['code'], 'en') - self.assertEqual(data['languages'][0]['description'], 'English') + self.assertEqual(data["name"], "Deep Learning Indaba") + self.assertEqual(data["small_logo"], "blah.png") + self.assertEqual(data["large_logo"], "blah_big.png") + self.assertEqual(data["domain"], "deeplearningindaba") + self.assertEqual(data["languages"][0]["code"], "en") + self.assertEqual(data["languages"][0]["description"], "English") def test_organisation_from_referer(self): with app.app_context(): response = self.app.get( - '/api/v1/organisation', - headers={'Origin': '', 'referer': 'https://baobab.deeplearningindaba.com/apply'}) + "/api/v1/organisation", + headers={ + "Origin": "", + "referer": "https://baobab.deeplearningindaba.com/apply", + }, + ) self.assertEqual(response.status_code, 200) data = json.loads(response.data) - self.assertEqual(data['name'], 'Deep Learning Indaba') - self.assertEqual(data['small_logo'], 'blah.png') - self.assertEqual(data['large_logo'], 'blah_big.png') - self.assertEqual(data['domain'], 'deeplearningindaba') - self.assertEqual(data['languages'][0]['code'], 'en') - self.assertEqual(data['languages'][0]['description'], 'English') + self.assertEqual(data["name"], "Deep Learning Indaba") + self.assertEqual(data["small_logo"], "blah.png") + self.assertEqual(data["large_logo"], "blah_big.png") + self.assertEqual(data["domain"], "deeplearningindaba") + self.assertEqual(data["languages"][0]["code"], "en") + self.assertEqual(data["languages"][0]["description"], "English") def test_organisation_error(self): with app.app_context(): response = self.app.get( - '/api/v1/organisation', - headers={'Origin': '', 'referer': ''}) + "/api/v1/organisation", headers={"Origin": "", "referer": ""} + ) self.assertEqual(response.status_code, 400) diff --git a/api/app/outcome/api.py b/api/app/outcome/api.py index e26f427ef..2f5b1fccf 100644 --- a/api/app/outcome/api.py +++ b/api/app/outcome/api.py @@ -1,20 +1,18 @@ import traceback -from flask_restful import reqparse, fields, marshal_with + import flask_restful as restful from flask import g, request +from flask_restful import fields, marshal_with, reqparse from sqlalchemy.exc import SQLAlchemyError +from app import LOGGER, db +from app.events.repository import EventRepository as event_repository 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.utils import errors, misc 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.utils.emailer import email_user def _extract_status(outcome): @@ -22,26 +20,27 @@ def _extract_status(outcome): return None return outcome.status.name + outcome_fields = { - 'id': fields.Integer, - 'status': fields.String(attribute=_extract_status), - 'timestamp': fields.DateTime(dt_format='iso8601'), + "id": fields.Integer, + "status": fields.String(attribute=_extract_status), + "timestamp": fields.DateTime(dt_format="iso8601"), } user_fields = { - 'id': fields.Integer, - 'email': fields.String, - 'firstname': fields.String, - 'lastname': fields.String, - 'user_title': fields.String + "id": fields.Integer, + "email": fields.String, + "firstname": fields.String, + "lastname": fields.String, + "user_title": fields.String, } 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) + "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), } @@ -50,23 +49,23 @@ class OutcomeAPI(restful.Resource): @marshal_with(outcome_fields) def get(self): req_parser = reqparse.RequestParser() - req_parser.add_argument('event_id', type=int, required=True) + req_parser.add_argument("event_id", type=int, required=True) args = req_parser.parse_args() - user_id = g.current_user['id'] - event_id = args['event_id'] + user_id = g.current_user["id"] + event_id = args["event_id"] try: outcome = outcome_repository.get_latest_by_user_for_event(user_id, event_id) if not outcome: return errors.OUTCOME_NOT_FOUND - + return outcome except SQLAlchemyError as e: - LOGGER.error("Database error encountered: {}".format(e)) + LOGGER.error("Database error encountered: {}".format(e)) return errors.DB_NOT_AVAILABLE - except: + except: LOGGER.error("Encountered unknown error: {}".format(traceback.format_exc())) return errors.DB_NOT_AVAILABLE @@ -74,45 +73,45 @@ def get(self): @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("user_id", type=int, required=True) + req_parser.add_argument("outcome", 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']) + user = user_repository.get_by_id(args["user_id"]) if not user: return errors.USER_NOT_FOUND try: - status = Status[args['outcome']] + status = Status[args["outcome"]] except KeyError: return errors.OUTCOME_STATUS_NOT_VALID 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 + ) for existing_outcome in existing_outcomes: existing_outcome.reset_latest() # Add new outcome - outcome = Outcome( - event_id, - args['user_id'], - status, - g.current_user['id']) + outcome = Outcome(event_id, args["user_id"], status, g.current_user["id"]) outcome_repository.add(outcome) db.session.commit() - if status != Status.ACCEPTED: # Email will be sent with offer for accepted candidates + if ( + status != Status.ACCEPTED + ): # Email will be sent with offer for accepted candidates email_user( - 'outcome-rejected' if status == Status.REJECTED else 'outcome-waitlist', - template_parameters=dict( - host=misc.get_baobab_host() - ), + "outcome-rejected" + if status == Status.REJECTED + else "outcome-waitlist", + template_parameters=dict(host=misc.get_baobab_host()), event=event, user=user, ) @@ -120,9 +119,9 @@ def post(self, event_id): return outcome, 201 except SQLAlchemyError as e: - LOGGER.error("Database error encountered: {}".format(e)) + LOGGER.error("Database error encountered: {}".format(e)) return errors.DB_NOT_AVAILABLE - except: + except: LOGGER.error("Encountered unknown error: {}".format(traceback.format_exc())) return errors.DB_NOT_AVAILABLE @@ -139,12 +138,8 @@ def get(self, event_id): outcomes = outcome_repository.get_latest_for_event(event_id) return outcomes except SQLAlchemyError as e: - LOGGER.error("Database error encountered: {}".format(e)) + LOGGER.error("Database error encountered: {}".format(e)) return errors.DB_NOT_AVAILABLE - except: + except: LOGGER.error("Encountered unknown error: {}".format(traceback.format_exc())) return errors.DB_NOT_AVAILABLE - - - - diff --git a/api/app/outcome/models.py b/api/app/outcome/models.py index 55b45058c..3c9b86771 100644 --- a/api/app/outcome/models.py +++ b/api/app/outcome/models.py @@ -1,31 +1,31 @@ from datetime import datetime -from app import db from enum import Enum +from app import db + + class Status(Enum): ACCEPTED = "accepted" REJECTED = "rejected" WAITLIST = "waitlist" + class Outcome(db.Model): - id = db.Column(db.Integer(), primary_key = True, nullable = False) - event_id = db.Column(db.Integer(), db.ForeignKey('event.id'), nullable = False) - user_id = db.Column(db.Integer(), db.ForeignKey('app_user.id'), nullable = False) - status = db.Column(db.Enum(Status, name='outcome_status'), nullable = False) - timestamp = db.Column(db.DateTime(), nullable = False) - latest = db.Column(db.Boolean(), nullable = False) - updated_by_user_id = db.Column(db.Integer(), db.ForeignKey('app_user.id'), nullable=False) - - event = db.relationship('Event', foreign_keys=[event_id]) - user = db.relationship('AppUser', foreign_keys=[user_id]) - updated_by_user = db.relationship('AppUser', foreign_keys=[updated_by_user_id]) - - def __init__(self, - event_id, - user_id, - status, - updated_by_user_id - ): + id = db.Column(db.Integer(), primary_key=True, nullable=False) + event_id = db.Column(db.Integer(), db.ForeignKey("event.id"), nullable=False) + user_id = db.Column(db.Integer(), db.ForeignKey("app_user.id"), nullable=False) + status = db.Column(db.Enum(Status, name="outcome_status"), nullable=False) + timestamp = db.Column(db.DateTime(), nullable=False) + latest = db.Column(db.Boolean(), nullable=False) + updated_by_user_id = db.Column( + db.Integer(), db.ForeignKey("app_user.id"), nullable=False + ) + + event = db.relationship("Event", foreign_keys=[event_id]) + user = db.relationship("AppUser", foreign_keys=[user_id]) + updated_by_user = db.relationship("AppUser", foreign_keys=[updated_by_user_id]) + + def __init__(self, event_id, user_id, status, updated_by_user_id): self.event_id = event_id self.user_id = user_id self.status = status @@ -34,4 +34,4 @@ def __init__(self, self.updated_by_user_id = updated_by_user_id def reset_latest(self): - self.latest = False \ No newline at end of file + self.latest = False diff --git a/api/app/outcome/repository.py b/api/app/outcome/repository.py index e673cc6d3..2685e7d29 100644 --- a/api/app/outcome/repository.py +++ b/api/app/outcome/repository.py @@ -2,31 +2,32 @@ from app.outcome.models import Outcome -class OutcomeRepository(): - +class OutcomeRepository: @staticmethod def get_latest_by_user_for_event(user_id, event_id): - outcome = (db.session.query(Outcome) - .filter_by(user_id=user_id, event_id=event_id, latest=True) - .first()) + outcome = ( + db.session.query(Outcome) + .filter_by(user_id=user_id, event_id=event_id, latest=True) + .first() + ) return outcome - + @staticmethod def get_all_by_user_for_event(user_id, event_id): - outcomes = (db.session.query(Outcome) - .filter_by(user_id=user_id, event_id=event_id) - .all()) + outcomes = ( + db.session.query(Outcome) + .filter_by(user_id=user_id, event_id=event_id) + .all() + ) return outcomes @staticmethod def get_latest_for_event(event_id): - outcomes = (db.session.query(Outcome) - .filter_by(latest=True, event_id=event_id) - .all()) + outcomes = ( + db.session.query(Outcome).filter_by(latest=True, event_id=event_id).all() + ) return outcomes @staticmethod def add(outcome): db.session.add(outcome) - - \ No newline at end of file diff --git a/api/app/outcome/tests.py b/api/app/outcome/tests.py index 3f65f0652..5fffe1a89 100644 --- a/api/app/outcome/tests.py +++ b/api/app/outcome/tests.py @@ -1,48 +1,59 @@ import json from datetime import datetime, timedelta -from app import db, LOGGER -from app.utils.testing import ApiTestCase +from app import LOGGER, db from app.outcome.models import Outcome, Status from app.outcome.repository import OutcomeRepository as outcome_repository +from app.utils.testing import ApiTestCase + class OutcomeApiTest(ApiTestCase): def seed_static_data(self): - db.session.expire_on_commit = False + db.session.expire_on_commit = False - self.event1 = self.add_event(name={'en': 'Event 1'}, key='event1') - self.event2 = self.add_event(name={'en': 'Event 2'}, key='event2') + self.event1 = self.add_event(name={"en": "Event 1"}, key="event1") + self.event2 = self.add_event(name={"en": "Event 2"}, key="event2") - self.test_user1 = self.add_user('something@email.com') - self.test_user2 = self.add_user('something_else@email.com') - self.event1_admin = self.add_user('event1admin@email.com') - self.event2_admin = self.add_user('event2admin@email.com') + self.test_user1 = self.add_user("something@email.com") + self.test_user2 = self.add_user("something_else@email.com") + self.event1_admin = self.add_user("event1admin@email.com") + self.event2_admin = self.add_user("event2admin@email.com") - self.event1.add_event_role('admin', self.event1_admin.id) - self.event2.add_event_role('admin', self.event2_admin.id) + self.event1.add_event_role("admin", self.event1_admin.id) + self.event2.add_event_role("admin", self.event2_admin.id) # Add 2 outcomes for event 1 to user 1 - self.event1_user1_outcome1 = Outcome(self.event1.id, self.test_user1.id, 'WAITLIST', self.event1_admin.id) + self.event1_user1_outcome1 = Outcome( + self.event1.id, self.test_user1.id, "WAITLIST", self.event1_admin.id + ) self.event1_user1_outcome1.reset_latest() - self.event1_user1_outcome2 = Outcome(self.event1.id, self.test_user1.id, 'ACCEPTED', self.event1_admin.id) + self.event1_user1_outcome2 = Outcome( + self.event1.id, self.test_user1.id, "ACCEPTED", self.event1_admin.id + ) # Add 1 outcome for event 1 to user 2 - self.event1_user2_outcome = Outcome(self.event1.id, self.test_user2.id, 'REJECTED', self.event1_admin.id) + self.event1_user2_outcome = Outcome( + self.event1.id, self.test_user2.id, "REJECTED", self.event1_admin.id + ) # Add 1 outcome for event 2 to user 1 - self.event2_user1_outcome = Outcome(self.event2.id, self.test_user1.id, 'WAITLIST', self.event2_admin.id) - - db.session.add_all([ - self.event1_user1_outcome1, - self.event1_user1_outcome2, - self.event1_user2_outcome, - self.event2_user1_outcome - ]) + self.event2_user1_outcome = Outcome( + self.event2.id, self.test_user1.id, "WAITLIST", self.event2_admin.id + ) + + db.session.add_all( + [ + self.event1_user1_outcome1, + self.event1_user1_outcome2, + self.event1_user2_outcome, + self.event2_user1_outcome, + ] + ) db.session.commit() - self.add_email_template('outcome-rejected') - self.add_email_template('outcome-waitlist') + self.add_email_template("outcome-rejected") + self.add_email_template("outcome-waitlist") db.session.flush() @@ -52,66 +63,82 @@ def seed_static_data(self): self.event2_user1_outcome_id = self.event2_user1_outcome.id self.test_user1_id = self.test_user1.id - def test_repository_get_latest_by_user_for_event(self): """Test that repository method gets the correct latest outcome for a user.""" self.seed_static_data() - result = outcome_repository.get_latest_by_user_for_event(self.test_user1.id, self.event1.id) + result = outcome_repository.get_latest_by_user_for_event( + self.test_user1.id, self.event1.id + ) self.assertEqual(result.id, self.event1_user1_outcome2.id) self.assertEqual(result.status, Status.ACCEPTED) - result = outcome_repository.get_latest_by_user_for_event(self.test_user2.id, self.event1.id) + result = outcome_repository.get_latest_by_user_for_event( + self.test_user2.id, self.event1.id + ) self.assertEqual(result.id, self.event1_user2_outcome.id) self.assertEqual(result.status, Status.REJECTED) def test_get_all_by_user_for_event(self): """Test that repository method gets all outcomes for a user.""" self.seed_static_data() - result = outcome_repository.get_all_by_user_for_event(self.test_user1.id, self.event1.id) + result = outcome_repository.get_all_by_user_for_event( + self.test_user1.id, self.event1.id + ) self.assertEqual(len(result), 2) - self.assertCountEqual([o.id for o in result], [self.event1_user1_outcome1.id, self.event1_user1_outcome2.id]) - + self.assertCountEqual( + [o.id for o in result], + [self.event1_user1_outcome1.id, self.event1_user1_outcome2.id], + ) + def test_get_latest_for_event(self): """Test that repository method gets the latest outcomes for an event.""" self.seed_static_data() result = outcome_repository.get_latest_for_event(self.event1.id) self.assertEqual(len(result), 2) - self.assertCountEqual([o.id for o in result], [self.event1_user1_outcome2.id, self.event1_user2_outcome.id]) + self.assertCountEqual( + [o.id for o in result], + [self.event1_user1_outcome2.id, self.event1_user2_outcome.id], + ) def test_outcome_get(self): """Test usual get case.""" self.seed_static_data() response = self.app.get( - '/api/v1/outcome', - data={'event_id': self.event1.id}, - headers=self.get_auth_header_for('something@email.com')) + "/api/v1/outcome", + data={"event_id": self.event1.id}, + headers=self.get_auth_header_for("something@email.com"), + ) data = json.loads(response.data) self.assertEqual(response.status_code, 200) - self.assertEqual(data['id'], self.event1_user1_outcome2_id) - self.assertEqual(data['status'], 'ACCEPTED') - self.assertEqual(data['timestamp'], self.event1_user1_outcome2.timestamp.isoformat()) + self.assertEqual(data["id"], self.event1_user1_outcome2_id) + self.assertEqual(data["status"], "ACCEPTED") + self.assertEqual( + data["timestamp"], self.event1_user1_outcome2.timestamp.isoformat() + ) def test_get_with_no_outcome(self): """Test get method when there is no outcome.""" self.seed_static_data() response = self.app.get( - '/api/v1/outcome', - data={'event_id': self.event2.id}, - headers=self.get_auth_header_for('something_else@email.com')) + "/api/v1/outcome", + data={"event_id": self.event2.id}, + headers=self.get_auth_header_for("something_else@email.com"), + ) self.assertEqual(response.status_code, 404) def test_outcome_post_non_event_admin(self): """Test that a forbidden status is given when the logged in user is not an event admin.""" self.seed_static_data() response = self.app.post( - '/api/v1/outcome', - data={ - 'event_id': self.event1.id, - 'user_id': self.test_user1.id, - 'outcome': 'ACCEPTED' - }, - headers=self.get_auth_header_for('something@email.com')) + "/api/v1/outcome", + data={ + "event_id": self.event1.id, + "user_id": self.test_user1.id, + "outcome": "ACCEPTED", + }, + headers=self.get_auth_header_for("something@email.com"), + ) data = json.loads(response.data) self.assertEqual(response.status_code, 403) @@ -119,17 +146,20 @@ def test_post(self): """Test typical post flow.""" self.seed_static_data() response = self.app.post( - '/api/v1/outcome', - data={ - 'event_id': self.event2.id, - 'user_id': self.test_user1.id, - 'outcome': 'REJECTED' - }, - headers=self.get_auth_header_for('event2admin@email.com')) + "/api/v1/outcome", + data={ + "event_id": self.event2.id, + "user_id": self.test_user1.id, + "outcome": "REJECTED", + }, + headers=self.get_auth_header_for("event2admin@email.com"), + ) data = json.loads(response.data) self.assertEqual(response.status_code, 201) - outcomes = outcome_repository.get_all_by_user_for_event(self.test_user1_id, self.event2.id) + outcomes = outcome_repository.get_all_by_user_for_event( + self.test_user1_id, self.event2.id + ) outcomes = list(sorted(outcomes, key=lambda o: o.timestamp)) print(outcomes) @@ -143,34 +173,35 @@ def test_outcome_list_get_event_admin(self): """Test that outcome list get can only be performed by an event admin.""" self.seed_static_data() response = self.app.get( - '/api/v1/outcome-list', - data={'event_id': self.event1.id}, - headers=self.get_auth_header_for('something_else@email.com')) + "/api/v1/outcome-list", + data={"event_id": self.event1.id}, + headers=self.get_auth_header_for("something_else@email.com"), + ) self.assertEqual(response.status_code, 403) def test_outcome_list_get_correct_event_admin(self): """Test that outcome list get can only be performed by the correct event admin.""" self.seed_static_data() response = self.app.get( - '/api/v1/outcome-list', - data={'event_id': self.event1.id}, - headers=self.get_auth_header_for('event2admin@email.com')) + "/api/v1/outcome-list", + data={"event_id": self.event1.id}, + headers=self.get_auth_header_for("event2admin@email.com"), + ) self.assertEqual(response.status_code, 403) def test_outcome_list_get(self): """Test getting all outcomes for an event.""" self.seed_static_data() response = self.app.get( - '/api/v1/outcome-list', - data={'event_id': self.event1.id}, - headers=self.get_auth_header_for('event1admin@email.com')) + "/api/v1/outcome-list", + data={"event_id": self.event1.id}, + headers=self.get_auth_header_for("event1admin@email.com"), + ) data = json.loads(response.data) self.assertEqual(response.status_code, 200) self.assertEqual(len(data), 2) - self.assertCountEqual([o['user']['email'] for o in data], ['something@email.com', 'something_else@email.com']) - - - - - + self.assertCountEqual( + [o["user"]["email"] for o in data], + ["something@email.com", "something_else@email.com"], + ) diff --git a/api/app/references/api.py b/api/app/references/api.py index 5443864b1..60a4300e6 100644 --- a/api/app/references/api.py +++ b/api/app/references/api.py @@ -1,104 +1,130 @@ from datetime import datetime -from flask import g, request import flask_restful as restful +from flask import g, request from flask_restful import fields, marshal_with from app import LOGGER - from app.applicationModel.models import ApplicationForm -from app.applicationModel.repository import ApplicationFormRepository as application_form_repository +from app.applicationModel.repository import ( + ApplicationFormRepository as application_form_repository, +) from app.events.models import Event from app.events.repository import EventRepository as event_repository -from app.utils.errors import EVENT_NOT_FOUND, USER_NOT_FOUND, RESPONSE_NOT_FOUND, FORBIDDEN,\ - REFRERENCE_REQUEST_WITH_TOKEN_NOT_FOUND, DUPLICATE_REFERENCE_SUBMISSION,\ - APPLICATIONS_CLOSED, REFERENCE_REQUEST_NOT_FOUND, BAD_CONFIGURATION - -from app.utils.auth import auth_optional, auth_required -from app.utils.emailer import email_user -from app.references.repository import ReferenceRequestRepository as reference_request_repository +from app.references.mixins import ( + ReferenceMixin, + ReferenceRequestDetailMixin, + ReferenceRequestsListMixin, + ReferenceRequestsMixin, +) +from app.references.models import Reference, ReferenceRequest from app.references.repository import ReferenceRepository as reference_repository -from app.references.models import ReferenceRequest, Reference -from app.references.mixins import ReferenceRequestsMixin, ReferenceRequestsListMixin, ReferenceRequestDetailMixin, \ - ReferenceMixin -from app.utils import misc -from app.users.repository import UserRepository as user_repository +from app.references.repository import ( + ReferenceRequestRepository as reference_request_repository, +) from app.responses.repository import ResponseRepository as response_repository +from app.users.repository import UserRepository as user_repository +from app.utils import misc +from app.utils.auth import auth_optional, auth_required +from app.utils.emailer import email_user +from app.utils.errors import ( + APPLICATIONS_CLOSED, + BAD_CONFIGURATION, + DUPLICATE_REFERENCE_SUBMISSION, + EVENT_NOT_FOUND, + FORBIDDEN, + REFERENCE_REQUEST_NOT_FOUND, + REFRERENCE_REQUEST_WITH_TOKEN_NOT_FOUND, + RESPONSE_NOT_FOUND, + USER_NOT_FOUND, +) reference_request_fields = { - 'id': fields.Integer, - 'title': fields.String, - 'firstname': fields.String, - 'lastname': fields.String, - 'relation': fields.String, - 'email_sent': fields.DateTime, - 'response_id': fields.Integer, - 'email': fields.String, - 'reference_submitted': fields.Boolean(attribute='has_reference') + "id": fields.Integer, + "title": fields.String, + "firstname": fields.String, + "lastname": fields.String, + "relation": fields.String, + "email_sent": fields.DateTime, + "response_id": fields.Integer, + "email": fields.String, + "reference_submitted": fields.Boolean(attribute="has_reference"), } reference_request_details_fields = { - 'candidate': fields.String, - 'nominator': fields.String, - 'relation': fields.String, - 'name': fields.String, - 'description': fields.String, - 'is_application_open': fields.Boolean, - 'email_from': fields.String, - 'reference_submitted_timestamp': fields.DateTime, - } + "candidate": fields.String, + "nominator": fields.String, + "relation": fields.String, + "name": fields.String, + "description": fields.String, + "is_application_open": fields.Boolean, + "email_from": fields.String, + "reference_submitted_timestamp": fields.DateTime, +} reference_fields = { - 'id': fields.Integer, - 'reference_request_id': fields.String, - 'uploaded_document': fields.String, - 'timestamp': fields.DateTime + "id": fields.Integer, + "reference_request_id": fields.String, + "uploaded_document": fields.String, + "timestamp": fields.DateTime, } + def _get_candidate_nominator(response): - nominating_capacity = response_repository.get_answer_by_question_key_and_response_id('nominating_capacity', response.id) + nominating_capacity = ( + response_repository.get_answer_by_question_key_and_response_id( + "nominating_capacity", response.id + ) + ) if not nominating_capacity: - raise ValueError('Missing nominating capacity answer') - is_nomination = nominating_capacity.value == 'other' + raise ValueError("Missing nominating capacity answer") + is_nomination = nominating_capacity.value == "other" if is_nomination: - question_answers = response_repository.get_question_answers_by_section_key_and_response_id( - 'nominee_section', response.id) - - nomination_info = { - qa.Question.key: qa.Answer.value - for qa in question_answers - } - candidate = '{nomination_title} {nomination_firstname} {nomination_lastname}'.format(**nomination_info) - candidate_firstname = nomination_info['nomination_firstname'] - nominator = '{} {} {}'.format(response.user.user_title, response.user.firstname, response.user.lastname) + question_answers = ( + response_repository.get_question_answers_by_section_key_and_response_id( + "nominee_section", response.id + ) + ) + + nomination_info = {qa.Question.key: qa.Answer.value for qa in question_answers} + candidate = ( + "{nomination_title} {nomination_firstname} {nomination_lastname}".format( + **nomination_info + ) + ) + candidate_firstname = nomination_info["nomination_firstname"] + nominator = "{} {} {}".format( + response.user.user_title, response.user.firstname, response.user.lastname + ) else: - candidate = '{} {} {}'.format(response.user.user_title, response.user.firstname, response.user.lastname) + candidate = "{} {} {}".format( + response.user.user_title, response.user.firstname, response.user.lastname + ) candidate_firstname = response.user.firstname nominator = None - + return candidate, candidate_firstname, nominator class ReferenceRequestAPI(ReferenceRequestsMixin, restful.Resource): - @marshal_with(reference_request_fields) @auth_required def get(self): args = self.get_req_parser.parse_args() - return reference_request_repository.get_by_id(args['id']), 200 + return reference_request_repository.get_by_id(args["id"]), 200 @marshal_with(reference_request_fields) @auth_required def post(self): args = self.post_req_parser.parse_args() - response_id = args['response_id'] - title = args['title'] - firstname = args['firstname'] - lastname = args['lastname'] - relation = args['relation'] - email = args['email'] - user = user_repository.get_by_id(g.current_user['id']) + response_id = args["response_id"] + title = args["title"] + firstname = args["firstname"] + lastname = args["lastname"] + relation = args["relation"] + email = args["email"] + user = user_repository.get_by_id(g.current_user["id"]) if not user: return USER_NOT_FOUND @@ -106,36 +132,47 @@ def post(self): event = event_repository.get_event_by_response_id(response_id) if not event: return EVENT_NOT_FOUND - + response = response_repository.get_by_id(response_id) if not response: return RESPONSE_NOT_FOUND - reference_request = ReferenceRequest(response_id=response_id,title=title, - firstname=firstname, lastname=lastname, relation=relation, email=email) + reference_request = ReferenceRequest( + response_id=response_id, + title=title, + firstname=firstname, + lastname=lastname, + relation=relation, + email=email, + ) reference_request_repository.create(reference_request) - link = "{host}/reference/{token}".format(host=misc.get_baobab_host(), - token=reference_request.token) + link = "{host}/reference/{token}".format( + host=misc.get_baobab_host(), token=reference_request.token + ) try: - candidate, candidate_firstname, nominator = _get_candidate_nominator(response) + candidate, candidate_firstname, nominator = _get_candidate_nominator( + response + ) except ValueError as e: LOGGER.error(e) return BAD_CONFIGURATION email_user( - 'reference-request-self-nomination' if nominator is None else 'reference-request', + "reference-request-self-nomination" + if nominator is None + else "reference-request", template_parameters=dict( candidate=candidate, candidate_firstname=candidate_firstname, nominator=nominator, event_url=event.url, application_close_date=event.application_close, - link=link + link=link, ), event=event, - user=user + user=user, ) reference_request.set_email_sent(datetime.now()) @@ -144,25 +181,28 @@ def post(self): class ReferenceRequestListAPI(ReferenceRequestsListMixin, restful.Resource): - @marshal_with(reference_request_fields) @auth_required def get(self): args = self.req_parser.parse_args() - response = response_repository.get_by_id_and_user_id(args['response_id'], g.current_user['id']) + response = response_repository.get_by_id_and_user_id( + args["response_id"], g.current_user["id"] + ) if not response: return RESPONSE_NOT_FOUND return reference_request_repository.get_all_by_response_id(response.id), 200 -class ReferenceRequestDetailAPI(ReferenceRequestDetailMixin, restful.Resource): +class ReferenceRequestDetailAPI(ReferenceRequestDetailMixin, restful.Resource): @marshal_with(reference_request_details_fields) def get(self): args = self.post_req_parser.parse_args() - token = args['token'] + token = args["token"] - reference_request = reference_request_repository.get_by_token(token) # type: ReferenceRequest + reference_request = reference_request_repository.get_by_token( + token + ) # type: ReferenceRequest if not reference_request: return REFRERENCE_REQUEST_WITH_TOKEN_NOT_FOUND @@ -176,7 +216,9 @@ def get(self): if not event: return EVENT_NOT_FOUND - reference = reference_repository.get_by_reference_request_id(reference_request.id) + reference = reference_repository.get_by_reference_request_id( + reference_request.id + ) app_form = event.get_application_form() # type: ApplicationForm # Determine whether the response is a nomination @@ -187,20 +229,22 @@ def get(self): return BAD_CONFIGURATION return_object = { - 'candidate': candidate, - 'nominator': nominator, - 'relation': reference_request.relation, - 'name': event.get_name('en'), - 'description': event.get_description('en'), - 'is_application_open': event.is_application_open, - 'email_from': event.email_from, - 'reference_submitted_timestamp': reference.timestamp if reference is not None else None + "candidate": candidate, + "nominator": nominator, + "relation": reference_request.relation, + "name": event.get_name("en"), + "description": event.get_description("en"), + "is_application_open": event.is_application_open, + "email_from": event.email_from, + "reference_submitted_timestamp": reference.timestamp + if reference is not None + else None, } return return_object, 200 -REFERENCE_REQUEST_EMAIL_BODY=""" +REFERENCE_REQUEST_EMAIL_BODY = """ Dear {title} {firstname} {lastname}, {candidate} {nomination_text} for the {event_name} ({event_url}). @@ -217,13 +261,12 @@ def get(self): class ReferenceAPI(ReferenceMixin, restful.Resource): - @marshal_with(reference_fields) @auth_required def get(self): args = self.get_req_parser.parse_args() - user = user_repository.get_by_id(g.current_user['id']) - response = response_repository.get_by_id(args['response_id']) + user = user_repository.get_by_id(g.current_user["id"]) + response = response_repository.get_by_id(args["response_id"]) if not response: return RESPONSE_NOT_FOUND @@ -231,13 +274,17 @@ def get(self): if not user.is_event_admin(event.id): return FORBIDDEN - reference_responses = reference_request_repository.get_references_by_response_id(response.id) - return [reference_response.Reference for reference_response in reference_responses], 200 + reference_responses = ( + reference_request_repository.get_references_by_response_id(response.id) + ) + return [ + reference_response.Reference for reference_response in reference_responses + ], 200 def post(self): args = self.post_req_parser.parse_args() - token = args['token'] - uploaded_document = args['uploaded_document'] + token = args["token"] + uploaded_document = args["uploaded_document"] reference_request = reference_request_repository.get_by_token(token) event = event_repository.get_event_by_response_id(reference_request.response_id) @@ -250,17 +297,22 @@ def post(self): if not event.is_application_open: return APPLICATIONS_CLOSED - reference = Reference(reference_request_id=reference_request.id, uploaded_document=uploaded_document) + reference = Reference( + reference_request_id=reference_request.id, + uploaded_document=uploaded_document, + ) reference_request_repository.add(reference) return {}, 201 def put(self): args = self.put_req_parser.parse_args() - token = args['token'] - uploaded_document = args['uploaded_document'] + token = args["token"] + uploaded_document = args["uploaded_document"] reference_request = reference_request_repository.get_by_token(token) event = event_repository.get_event_by_response_id(reference_request.response_id) - reference = reference_request_repository.get_reference_by_reference_request_id(reference_request.id) + reference = reference_request_repository.get_reference_by_reference_request_id( + reference_request.id + ) if not reference_request: return REFERENCE_REQUEST_NOT_FOUND diff --git a/api/app/references/mixins.py b/api/app/references/mixins.py index d290b36d8..52f1ca799 100644 --- a/api/app/references/mixins.py +++ b/api/app/references/mixins.py @@ -3,38 +3,39 @@ class ReferenceRequestsMixin(object): post_req_parser = reqparse.RequestParser() - post_req_parser.add_argument('response_id', type=int, required=True) - post_req_parser.add_argument('title', type=str, required=True) - post_req_parser.add_argument('firstname', type=str, required=True) - post_req_parser.add_argument('lastname', type=str, required=True) - post_req_parser.add_argument('relation', type=str, required=True) - post_req_parser.add_argument('email', type=str, required=True) + post_req_parser.add_argument("response_id", type=int, required=True) + post_req_parser.add_argument("title", type=str, required=True) + post_req_parser.add_argument("firstname", type=str, required=True) + post_req_parser.add_argument("lastname", type=str, required=True) + post_req_parser.add_argument("relation", type=str, required=True) + post_req_parser.add_argument("email", type=str, required=True) get_req_parser = reqparse.RequestParser() - get_req_parser.add_argument('id', type=int, required=False) + get_req_parser.add_argument("id", type=int, required=False) + class ReferenceRequestsListMixin(object): req_parser = reqparse.RequestParser() - req_parser.add_argument('response_id', type=int, required=True) + req_parser.add_argument("response_id", type=int, required=True) class ReferenceRequestDetailMixin(object): get_req_parser = reqparse.RequestParser() - get_req_parser.add_argument('response_id', type=int, required=True) + get_req_parser.add_argument("response_id", type=int, required=True) post_req_parser = reqparse.RequestParser() - post_req_parser.add_argument('token', type=str, required=True) + post_req_parser.add_argument("token", type=str, required=True) class ReferenceMixin(object): get_req_parser = reqparse.RequestParser() - get_req_parser.add_argument('response_id', type=int, required=True) + get_req_parser.add_argument("response_id", type=int, required=True) post_req_parser = reqparse.RequestParser() - post_req_parser.add_argument('token', type=str, required=True) - post_req_parser.add_argument('uploaded_document', type=str, required=True) + post_req_parser.add_argument("token", type=str, required=True) + post_req_parser.add_argument("uploaded_document", type=str, required=True) put_req_parser = reqparse.RequestParser() - put_req_parser.add_argument('token', type=str, required=True) - put_req_parser.add_argument('uploaded_document', type=str, required=True) + put_req_parser.add_argument("token", type=str, required=True) + put_req_parser.add_argument("uploaded_document", type=str, required=True) diff --git a/api/app/references/models.py b/api/app/references/models.py index 7cf7273e2..3ae7e86cf 100644 --- a/api/app/references/models.py +++ b/api/app/references/models.py @@ -1,14 +1,14 @@ - from datetime import datetime from app import db + class ReferenceRequest(db.Model): __tablename__ = "reference_request" id = db.Column(db.Integer(), primary_key=True) - response_id = db.Column(db.Integer(), db.ForeignKey('response.id'), nullable=False) + response_id = db.Column(db.Integer(), db.ForeignKey("response.id"), nullable=False) title = db.Column(db.String(50), nullable=False) firstname = db.Column(db.String(50), nullable=False) lastname = db.Column(db.String(50), nullable=False) @@ -17,17 +17,10 @@ class ReferenceRequest(db.Model): email = db.Column(db.String(50), nullable=True) email_sent = db.Column(db.DateTime(), nullable=True) - references = db.relationship('Reference') - response = db.relationship('Response', foreign_keys=[response_id]) - - def __init__(self, - response_id, - title, - firstname, - lastname, - relation, - email - ): + references = db.relationship("Reference") + response = db.relationship("Response", foreign_keys=[response_id]) + + def __init__(self, response_id, title, firstname, lastname, relation, email): self.response_id = response_id self.title = title self.firstname = firstname @@ -46,18 +39,21 @@ def has_reference(self): return len(self.references) > 0 - class Reference(db.Model): __tablename__ = "reference" id = db.Column(db.Integer(), primary_key=True) - reference_request_id = db.Column(db.Integer(), db.ForeignKey('reference_request.id'), nullable=False) + reference_request_id = db.Column( + db.Integer(), db.ForeignKey("reference_request.id"), nullable=False + ) uploaded_document = db.Column(db.String(128), nullable=False) timestamp = db.Column(db.DateTime(), nullable=False) - reference_request = db.relationship('ReferenceRequest', foreign_keys=[reference_request_id]) + reference_request = db.relationship( + "ReferenceRequest", foreign_keys=[reference_request_id] + ) def __init__(self, reference_request_id, uploaded_document): self.reference_request_id = reference_request_id self.uploaded_document = uploaded_document - self.timestamp = datetime.now() \ No newline at end of file + self.timestamp = datetime.now() diff --git a/api/app/references/repository.py b/api/app/references/repository.py index a323d7bcc..32d235aaf 100644 --- a/api/app/references/repository.py +++ b/api/app/references/repository.py @@ -1,11 +1,11 @@ -from app import db -from app.references.models import ReferenceRequest, Reference -from app.events.models import Event import uuid +from app import db +from app.events.models import Event +from app.references.models import Reference, ReferenceRequest -class ReferenceRequestRepository(): +class ReferenceRequestRepository: @staticmethod def get_by_id(reference_request_id): return db.session.query(ReferenceRequest).get(reference_request_id) @@ -31,32 +31,35 @@ def commit(): @staticmethod def get_all_by_response_id(response_id): - return db.session.query(ReferenceRequest)\ - .filter_by(response_id=response_id)\ - .all() + return ( + db.session.query(ReferenceRequest).filter_by(response_id=response_id).all() + ) @staticmethod def get_by_token(token): - return db.session.query(ReferenceRequest)\ - .filter_by(token=token)\ - .first() + return db.session.query(ReferenceRequest).filter_by(token=token).first() @staticmethod def get_references_by_response_id(response_id): - return db.session.query(ReferenceRequest.response_id, Reference)\ - .join(ReferenceRequest, ReferenceRequest.id == Reference.reference_request_id)\ - .filter_by(response_id=response_id)\ - .all() + return ( + db.session.query(ReferenceRequest.response_id, Reference) + .join( + ReferenceRequest, ReferenceRequest.id == Reference.reference_request_id + ) + .filter_by(response_id=response_id) + .all() + ) @staticmethod def get_reference_by_reference_request_id(reference_request_id): - return db.session.query(Reference)\ - .filter_by(reference_request_id=reference_request_id)\ - .first() - + return ( + db.session.query(Reference) + .filter_by(reference_request_id=reference_request_id) + .first() + ) -class ReferenceRepository(): +class ReferenceRepository: @staticmethod def get_by_id(reference_id): return db.session.query(Reference).get(reference_id) @@ -72,13 +75,19 @@ def add(reference): @staticmethod def get_by_response_id(response_id): - return db.session.query(ReferenceRequest.response_id, Reference)\ - .join(ReferenceRequest, ReferenceRequest.id == Reference.reference_request_id)\ - .filter_by(response_id=response_id)\ + return ( + db.session.query(ReferenceRequest.response_id, Reference) + .join( + ReferenceRequest, ReferenceRequest.id == Reference.reference_request_id + ) + .filter_by(response_id=response_id) .all() + ) @staticmethod def get_by_reference_request_id(reference_request_id): - return db.session.query(Reference)\ - .filter_by(reference_request_id=reference_request_id)\ + return ( + db.session.query(Reference) + .filter_by(reference_request_id=reference_request_id) .first() + ) diff --git a/api/app/references/tests.py b/api/app/references/tests.py index b544b67cb..4a185faa8 100644 --- a/api/app/references/tests.py +++ b/api/app/references/tests.py @@ -1,89 +1,95 @@ import copy import json +from datetime import date, datetime -from datetime import datetime, date -from app import app, db, LOGGER -from app.events.models import Event -from app.utils.testing import ApiTestCase -from app.responses.models import Response, Answer +from app import LOGGER, app, db +from app.applicationModel.models import ApplicationForm, Question, Section from app.events.models import Event, EventRole -from app.users.models import AppUser, Country, UserCategory -from app.applicationModel.models import ApplicationForm, Section, Question -from app.utils.errors import FORBIDDEN from app.organisation.models import Organisation -from app.references.repository import ReferenceRequestRepository as reference_request_repository from app.references.models import ReferenceRequest +from app.references.repository import ( + ReferenceRequestRepository as reference_request_repository, +) +from app.responses.models import Answer, Response +from app.users.models import AppUser, Country, UserCategory +from app.utils.errors import FORBIDDEN +from app.utils.testing import ApiTestCase class ReferenceAPITest(ApiTestCase): - def _seed_data(self): - self.add_organisation('Deep Learning Indaba', 'blah.png', 'blah_big.png') - self.first_user_data = self.add_user('firstuser@mail.com', 'First', 'User', 'Mx') - self.other_user_data = self.add_user('someuser@mail.com') + self.add_organisation("Deep Learning Indaba", "blah.png", "blah_big.png") + self.first_user_data = self.add_user( + "firstuser@mail.com", "First", "User", "Mx" + ) + self.other_user_data = self.add_user("someuser@mail.com") test_event = self.add_event() - test_event.add_event_role('admin', 1) + test_event.add_event_role("admin", 1) self.test_event_data = copy.deepcopy(test_event.__dict__) - self.test_event_data['name'] = test_event.get_name('en') - self.test_event_data['description'] = test_event.get_description('en') + self.test_event_data["name"] = test_event.get_name("en") + self.test_event_data["description"] = test_event.get_description("en") self.add_to_db(test_event) nomination_event = self.add_event(key="AWARD_NOMINATIONS_ONLY") - nomination_event.add_event_role('admin', 1) + nomination_event.add_event_role("admin", 1) self.test_nomination_event_data = copy.deepcopy(nomination_event.__dict__) self.add_to_db(nomination_event) self.test_form = self.create_application_form(test_event.id, True, False) self.add_to_db(self.test_form) - self.test_nomination_form = self.create_application_form(nomination_event.id, True, True) + self.test_nomination_form = self.create_application_form( + nomination_event.id, True, True + ) sections = [ Section(test_event.id, 1), - Section(nomination_event.id, 1, key='nominee_section') + Section(nomination_event.id, 1, key="nominee_section"), ] db.session.add_all(sections) db.session.commit() questions = [ - Question(self.test_form.id, sections[0].id, 1, 'long_text'), - Question(self.test_form.id, sections[0].id, 2, 'long_text'), - Question(self.test_nomination_form.id, sections[1].id, 1, 'long_text'), - Question(self.test_nomination_form.id, sections[1].id, 2, 'long_text'), - Question(self.test_nomination_form.id, sections[1].id, 3, 'long_text'), - Question(self.test_nomination_form.id, sections[1].id, 4, 'long_text'), + Question(self.test_form.id, sections[0].id, 1, "long_text"), + Question(self.test_form.id, sections[0].id, 2, "long_text"), + Question(self.test_nomination_form.id, sections[1].id, 1, "long_text"), + Question(self.test_nomination_form.id, sections[1].id, 2, "long_text"), + Question(self.test_nomination_form.id, sections[1].id, 3, "long_text"), + Question(self.test_nomination_form.id, sections[1].id, 4, "long_text"), ] - questions[0].key = 'nominating_capacity' - questions[2].key = 'nomination_title' - questions[3].key = 'nomination_firstname' - questions[4].key = 'nomination_lastname' - questions[5].key = 'nomination_email' + questions[0].key = "nominating_capacity" + questions[2].key = "nomination_title" + questions[3].key = "nomination_firstname" + questions[4].key = "nomination_lastname" + questions[5].key = "nomination_email" db.session.add_all(questions) db.session.commit() self.test_response1 = self.add_response( # Self nomination - self.test_form.id, self.first_user_data.id) + self.test_form.id, self.first_user_data.id + ) self.add_to_db(self.test_response1) answers = [ - Answer(self.test_response1.id, questions[0].id, 'self'), - Answer(self.test_response1.id, questions[1].id, 'Blah') + Answer(self.test_response1.id, questions[0].id, "self"), + Answer(self.test_response1.id, questions[1].id, "Blah"), ] db.session.add_all(answers) db.session.commit() self.test_response2 = self.add_response( # Nominating other - self.test_form.id, self.other_user_data.id) + self.test_form.id, self.other_user_data.id + ) self.add_to_db(self.test_response2) answers = [ - Answer(self.test_response2.id, questions[0].id, 'other'), - Answer(self.test_response2.id, questions[1].id, 'Blah'), - Answer(self.test_response2.id, questions[2].id, 'Mx'), - Answer(self.test_response2.id, questions[3].id, 'Skittles'), - Answer(self.test_response2.id, questions[4].id, 'Cat'), - Answer(self.test_response2.id, questions[5].id, 'skittles@box.com'), + Answer(self.test_response2.id, questions[0].id, "other"), + Answer(self.test_response2.id, questions[1].id, "Blah"), + Answer(self.test_response2.id, questions[2].id, "Mx"), + Answer(self.test_response2.id, questions[3].id, "Skittles"), + Answer(self.test_response2.id, questions[4].id, "Cat"), + Answer(self.test_response2.id, questions[5].id, "skittles@box.com"), ] db.session.add_all(answers) db.session.commit() @@ -91,106 +97,130 @@ def _seed_data(self): self.first_headers = self.get_auth_header_for("firstuser@mail.com") self.other_headers = self.get_auth_header_for("someuser@mail.com") - self.add_email_template('reference-request-self-nomination') - self.add_email_template('reference-request') + self.add_email_template("reference-request-self-nomination") + self.add_email_template("reference-request") db.session.flush() def test_create_reference_request(self): self._seed_data() REFERENCE_REQUEST_DETAIL = { - 'response_id':1, - 'title': 'Mr', - 'firstname': 'John', - 'lastname':'Snow', - 'relation' : 'Suppervisor', - 'email' :'common@email.com' + "response_id": 1, + "title": "Mr", + "firstname": "John", + "lastname": "Snow", + "relation": "Suppervisor", + "email": "common@email.com", } response = self.app.post( - '/api/v1/reference-request', data=REFERENCE_REQUEST_DETAIL, headers=self.first_headers) + "/api/v1/reference-request", + data=REFERENCE_REQUEST_DETAIL, + headers=self.first_headers, + ) resference_requests = reference_request_repository.get_all() self.assertEqual(response.status_code, 201) self.assertEqual(len(resference_requests), 1) - def test_get_reference_request_by_id(self): self._seed_data() - reference_req = ReferenceRequest(1, 'Mr', 'John', 'Snow', 'Supervisor', 'common@email.com') + reference_req = ReferenceRequest( + 1, "Mr", "John", "Snow", "Supervisor", "common@email.com" + ) reference_request_repository.create(reference_req) response = self.app.get( - '/api/v1/reference-request', data={'id':1}, headers=self.first_headers) + "/api/v1/reference-request", data={"id": 1}, headers=self.first_headers + ) data = json.loads(response.data) self.assertEqual(response.status_code, 200) - self.assertEqual(data['title'], 'Mr') - self.assertEqual(data['firstname'], 'John') - self.assertEqual(data['lastname'], 'Snow') - self.assertEqual(data['relation'], 'Supervisor') - self.assertEqual(data['email'], 'common@email.com') - self.assertIsNone(data['email_sent']) - self.assertFalse(data['reference_submitted']) - + self.assertEqual(data["title"], "Mr") + self.assertEqual(data["firstname"], "John") + self.assertEqual(data["lastname"], "Snow") + self.assertEqual(data["relation"], "Supervisor") + self.assertEqual(data["email"], "common@email.com") + self.assertIsNone(data["email_sent"]) + self.assertFalse(data["reference_submitted"]) def test_get_reference_request_by_response_id(self): self._seed_data() - reference_req = ReferenceRequest(1, 'Mr', 'John', 'Snow', 'Supervisor', 'common@email.com') - reference_req2 = ReferenceRequest(1, 'Mrs', 'John', 'Jones', 'Manager', 'john@email.com') + reference_req = ReferenceRequest( + 1, "Mr", "John", "Snow", "Supervisor", "common@email.com" + ) + reference_req2 = ReferenceRequest( + 1, "Mrs", "John", "Jones", "Manager", "john@email.com" + ) reference_request_repository.create(reference_req) reference_request_repository.create(reference_req2) response = self.app.get( - '/api/v1/reference-request/list', data={'response_id':1}, headers=self.first_headers) + "/api/v1/reference-request/list", + data={"response_id": 1}, + headers=self.first_headers, + ) data = json.loads(response.data) self.assertEqual(response.status_code, 200) self.assertEqual(len(data), 2) def test_get_reference_request_detail_by_token_self_nomination(self): self._seed_data() - reference_req = ReferenceRequest(1, 'Mr', 'John', 'Snow', 'Supervisor', 'common@email.com') + reference_req = ReferenceRequest( + 1, "Mr", "John", "Snow", "Supervisor", "common@email.com" + ) reference_request_repository.create(reference_req) response = self.app.get( - '/api/v1/reference-request/detail', data={'token': reference_req.token}, headers=self.first_headers) + "/api/v1/reference-request/detail", + data={"token": reference_req.token}, + headers=self.first_headers, + ) data = json.loads(response.data) self.assertEqual(response.status_code, 200) - self.assertEqual(data['candidate'], 'Mx First User') - self.assertEqual(data['relation'], reference_req.relation) - self.assertEqual(data['name'], self.test_event_data['name']) - self.assertEqual(data['description'], self.test_event_data['description']) - self.assertEqual(data['is_application_open'], True) - self.assertEqual(data['email_from'], self.test_event_data['email_from']) - self.assertIsNone(data['reference_submitted_timestamp']) - self.assertIsNone(data['nominator']) + self.assertEqual(data["candidate"], "Mx First User") + self.assertEqual(data["relation"], reference_req.relation) + self.assertEqual(data["name"], self.test_event_data["name"]) + self.assertEqual(data["description"], self.test_event_data["description"]) + self.assertEqual(data["is_application_open"], True) + self.assertEqual(data["email_from"], self.test_event_data["email_from"]) + self.assertIsNone(data["reference_submitted_timestamp"]) + self.assertIsNone(data["nominator"]) def test_get_reference_request_detail_by_token_other_nomination(self): self._seed_data() - reference_req = ReferenceRequest(2, 'Mr', 'John', 'Snow', 'Supervisor', 'common@email.com') + reference_req = ReferenceRequest( + 2, "Mr", "John", "Snow", "Supervisor", "common@email.com" + ) reference_request_repository.create(reference_req) response = self.app.get( - '/api/v1/reference-request/detail', data={'token': reference_req.token}, headers=self.other_headers) + "/api/v1/reference-request/detail", + data={"token": reference_req.token}, + headers=self.other_headers, + ) data = json.loads(response.data) self.assertEqual(response.status_code, 200) - self.assertEqual(data['candidate'], 'Mx Skittles Cat') - self.assertEqual(data['nominator'], 'Mrs User Lastname') - self.assertEqual(data['relation'], reference_req.relation) - self.assertEqual(data['name'], self.test_event_data['name']) - self.assertEqual(data['description'], self.test_event_data['description']) - self.assertEqual(data['is_application_open'], True) - self.assertEqual(data['email_from'], self.test_event_data['email_from']) - self.assertIsNone(data['reference_submitted_timestamp']) - + self.assertEqual(data["candidate"], "Mx Skittles Cat") + self.assertEqual(data["nominator"], "Mrs User Lastname") + self.assertEqual(data["relation"], reference_req.relation) + self.assertEqual(data["name"], self.test_event_data["name"]) + self.assertEqual(data["description"], self.test_event_data["description"]) + self.assertEqual(data["is_application_open"], True) + self.assertEqual(data["email_from"], self.test_event_data["email_from"]) + self.assertIsNone(data["reference_submitted_timestamp"]) def test_reference_api(self): self._seed_data() - reference_req = ReferenceRequest(1, 'Mr', 'John', 'Snow', 'Supervisor', 'common@email.com') + reference_req = ReferenceRequest( + 1, "Mr", "John", "Snow", "Supervisor", "common@email.com" + ) reference_request_repository.create(reference_req) REFERENCE_DETAIL = { - 'token':reference_req.token, - 'uploaded_document': 'DOCT-UPLOAD-78999', + "token": reference_req.token, + "uploaded_document": "DOCT-UPLOAD-78999", } response = self.app.post( - '/api/v1/reference', data=REFERENCE_DETAIL, headers=self.first_headers) + "/api/v1/reference", data=REFERENCE_DETAIL, headers=self.first_headers + ) self.assertEqual(response.status_code, 201) response = self.app.get( - '/api/v1/reference', data={'response_id':1}, headers=self.first_headers) + "/api/v1/reference", data={"response_id": 1}, headers=self.first_headers + ) LOGGER.debug(response.data) data = json.loads(response.data) self.assertEqual(response.status_code, 200) @@ -200,42 +230,51 @@ def test_reference_api(self): def test_reference_api_update(self): self._seed_data() - reference_req = ReferenceRequest(1, 'Mr', 'John', 'Snow', 'Supervisor', 'common@email.com') + reference_req = ReferenceRequest( + 1, "Mr", "John", "Snow", "Supervisor", "common@email.com" + ) reference_request_repository.create(reference_req) REFERENCE_DETAIL = { - 'token':reference_req.token, - 'uploaded_document': 'DOCT-UPLOAD-78999', + "token": reference_req.token, + "uploaded_document": "DOCT-UPLOAD-78999", } REFERENCE_DETAIL_2 = { - 'token':reference_req.token, - 'uploaded_document': 'DOCT-UPLOAD-79000', + "token": reference_req.token, + "uploaded_document": "DOCT-UPLOAD-79000", } response = self.app.post( - '/api/v1/reference', data=REFERENCE_DETAIL, headers=self.first_headers) + "/api/v1/reference", data=REFERENCE_DETAIL, headers=self.first_headers + ) self.assertEqual(response.status_code, 201) response = self.app.put( - '/api/v1/reference', data=REFERENCE_DETAIL_2, headers=self.first_headers) + "/api/v1/reference", data=REFERENCE_DETAIL_2, headers=self.first_headers + ) self.assertEqual(response.status_code, 200) response = self.app.get( - '/api/v1/reference', data={'response_id':1}, headers=self.first_headers) + "/api/v1/reference", data={"response_id": 1}, headers=self.first_headers + ) LOGGER.debug(response.data) data = json.loads(response.data) self.assertEqual(response.status_code, 200) self.assertEqual(len(data), 1) reference_request = reference_request_repository.get_by_id(1) self.assertEqual(reference_request.has_reference, True) - reference = reference_request_repository.get_reference_by_reference_request_id(reference_request.id) - self.assertEqual(reference.uploaded_document, REFERENCE_DETAIL_2['uploaded_document']) + reference = reference_request_repository.get_reference_by_reference_request_id( + reference_request.id + ) + self.assertEqual( + reference.uploaded_document, REFERENCE_DETAIL_2["uploaded_document"] + ) def test_reference_application_closed(self): - self.add_organisation('Deep Learning Indaba', 'blah.png', 'blah_big.png') - other_user_data = self.add_user('someuser@mail.com') + self.add_organisation("Deep Learning Indaba", "blah.png", "blah_big.png") + other_user_data = self.add_user("someuser@mail.com") test_event = self.add_event() - test_event.add_event_role('admin', 1) + test_event.add_event_role("admin", 1) test_event.set_application_close(datetime.now()) self.add_to_db(test_event) @@ -247,16 +286,20 @@ def test_reference_application_closed(self): db.session.flush() - reference_req = ReferenceRequest(1, 'Mr', 'John', 'Snow', 'Supervisor', 'common@email.com') + reference_req = ReferenceRequest( + 1, "Mr", "John", "Snow", "Supervisor", "common@email.com" + ) reference_request_repository.create(reference_req) REFERENCE_DETAIL = { - 'token':reference_req.token, - 'uploaded_document': 'DOCT-UPLOAD-78999', + "token": reference_req.token, + "uploaded_document": "DOCT-UPLOAD-78999", } response = self.app.post( - '/api/v1/reference', data=REFERENCE_DETAIL, headers=self.headers) + "/api/v1/reference", data=REFERENCE_DETAIL, headers=self.headers + ) self.assertEqual(response.status_code, 403) response = self.app.put( - '/api/v1/reference', data=REFERENCE_DETAIL, headers=self.headers) + "/api/v1/reference", data=REFERENCE_DETAIL, headers=self.headers + ) self.assertEqual(response.status_code, 403) diff --git a/api/app/registration/api.py b/api/app/registration/api.py index 51d316e71..f01e0d818 100644 --- a/api/app/registration/api.py +++ b/api/app/registration/api.py @@ -1,106 +1,127 @@ +import traceback from datetime import datetime + import flask_restful as restful -from sqlalchemy.exc import IntegrityError -from app.applicationModel.models import Question -from app.responses.models import Answer, Response -from app.registration.models import RegistrationSection -from app.registration.models import RegistrationQuestion -from app.registration.models import RegistrationForm -from app.registration.mixins import RegistrationFormMixin, RegistrationSectionMixin, RegistrationQuestionMixin -from app.utils.auth import verify_token -import traceback from flask import g, request -from flask_restful import fields, marshal_with, marshal -from sqlalchemy.exc import SQLAlchemyError +from flask_restful import fields, marshal, marshal_with +from sqlalchemy.exc import IntegrityError, SQLAlchemyError + +from app import LOGGER, db +from app.applicationModel.models import Question from app.events.models import Event -from app.registration.models import Offer -from app.registration.mixins import OfferMixin -from app.users.models import AppUser -from app import db, LOGGER -from app.utils import errors -from app.utils.auth import auth_required, admin_required -from app.utils.emailer import email_user -from app.utils import misc from app.outcome.models import Outcome, Status from app.outcome.repository import OutcomeRepository as outcome_repository +from app.registration.mixins import ( + OfferMixin, + RegistrationFormMixin, + RegistrationQuestionMixin, + RegistrationSectionMixin, +) +from app.registration.models import ( + Offer, + RegistrationForm, + RegistrationQuestion, + RegistrationSection, +) +from app.responses.models import Answer, Response from app.responses.repository import ResponseRepository as response_repository +from app.users.models import AppUser +from app.utils import errors, misc +from app.utils.auth import admin_required, auth_required, verify_token +from app.utils.emailer import email_user def offer_info(offer_entity, requested_travel=None): return { - 'id': offer_entity.id, - 'user_id': offer_entity.user_id, - 'event_id': offer_entity.event_id, - 'offer_date': offer_entity.offer_date.strftime('%Y-%m-%d'), - 'expiry_date': offer_entity.expiry_date.strftime('%Y-%m-%d'), - 'responded_at': offer_entity.responded_at and offer_entity.responded_at.strftime('%Y-%m-%d'), - 'candidate_response': offer_entity.candidate_response, - 'payment_required': offer_entity.payment_required, - 'travel_award': offer_entity.travel_award, - 'accommodation_award': offer_entity.accommodation_award, - 'accepted_accommodation_award': offer_entity.accepted_accommodation_award, - 'accepted_travel_award': offer_entity.accepted_travel_award, - 'requested_travel': requested_travel and (requested_travel.value == 'transport' or requested_travel.value == 'transport-accommodation'), - 'requested_accommodation': requested_travel and (requested_travel.value == 'accommodation' or requested_travel.value == 'transport-accommodation'), - 'rejected_reason': offer_entity.rejected_reason + "id": offer_entity.id, + "user_id": offer_entity.user_id, + "event_id": offer_entity.event_id, + "offer_date": offer_entity.offer_date.strftime("%Y-%m-%d"), + "expiry_date": offer_entity.expiry_date.strftime("%Y-%m-%d"), + "responded_at": offer_entity.responded_at + and offer_entity.responded_at.strftime("%Y-%m-%d"), + "candidate_response": offer_entity.candidate_response, + "payment_required": offer_entity.payment_required, + "travel_award": offer_entity.travel_award, + "accommodation_award": offer_entity.accommodation_award, + "accepted_accommodation_award": offer_entity.accepted_accommodation_award, + "accepted_travel_award": offer_entity.accepted_travel_award, + "requested_travel": requested_travel + and ( + requested_travel.value == "transport" + or requested_travel.value == "transport-accommodation" + ), + "requested_accommodation": requested_travel + and ( + requested_travel.value == "accommodation" + or requested_travel.value == "transport-accommodation" + ), + "rejected_reason": offer_entity.rejected_reason, } def offer_update_info(offer_entity): return { - 'id': offer_entity.id, - 'user_id': offer_entity.user_id, - 'event_id': offer_entity.event_id, - 'offer_date': offer_entity.offer_date.strftime('%Y-%m-%d'), - 'expiry_date': offer_entity.expiry_date.strftime('%Y-%m-%d'), - 'payment_required': offer_entity.payment_required, - 'travel_award': offer_entity.travel_award, - 'accommodation_award': offer_entity.accommodation_award, - 'accepted_accommodation_award': offer_entity.accepted_accommodation_award, - 'accepted_travel_award': offer_entity.accepted_travel_award, - 'rejected_reason': offer_entity.rejected_reason, - 'candidate_response': offer_entity.candidate_response, - 'responded_at': offer_entity.responded_at.strftime('%Y-%m-%d') + "id": offer_entity.id, + "user_id": offer_entity.user_id, + "event_id": offer_entity.event_id, + "offer_date": offer_entity.offer_date.strftime("%Y-%m-%d"), + "expiry_date": offer_entity.expiry_date.strftime("%Y-%m-%d"), + "payment_required": offer_entity.payment_required, + "travel_award": offer_entity.travel_award, + "accommodation_award": offer_entity.accommodation_award, + "accepted_accommodation_award": offer_entity.accepted_accommodation_award, + "accepted_travel_award": offer_entity.accepted_travel_award, + "rejected_reason": offer_entity.rejected_reason, + "candidate_response": offer_entity.candidate_response, + "responded_at": offer_entity.responded_at.strftime("%Y-%m-%d"), } class OfferAPI(OfferMixin, restful.Resource): offer_fields = { - 'id': fields.Integer, - 'user_id': fields.Integer, - 'event_id': fields.Integer, - 'offer_date': fields.DateTime('iso8601'), - 'expiry_date': fields.DateTime('iso8601'), - 'payment_required': fields.Boolean, - 'travel_award': fields.Boolean, - 'accommodation_award': fields.Boolean, - 'accepted_accommodation_award': fields.Boolean, - 'accepted_travel_award': fields.Boolean, - 'rejected_reason': fields.String, - 'candidate_response': fields.Boolean, - 'responded_at': fields.DateTime('iso8601') + "id": fields.Integer, + "user_id": fields.Integer, + "event_id": fields.Integer, + "offer_date": fields.DateTime("iso8601"), + "expiry_date": fields.DateTime("iso8601"), + "payment_required": fields.Boolean, + "travel_award": fields.Boolean, + "accommodation_award": fields.Boolean, + "accepted_accommodation_award": fields.Boolean, + "accepted_travel_award": fields.Boolean, + "rejected_reason": fields.String, + "candidate_response": fields.Boolean, + "responded_at": fields.DateTime("iso8601"), } @auth_required def put(self): # update existing offer args = self.req_parser.parse_args() - offer_id = args['offer_id'] - candidate_response = args['candidate_response'] - accepted_accommodation_award = args['accepted_accommodation_award'] - accepted_travel_award = args['accepted_travel_award'] - rejected_reason = args['rejected_reason'] + offer_id = args["offer_id"] + candidate_response = args["candidate_response"] + accepted_accommodation_award = args["accepted_accommodation_award"] + accepted_travel_award = args["accepted_travel_award"] + rejected_reason = args["rejected_reason"] offer = db.session.query(Offer).filter(Offer.id == offer_id).first() - LOGGER.info('Updating offer {} with values: candidate response: {}, Accepted accommodation: {}, ' - 'Accepted travel: {}, Rejected Reason: {}'.format(offer_id, candidate_response, accepted_accommodation_award, - accepted_travel_award, rejected_reason)) + LOGGER.info( + "Updating offer {} with values: candidate response: {}, Accepted accommodation: {}, " + "Accepted travel: {}, Rejected Reason: {}".format( + offer_id, + candidate_response, + accepted_accommodation_award, + accepted_travel_award, + rejected_reason, + ) + ) if not offer: return errors.OFFER_NOT_FOUND try: - user_id = verify_token(request.headers.get('Authorization'))['id'] + user_id = verify_token(request.headers.get("Authorization"))["id"] if offer and offer.user_id != user_id: return errors.FORBIDDEN @@ -114,42 +135,47 @@ def put(self): db.session.commit() except Exception as e: - LOGGER.error("Failed to update offer with id {} due to {}".format(args['offer_id'], e)) + LOGGER.error( + "Failed to update offer with id {} due to {}".format( + args["offer_id"], e + ) + ) return errors.ADD_OFFER_FAILED return offer_update_info(offer), 201 @admin_required def post(self): args = self.req_parser.parse_args() - user_id = args['user_id'] - event_id = args['event_id'] - email_template = args['email_template'] - offer_date = datetime.strptime((args['offer_date']), '%Y-%m-%dT%H:%M:%S.%fZ') - expiry_date = datetime.strptime((args['expiry_date']), '%Y-%m-%dT%H:%M:%S.%fZ') - payment_required = args['payment_required'] - travel_award = args['travel_award'] - accommodation_award = args['accommodation_award'] + user_id = args["user_id"] + event_id = args["event_id"] + email_template = args["email_template"] + offer_date = datetime.strptime((args["offer_date"]), "%Y-%m-%dT%H:%M:%S.%fZ") + expiry_date = datetime.strptime((args["expiry_date"]), "%Y-%m-%dT%H:%M:%S.%fZ") + payment_required = args["payment_required"] + travel_award = args["travel_award"] + accommodation_award = args["accommodation_award"] user = db.session.query(AppUser).filter(AppUser.id == user_id).first() event = db.session.query(Event).filter(Event.id == event_id).first() - event_name = event.get_name('en') + event_name = event.get_name("en") event_email_from = event.email_from - existing_offer = db.session.query(Offer).filter(Offer.user_id == user_id, Offer.event_id == event_id).first() + existing_offer = ( + db.session.query(Offer) + .filter(Offer.user_id == user_id, Offer.event_id == event_id) + .first() + ) if existing_offer: return errors.DUPLICATE_OFFER - existing_outcome = outcome_repository.get_latest_by_user_for_event(user_id, event_id) + existing_outcome = outcome_repository.get_latest_by_user_for_event( + user_id, event_id + ) if existing_outcome: if existing_outcome.status == Status.REJECTED: return errors.CANDIDATE_REJECTED existing_outcome.reset_latest() - new_outcome = Outcome( - event_id, - user_id, - Status.ACCEPTED, - g.current_user['id'] - ) + new_outcome = Outcome(event_id, user_id, Status.ACCEPTED, g.current_user["id"]) outcome_repository.add(new_outcome) offer_entity = Offer( @@ -159,37 +185,49 @@ def post(self): expiry_date=expiry_date, payment_required=payment_required, travel_award=travel_award, - accommodation_award=accommodation_award + accommodation_award=accommodation_award, ) db.session.add(offer_entity) db.session.commit() email_user( - 'offer', + "offer", template_parameters=dict( host=misc.get_baobab_host(), expiry_date=offer_entity.expiry_date.strftime("%Y-%m-%d"), - event_email_from=event_email_from + event_email_from=event_email_from, ), event=event, - user=user) + user=user, + ) return offer_info(offer_entity), 201 @auth_required def get(self): args = self.req_parser.parse_args() - event_id = args['event_id'] - user_id = g.current_user['id'] + event_id = args["event_id"] + user_id = g.current_user["id"] try: - offer = db.session.query(Offer).filter(Offer.event_id == event_id).filter(Offer.user_id == user_id).first() - response = response_repository.get_submitted_by_user_id_for_event(user_id, event_id) + offer = ( + db.session.query(Offer) + .filter(Offer.event_id == event_id) + .filter(Offer.user_id == user_id) + .first() + ) + response = response_repository.get_submitted_by_user_id_for_event( + user_id, event_id + ) if not response: return errors.RESPONSE_NOT_FOUND - request_travel = response_repository.get_answer_by_question_key_and_response_id('travel_grant', response.id) - + request_travel = ( + response_repository.get_answer_by_question_key_and_response_id( + "travel_grant", response.id + ) + ) + if not offer: return errors.OFFER_NOT_FOUND elif offer.is_expired(): @@ -201,106 +239,133 @@ def get(self): LOGGER.error("Database error encountered: {}".format(e)) return errors.DB_NOT_AVAILABLE except: - LOGGER.error("Encountered unknown error: {}".format( - traceback.format_exc())) + LOGGER.error("Encountered unknown error: {}".format(traceback.format_exc())) return errors.DB_NOT_AVAILABLE def registration_form_info(registration_form): return { - 'registration_form_id': registration_form.id, - 'event_id': registration_form.event_id + "registration_form_id": registration_form.id, + "event_id": registration_form.event_id, } class RegistrationFormAPI(RegistrationFormMixin, restful.Resource): - option_fields = { - 'value': fields.String, - 'label': fields.String - } + option_fields = {"value": fields.String, "label": fields.String} registration_question_fields = { - 'id': fields.Integer, - 'description': fields.String, - 'headline': fields.String, - 'placeholder': fields.String, - 'validation_regex': fields.String, - 'validation_text': fields.String, - 'depends_on_question_id': fields.String, - 'hide_for_dependent_value': fields.String, - 'type': fields.String, - 'is_required': fields.Boolean, - 'order': fields.Integer, - 'options': fields.List(fields.Nested(option_fields)) + "id": fields.Integer, + "description": fields.String, + "headline": fields.String, + "placeholder": fields.String, + "validation_regex": fields.String, + "validation_text": fields.String, + "depends_on_question_id": fields.String, + "hide_for_dependent_value": fields.String, + "type": fields.String, + "is_required": fields.Boolean, + "order": fields.Integer, + "options": fields.List(fields.Nested(option_fields)), } registration_section_fields = { - 'id': fields.Integer, - 'name': fields.String, - 'description': fields.String, - 'order': fields.Integer, - 'registration_questions': fields.List(fields.Nested(registration_question_fields)) + "id": fields.Integer, + "name": fields.String, + "description": fields.String, + "order": fields.Integer, + "registration_questions": fields.List( + fields.Nested(registration_question_fields) + ), } registration_form_fields = { - 'id':fields.Integer, - 'event_id': fields.Integer, - 'registration_sections': fields.List(fields.Nested(registration_section_fields)) + "id": fields.Integer, + "event_id": fields.Integer, + "registration_sections": fields.List( + fields.Nested(registration_section_fields) + ), } @auth_required def get(self): args = self.req_parser.parse_args() - event_id = args['event_id'] - offer_id = args['offer_id'] + event_id = args["event_id"] + offer_id = args["offer_id"] try: - offer = db.session.query(Offer).filter( - Offer.id == offer_id).first() + offer = db.session.query(Offer).filter(Offer.id == offer_id).first() - user_id = verify_token(request.headers.get('Authorization'))['id'] + user_id = verify_token(request.headers.get("Authorization"))["id"] if offer and (not offer.user_id == user_id): return errors.FORBIDDEN - + if not offer.candidate_response: return errors.OFFER_NOT_ACCEPTED - registration_form = db.session.query(RegistrationForm).filter( - RegistrationForm.event_id == event_id).first() + registration_form = ( + db.session.query(RegistrationForm) + .filter(RegistrationForm.event_id == event_id) + .first() + ) if not registration_form: return errors.REGISTRATION_FORM_NOT_FOUND - sections = db.session.query(RegistrationSection).filter( - RegistrationSection.registration_form_id == registration_form.id).all() + sections = ( + db.session.query(RegistrationSection) + .filter( + RegistrationSection.registration_form_id == registration_form.id + ) + .all() + ) if not sections: LOGGER.warn( - 'Sections not found for event_id: {}'.format(args['event_id'])) + "Sections not found for event_id: {}".format(args["event_id"]) + ) return errors.SECTION_NOT_FOUND included_sections = [] for section in sections: - if (section.show_for_travel_award is None) and (section.show_for_accommodation_award is None) and \ - (section.show_for_payment_required is None): + if ( + (section.show_for_travel_award is None) + and (section.show_for_accommodation_award is None) + and (section.show_for_payment_required is None) + ): included_sections.append(section) - elif (section.show_for_travel_award and offer.travel_award and offer.accepted_travel_award) or \ - (section.show_for_accommodation_award and offer.accommodation_award and offer.accepted_accommodation_award) or \ - (section.show_for_payment_required and offer.payment_required): + elif ( + ( + section.show_for_travel_award + and offer.travel_award + and offer.accepted_travel_award + ) + or ( + section.show_for_accommodation_award + and offer.accommodation_award + and offer.accepted_accommodation_award + ) + or (section.show_for_payment_required and offer.payment_required) + ): included_sections.append(section) registration_form.registration_sections = included_sections - questions = db.session.query(RegistrationQuestion).filter( - RegistrationQuestion.registration_form_id == registration_form.id).all() + questions = ( + db.session.query(RegistrationQuestion) + .filter( + RegistrationQuestion.registration_form_id == registration_form.id + ) + .all() + ) if not questions: LOGGER.warn( - 'Questions not found for event_id: {}'.format(args['event_id'])) + "Questions not found for event_id: {}".format(args["event_id"]) + ) return errors.QUESTION_NOT_FOUND for s in registration_form.registration_sections: @@ -315,25 +380,20 @@ def get(self): LOGGER.error("Database error encountered: {}".format(e)) return errors.DB_NOT_AVAILABLE except: - LOGGER.error("Encountered unknown error: {}".format( - traceback.format_exc())) + LOGGER.error("Encountered unknown error: {}".format(traceback.format_exc())) return errors.DB_NOT_AVAILABLE @admin_required def post(self): args = self.req_parser.parse_args() - event_id = args['event_id'] + event_id = args["event_id"] - event = db.session.query(Event).filter( - Event.id == event_id).first() + event = db.session.query(Event).filter(Event.id == event_id).first() if not event: return errors.EVENT_NOT_FOUND - registration_form = RegistrationForm( - - event_id=event_id - ) + registration_form = RegistrationForm(event_id=event_id) db.session.add(registration_form) @@ -341,7 +401,8 @@ def post(self): db.session.commit() except IntegrityError: LOGGER.error( - "Failed to add registration form for event : {}".format(event_id)) + "Failed to add registration form for event : {}".format(event_id) + ) return errors.ADD_REGISTRATION_FORM_FAILED return registration_form_info(registration_form), 201 @@ -349,35 +410,38 @@ def post(self): def registration_section_info(registration_section): return { - 'registration_form_id': registration_section.registration_form_id, - 'name': registration_section.name, - 'description': registration_section.description, - 'order': registration_section.order, - 'show_for_accommodation_award': registration_section.show_for_accommodation_award, - 'show_for_travel_award': registration_section.show_for_travel_award, - 'show_for_payment_required': registration_section.show_for_payment_required + "registration_form_id": registration_section.registration_form_id, + "name": registration_section.name, + "description": registration_section.description, + "order": registration_section.order, + "show_for_accommodation_award": registration_section.show_for_accommodation_award, + "show_for_travel_award": registration_section.show_for_travel_award, + "show_for_payment_required": registration_section.show_for_payment_required, } class RegistrationSectionAPI(RegistrationSectionMixin, restful.Resource): registration_section_fields = { - 'sectionId': fields.Integer, - 'name': fields.Boolean, - 'description': fields.String, - 'order': fields.String, - 'questions': RegistrationQuestion, + "sectionId": fields.Integer, + "name": fields.Boolean, + "description": fields.String, + "order": fields.String, + "questions": RegistrationQuestion, } @auth_required @marshal_with(registration_section_fields) def get(self): args = self.req_parser.parse_args() - section_id = args['section_id'] + section_id = args["section_id"] try: - registration_section = db.session.query(RegistrationSection).filter( - RegistrationSection.id == section_id).first() + registration_section = ( + db.session.query(RegistrationSection) + .filter(RegistrationSection.id == section_id) + .first() + ) if not registration_section: return errors.REGISTRATION_SECTION_NOT_FOUND @@ -388,36 +452,37 @@ def get(self): LOGGER.error("Database error encountered: {}".format(e)) return errors.DB_NOT_AVAILABLE except: - LOGGER.error("Encountered unknown error: {}".format( - traceback.format_exc())) + LOGGER.error("Encountered unknown error: {}".format(traceback.format_exc())) return errors.DB_NOT_AVAILABLE @admin_required def post(self): args = self.req_parser.parse_args() - registration_form_id = args['registration_form_id'] - name = args['name'] - description = args['description'] - order = args['order'] - show_for_travel_award = args['show_for_travel_award'] - show_for_accommodation_award = args['show_for_accommodation_award'] - show_for_payment_required = args['show_for_payment_required'] - - registration_form = db.session.query(RegistrationForm).filter( - RegistrationForm.id == registration_form_id).first() + registration_form_id = args["registration_form_id"] + name = args["name"] + description = args["description"] + order = args["order"] + show_for_travel_award = args["show_for_travel_award"] + show_for_accommodation_award = args["show_for_accommodation_award"] + show_for_payment_required = args["show_for_payment_required"] + + registration_form = ( + db.session.query(RegistrationForm) + .filter(RegistrationForm.id == registration_form_id) + .first() + ) if not registration_form: return errors.REGISTRATION_FORM_NOT_FOUND registration_section = RegistrationSection( - registration_form_id=registration_form_id, name=name, description=description, order=order, show_for_accommodation_award=show_for_accommodation_award, show_for_travel_award=show_for_travel_award, - show_for_payment_required=show_for_payment_required + show_for_payment_required=show_for_payment_required, ) db.session.add(registration_section) @@ -426,7 +491,10 @@ def post(self): db.session.commit() except IntegrityError: LOGGER.error( - "Failed to add registration section with form id : {}".format(registration_form_id)) + "Failed to add registration section with form id : {}".format( + registration_form_id + ) + ) return errors.ADD_REGISTRATION_SECTION_FAILED return registration_section_info(registration_section), 201 @@ -434,37 +502,41 @@ def post(self): def registration_question_info(registration_question): return { - 'registration_form_id': registration_question.registration_form_id, - 'section_id': registration_question.section_id, - 'type': registration_question.type, - 'description': registration_question.description, - 'headline': registration_question.headline, - 'placeholder': registration_question.placeholder, - 'validation_regex': registration_question.validation_regex, - 'validation_text': registration_question.validation_text, - 'order': registration_question.order, - 'options': registration_question.options, - 'is_required': registration_question.is_required + "registration_form_id": registration_question.registration_form_id, + "section_id": registration_question.section_id, + "type": registration_question.type, + "description": registration_question.description, + "headline": registration_question.headline, + "placeholder": registration_question.placeholder, + "validation_regex": registration_question.validation_regex, + "validation_text": registration_question.validation_text, + "order": registration_question.order, + "options": registration_question.options, + "is_required": registration_question.is_required, } class RegistrationQuestionAPI(RegistrationQuestionMixin, restful.Resource): registration_section_fields = { - 'description': fields.String, - 'type': fields.String, - 'required': fields.Boolean, - 'order': fields.Integer, + "description": fields.String, + "type": fields.String, + "required": fields.Boolean, + "order": fields.Integer, } + @auth_required @marshal_with(registration_section_fields) def get(self): args = self.req_parser.parse_args() - question_id = args['question_id'] + question_id = args["question_id"] try: - question = db.session.query(RegistrationQuestion).filter( - RegistrationQuestion.id == question_id).first() + question = ( + db.session.query(RegistrationQuestion) + .filter(RegistrationQuestion.id == question_id) + .first() + ) if not question: return errors.REGISTRATION_QUESTION_NOT_FOUND @@ -475,30 +547,35 @@ def get(self): LOGGER.error("Database error encountered: {}".format(e)) return errors.DB_NOT_AVAILABLE except: - LOGGER.error("Encountered unknown error: {}".format( - traceback.format_exc())) + LOGGER.error("Encountered unknown error: {}".format(traceback.format_exc())) return errors.DB_NOT_AVAILABLE @admin_required def post(self): args = self.req_parser.parse_args() - registration_form_id = args['registration_form_id'] - section_id = args['section_id'] - type = args['type'] - description = args['description'] - headline = args['headline'] - placeholder = args['placeholder'] - validation_regex = args['validation_regex'] - validation_text = args['validation_text'] - order = args['order'] - options = args['options'] - is_required = args['is_required'] - - registration_form = db.session.query(RegistrationForm).filter( - RegistrationForm.id == registration_form_id).first() - - registration_section = db.session.query(RegistrationSection).filter( - RegistrationSection.id == section_id).first() + registration_form_id = args["registration_form_id"] + section_id = args["section_id"] + type = args["type"] + description = args["description"] + headline = args["headline"] + placeholder = args["placeholder"] + validation_regex = args["validation_regex"] + validation_text = args["validation_text"] + order = args["order"] + options = args["options"] + is_required = args["is_required"] + + registration_form = ( + db.session.query(RegistrationForm) + .filter(RegistrationForm.id == registration_form_id) + .first() + ) + + registration_section = ( + db.session.query(RegistrationSection) + .filter(RegistrationSection.id == section_id) + .first() + ) if not registration_form: return errors.REGISTRATION_FORM_NOT_FOUND @@ -516,7 +593,7 @@ def post(self): validation_text=validation_text, order=order, options=options, - is_required=is_required + is_required=is_required, ) db.session.add(registration_question) @@ -525,7 +602,10 @@ def post(self): db.session.commit() except IntegrityError: LOGGER.error( - "Failed to add registration question with form id : {}".format(section_id)) + "Failed to add registration question with form id : {}".format( + section_id + ) + ) return errors.ADD_REGISTRATION_QUESTION_FAILED return registration_question_info(registration_question), 201 diff --git a/api/app/registration/mixins.py b/api/app/registration/mixins.py index 184ec64a6..f8218eb46 100644 --- a/api/app/registration/mixins.py +++ b/api/app/registration/mixins.py @@ -3,34 +3,34 @@ class OfferMixin(object): req_parser = reqparse.RequestParser() - req_parser.add_argument('id', type=int, required=False) - req_parser.add_argument('event_id', type=int, required=False) - req_parser.add_argument('offer_id', type=int, required=False) - req_parser.add_argument('user_id', type=int, required=False) - req_parser.add_argument('offer_date', type=str, required=False) - req_parser.add_argument('expiry_date', type=str, required=False) - req_parser.add_argument('payment_required', type=bool, required=False) - req_parser.add_argument('travel_award', type=bool, required=False) - req_parser.add_argument('accommodation_award', type=bool, required=False) - req_parser.add_argument('accepted_accommodation_award', type=bool, required=False) - req_parser.add_argument('accepted_travel_award', type=bool, required=False) - req_parser.add_argument('rejected_reason', type=str, required=False) - req_parser.add_argument('candidate_response', type=bool, required=False) - req_parser.add_argument('responded_at', type=str, required=False) - req_parser.add_argument('email_template', type=str, required=False) + req_parser.add_argument("id", type=int, required=False) + req_parser.add_argument("event_id", type=int, required=False) + req_parser.add_argument("offer_id", type=int, required=False) + req_parser.add_argument("user_id", type=int, required=False) + req_parser.add_argument("offer_date", type=str, required=False) + req_parser.add_argument("expiry_date", type=str, required=False) + req_parser.add_argument("payment_required", type=bool, required=False) + req_parser.add_argument("travel_award", type=bool, required=False) + req_parser.add_argument("accommodation_award", type=bool, required=False) + req_parser.add_argument("accepted_accommodation_award", type=bool, required=False) + req_parser.add_argument("accepted_travel_award", type=bool, required=False) + req_parser.add_argument("rejected_reason", type=str, required=False) + req_parser.add_argument("candidate_response", type=bool, required=False) + req_parser.add_argument("responded_at", type=str, required=False) + req_parser.add_argument("email_template", type=str, required=False) class RegistrationFormMixin(object): req_parser = reqparse.RequestParser() - req_parser.add_argument('event_id', type=int, required=True) - req_parser.add_argument('offer_id', type=int, required=False) + req_parser.add_argument("event_id", type=int, required=True) + req_parser.add_argument("offer_id", type=int, required=False) class RegistrationSectionMixin(object): req_parser = reqparse.RequestParser() - req_parser.add_argument('section_id', type=int, required=True) + req_parser.add_argument("section_id", type=int, required=True) class RegistrationQuestionMixin(object): req_parser = reqparse.RequestParser() - req_parser.add_argument('question_id', type=int, required=True) + req_parser.add_argument("question_id", type=int, required=True) diff --git a/api/app/registration/models.py b/api/app/registration/models.py index 90a021376..6abbee07d 100644 --- a/api/app/registration/models.py +++ b/api/app/registration/models.py @@ -1,17 +1,17 @@ -from app import db -from datetime import datetime, date, time +from datetime import date, datetime, time + from sqlalchemy import func +from app import db + class Offer(db.Model): __tablename__ = "offer" id = db.Column(db.Integer(), primary_key=True) - user_id = db.Column(db.Integer(), db.ForeignKey( - "app_user.id"), nullable=False) - event_id = db.Column(db.Integer(), db.ForeignKey( - "event.id"), nullable=False) + user_id = db.Column(db.Integer(), db.ForeignKey("app_user.id"), nullable=False) + event_id = db.Column(db.Integer(), db.ForeignKey("event.id"), nullable=False) offer_date = db.Column(db.DateTime(), nullable=False) expiry_date = db.Column(db.DateTime(), nullable=False) payment_required = db.Column(db.Boolean(), nullable=False) @@ -33,10 +33,9 @@ class RegistrationForm(db.Model): __tablename__ = "registration_form" id = db.Column(db.Integer(), primary_key=True) - event_id = db.Column(db.Integer(), db.ForeignKey( - "event.id"), nullable=False) + event_id = db.Column(db.Integer(), db.ForeignKey("event.id"), nullable=False) - event = db.relationship('Event', foreign_keys=[event_id]) + event = db.relationship("Event", foreign_keys=[event_id]) def __init__(self, event_id): self.event_id = event_id @@ -47,8 +46,9 @@ class RegistrationSection(db.Model): __tablename__ = "registration_section" id = db.Column(db.Integer(), primary_key=True) - registration_form_id = db.Column(db.Integer(), db.ForeignKey( - "registration_form.id"), nullable=False) + registration_form_id = db.Column( + db.Integer(), db.ForeignKey("registration_form.id"), nullable=False + ) name = db.Column(db.String(), nullable=False) description = db.Column(db.String(), nullable=False) order = db.Column(db.Integer(), nullable=False) @@ -56,7 +56,16 @@ class RegistrationSection(db.Model): show_for_accommodation_award = db.Column(db.Boolean(), nullable=True) show_for_payment_required = db.Column(db.Boolean(), nullable=True) - def __init__(self, registration_form_id, name, description, order, show_for_travel_award, show_for_accommodation_award, show_for_payment_required): + def __init__( + self, + registration_form_id, + name, + description, + order, + show_for_travel_award, + show_for_accommodation_award, + show_for_payment_required, + ): self.registration_form_id = registration_form_id self.name = name self.description = description @@ -67,8 +76,11 @@ def __init__(self, registration_form_id, name, description, order, show_for_trav def get_registration_answer_based_headline(user_id, headline): - question = db.session.query(RegistrationQuestion).filter( - func.lower(RegistrationQuestion.headline) == func.lower(headline)).first() + question = ( + db.session.query(RegistrationQuestion) + .filter(func.lower(RegistrationQuestion.headline) == func.lower(headline)) + .first() + ) answer = None if question is not None: answer = ( @@ -87,10 +99,12 @@ class RegistrationQuestion(db.Model): __tablename__ = "registration_question" id = db.Column(db.Integer(), primary_key=True) - registration_form_id = db.Column(db.Integer(), db.ForeignKey( - "registration_form.id"), nullable=False) - section_id = db.Column(db.Integer(), db.ForeignKey( - "registration_section.id"), nullable=False) + registration_form_id = db.Column( + db.Integer(), db.ForeignKey("registration_form.id"), nullable=False + ) + section_id = db.Column( + db.Integer(), db.ForeignKey("registration_section.id"), nullable=False + ) type = db.Column(db.String(), nullable=False) description = db.Column(db.String(), nullable=False) headline = db.Column(db.String(), nullable=False) @@ -101,11 +115,25 @@ class RegistrationQuestion(db.Model): options = db.Column(db.JSON(), nullable=True) is_required = db.Column(db.Boolean(), nullable=False) required_value = db.Column(db.String(), nullable=True) - depends_on_question_id = db.Column(db.Integer(), db.ForeignKey( - "registration_question.id"), nullable=True) + depends_on_question_id = db.Column( + db.Integer(), db.ForeignKey("registration_question.id"), nullable=True + ) hide_for_dependent_value = db.Column(db.String(), nullable=True) - def __init__(self, registration_form_id, section_id, headline, placeholder, order, type, validation_regex, validation_text=None, is_required=True, description=None, options=None): + def __init__( + self, + registration_form_id, + section_id, + headline, + placeholder, + order, + type, + validation_regex, + validation_text=None, + is_required=True, + description=None, + options=None, + ): self.registration_form_id = registration_form_id self.section_id = section_id self.headline = headline @@ -118,6 +146,7 @@ def __init__(self, registration_form_id, section_id, headline, placeholder, orde self.validation_regex = validation_regex self.validation_text = validation_text + # Registration @@ -125,15 +154,21 @@ class Registration(db.Model): __tablename__ = "registration" id = db.Column(db.Integer(), primary_key=True) - offer_id = db.Column(db.Integer(), db.ForeignKey( - "offer.id"), nullable=False) - registration_form_id = db.Column(db.Integer(), db.ForeignKey( - "registration_form.id"), nullable=False) + offer_id = db.Column(db.Integer(), db.ForeignKey("offer.id"), nullable=False) + registration_form_id = db.Column( + db.Integer(), db.ForeignKey("registration_form.id"), nullable=False + ) confirmed = db.Column(db.Boolean(), nullable=False) created_at = db.Column(db.DateTime(), nullable=True) confirmation_email_sent_at = db.Column(db.DateTime(), nullable=True) - def __init__(self, offer_id, registration_form_id, confirmed=False, confirmation_email_sent_at=None): + def __init__( + self, + offer_id, + registration_form_id, + confirmed=False, + confirmation_email_sent_at=None, + ): self.offer_id = offer_id self.registration_form_id = registration_form_id self.confirmed = confirmed @@ -148,8 +183,10 @@ class RegistrationAnswer(db.Model): __tablename__ = "registration_answer" id = db.Column(db.Integer(), primary_key=True) - registration_id = db.Column(db.Integer(), db.ForeignKey( - 'registration.id'), nullable=False) - registration_question_id = db.Column(db.Integer(), db.ForeignKey( - 'registration_question.id'), nullable=False) + registration_id = db.Column( + db.Integer(), db.ForeignKey("registration.id"), nullable=False + ) + registration_question_id = db.Column( + db.Integer(), db.ForeignKey("registration_question.id"), nullable=False + ) value = db.Column(db.String(), nullable=False) diff --git a/api/app/registration/repository.py b/api/app/registration/repository.py index b52042e95..cb5771716 100644 --- a/api/app/registration/repository.py +++ b/api/app/registration/repository.py @@ -1,59 +1,82 @@ +from sqlalchemy import Date, and_, cast, func + from app import db from app.registration.models import Offer, Registration, RegistrationForm -from sqlalchemy import and_, func, cast, Date - -class OfferRepository(): +class OfferRepository: @staticmethod def get_by_user_id_for_event(user_id, event_id): - return db.session.query(Offer).filter_by(user_id=user_id, event_id=event_id).first() + return ( + db.session.query(Offer) + .filter_by(user_id=user_id, event_id=event_id) + .first() + ) @staticmethod def count_offers_allocated(event_id): - count = (db.session.query(Offer) - .filter_by(event_id=event_id) - .count()) + count = db.session.query(Offer).filter_by(event_id=event_id).count() return count @staticmethod def count_offers_accepted(event_id): - count = (db.session.query(Offer) - .filter_by(event_id=event_id, candidate_response=True) - .count()) + count = ( + db.session.query(Offer) + .filter_by(event_id=event_id, candidate_response=True) + .count() + ) return count @staticmethod def count_offers_rejected(event_id): - count = (db.session.query(Offer) - .filter_by(event_id=event_id, candidate_response=False) - .count()) + count = ( + db.session.query(Offer) + .filter_by(event_id=event_id, candidate_response=False) + .count() + ) return count - + @staticmethod def timeseries_offers_accepted(event_id): - timeseries = (db.session.query(cast(Offer.responded_at, Date), func.count(Offer.responded_at)) - .filter_by(event_id=event_id, candidate_response=True) - .group_by(cast(Offer.responded_at, Date)) - .order_by(cast(Offer.responded_at, Date)) - .all()) + timeseries = ( + db.session.query( + cast(Offer.responded_at, Date), func.count(Offer.responded_at) + ) + .filter_by(event_id=event_id, candidate_response=True) + .group_by(cast(Offer.responded_at, Date)) + .order_by(cast(Offer.responded_at, Date)) + .all() + ) return timeseries -class RegistrationRepository(): + +class RegistrationRepository: @staticmethod def count_registrations(event_id): - count = (db.session.query(Registration) - .join(RegistrationForm, Registration.registration_form_id == RegistrationForm.id) - .filter_by(event_id=event_id) - .count()) + count = ( + db.session.query(Registration) + .join( + RegistrationForm, + Registration.registration_form_id == RegistrationForm.id, + ) + .filter_by(event_id=event_id) + .count() + ) return count @staticmethod def timeseries_registrations(event_id): - timeseries = (db.session.query(cast(Registration.created_at, Date), func.count(Registration.created_at)) - .join(RegistrationForm, Registration.registration_form_id == RegistrationForm.id) - .filter_by(event_id=event_id) - .group_by(cast(Registration.created_at, Date)) - .order_by(cast(Registration.created_at, Date)) - .all()) - return timeseries \ No newline at end of file + timeseries = ( + db.session.query( + cast(Registration.created_at, Date), func.count(Registration.created_at) + ) + .join( + RegistrationForm, + Registration.registration_form_id == RegistrationForm.id, + ) + .filter_by(event_id=event_id) + .group_by(cast(Registration.created_at, Date)) + .order_by(cast(Registration.created_at, Date)) + .all() + ) + return timeseries diff --git a/api/app/registration/tests.py b/api/app/registration/tests.py index b9e607e71..7904d83f3 100644 --- a/api/app/registration/tests.py +++ b/api/app/registration/tests.py @@ -1,78 +1,81 @@ -from app.registration.models import RegistrationForm -from app.registration.models import RegistrationQuestion -from app.registration.models import RegistrationSection import json from datetime import datetime, timedelta -from app import db, LOGGER -from app.utils.testing import ApiTestCase -from app.users.models import AppUser, UserCategory, Country + +from app import LOGGER, db from app.events.models import Event -from app.registration.models import Offer from app.organisation.models import Organisation -from app.outcome.repository import OutcomeRepository as outcome_repository from app.outcome.models import Status +from app.outcome.repository import OutcomeRepository as outcome_repository +from app.registration.models import ( + Offer, + RegistrationForm, + RegistrationQuestion, + RegistrationSection, +) from app.responses.models import Response - +from app.users.models import AppUser, Country, UserCategory +from app.utils.testing import ApiTestCase OFFER_DATA = { - 'id': 1, - 'user_id': 1, - 'event_id': 1, - 'offer_date': datetime(1984, 12, 12).strftime('%Y-%m-%dT%H:%M:%S.%fZ'), - 'expiry_date': datetime(1984, 12, 12).strftime('%Y-%m-%dT%H:%M:%S.%fZ'), - 'payment_required': False, - 'travel_award': False, - 'accommodation_award': True, - 'accepted_accommodation_award': None, - 'accepted_travel_award': None, - 'rejected_reason': 'N/A', + "id": 1, + "user_id": 1, + "event_id": 1, + "offer_date": datetime(1984, 12, 12).strftime("%Y-%m-%dT%H:%M:%S.%fZ"), + "expiry_date": datetime(1984, 12, 12).strftime("%Y-%m-%dT%H:%M:%S.%fZ"), + "payment_required": False, + "travel_award": False, + "accommodation_award": True, + "accepted_accommodation_award": None, + "accepted_travel_award": None, + "rejected_reason": "N/A", } REGISTRATION_FORM = { - 'event_id': 1, + "event_id": 1, } REGISTRATION_SECTION = { - 'registration_form_id': 1, - 'name': "section 2", - 'description': "this is the second section", - 'order': 1, - 'show_for_accommodation_award': True, - 'show_for_travel_award': True, - 'show_for_payment_required': True + "registration_form_id": 1, + "name": "section 2", + "description": "this is the second section", + "order": 1, + "show_for_accommodation_award": True, + "show_for_travel_award": True, + "show_for_payment_required": True, } REGISTRATION_QUESTION = { - 'registration_form_id': 1, - 'section_id': 1, - 'type': "open-ended", - 'description': "just a question", - 'headline': "question headline", - 'placeholder': "answer the this question", - 'validation_regex': "/[a-d]", - 'validation_text': "regex are cool", - 'order': 1, - 'options': "{'a': 'both a and b', 'b': 'none of the above'}", - 'is_required': True + "registration_form_id": 1, + "section_id": 1, + "type": "open-ended", + "description": "just a question", + "headline": "question headline", + "placeholder": "answer the this question", + "validation_regex": "/[a-d]", + "validation_text": "regex are cool", + "order": 1, + "options": "{'a': 'both a and b', 'b': 'none of the above'}", + "is_required": True, } class OfferApiTest(ApiTestCase): - def seed_static_data(self, add_offer=True): - test_user = self.add_user('something@email.com') - offer_admin = self.add_user('offer_admin@ea.com', 'event_admin', is_admin=True) - self.add_organisation('Deep Learning Indaba', 'blah.png', 'blah_big.png', 'deeplearningindaba') - db.session.add(UserCategory('Offer Category')) - db.session.add(Country('Suid Afrika')) + test_user = self.add_user("something@email.com") + offer_admin = self.add_user("offer_admin@ea.com", "event_admin", is_admin=True) + self.add_organisation( + "Deep Learning Indaba", "blah.png", "blah_big.png", "deeplearningindaba" + ) + db.session.add(UserCategory("Offer Category")) + db.session.add(Country("Suid Afrika")) db.session.commit() event = self.add_event( - name={'en': "Tech Talk"}, - description={'en': "tech talking"}, + name={"en": "Tech Talk"}, + description={"en": "tech talking"}, start_date=datetime(2019, 12, 12), end_date=datetime(2020, 12, 12), - key='SPEEDNET' + key="SPEEDNET", ) db.session.commit() @@ -87,47 +90,50 @@ def seed_static_data(self, add_offer=True): expiry_date=datetime.now() + timedelta(days=15), payment_required=False, travel_award=True, - accommodation_award=False) + accommodation_award=False, + ) db.session.add(offer) db.session.commit() self.headers = self.get_auth_header_for("something@email.com") self.adminHeaders = self.get_auth_header_for("offer_admin@ea.com") - self.add_email_template('offer') + self.add_email_template("offer") db.session.flush() def get_auth_header_for(self, email): - body = { - 'email': email, - 'password': 'abc' - } - response = self.app.post('api/v1/authenticate', data=body) + body = {"email": email, "password": "abc"} + response = self.app.post("api/v1/authenticate", data=body) data = json.loads(response.data) - header = {'Authorization': data['token']} + header = {"Authorization": data["token"]} return header def test_create_offer(self): self.seed_static_data(add_offer=False) - response = self.app.post('/api/v1/offer', data=OFFER_DATA, - headers=self.adminHeaders) + response = self.app.post( + "/api/v1/offer", data=OFFER_DATA, headers=self.adminHeaders + ) data = json.loads(response.data) self.assertEqual(response.status_code, 201) - self.assertTrue(data['payment_required']) - self.assertTrue(data['travel_award']) - self.assertTrue(data['accommodation_award']) + self.assertTrue(data["payment_required"]) + self.assertTrue(data["travel_award"]) + self.assertTrue(data["accommodation_award"]) - outcome = outcome_repository.get_latest_by_user_for_event(OFFER_DATA['user_id'], OFFER_DATA['event_id']) + outcome = outcome_repository.get_latest_by_user_for_event( + OFFER_DATA["user_id"], OFFER_DATA["event_id"] + ) self.assertEqual(outcome.status, Status.ACCEPTED) def test_create_offer_with_template(self): self.seed_static_data(add_offer=False) - + offer_data = OFFER_DATA.copy() - offer_data['email_template'] = """Dear {user_title} {first_name} {last_name}, + offer_data[ + "email_template" + ] = """Dear {user_title} {first_name} {last_name}, This is a custom email notifying you about your place at the {event_name}. @@ -136,20 +142,22 @@ def test_create_offer_with_template(self): kthanksbye! """ - response = self.app.post('/api/v1/offer', data=offer_data, - headers=self.adminHeaders) + response = self.app.post( + "/api/v1/offer", data=offer_data, headers=self.adminHeaders + ) data = json.loads(response.data) self.assertEqual(response.status_code, 201) - self.assertTrue(data['payment_required']) - self.assertTrue(data['travel_award']) - self.assertTrue(data['accommodation_award']) + self.assertTrue(data["payment_required"]) + self.assertTrue(data["travel_award"]) + self.assertTrue(data["accommodation_award"]) def test_create_duplicate_offer(self): self.seed_static_data(add_offer=True) - response = self.app.post('/api/v1/offer', data=OFFER_DATA, - headers=self.adminHeaders) + response = self.app.post( + "/api/v1/offer", data=OFFER_DATA, headers=self.adminHeaders + ) data = json.loads(response.data) self.assertEqual(response.status_code, 409) @@ -158,8 +166,7 @@ def test_get_offer(self): self.seed_static_data(add_offer=True) event_id = 1 - url = "/api/v1/offer?event_id=%d" % ( - event_id) + url = "/api/v1/offer?event_id=%d" % (event_id) response = self.app.get(url, headers=self.headers) @@ -169,8 +176,7 @@ def test_get_offer_not_found(self): self.seed_static_data() event_id = 12 - url = "/api/v1/offer?event_id=%d" % ( - event_id) + url = "/api/v1/offer?event_id=%d" % (event_id) response = self.app.get(url, headers=self.headers) @@ -182,8 +188,10 @@ def test_update_offer(self): offer_id = 1 candidate_response = True rejected_reason = "the reason for rejection" - url = "/api/v1/offer?offer_id=%d&event_id=%d&candidate_response=%s&rejected_reason=%s" % ( - offer_id, event_id, candidate_response, rejected_reason) + url = ( + "/api/v1/offer?offer_id=%d&event_id=%d&candidate_response=%s&rejected_reason=%s" + % (offer_id, event_id, candidate_response, rejected_reason) + ) response = self.app.put(url, headers=self.headers) @@ -191,25 +199,24 @@ def test_update_offer(self): LOGGER.debug("Offer-PUT: {}".format(response.data)) self.assertEqual(response.status_code, 201) - self.assertTrue(data['candidate_response']) + self.assertTrue(data["candidate_response"]) class RegistrationTest(ApiTestCase): - def seed_static_data(self): - test_user = self.add_user('something@email.com', 'Some', 'Thing', 'Mr') - event_admin = self.add_user('event_admin@ea.com', 'event_admin', is_admin=True) - self.add_organisation('Deep Learning Indaba', 'blah.png', 'blah_big.png') - db.session.add(UserCategory('Postdoc')) - db.session.add(Country('South Africa')) + test_user = self.add_user("something@email.com", "Some", "Thing", "Mr") + event_admin = self.add_user("event_admin@ea.com", "event_admin", is_admin=True) + self.add_organisation("Deep Learning Indaba", "blah.png", "blah_big.png") + db.session.add(UserCategory("Postdoc")) + db.session.add(Country("South Africa")) db.session.commit() event = self.add_event( - name={'en': "Tech Talk"}, - description={'en': "tech talking"}, + name={"en": "Tech Talk"}, + description={"en": "tech talking"}, start_date=datetime(2019, 12, 12, 10, 10, 10), end_date=datetime(2020, 12, 12, 10, 10, 10), - key='SPEEDNET' + key="SPEEDNET", ) db.session.commit() @@ -222,7 +229,8 @@ def seed_static_data(self): expiry_date=datetime.now() + timedelta(days=15), payment_required=False, travel_award=True, - accommodation_award=False) + accommodation_award=False, + ) offer.candidate_response = True offer.accepted_travel_award = True @@ -231,9 +239,7 @@ def seed_static_data(self): db.session.commit() self.offer_id = offer.id - form = RegistrationForm( - event_id=event.id - ) + form = RegistrationForm(event_id=event.id) db.session.add(form) db.session.commit() @@ -271,7 +277,7 @@ def seed_static_data(self): placeholder="the placeholder", headline="the headline", validation_regex="[]/", - validation_text=" text" + validation_text=" text", ) db.session.add(question) db.session.commit() @@ -286,7 +292,7 @@ def seed_static_data(self): placeholder="the placeholder", headline="the headline", validation_regex="[]/", - validation_text=" text" + validation_text=" text", ) db.session.add(question2) db.session.commit() @@ -299,26 +305,32 @@ def seed_static_data(self): def test_create_registration_form(self): self.seed_static_data() response = self.app.post( - '/api/v1/registration-form', data=REGISTRATION_FORM, headers=self.adminHeaders) + "/api/v1/registration-form", + data=REGISTRATION_FORM, + headers=self.adminHeaders, + ) data = json.loads(response.data) - LOGGER.debug( - "Reg-form: {}".format(data)) + LOGGER.debug("Reg-form: {}".format(data)) assert response.status_code == 201 - assert data['registration_form_id'] == 2 + assert data["registration_form_id"] == 2 def test_get_form(self): self.seed_static_data() url = "/api/v1/registration-form?offer_id=%d&event_id=%d" % ( - self.offer_id, self.event_id) + self.offer_id, + self.event_id, + ) LOGGER.debug(url) response = self.app.get(url, headers=self.headers) if response.status_code == 403: return - LOGGER.debug( - "form: {}".format(json.loads(response.data))) + LOGGER.debug("form: {}".format(json.loads(response.data))) form = json.loads(response.data) - assert form['registration_sections'][0]['registration_questions'][0]['type'] == 'short-text' - assert form['registration_sections'][0]['name'] == 'Section 1' + assert ( + form["registration_sections"][0]["registration_questions"][0]["type"] + == "short-text" + ) + assert form["registration_sections"][0]["name"] == "Section 1" diff --git a/api/app/registrationResponse/api.py b/api/app/registrationResponse/api.py index 1d97ab90c..86316f21c 100644 --- a/api/app/registrationResponse/api.py +++ b/api/app/registrationResponse/api.py @@ -1,96 +1,111 @@ -from datetime import date, datetime +import itertools import traceback -from flask_restful import reqparse, fields, marshal_with, marshal +from datetime import date, datetime + import flask_restful as restful from flask import g, request +from flask_restful import fields, marshal, marshal_with, reqparse from sqlalchemy.exc import SQLAlchemyError -from app.utils.auth import verify_token -from app.registrationResponse.mixins import RegistrationResponseMixin, RegistrationAdminMixin, RegistrationConfirmMixin -from app.registration.models import Offer, Registration, RegistrationAnswer, RegistrationForm, RegistrationQuestion -from app.users.models import AppUser + +from app import LOGGER, db from app.events.models import Event -from app.utils.auth import auth_required, admin_required -from app.users.repository import UserRepository from app.events.repository import EventRepository -from app.utils import errors, emailer, strings -from app import LOGGER -from app.users.repository import UserRepository as user_repository -from app.registrationResponse.repository import RegistrationRepository -from app.guestRegistrations.repository import GuestRegistrationRepository from app.events.repository import EventRepository as event_repository -import itertools - - -from app import db +from app.guestRegistrations.repository import GuestRegistrationRepository +from app.registration.models import ( + Offer, + Registration, + RegistrationAnswer, + RegistrationForm, + RegistrationQuestion, +) +from app.registrationResponse.mixins import ( + RegistrationAdminMixin, + RegistrationConfirmMixin, + RegistrationResponseMixin, +) +from app.registrationResponse.repository import RegistrationRepository +from app.users.models import AppUser +from app.users.repository import UserRepository +from app.users.repository import UserRepository as user_repository +from app.utils import emailer, errors, strings +from app.utils.auth import admin_required, auth_required, verify_token def _get_answer_value(answer, question): - if question.type == 'multi-choice' and question.options is not None: - value = [o for o in question.options if o['value'] == answer.value] + if question.type == "multi-choice" and question.options is not None: + value = [o for o in question.options if o["value"] == answer.value] if not value: return answer.value - return value[0]['label'] + return value[0]["label"] - if question.type == 'file' and answer.value: - return 'Uploaded File' + if question.type == "file" and answer.value: + return "Uploaded File" return answer.value class RegistrationApi(RegistrationResponseMixin, restful.Resource): answer_fields = { - 'id': fields.Integer, - 'registration_id': fields.Integer, - 'registration_question_id': fields.Integer, - 'value': fields.String + "id": fields.Integer, + "registration_id": fields.Integer, + "registration_question_id": fields.Integer, + "value": fields.String, } registration_fields = { - 'id': fields.Integer, - 'offer_id': fields.Integer, - 'registration_form_id': fields.Integer, - 'confirmed': fields.Boolean, - 'created_at': fields.DateTime, - 'confirmation_email_sent_at': fields.DateTime - + "id": fields.Integer, + "offer_id": fields.Integer, + "registration_form_id": fields.Integer, + "confirmed": fields.Boolean, + "created_at": fields.DateTime, + "confirmation_email_sent_at": fields.DateTime, } response_fields = { - 'registration_id': fields.Integer, - 'offer_id': fields.Integer, - 'registration_form_id': fields.Integer, - 'answers': fields.List(fields.Nested(answer_fields)) + "registration_id": fields.Integer, + "offer_id": fields.Integer, + "registration_form_id": fields.Integer, + "answers": fields.List(fields.Nested(answer_fields)), } @auth_required def get(self): try: - user_id = verify_token(request.headers.get('Authorization'))['id'] + user_id = verify_token(request.headers.get("Authorization"))["id"] - db_offer = db.session.query(Offer).filter( - Offer.user_id == user_id).first() + db_offer = db.session.query(Offer).filter(Offer.user_id == user_id).first() if db_offer is None: return errors.OFFER_NOT_FOUND - registration = db.session.query(Registration).filter( - Registration.offer_id == db_offer.id).first() + registration = ( + db.session.query(Registration) + .filter(Registration.offer_id == db_offer.id) + .first() + ) if registration is None: - return 'no Registration', 404 - registration_form = db.session.query(RegistrationForm).filter(RegistrationForm.id == registration. - registration_form_id).first() + return "no Registration", 404 + registration_form = ( + db.session.query(RegistrationForm) + .filter(RegistrationForm.id == registration.registration_form_id) + .first() + ) if registration_form is None: return errors.REGISTRATION_FORM_NOT_FOUND - db_answers = db.session.query(RegistrationAnswer).filter(RegistrationAnswer.registration_id == - registration.id).all() + db_answers = ( + db.session.query(RegistrationAnswer) + .filter(RegistrationAnswer.registration_id == registration.id) + .all() + ) response = { - 'registration_id': registration.id, - 'offer_id': db_offer.id, - 'registration_form_id': registration_form.id, - 'answers': db_answers + "registration_id": registration.id, + "offer_id": db_offer.id, + "registration_form_id": registration_form.id, + "answers": db_answers, } return marshal(response, self.response_fields) @@ -104,32 +119,35 @@ def post(self): # Save a new response for the logged-in user. req_parser = reqparse.RequestParser() args = self.req_parser.parse_args() - offer_id = args['offer_id'] + offer_id = args["offer_id"] try: - offer = db.session.query(Offer).filter( - Offer.id == offer_id).first() + offer = db.session.query(Offer).filter(Offer.id == offer_id).first() if not offer: return errors.OFFER_NOT_FOUND - user_id = verify_token(request.headers.get('Authorization'))['id'] + user_id = verify_token(request.headers.get("Authorization"))["id"] if not user_id: return errors.USER_NOT_FOUND - current_user = db.session.query(AppUser).filter( - AppUser.id == user_id).first() + current_user = ( + db.session.query(AppUser).filter(AppUser.id == user_id).first() + ) - registration_form = db.session.query(RegistrationForm).filter( - RegistrationForm.id == args['registration_form_id']).first() + registration_form = ( + db.session.query(RegistrationForm) + .filter(RegistrationForm.id == args["registration_form_id"]) + .first() + ) if not registration_form: return errors.REGISTRATION_FORM_NOT_FOUND registration = Registration( - offer_id=args['offer_id'], - registration_form_id=args['registration_form_id'], + offer_id=args["offer_id"], + registration_form_id=args["registration_form_id"], confirmed=True if (not offer.payment_required) else False, - confirmation_email_sent_at=date.today() + confirmation_email_sent_at=date.today(), ) db.session.add(registration) @@ -137,23 +155,48 @@ def post(self): event = event_repository.get_by_id(registration_form.event_id) - for answer_args in args['answers']: - if db.session.query(RegistrationQuestion).filter( - RegistrationQuestion.id == answer_args['registration_question_id']).first(): - - answer = RegistrationAnswer(registration_id=registration.id, - registration_question_id=answer_args['registration_question_id'], - value=answer_args['value']) + for answer_args in args["answers"]: + if ( + db.session.query(RegistrationQuestion) + .filter( + RegistrationQuestion.id + == answer_args["registration_question_id"] + ) + .first() + ): + + answer = RegistrationAnswer( + registration_id=registration.id, + registration_question_id=answer_args[ + "registration_question_id" + ], + value=answer_args["value"], + ) db.session.add(answer) db.session.commit() - registration_answers = db.session.query(RegistrationAnswer).filter( - RegistrationAnswer.registration_id == registration.id).all() - registration_questions = db.session.query(RegistrationQuestion).filter( - RegistrationQuestion.registration_form_id == args['registration_form_id']).all() + registration_answers = ( + db.session.query(RegistrationAnswer) + .filter(RegistrationAnswer.registration_id == registration.id) + .all() + ) + registration_questions = ( + db.session.query(RegistrationQuestion) + .filter( + RegistrationQuestion.registration_form_id + == args["registration_form_id"] + ) + .all() + ) - self.send_confirmation(current_user, registration_questions, registration_answers, registration.confirmed, event) + self.send_confirmation( + current_user, + registration_questions, + registration_answers, + registration.confirmed, + event, + ) # 201 is 'CREATED' status code return marshal(registration, self.registration_fields), 201 @@ -162,11 +205,8 @@ def post(self): LOGGER.error("Database error encountered: {}".format(e)) return errors.DB_NOT_AVAILABLE except Exception as e: - LOGGER.error("Encountered unknown error: {}".format( - traceback.format_exc())) + LOGGER.error("Encountered unknown error: {}".format(traceback.format_exc())) return errors.DB_NOT_AVAILABLE - - @auth_required def put(self): @@ -174,15 +214,21 @@ def put(self): req_parser = reqparse.RequestParser() args = self.req_parser.parse_args() try: - user_id = verify_token(request.headers.get('Authorization'))['id'] + user_id = verify_token(request.headers.get("Authorization"))["id"] - registration = db.session.query(Registration).filter( - Registration.id == args['registration_id']).one_or_none() + registration = ( + db.session.query(Registration) + .filter(Registration.id == args["registration_id"]) + .one_or_none() + ) if registration is None: - return 'Registration not found', 404 + return "Registration not found", 404 - db_offer = db.session.query(Offer).filter( - Offer.id == registration.offer_id).one_or_none() + db_offer = ( + db.session.query(Offer) + .filter(Offer.id == registration.offer_id) + .one_or_none() + ) if db_offer is None: return errors.OFFER_NOT_FOUND @@ -190,54 +236,92 @@ def put(self): if db_offer.user_id != user_id: return errors.FORBIDDEN - registration.registration_form_id = args['registration_form_id'] + registration.registration_form_id = args["registration_form_id"] db.session.commit() - for answer_args in args['answers']: - answer = db.session.query(RegistrationAnswer).filter( - RegistrationAnswer.registration_question_id == answer_args[ - 'registration_question_id'], - RegistrationAnswer.registration_id == args['registration_id']).one_or_none() + for answer_args in args["answers"]: + answer = ( + db.session.query(RegistrationAnswer) + .filter( + RegistrationAnswer.registration_question_id + == answer_args["registration_question_id"], + RegistrationAnswer.registration_id == args["registration_id"], + ) + .one_or_none() + ) if answer is not None: - answer.value = answer_args['value'] - - elif db.session.query(RegistrationQuestion).filter( - RegistrationQuestion.id == answer_args['registration_question_id']).one(): - - answer = RegistrationAnswer(registration_id=registration.id, - registration_question_id=answer_args['registration_question_id'], - value=answer_args['value']) + answer.value = answer_args["value"] + + elif ( + db.session.query(RegistrationQuestion) + .filter( + RegistrationQuestion.id + == answer_args["registration_question_id"] + ) + .one() + ): + + answer = RegistrationAnswer( + registration_id=registration.id, + registration_question_id=answer_args[ + "registration_question_id" + ], + value=answer_args["value"], + ) db.session.add(answer) db.session.commit() current_user = user_repository.get_by_id(user_id) - registration_answers = db.session.query(RegistrationAnswer).filter( - RegistrationAnswer.registration_id == registration.id).all() - - registration_questions = db.session.query(RegistrationQuestion).filter( - RegistrationQuestion.registration_form_id == args['registration_form_id']).all() + registration_answers = ( + db.session.query(RegistrationAnswer) + .filter(RegistrationAnswer.registration_id == registration.id) + .all() + ) + + registration_questions = ( + db.session.query(RegistrationQuestion) + .filter( + RegistrationQuestion.registration_form_id + == args["registration_form_id"] + ) + .all() + ) - registration_form = db.session.query(RegistrationForm).filter( - RegistrationForm.id == args['registration_form_id']).first() + registration_form = ( + db.session.query(RegistrationForm) + .filter(RegistrationForm.id == args["registration_form_id"]) + .first() + ) event = event_repository.get_by_id(registration_form.event_id) self.send_confirmation( - current_user, registration_questions, registration_answers, registration.confirmed, event) + current_user, + registration_questions, + registration_answers, + registration.confirmed, + event, + ) return 200 except Exception as e: - return 'Could not access DB', 400 + return "Could not access DB", 400 def send_confirmation(self, user, questions, answers, confirmed, event): if answers is None: LOGGER.warn( - 'Found no answers associated with response with id {response_id}'.format(response_id=user.id)) + "Found no answers associated with response with id {response_id}".format( + response_id=user.id + ) + ) if questions is None: LOGGER.warn( - 'Found no questions associated with application form with id {form_id}'.format(form_id=user.id)) + "Found no questions associated with application form with id {form_id}".format( + form_id=user.id + ) + ) try: # Building the summary, where the summary is a dictionary whose key is the question headline, and the value # is the relevant answer @@ -245,68 +329,81 @@ def send_confirmation(self, user, questions, answers, confirmed, event): for answer in answers: for question in questions: if answer.registration_question_id == question.id: - summary += "Question:" + question.headline + "\nAnswer:" + _get_answer_value( - answer, question) + "\n" + summary += ( + "Question:" + + question.headline + + "\nAnswer:" + + _get_answer_value(answer, question) + + "\n" + ) emailer.email_user( - 'registration-with-confirmation' if confirmed else 'registration-pending-confirmation', - template_parameters=dict( - summary=summary - ), + "registration-with-confirmation" + if confirmed + else "registration-pending-confirmation", + template_parameters=dict(summary=summary), event=event, - user=user) + user=user, + ) except Exception as e: - LOGGER.error('Could not send confirmation email for response with id : {response_id}'.format( - response_id=user.id)) + LOGGER.error( + "Could not send confirmation email for response with id : {response_id}".format( + response_id=user.id + ) + ) def map_registration_info(registration_info): return { - 'registration_id': registration_info.Registration.id, - 'user_id': registration_info.AppUser.id, - 'firstname': registration_info.AppUser.firstname, - 'lastname': registration_info.AppUser.lastname, - 'email': registration_info.AppUser.email, + "registration_id": registration_info.Registration.id, + "user_id": registration_info.AppUser.id, + "firstname": registration_info.AppUser.firstname, + "lastname": registration_info.AppUser.lastname, + "email": registration_info.AppUser.email, # TODO get this from somewhere else # 'user_category': registration_info.AppUser.user_category.name, # 'affiliation': registration_info.AppUser.affiliation, - 'created_at': registration_info.Registration.created_at, - 'confirmed': registration_info.Registration.confirmed, + "created_at": registration_info.Registration.created_at, + "confirmed": registration_info.Registration.confirmed, } def map_registration_info_guests(registration_info): - if(registration_info and registration_info.GuestRegistration and registration_info.GuestRegistration.id): + if ( + registration_info + and registration_info.GuestRegistration + and registration_info.GuestRegistration.id + ): reg_id = registration_info.GuestRegistration.id created_at = registration_info.GuestRegistration.created_at else: reg_id = None created_at = None return { - 'registration_id': reg_id, - 'user_id': registration_info.AppUser.id, - 'firstname': registration_info.AppUser.firstname, - 'lastname': registration_info.AppUser.lastname, - 'email': registration_info.AppUser.email, + "registration_id": reg_id, + "user_id": registration_info.AppUser.id, + "firstname": registration_info.AppUser.firstname, + "lastname": registration_info.AppUser.lastname, + "email": registration_info.AppUser.email, # TODO get this information from somewhere else # 'user_category': registration_info.AppUser.user_category.name, # 'affiliation': registration_info.AppUser.affiliation, - 'created_at': created_at, - 'confirmed' : True, + "created_at": created_at, + "confirmed": True, } registration_admin_fields = { - 'registration_id': fields.Integer(), - 'user_id': fields.Integer(), - 'firstname': fields.String(), - 'lastname': fields.String(), - 'email': fields.String(), - 'user_category': fields.String(), - 'affiliation': fields.String(), - 'created_at': fields.DateTime('iso8601'), - 'confirmed': fields.Boolean, + "registration_id": fields.Integer(), + "user_id": fields.Integer(), + "firstname": fields.String(), + "lastname": fields.String(), + "email": fields.String(), + "user_category": fields.String(), + "affiliation": fields.String(), + "created_at": fields.DateTime("iso8601"), + "confirmed": fields.Boolean, } @@ -315,99 +412,116 @@ def _get_registrations(event_id, user_id, confirmed, exclude_already_signed_in=F current_user = UserRepository.get_by_id(user_id) if not current_user.is_registration_volunteer(event_id): return errors.FORBIDDEN - if(exclude_already_signed_in == True): + if exclude_already_signed_in == True: registrations = RegistrationRepository.get_unsigned_in_attendees( - event_id, confirmed=confirmed) + event_id, confirmed=confirmed + ) guest_registration = GuestRegistrationRepository.get_all_unsigned_guests( - event_id) + event_id + ) else: - if confirmed is None: - registrations = RegistrationRepository.get_all_for_event( - event_id) + if confirmed is None: + registrations = RegistrationRepository.get_all_for_event(event_id) else: registrations = RegistrationRepository.get_confirmed_for_event( - event_id, confirmed=confirmed) - guest_registration = GuestRegistrationRepository.get_all_guests( - event_id) + event_id, confirmed=confirmed + ) + guest_registration = GuestRegistrationRepository.get_all_guests(event_id) registrations = [map_registration_info(info) for info in registrations] - guest_registrations = [map_registration_info_guests( - info) for info in guest_registration] + guest_registrations = [ + map_registration_info_guests(info) for info in guest_registration + ] all_registrations = registrations + guest_registrations - # remove duplicates + # remove duplicates all_registrations_no_duplicates = list() - for name, group in itertools.groupby(sorted(all_registrations, key=lambda d : d['user_id']), key=lambda d : d['user_id']): + for name, group in itertools.groupby( + sorted(all_registrations, key=lambda d: d["user_id"]), + key=lambda d: d["user_id"], + ): all_registrations_no_duplicates.append(next(group)) return marshal(all_registrations_no_duplicates, registration_admin_fields) except Exception as e: LOGGER.error( - 'Error occured while retrieving unconfirmed registrations: {}'.format(e)) + "Error occured while retrieving unconfirmed registrations: {}".format(e) + ) return errors.DB_NOT_AVAILABLE class RegistrationUnconfirmedAPI(RegistrationAdminMixin, restful.Resource): - @auth_required def get(self): args = self.req_parser.parse_args() - event_id = args['event_id'] - user_id = g.current_user['id'] + event_id = args["event_id"] + user_id = g.current_user["id"] return _get_registrations(event_id, user_id, confirmed=False) class RegistrationConfirmedAPI(RegistrationAdminMixin, restful.Resource): - @auth_required def get(self): - + args = self.req_parser.parse_args() - event_id = args['event_id'] - user_id = g.current_user['id'] - exclude_already_signed_in = args['exclude_already_signed_in'] or None + event_id = args["event_id"] + user_id = g.current_user["id"] + exclude_already_signed_in = args["exclude_already_signed_in"] or None # This is just for Indaba - return _get_registrations(event_id, user_id, confirmed=None, exclude_already_signed_in=exclude_already_signed_in) + return _get_registrations( + event_id, + user_id, + confirmed=None, + exclude_already_signed_in=exclude_already_signed_in, + ) def _send_registration_confirmation_mail(user, event): try: - emailer.email_user( - 'registration-confirmed', - event=event, - user=user) - + emailer.email_user("registration-confirmed", event=event, user=user) + return True except Exception as e: LOGGER.error( - 'Error occured while sending email to {}: {}'.format(user.email, e)) + "Error occured while sending email to {}: {}".format(user.email, e) + ) return False class RegistrationConfirmAPI(RegistrationConfirmMixin, restful.Resource): - @auth_required def post(self): args = self.req_parser.parse_args() - registration_id = args['registration_id'] - user_id = g.current_user['id'] + registration_id = args["registration_id"] + user_id = g.current_user["id"] try: current_user = UserRepository.get_by_id(user_id) registration, offer = RegistrationRepository.get_by_id_with_offer( - registration_id) + registration_id + ) if not current_user.is_registration_admin(offer.event_id): return errors.FORBIDDEN registration.confirm() registration_user = UserRepository.get_by_id(offer.user_id) registration_event = EventRepository.get_by_id(offer.event_id) - if _send_registration_confirmation_mail(registration_user, registration_event): + if _send_registration_confirmation_mail( + registration_user, registration_event + ): registration.confirmation_email_sent_at = datetime.now() db.session.commit() - return 'Confirmed Registration for {} {}'.format(registration_user.firstname, registration_user.lastname), 200 + return ( + "Confirmed Registration for {} {}".format( + registration_user.firstname, registration_user.lastname + ), + 200, + ) except Exception as e: - LOGGER.error('Error occured while confirming registration with id {}: {}'.format( - registration_id, e)) + LOGGER.error( + "Error occured while confirming registration with id {}: {}".format( + registration_id, e + ) + ) return errors.DB_NOT_AVAILABLE diff --git a/api/app/registrationResponse/mixins.py b/api/app/registrationResponse/mixins.py index f35ef88e3..4064e32e8 100644 --- a/api/app/registrationResponse/mixins.py +++ b/api/app/registrationResponse/mixins.py @@ -1,22 +1,23 @@ from flask_restful import reqparse from flask_restplus import inputs + class RegistrationResponseMixin(object): req_parser = reqparse.RequestParser() - req_parser.add_argument('registration_id', type=int, required=False) - req_parser.add_argument('offer_id', type=int, required=True) - req_parser.add_argument('registration_form_id', type=int, required=True) - req_parser.add_argument('answers', type=list, - required=True, location='json') + req_parser.add_argument("registration_id", type=int, required=False) + req_parser.add_argument("offer_id", type=int, required=True) + req_parser.add_argument("registration_form_id", type=int, required=True) + req_parser.add_argument("answers", type=list, required=True, location="json") class RegistrationAdminMixin(object): req_parser = reqparse.RequestParser() - req_parser.add_argument('event_id', type=int, required=True) - req_parser.add_argument('exclude_already_signed_in', - type=inputs.boolean, required=False) + req_parser.add_argument("event_id", type=int, required=True) + req_parser.add_argument( + "exclude_already_signed_in", type=inputs.boolean, required=False + ) class RegistrationConfirmMixin(object): req_parser = reqparse.RequestParser() - req_parser.add_argument('registration_id', type=int, required=True) + req_parser.add_argument("registration_id", type=int, required=True) diff --git a/api/app/registrationResponse/repository.py b/api/app/registrationResponse/repository.py index 6a6552af0..fdee4828b 100644 --- a/api/app/registrationResponse/repository.py +++ b/api/app/registrationResponse/repository.py @@ -1,75 +1,73 @@ -from app import db -from app.registration.models import Offer, Registration -from app.users.models import AppUser -from app.events.models import Event -from app.attendance.models import Attendance from sqlalchemy.sql import exists -from app import LOGGER +from app import LOGGER, db +from app.attendance.models import Attendance +from app.events.models import Event +from app.registration.models import Offer, Registration +from app.users.models import AppUser -class RegistrationRepository(): +class RegistrationRepository: @staticmethod def get_by_id_with_offer(registration_id): """Get a registration by its id.""" - return db.session.query(Registration, Offer).filter( - Registration.id == registration_id).join( - Offer, Offer.id == Registration.offer_id - ).one_or_none() + return ( + db.session.query(Registration, Offer) + .filter(Registration.id == registration_id) + .join(Offer, Offer.id == Registration.offer_id) + .one_or_none() + ) @staticmethod def get_by_user_id(user_id, event_id): """Get the registration for a given user id.""" - return db.session.query(Registration).join( - Offer, Registration.offer_id == Offer.id - ).filter( - Offer.user_id == user_id, - Offer.event_id == event_id - ).first() + return ( + db.session.query(Registration) + .join(Offer, Registration.offer_id == Offer.id) + .filter(Offer.user_id == user_id, Offer.event_id == event_id) + .first() + ) @staticmethod def get_all_for_event(event_id): """Get all registrations for an event""" - return db.session.query(Registration, Offer, AppUser).join( - Offer, Registration.offer_id == Offer.id - ).join( - AppUser, Offer.user_id == AppUser.id - ).filter( - Offer.event_id == event_id - ).all() + return ( + db.session.query(Registration, Offer, AppUser) + .join(Offer, Registration.offer_id == Offer.id) + .join(AppUser, Offer.user_id == AppUser.id) + .filter(Offer.event_id == event_id) + .all() + ) @staticmethod def get_confirmed_for_event(event_id, confirmed): """Get registrations for an event according to confirmed status.""" - return db.session.query(Registration, Offer, AppUser).filter( - Registration.confirmed == confirmed - ).join( - Offer, Registration.offer_id == Offer.id - ).join( - AppUser, Offer.user_id == AppUser.id - ).filter( - Offer.event_id == event_id - ).all() - - + return ( + db.session.query(Registration, Offer, AppUser) + .filter(Registration.confirmed == confirmed) + .join(Offer, Registration.offer_id == Offer.id) + .join(AppUser, Offer.user_id == AppUser.id) + .filter(Offer.event_id == event_id) + .all() + ) @staticmethod def get_unsigned_in_attendees(event_id, confirmed): """Get attendees who have confirmed they will attend and have not already signed in.""" - stmt = ~ exists().where(Attendance.user_id == AppUser.id) + stmt = ~exists().where(Attendance.user_id == AppUser.id) - registration_stmt = db.session.query(Registration, Offer, AppUser).join( - Offer, Registration.offer_id == Offer.id - ).join( - AppUser, Offer.user_id == AppUser.id - ).filter( - Offer.event_id == event_id - ).filter(stmt) + registration_stmt = ( + db.session.query(Registration, Offer, AppUser) + .join(Offer, Registration.offer_id == Offer.id) + .join(AppUser, Offer.user_id == AppUser.id) + .filter(Offer.event_id == event_id) + .filter(stmt) + ) - - if confirmed is not None: + if confirmed is not None: registration_stmt = registration_stmt.filter( - Registration.confirmed == confirmed) + Registration.confirmed == confirmed + ) reg = registration_stmt.all() return reg diff --git a/api/app/registrationResponse/tests.py b/api/app/registrationResponse/tests.py index 54c62ddc5..77e2305b0 100644 --- a/api/app/registrationResponse/tests.py +++ b/api/app/registrationResponse/tests.py @@ -1,34 +1,39 @@ -from app.registration.models import RegistrationForm, Registration -from app.registration.models import RegistrationSection import json from datetime import datetime, timedelta -from app.utils.testing import ApiTestCase -from app.users.models import AppUser, UserCategory, Country -from app.events.models import Event -from app.registration.models import Offer -from app.registration.models import RegistrationQuestion + from app import app, db +from app.events.models import Event from app.organisation.models import Organisation +from app.registration.models import ( + Offer, + Registration, + RegistrationForm, + RegistrationQuestion, + RegistrationSection, +) +from app.users.models import AppUser, Country, UserCategory +from app.utils.testing import ApiTestCase class RegistrationApiTest(ApiTestCase): - def seed_static_data(self, create_registration=False): - test_user = self.add_user('something@email.com', 'Some', 'Thing', 'Mr') - test_user2 = self.add_user('something2@email.com', 'Something2', 'Thing2', 'Mrs') - event_admin = self.add_user('event_admin@ea.com', 'event_admin', is_admin=True) - - self.add_organisation('Deep Learning Indaba', 'blah.png', 'blah_big.png') - db.session.add(UserCategory('Postdoc')) - db.session.add(Country('South Africa')) + test_user = self.add_user("something@email.com", "Some", "Thing", "Mr") + test_user2 = self.add_user( + "something2@email.com", "Something2", "Thing2", "Mrs" + ) + event_admin = self.add_user("event_admin@ea.com", "event_admin", is_admin=True) + + self.add_organisation("Deep Learning Indaba", "blah.png", "blah_big.png") + db.session.add(UserCategory("Postdoc")) + db.session.add(Country("South Africa")) db.session.commit() event = self.add_event( - name={'en': "Tech Talk"}, - description={'en': "tech talking"}, + name={"en": "Tech Talk"}, + description={"en": "tech talking"}, start_date=datetime(2019, 12, 12, 10, 10, 10), end_date=datetime(2020, 12, 12, 10, 10, 10), - key='SPEEDNET' + key="SPEEDNET", ) db.session.commit() @@ -40,7 +45,8 @@ def seed_static_data(self, create_registration=False): payment_required=False, travel_award=True, accommodation_award=False, - responded_at=datetime.now()) + responded_at=datetime.now(), + ) self.offer.candidate_response = True self.offer.accepted_travel_award = True db.session.add(self.offer) @@ -54,7 +60,8 @@ def seed_static_data(self, create_registration=False): payment_required=True, travel_award=True, accommodation_award=False, - responded_at=datetime.now()) + responded_at=datetime.now(), + ) db.session.add(self.offer2) db.session.commit() @@ -66,13 +73,12 @@ def seed_static_data(self, create_registration=False): payment_required=True, travel_award=False, accommodation_award=True, - responded_at=datetime.now()) + responded_at=datetime.now(), + ) db.session.add(self.offer3) db.session.commit() - self.form = RegistrationForm( - event_id=event.id - ) + self.form = RegistrationForm(event_id=event.id) db.session.add(self.form) db.session.commit() @@ -110,7 +116,7 @@ def seed_static_data(self, create_registration=False): placeholder="the placeholder", headline="the headline", validation_regex="[]/", - validation_text=" text" + validation_text=" text", ) db.session.add(self.question) db.session.commit() @@ -125,7 +131,7 @@ def seed_static_data(self, create_registration=False): placeholder="the placeholder", headline="the headline", validation_regex="[]/", - validation_text=" text" + validation_text=" text", ) db.session.add(self.question2) db.session.commit() @@ -140,7 +146,7 @@ def seed_static_data(self, create_registration=False): placeholder="the placeholder", headline="the headline", validation_regex="[]/", - validation_text=" text" + validation_text=" text", ) db.session.add(self.question3) db.session.commit() @@ -150,59 +156,60 @@ def seed_static_data(self, create_registration=False): self.adminHeaders = self.get_auth_header_for("event_admin@ea.com") if create_registration: - self.registration1 = Registration(self.offer.id, self.form.id, confirmed=False) + self.registration1 = Registration( + self.offer.id, self.form.id, confirmed=False + ) db.session.add(self.registration1) db.session.commit() - self.registration2 = Registration(self.offer2.id, self.form.id, confirmed=True) + self.registration2 = Registration( + self.offer2.id, self.form.id, confirmed=True + ) db.session.add(self.registration2) db.session.commit() - self.registration3 = Registration(self.offer3.id, self.form.id, confirmed=False) + self.registration3 = Registration( + self.offer3.id, self.form.id, confirmed=False + ) db.session.add(self.registration3) db.session.commit() - self.add_email_template('registration-with-confirmation') - self.add_email_template('registration-pending-confirmation') - self.add_email_template('registration-confirmed') + self.add_email_template("registration-with-confirmation") + self.add_email_template("registration-pending-confirmation") + self.add_email_template("registration-confirmed") db.session.flush() def get_auth_header_for(self, email): - body = { - 'email': email, - 'password': 'abc' - } - response = self.app.post('api/v1/authenticate', data=body) + body = {"email": email, "password": "abc"} + response = self.app.post("api/v1/authenticate", data=body) data = json.loads(response.data) - header = {'Authorization': data['token']} + header = {"Authorization": data["token"]} return header def test_create_registration(self): with app.app_context(): self.seed_static_data(create_registration=False) registration_data = { - 'offer_id': self.offer.id, - 'registration_form_id': self.form.id, - 'answers': [ + "offer_id": self.offer.id, + "registration_form_id": self.form.id, + "answers": [ + {"registration_question_id": self.question.id, "value": "Answer 1"}, { - 'registration_question_id': self.question.id, - 'value': 'Answer 1' + "registration_question_id": self.question2.id, + "value": "Hello world, this is the 2nd answer.", }, { - 'registration_question_id': self.question2.id, - 'value': 'Hello world, this is the 2nd answer.' + "registration_question_id": self.question3.id, + "value": "Hello world, this is the 3rd answer.", }, - { - 'registration_question_id': self.question3.id, - 'value': 'Hello world, this is the 3rd answer.' - } - ] + ], } response = self.app.post( - '/api/v1/registration-response', + "/api/v1/registration-response", data=json.dumps(registration_data), - content_type='application/json', - headers=self.headers) + content_type="application/json", + headers=self.headers, + ) self.assertEqual(response.status_code, 201) def test_get_registration(self): @@ -210,33 +217,31 @@ def test_get_registration(self): self.seed_static_data() registration_data = { - 'offer_id': self.offer.id, - 'registration_form_id': self.form.id, - 'answers': [ + "offer_id": self.offer.id, + "registration_form_id": self.form.id, + "answers": [ + {"registration_question_id": self.question.id, "value": "Answer 1"}, { - 'registration_question_id': self.question.id, - 'value': 'Answer 1' + "registration_question_id": self.question2.id, + "value": "Hello world, this is the 2nd answer.", }, { - 'registration_question_id': self.question2.id, - 'value': 'Hello world, this is the 2nd answer.' + "registration_question_id": self.question3.id, + "value": "Hello world, this is the 3rd answer.", }, - { - 'registration_question_id': self.question3.id, - 'value': 'Hello world, this is the 3rd answer.' - } - ] + ], } response = self.app.post( - '/api/v1/registration-response', + "/api/v1/registration-response", data=json.dumps(registration_data), - content_type='application/json', - headers=self.headers + content_type="application/json", + headers=self.headers, ) response = self.app.get( - '/api/v1/registration-response', - content_type='application/json', - headers=self.headers) + "/api/v1/registration-response", + content_type="application/json", + headers=self.headers, + ) self.assertEqual(response.status_code, 200) def test_update_200(self): @@ -244,55 +249,50 @@ def test_update_200(self): with app.app_context(): self.seed_static_data() registration_data = { - 'offer_id': self.offer.id, - 'registration_form_id': self.form.id, - 'answers': [ + "offer_id": self.offer.id, + "registration_form_id": self.form.id, + "answers": [ + {"registration_question_id": self.question.id, "value": "Answer 1"}, { - 'registration_question_id': self.question.id, - 'value': 'Answer 1' + "registration_question_id": self.question2.id, + "value": "Hello world, this is the 2nd answer.", }, { - 'registration_question_id': self.question2.id, - 'value': 'Hello world, this is the 2nd answer.' + "registration_question_id": self.question3.id, + "value": "Hello world, this is the 3rd answer.", }, - { - 'registration_question_id': self.question3.id, - 'value': 'Hello world, this is the 3rd answer.' - } - ] + ], } response = self.app.post( - '/api/v1/registration-response', + "/api/v1/registration-response", data=json.dumps(registration_data), - content_type='application/json', - headers=self.headers + content_type="application/json", + headers=self.headers, ) data = json.loads(response.data) put_registration_data = { - 'registration_id': data['id'], - 'offer_id': self.offer.id, - 'registration_form_id': self.form.id, - 'answers': [ + "registration_id": data["id"], + "offer_id": self.offer.id, + "registration_form_id": self.form.id, + "answers": [ + {"registration_question_id": self.question.id, "value": "Answer 1"}, { - 'registration_question_id': self.question.id, - 'value': 'Answer 1' + "registration_question_id": self.question2.id, + "value": "Hello world, this is the 2nd answer.", }, { - 'registration_question_id': self.question2.id, - 'value': 'Hello world, this is the 2nd answer.' + "registration_question_id": self.question3.id, + "value": "Hello world, this is the 3rd answer.", }, - { - 'registration_question_id': self.question3.id, - 'value': 'Hello world, this is the 3rd answer.' - } - ] + ], } post_response = self.app.put( - '/api/v1/registration-response', + "/api/v1/registration-response", data=json.dumps(put_registration_data), - content_type='application/json', - headers=self.headers) + content_type="application/json", + headers=self.headers, + ) self.assertEqual(post_response.status_code, 200) def test_update_missing(self): @@ -300,83 +300,86 @@ def test_update_missing(self): with app.app_context(): self.seed_static_data() registration_data = { - 'registration_id': 50, - 'offer_id': self.offer.id, - 'registration_form_id': self.form.id, - 'answers': [ + "registration_id": 50, + "offer_id": self.offer.id, + "registration_form_id": self.form.id, + "answers": [ + {"registration_question_id": self.question.id, "value": "Answer 1"}, { - 'registration_question_id': self.question.id, - 'value': 'Answer 1' + "registration_question_id": self.question2.id, + "value": "Hello world, this is the 2nd answer.", }, { - 'registration_question_id': self.question2.id, - 'value': 'Hello world, this is the 2nd answer.' + "registration_question_id": self.question3.id, + "value": "Hello world, this is the 3rd answer.", }, - { - 'registration_question_id': self.question3.id, - 'value': 'Hello world, this is the 3rd answer.' - } - ] + ], } response = self.app.put( - '/api/v1/registration-response', + "/api/v1/registration-response", data=json.dumps(registration_data), - content_type='application/json', - headers=self.headers) + content_type="application/json", + headers=self.headers, + ) self.assertEqual(response.status_code, 404) - def test_get_unconfirmed_not_event_admin(self): with app.app_context(): self.seed_static_data() - response = self.app.get('/api/v1/registration/unconfirmed?event_id=1', - headers=self.headers) + response = self.app.get( + "/api/v1/registration/unconfirmed?event_id=1", headers=self.headers + ) self.assertEqual(response.status_code, 403) def test_get_unconfirmed(self): with app.app_context(): self.seed_static_data(create_registration=True) - response = self.app.get('/api/v1/registration/unconfirmed?event_id=1', - headers=self.adminHeaders) + response = self.app.get( + "/api/v1/registration/unconfirmed?event_id=1", headers=self.adminHeaders + ) responses = json.loads(response.data) self.assertEqual(response.status_code, 200) self.assertEqual(len(responses), 2) - self.assertEqual(responses[0]['registration_id'], self.registration1.id) - self.assertEqual(responses[0]['user_id'], self.offer.user_id) - self.assertEqual(responses[0]['firstname'], 'Some') - self.assertEqual(responses[0]['lastname'], 'Thing') - self.assertEqual(responses[0]['email'], 'something@email.com') + self.assertEqual(responses[0]["registration_id"], self.registration1.id) + self.assertEqual(responses[0]["user_id"], self.offer.user_id) + self.assertEqual(responses[0]["firstname"], "Some") + self.assertEqual(responses[0]["lastname"], "Thing") + self.assertEqual(responses[0]["email"], "something@email.com") # TODO re-add once we get these fields outside of AppUser # self.assertEqual(responses[0]['user_category'], 'Postdoc') # self.assertEqual(responses[0]['affiliation'], 'University') - self.assertEqual(responses[0]['created_at'][:9], datetime.today().isoformat()[:9]) + self.assertEqual( + responses[0]["created_at"][:9], datetime.today().isoformat()[:9] + ) - self.assertEqual(responses[1]['registration_id'], self.registration3.id) - self.assertEqual(responses[1]['user_id'], self.offer3.user_id) - self.assertEqual(responses[1]['firstname'], 'event_admin') - self.assertEqual(responses[1]['lastname'], 'Lastname') - self.assertEqual(responses[1]['email'], 'event_admin@ea.com') + self.assertEqual(responses[1]["registration_id"], self.registration3.id) + self.assertEqual(responses[1]["user_id"], self.offer3.user_id) + self.assertEqual(responses[1]["firstname"], "event_admin") + self.assertEqual(responses[1]["lastname"], "Lastname") + self.assertEqual(responses[1]["email"], "event_admin@ea.com") # TODO re-add once we get these fields outside of AppUser # self.assertEqual(responses[1]['user_category'], 'Postdoc') # self.assertEqual(responses[1]['affiliation'], 'NWU') - self.assertEqual(responses[1]['created_at'][:9], datetime.today().isoformat()[:9]) + self.assertEqual( + responses[1]["created_at"][:9], datetime.today().isoformat()[:9] + ) def test_get_confirmed_not_event_admin(self): with app.app_context(): self.seed_static_data() - response = self.app.get('/api/v1/registration/confirmed?event_id=1', - headers=self.headers) + response = self.app.get( + "/api/v1/registration/confirmed?event_id=1", headers=self.headers + ) self.assertEqual(response.status_code, 403) - - def test_get_confirmed(self): with app.app_context(): self.seed_static_data(create_registration=True) - response = self.app.get('/api/v1/registration/confirmed?event_id=1', - headers=self.adminHeaders) + response = self.app.get( + "/api/v1/registration/confirmed?event_id=1", headers=self.adminHeaders + ) responses = json.loads(response.data) self.assertEqual(response.status_code, 200) self.assertEqual(len(responses), 3) @@ -384,18 +387,26 @@ def test_get_confirmed(self): def test_confirm_admin(self): with app.app_context(): self.seed_static_data(create_registration=True) - response = self.app.post('/api/v1/registration/confirm', - data={'registration_id': self.registration1.id}, - headers=self.headers) + response = self.app.post( + "/api/v1/registration/confirm", + data={"registration_id": self.registration1.id}, + headers=self.headers, + ) self.assertEqual(response.status_code, 403) def test_confirm(self): with app.app_context(): self.seed_static_data(create_registration=True) - response = self.app.post('/api/v1/registration/confirm', - data={'registration_id': self.registration1.id}, - headers=self.adminHeaders) + response = self.app.post( + "/api/v1/registration/confirm", + data={"registration_id": self.registration1.id}, + headers=self.adminHeaders, + ) self.assertEqual(response.status_code, 200) - updated_registration = db.session.query(Registration).filter(Registration.id == self.registration1.id).one() + updated_registration = ( + db.session.query(Registration) + .filter(Registration.id == self.registration1.id) + .one() + ) self.assertTrue(updated_registration.confirmed) diff --git a/api/app/responses/api.py b/api/app/responses/api.py index 221c005f9..7fd83ebda 100644 --- a/api/app/responses/api.py +++ b/api/app/responses/api.py @@ -1,60 +1,63 @@ import datetime import io import itertools -import traceback import json import tempfile +import traceback import flask_restful as restful +from flask import g, request, send_file +from flask_restful import fields, inputs, marshal_with, reqparse +from sqlalchemy.exc import SQLAlchemyError + from app import LOGGER, bcrypt, db from app.applicationModel.models import ApplicationForm, Question -from app.applicationModel.repository import \ - ApplicationFormRepository as application_form_repository +from app.applicationModel.repository import ( + ApplicationFormRepository as application_form_repository, +) from app.events.models import Event, EventType from app.events.repository import EventRepository as event_repository from app.responses.mixins import ResponseMixin, ResponseTagMixin from app.responses.models import Answer, Response, ResponseTag from app.responses.repository import ResponseRepository as response_repository -from app.reviews.repository import \ - ReviewConfigurationRepository as review_configuration_repository +from app.reviews.repository import ( + ReviewConfigurationRepository as review_configuration_repository, +) from app.reviews.repository import ReviewRepository as review_repository from app.users.models import AppUser from app.users.repository import UserRepository as user_repository -from app.utils import emailer, errors, strings, pdfconvertor, zipping, storage -from app.utils.zipping import zip_in_memory +from app.utils import emailer, errors, pdfconvertor, storage, strings, zipping from app.utils.auth import auth_required, event_admin_required -from flask import g, request, send_file -from flask_restful import fields, inputs, marshal_with, reqparse -from sqlalchemy.exc import SQLAlchemyError +from app.utils.zipping import zip_in_memory class ResponseAPI(ResponseMixin, restful.Resource): answer_fields = { - 'id': fields.Integer, - 'question_id': fields.Integer, - 'value': fields.String + "id": fields.Integer, + "question_id": fields.Integer, + "value": fields.String, } 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)), - 'language': fields.String + "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)), + "language": fields.String, } @auth_required @marshal_with(response_fields) def get(self): args = self.get_req_parser.parse_args() - event_id = args['event_id'] - current_user_id = g.current_user['id'] + event_id = args["event_id"] + current_user_id = g.current_user["id"] event = event_repository.get_by_id(event_id) if not event: @@ -64,26 +67,30 @@ def get(self): return errors.FORM_NOT_FOUND form = event.get_application_form() - responses = response_repository.get_all_for_user_application(current_user_id, form.id) + responses = response_repository.get_all_for_user_application( + current_user_id, form.id + ) return responses @auth_required @marshal_with(response_fields) def post(self): args = self.post_req_parser.parse_args() - user_id = g.current_user['id'] - is_submitted = args['is_submitted'] - application_form_id = args['application_form_id'] - language = args['language'] + user_id = g.current_user["id"] + is_submitted = args["is_submitted"] + application_form_id = args["application_form_id"] + language = args["language"] if len(language) != 2: - language = 'en' # Fallback to English if language doesn't look like an ISO 639-1 code + language = "en" # Fallback to English if language doesn't look like an ISO 639-1 code application_form = application_form_repository.get_by_id(application_form_id) if application_form is None: return errors.FORM_NOT_FOUND_BY_ID - + user = user_repository.get_by_id(user_id) - responses = response_repository.get_all_for_user_application(user_id, application_form_id) + responses = response_repository.get_all_for_user_application( + user_id, application_form_id + ) if not application_form.nominations and len(responses) > 0: return errors.RESPONSE_ALREADY_SUBMITTED @@ -91,23 +98,35 @@ def post(self): response = Response(application_form_id, user_id, language) if is_submitted: response.submit() - + response_repository.save(response) answers = [] - for answer_args in args['answers']: - answer = Answer(response.id, answer_args['question_id'], answer_args['value']) + for answer_args in args["answers"]: + answer = Answer( + response.id, answer_args["question_id"], answer_args["value"] + ) answers.append(answer) response_repository.save_answers(answers) try: if response.is_submitted: - LOGGER.info('Sending confirmation email for response with ID : {id}'.format(id=response.id)) + LOGGER.info( + "Sending confirmation email for response with ID : {id}".format( + id=response.id + ) + ) user = user_repository.get_by_id(user_id) - response = response_repository.get_by_id_and_user_id(response.id, user_id) + response = response_repository.get_by_id_and_user_id( + response.id, user_id + ) self.send_confirmation(user, response) except: - LOGGER.warn('Failed to send confirmation email for response with ID : {id}, but the response was submitted succesfully'.format(id=response.id)) + LOGGER.warn( + "Failed to send confirmation email for response with ID : {id}, but the response was submitted succesfully".format( + id=response.id + ) + ) finally: return response, 201 @@ -115,16 +134,16 @@ def post(self): @marshal_with(response_fields) def put(self): args = self.put_req_parser.parse_args() - user_id = g.current_user['id'] - is_submitted = args['is_submitted'] - language = args['language'] + user_id = g.current_user["id"] + is_submitted = args["is_submitted"] + language = args["language"] - response = response_repository.get_by_id(args['id']) + response = response_repository.get_by_id(args["id"]) if not response: return errors.RESPONSE_NOT_FOUND if response.user_id != user_id: return errors.UNAUTHORIZED - if response.application_form_id != args['application_form_id']: + if response.application_form_id != args["application_form_id"]: return errors.UPDATE_CONFLICT response.is_submitted = is_submitted @@ -134,35 +153,49 @@ def put(self): response_repository.save(response) answers = [] - for answer_args in args['answers']: - answer = response_repository.get_answer_by_question_id_and_response_id(answer_args['question_id'], response.id) + for answer_args in args["answers"]: + answer = response_repository.get_answer_by_question_id_and_response_id( + answer_args["question_id"], response.id + ) if answer: - if answer.value == answer_args['value']: + if answer.value == answer_args["value"]: # Value not changed, ignore continue answer.deactivate() response_repository.merge_answer(answer) - active_answer = Answer(response.id, answer_args['question_id'], answer_args['value']) + active_answer = Answer( + response.id, answer_args["question_id"], answer_args["value"] + ) answers.append(active_answer) response_repository.save_answers(answers) try: if response.is_submitted: - LOGGER.info('Sending confirmation email for response with ID : {id}'.format(id=response.id)) + LOGGER.info( + "Sending confirmation email for response with ID : {id}".format( + id=response.id + ) + ) user = user_repository.get_by_id(user_id) - response = response_repository.get_by_id_and_user_id(response.id, user_id) + response = response_repository.get_by_id_and_user_id( + response.id, user_id + ) self.send_confirmation(user, response) - except: - LOGGER.warn('Failed to send confirmation email for response with ID : {id}, but the response was submitted succesfully'.format(id=response.id)) + except: + LOGGER.warn( + "Failed to send confirmation email for response with ID : {id}, but the response was submitted succesfully".format( + id=response.id + ) + ) finally: return response, 200 @auth_required def delete(self): args = self.del_req_parser.parse_args() - current_user_id = g.current_user['id'] + current_user_id = g.current_user["id"] - response = response_repository.get_by_id(args['id']) + response = response_repository.get_by_id(args["id"]) if not response: return errors.RESPONSE_NOT_FOUND @@ -170,7 +203,7 @@ def delete(self): return errors.UNAUTHORIZED response.withdraw() - response_repository.save(response) + response_repository.save(response) try: user = user_repository.get_by_id(current_user_id) @@ -178,16 +211,18 @@ def delete(self): organisation = event.organisation emailer.email_user( - 'withdrawal', - template_parameters=dict( - organisation_name=organisation.name - ), + "withdrawal", + template_parameters=dict(organisation_name=organisation.name), event=event, - user=user + user=user, ) - except: - LOGGER.error('Failed to send withdrawal confirmation email for response with ID : {id}, but the response was withdrawn succesfully'.format(id=args['id'])) + except: + LOGGER.error( + "Failed to send withdrawal confirmation email for response with ID : {id}, but the response was withdrawn succesfully".format( + id=args["id"] + ) + ) return {}, 204 @@ -195,38 +230,62 @@ def send_confirmation(self, user, response): try: answers = response.answers if not answers: - LOGGER.warn('Found no answers associated with response with id {response_id}'.format(response_id=response.id)) + LOGGER.warn( + "Found no answers associated with response with id {response_id}".format( + response_id=response.id + ) + ) application_form = response.application_form if application_form is None: - LOGGER.warn('Found no application form with id {form_id}'.format(form_id=response.application_form_id)) + LOGGER.warn( + "Found no application form with id {form_id}".format( + form_id=response.application_form_id + ) + ) event = application_form.event if event is None: - LOGGER.warn('Found no event id {event_id}'.format(form_id=application_form.event_id)) + LOGGER.warn( + "Found no event id {event_id}".format( + form_id=application_form.event_id + ) + ) except: - LOGGER.error('Could not connect to the database to retrieve response confirmation email data on response with ID : {response_id}'.format(response_id=response.id)) + LOGGER.error( + "Could not connect to the database to retrieve response confirmation email data on response with ID : {response_id}".format( + response_id=response.id + ) + ) try: - question_answer_summary = strings.build_response_email_body(answers, user.user_primaryLanguage, application_form) + question_answer_summary = strings.build_response_email_body( + answers, user.user_primaryLanguage, application_form + ) if event.has_specific_translation(user.user_primaryLanguage): event_description = event.get_description(user.user_primaryLanguage) else: - event_description = event.get_description('en') + event_description = event.get_description("en") emailer.email_user( - 'confirmation-response-call' if event.event_type == EventType.CALL else 'confirmation-response', + "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 + 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)) + LOGGER.error( + "Could not send confirmation email for response with id : {response_id} due to: {e}".format( + response_id=response.id, e=e + ) + ) def _pad_list(lst, length): @@ -234,101 +293,135 @@ def _pad_list(lst, length): lst.extend([None] * diff) return lst + def _review_response_status(review_response): - status = 'not_started' + status = "not_started" if review_response is not None: - status = 'completed' if review_response.is_submitted else 'started' + status = "completed" if review_response.is_submitted else "started" return status + def _serialize_reviewer(response_reviewer, review_response): - + return { - 'reviewer_id': response_reviewer.reviewer_user_id, - 'reviewer_name': '{} {} {}'.format(response_reviewer.user.user_title, response_reviewer.user.firstname, response_reviewer.user.lastname), - 'review_response_id': None if review_response is None else review_response.id, - 'status': _review_response_status(review_response) + "reviewer_id": response_reviewer.reviewer_user_id, + "reviewer_name": "{} {} {}".format( + response_reviewer.user.user_title, + response_reviewer.user.firstname, + response_reviewer.user.lastname, + ), + "review_response_id": None if review_response is None else review_response.id, + "status": _review_response_status(review_response), } + def _serialize_answer(answer, language): question = answer.question translation = question.get_translation(language) if translation is None: - LOGGER.warn('No {} translation found for question id {}'.format(language, question.id)) - translation = question.get_translation('en') + LOGGER.warn( + "No {} translation found for question id {}".format(language, question.id) + ) + translation = question.get_translation("en") return { - 'question_id': answer.question_id, - 'value': answer.value, - 'type': answer.question.type, - 'options': translation.options, - 'headline': translation.headline + "question_id": answer.question_id, + "value": answer.value, + "type": answer.question.type, + "options": translation.options, + "headline": translation.headline, } + def _serialize_tag(tag, language): tag_translation = tag.get_translation(language) if not tag_translation: - LOGGER.warn('Could not find {} translation for tag id {}'.format(language, tag.id)) - tag_translation = tag.get_translation('en') - return { - 'id': tag.id, - 'name': tag_translation.name - } + LOGGER.warn( + "Could not find {} translation for tag id {}".format(language, tag.id) + ) + tag_translation = tag.get_translation("en") + return {"id": tag.id, "name": tag_translation.name} -class ResponseListAPI(restful.Resource): +class ResponseListAPI(restful.Resource): @event_admin_required def get(self, event_id): req_parser = reqparse.RequestParser() - req_parser.add_argument('include_unsubmitted', type=inputs.boolean, required=True) + req_parser.add_argument( + "include_unsubmitted", type=inputs.boolean, required=True + ) # Note: Including [] in the question_ids parameter because that gets added by Axios on the front-end - req_parser.add_argument('question_ids[]', type=int, required=False, action='append') - req_parser.add_argument('language', type=str, required=True) + req_parser.add_argument( + "question_ids[]", type=int, required=False, action="append" + ) + req_parser.add_argument("language", type=str, required=True) args = req_parser.parse_args() - include_unsubmitted = args['include_unsubmitted'] - question_ids = args['question_ids[]'] - language = args['language'] - - print(('Include unsubmitted:', include_unsubmitted)) + include_unsubmitted = args["include_unsubmitted"] + question_ids = args["question_ids[]"] + language = args["language"] - responses = response_repository.get_all_for_event(event_id, not include_unsubmitted) + print(("Include unsubmitted:", include_unsubmitted)) - review_config = review_configuration_repository.get_configuration_for_event(event_id) - required_reviewers = 1 if review_config is None else review_config.num_reviews_required + review_config.num_optional_reviews + responses = response_repository.get_all_for_event( + event_id, not include_unsubmitted + ) - response_reviewers = review_repository.get_response_reviewers_for_event(event_id) + review_config = review_configuration_repository.get_configuration_for_event( + event_id + ) + required_reviewers = ( + 1 + if review_config is None + else review_config.num_reviews_required + review_config.num_optional_reviews + ) + + response_reviewers = review_repository.get_response_reviewers_for_event( + event_id + ) response_to_reviewers = { - k: list(g) for k, g in itertools.groupby(response_reviewers, lambda r: r.response_id) + k: list(g) + for k, g in itertools.groupby(response_reviewers, lambda r: r.response_id) } review_responses = review_repository.get_review_responses_for_event(event_id) - reviewer_to_review_response = { - r.reviewer_user_id: r for r in review_responses - } + reviewer_to_review_response = {r.reviewer_user_id: r for r in review_responses} serialized_responses = [] for response in responses: - reviewers = [_serialize_reviewer(r, reviewer_to_review_response.get(r.reviewer_user_id, None)) - for r in response_to_reviewers.get(response.id, [])] + reviewers = [ + _serialize_reviewer( + r, reviewer_to_review_response.get(r.reviewer_user_id, None) + ) + for r in response_to_reviewers.get(response.id, []) + ] reviewers = _pad_list(reviewers, required_reviewers) if question_ids: - answers = [_serialize_answer(answer, language) for answer in response.answers if answer.question_id in question_ids] + answers = [ + _serialize_answer(answer, language) + for answer in response.answers + if answer.question_id in question_ids + ] else: answers = [] serialized = { - 'response_id': response.id, - 'user_title': response.user.user_title, - 'firstname': response.user.firstname, - 'lastname': response.user.lastname, - 'start_date': response.started_timestamp.isoformat(), - 'is_submitted': response.is_submitted, - 'is_withdrawn': response.is_withdrawn, - 'submitted_date': None if response.submitted_timestamp is None else response.submitted_timestamp.isoformat(), - 'language': response.language, - 'answers': answers, - 'reviewers': reviewers, - 'tags': [_serialize_tag(rt.tag, language) for rt in response.response_tags] + "response_id": response.id, + "user_title": response.user.user_title, + "firstname": response.user.firstname, + "lastname": response.user.lastname, + "start_date": response.started_timestamp.isoformat(), + "is_submitted": response.is_submitted, + "is_withdrawn": response.is_withdrawn, + "submitted_date": None + if response.submitted_timestamp is None + else response.submitted_timestamp.isoformat(), + "language": response.language, + "answers": answers, + "reviewers": reviewers, + "tags": [ + _serialize_tag(rt.tag, language) for rt in response.response_tags + ], } serialized_responses.append(serialized) @@ -342,17 +435,20 @@ def _validate_user_admin_or_reviewer(user_id, event_id, response_id): permitted = user.is_event_admin(event_id) # If they're not an event admin, check if they're a reviewer for the relevant response if not permitted and user.is_reviewer(event_id): - response_reviewer = review_repository.get_response_reviewer(response_id, user.id) + response_reviewer = review_repository.get_response_reviewer( + response_id, user.id + ) if response_reviewer is not None: permitted = True - + return permitted + class ResponseTagAPI(restful.Resource, ResponseTagMixin): response_tag_fields = { - 'id': fields.Integer, - 'response_id': fields.Integer, - 'tag_id': fields.Integer + "id": fields.Integer, + "response_id": fields.Integer, + "tag_id": fields.Integer, } @marshal_with(response_tag_fields) @@ -360,11 +456,13 @@ class ResponseTagAPI(restful.Resource, ResponseTagMixin): def post(self): args = self.req_parser.parse_args() - event_id = args['event_id'] - tag_id = args['tag_id'] - response_id = args['response_id'] + event_id = args["event_id"] + tag_id = args["tag_id"] + response_id = args["response_id"] - if not _validate_user_admin_or_reviewer(g.current_user['id'], event_id, response_id): + if not _validate_user_admin_or_reviewer( + g.current_user["id"], event_id, response_id + ): return errors.FORBIDDEN return response_repository.tag_response(response_id, tag_id), 201 @@ -373,11 +471,13 @@ def post(self): def delete(self): args = self.req_parser.parse_args() - event_id = args['event_id'] - tag_id = args['tag_id'] - response_id = args['response_id'] + event_id = args["event_id"] + tag_id = args["tag_id"] + response_id = args["response_id"] - if not _validate_user_admin_or_reviewer(g.current_user['id'], event_id, response_id): + if not _validate_user_admin_or_reviewer( + g.current_user["id"], event_id, response_id + ): return errors.FORBIDDEN response_repository.remove_tag_from_response(response_id, tag_id) @@ -385,9 +485,7 @@ def delete(self): return {} - class ResponseDetailAPI(restful.Resource): - @staticmethod def _serialize_date(date): if date is None: @@ -397,80 +495,104 @@ def _serialize_date(date): @staticmethod def _serialize_answer(answer): return { - 'id': answer.id, - 'question_id': answer.question_id, - 'value': answer.value + "id": answer.id, + "question_id": answer.question_id, + "value": answer.value, } @staticmethod def _serialize_tag(tag, language): translation = tag.get_translation(language) if translation is None: - LOGGER.warn('Could not find {} translation for tag id {}'.format(language, tag.id)) - return { - 'id': tag.id, - 'name': translation.name - } + LOGGER.warn( + "Could not find {} translation for tag id {}".format(language, tag.id) + ) + return {"id": tag.id, "name": translation.name} @staticmethod def _serialize_reviewer(response_reviewer, review_form_id): if review_form_id is None: completed = False else: - review_response = review_repository.get_review_response(review_form_id, response_reviewer.response_id, response_reviewer.reviewer_user_id) + review_response = review_repository.get_review_response( + review_form_id, + response_reviewer.response_id, + response_reviewer.reviewer_user_id, + ) return { - 'reviewer_user_id': response_reviewer.user.id, - 'user_title': response_reviewer.user.user_title, - 'firstname': response_reviewer.user.firstname, - 'lastname': response_reviewer.user.lastname, - 'status': _review_response_status(review_response) + "reviewer_user_id": response_reviewer.user.id, + "user_title": response_reviewer.user.user_title, + "firstname": response_reviewer.user.firstname, + "lastname": response_reviewer.user.lastname, + "status": _review_response_status(review_response), } - @staticmethod def _serialize_response(response, language, review_form_id, num_reviewers): return { - 'id': response.id, - 'application_form_id': response.application_form_id, - 'user_id': response.user_id, - 'is_submitted': response.is_submitted, - 'submitted_timestamp': ResponseDetailAPI._serialize_date(response.submitted_timestamp), - 'is_withdrawn': response.is_withdrawn, - 'withdrawn_timestamp': ResponseDetailAPI._serialize_date(response.withdrawn_timestamp), - 'started_timestamp': ResponseDetailAPI._serialize_date(response.started_timestamp), - 'answers': [ResponseDetailAPI._serialize_answer(answer) for answer in response.answers], - 'language': response.language, - 'user_title': response.user.user_title, - 'firstname': response.user.firstname, - 'lastname': response.user.lastname, - 'tags': [ResponseDetailAPI._serialize_tag(rt.tag, language) for rt in response.response_tags], - 'reviewers': [ResponseDetailAPI._serialize_reviewer(r, review_form_id) for r in response.reviewers] + "id": response.id, + "application_form_id": response.application_form_id, + "user_id": response.user_id, + "is_submitted": response.is_submitted, + "submitted_timestamp": ResponseDetailAPI._serialize_date( + response.submitted_timestamp + ), + "is_withdrawn": response.is_withdrawn, + "withdrawn_timestamp": ResponseDetailAPI._serialize_date( + response.withdrawn_timestamp + ), + "started_timestamp": ResponseDetailAPI._serialize_date( + response.started_timestamp + ), + "answers": [ + ResponseDetailAPI._serialize_answer(answer) + for answer in response.answers + ], + "language": response.language, + "user_title": response.user.user_title, + "firstname": response.user.firstname, + "lastname": response.user.lastname, + "tags": [ + ResponseDetailAPI._serialize_tag(rt.tag, language) + for rt in response.response_tags + ], + "reviewers": [ + ResponseDetailAPI._serialize_reviewer(r, review_form_id) + for r in response.reviewers + ], } @event_admin_required def get(self, event_id): req_parser = reqparse.RequestParser() - req_parser.add_argument('response_id', type=int, required=True) - req_parser.add_argument('language', type=str, required=True) + req_parser.add_argument("response_id", type=int, required=True) + req_parser.add_argument("language", type=str, required=True) args = req_parser.parse_args() - response_id = args['response_id'] - language = args['language'] + response_id = args["response_id"] + language = args["language"] response = response_repository.get_by_id(response_id) review_form = review_repository.get_review_form(event_id) review_form_id = None if review_form is None else review_form.id - review_config = review_configuration_repository.get_configuration_for_event(event_id) - num_reviewers = review_config.num_reviews_required + review_config.num_optional_reviews if review_config is not None else 1 + review_config = review_configuration_repository.get_configuration_for_event( + event_id + ) + num_reviewers = ( + review_config.num_reviews_required + review_config.num_optional_reviews + if review_config is not None + else 1 + ) - return ResponseDetailAPI._serialize_response(response, language, review_form_id, num_reviewers) + return ResponseDetailAPI._serialize_response( + response, language, review_form_id, num_reviewers + ) -class ResponseExportAPI(restful.Resource): +class ResponseExportAPI(restful.Resource): def get(self): - def _get_answer(question_id, answers): # Get the answer for a question for a in answers: @@ -479,54 +601,61 @@ def _get_answer(question_id, answers): return None - def _get_files(application_form, answers): + def _get_files(application_form, answers): # Get the response files that should be exported in the ZIP file file_names = [] for section in application_form.sections: for question in section.questions: - answer = _get_answer(question.id, answers) + answer = _get_answer(question.id, answers) if answer is not None: - # We are only interested in the files, + # We are only interested in the files, # the text answers will be in the main PDF file - if question.type == 'multi-file': + if question.type == "multi-file": file_names.extend(json.loads(answer.value)) - if question.type == 'file': + if question.type == "file": file_names.append(json.loads(answer.value)) return file_names req_parser = reqparse.RequestParser() - req_parser.add_argument('response_id', type=int, required=True) - req_parser.add_argument('language', type=str, required=True) + req_parser.add_argument("response_id", type=int, required=True) + req_parser.add_argument("language", type=str, required=True) args = req_parser.parse_args() - response_id = args['response_id'] - language = args['language'] + response_id = args["response_id"] + language = args["language"] - response = response_repository.get_by_id(response_id) - application_form = application_form_repository.get_by_id(response_id) + response = response_repository.get_by_id(response_id) + application_form = application_form_repository.get_by_id(response_id) # Build the HTML string - response_string = strings.build_response_html_app_info(response, language) + \ - strings.build_response_html_answers(response.answers, language, application_form) + response_string = strings.build_response_html_app_info( + response, language + ) + strings.build_response_html_answers( + response.answers, language, application_form + ) # Convert to PDF - files_to_compress = [( - 'response.pdf', - pdfconvertor.html_to_pdf(response.id, response_string) - )] + files_to_compress = [ + ("response.pdf", pdfconvertor.html_to_pdf(response.id, response_string)) + ] # The files that were uploaded as part of the response files_to_get = _get_files(application_form, response.answers) bucket = storage.get_storage_bucket() # Download and name the files - files_to_compress.extend([( - f['rename'] or f['filename'], - io.BytesIO(bucket.blob(f['filename']).download_as_bytes()) - ) for f in files_to_get]) + files_to_compress.extend( + [ + ( + f["rename"] or f["filename"], + io.BytesIO(bucket.blob(f["filename"]).download_as_bytes()), + ) + for f in files_to_get + ] + ) # Zip files zipped_files = zip_in_memory(files_to_compress) @@ -534,9 +663,7 @@ def _get_files(application_form, answers): with tempfile.NamedTemporaryFile() as temp: temp.write(zipped_files.getvalue()) return send_file( - temp.name, - as_attachment=True, - attachment_filename=f"response_{response.id}.zip" + temp.name, + as_attachment=True, + attachment_filename=f"response_{response.id}.zip", ) - - diff --git a/api/app/responses/mixins.py b/api/app/responses/mixins.py index 6da7eca5f..5ccd6465b 100644 --- a/api/app/responses/mixins.py +++ b/api/app/responses/mixins.py @@ -1,27 +1,34 @@ from flask_restful import reqparse + class ResponseMixin(object): get_req_parser = reqparse.RequestParser() - get_req_parser.add_argument('event_id', type=int, required=True, help = 'Invalid event_id requested. Event_id\'s should be of type int.') + get_req_parser.add_argument( + "event_id", + type=int, + required=True, + help="Invalid event_id requested. Event_id's should be of type int.", + ) post_req_parser = reqparse.RequestParser() - post_req_parser.add_argument('is_submitted', type=bool, required=True) - post_req_parser.add_argument('application_form_id', type=int, required=True) - post_req_parser.add_argument('answers', type=list, required=True, location='json') - post_req_parser.add_argument('language', type=str, required=True) + post_req_parser.add_argument("is_submitted", type=bool, required=True) + post_req_parser.add_argument("application_form_id", type=int, required=True) + post_req_parser.add_argument("answers", type=list, required=True, location="json") + post_req_parser.add_argument("language", type=str, required=True) put_req_parser = reqparse.RequestParser() - put_req_parser.add_argument('id', type=int, required=True) - put_req_parser.add_argument('is_submitted', type=bool, required=True) - put_req_parser.add_argument('application_form_id', type=int, required=True) - put_req_parser.add_argument('answers', type=list, required=True, location='json') - put_req_parser.add_argument('language', type=str, required=True) + put_req_parser.add_argument("id", type=int, required=True) + put_req_parser.add_argument("is_submitted", type=bool, required=True) + put_req_parser.add_argument("application_form_id", type=int, required=True) + put_req_parser.add_argument("answers", type=list, required=True, location="json") + put_req_parser.add_argument("language", type=str, required=True) del_req_parser = reqparse.RequestParser() - del_req_parser.add_argument('id', type=int, required=True) + del_req_parser.add_argument("id", type=int, required=True) + class ResponseTagMixin(object): req_parser = reqparse.RequestParser() - req_parser.add_argument('event_id', type=int, required=True) - req_parser.add_argument('tag_id', type=int, required=True) - req_parser.add_argument('response_id', type=int, required=True) \ No newline at end of file + req_parser.add_argument("event_id", type=int, required=True) + req_parser.add_argument("tag_id", type=int, required=True) + req_parser.add_argument("response_id", type=int, required=True) diff --git a/api/app/responses/models.py b/api/app/responses/models.py index 2d9a4b7a7..008a1586e 100644 --- a/api/app/responses/models.py +++ b/api/app/responses/models.py @@ -3,7 +3,7 @@ from sqlalchemy import select from sqlalchemy.orm import column_property -from app import db, LOGGER +from app import LOGGER, db from app.applicationModel.models import Question from app.tags.models import Tag from app.users.models import AppUser @@ -13,7 +13,9 @@ class Response(db.Model): __tablename__ = "response" id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey("application_form.id"), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) user_id = db.Column(db.Integer(), db.ForeignKey("app_user.id"), nullable=False) is_submitted = db.Column(db.Boolean(), nullable=False) submitted_timestamp = db.Column(db.DateTime(), nullable=True) @@ -22,12 +24,17 @@ class Response(db.Model): started_timestamp = db.Column(db.DateTime(), nullable=True) language = db.Column(db.String(2), nullable=False) - application_form = db.relationship('ApplicationForm', foreign_keys=[application_form_id]) - user: AppUser = db.relationship('AppUser', foreign_keys=[user_id]) - answers = db.relationship('Answer', order_by='Answer.order', primaryjoin="and_(Response.id==Answer.response_id, " - "Answer.is_active==True)") - response_tags = db.relationship('ResponseTag') - reviewers = db.relationship('ResponseReviewer') + application_form = db.relationship( + "ApplicationForm", foreign_keys=[application_form_id] + ) + user: AppUser = db.relationship("AppUser", foreign_keys=[user_id]) + answers = db.relationship( + "Answer", + order_by="Answer.order", + primaryjoin="and_(Response.id==Answer.response_id, " "Answer.is_active==True)", + ) + response_tags = db.relationship("ResponseTag") + reviewers = db.relationship("ResponseReviewer") def __init__(self, application_form_id, user_id, language): self.application_form_id = application_form_id @@ -62,9 +69,13 @@ class Answer(db.Model): is_active = db.Column(db.Boolean(), nullable=False) created_on = db.Column(db.DateTime, nullable=False) - response = db.relationship('Response', foreign_keys=[response_id]) - question = db.relationship('Question', foreign_keys=[question_id]) - order = column_property(select([Question.order]).where(Question.id == question_id).correlate_except(Question)) + response = db.relationship("Response", foreign_keys=[response_id]) + question = db.relationship("Question", foreign_keys=[question_id]) + order = column_property( + select([Question.order]) + .where(Question.id == question_id) + .correlate_except(Question) + ) def __init__(self, response_id, question_id, value): self.response_id = response_id @@ -80,23 +91,36 @@ def deactivate(self): def value_display(self): question_translation = self.question.get_translation(self.response.language) if question_translation is None: - LOGGER.error('Missing {} translation for question {}'.format(self.response.language, self.question.id)) - question_translation = self.question.get_translation('en') - if self.question.type == 'multi-choice' and question_translation.options is not None: - option = [option for option in question_translation.options if option['value'] == self.value] + LOGGER.error( + "Missing {} translation for question {}".format( + self.response.language, self.question.id + ) + ) + question_translation = self.question.get_translation("en") + if ( + self.question.type == "multi-choice" + and question_translation.options is not None + ): + option = [ + option + for option in question_translation.options + if option["value"] == self.value + ] if option: - return option[0]['label'] + return option[0]["label"] return self.value class ResponseReviewer(db.Model): id = db.Column(db.Integer(), primary_key=True) - response_id = db.Column(db.Integer(), db.ForeignKey('response.id'), nullable=False) - reviewer_user_id = db.Column(db.Integer(), db.ForeignKey('app_user.id'), nullable=False) + response_id = db.Column(db.Integer(), db.ForeignKey("response.id"), nullable=False) + reviewer_user_id = db.Column( + db.Integer(), db.ForeignKey("app_user.id"), nullable=False + ) active = db.Column(db.Boolean(), nullable=False) - response = db.relationship('Response', foreign_keys=[response_id]) - user = db.relationship('AppUser', foreign_keys=[reviewer_user_id]) + response = db.relationship("Response", foreign_keys=[response_id]) + user = db.relationship("AppUser", foreign_keys=[reviewer_user_id]) def __init__(self, response_id, reviewer_user_id): self.response_id = response_id @@ -109,11 +133,11 @@ def deactivate(self): class ResponseTag(db.Model): id = db.Column(db.Integer(), primary_key=True) - response_id = db.Column(db.Integer(), db.ForeignKey('response.id'), nullable=False) - tag_id = db.Column(db.Integer(), db.ForeignKey('tag.id'), nullable=False) + response_id = db.Column(db.Integer(), db.ForeignKey("response.id"), nullable=False) + tag_id = db.Column(db.Integer(), db.ForeignKey("tag.id"), nullable=False) - response = db.relationship('Response', foreign_keys=[response_id]) - tag = db.relationship('Tag', foreign_keys=[tag_id]) + response = db.relationship("Response", foreign_keys=[response_id]) + tag = db.relationship("Tag", foreign_keys=[tag_id]) def __init__(self, response_id, tag_id): self.response_id = response_id diff --git a/api/app/responses/repository.py b/api/app/responses/repository.py index 48ea1caf0..0ab777f7c 100644 --- a/api/app/responses/repository.py +++ b/api/app/responses/repository.py @@ -1,15 +1,15 @@ +import itertools from typing import List +from sqlalchemy import Date, cast, func + from app import db -from app.responses.models import Response, Answer, ResponseTag from app.applicationModel.models import ApplicationForm, Question, Section +from app.responses.models import Answer, Response, ResponseTag from app.users.models import AppUser -from sqlalchemy import func, cast, Date -import itertools - -class ResponseRepository(): +class ResponseRepository: @staticmethod def get_by_id(response_id): return db.session.query(Response).get(response_id) @@ -31,101 +31,137 @@ def merge_answer(answer): @staticmethod def get_by_id_and_user_id(response_id, user_id): - return db.session.query(Response) \ - .filter_by(id=response_id, user_id=user_id) \ + return ( + db.session.query(Response) + .filter_by(id=response_id, user_id=user_id) .first() + ) @staticmethod def get_by_user_id_for_event(user_id, event_id): - return db.session.query(Response) \ - .filter_by(user_id=user_id) \ - .join(ApplicationForm, Response.application_form_id == ApplicationForm.id) \ - .filter_by(event_id=event_id) \ + return ( + db.session.query(Response) + .filter_by(user_id=user_id) + .join(ApplicationForm, Response.application_form_id == ApplicationForm.id) + .filter_by(event_id=event_id) .first() + ) @staticmethod def get_submitted_by_user_id_for_event(user_id, event_id): - return db.session.query(Response) \ - .filter_by(user_id=user_id, is_submitted=True) \ - .join(ApplicationForm, Response.application_form_id == ApplicationForm.id) \ - .filter_by(event_id=event_id) \ + return ( + db.session.query(Response) + .filter_by(user_id=user_id, is_submitted=True) + .join(ApplicationForm, Response.application_form_id == ApplicationForm.id) + .filter_by(event_id=event_id) .first() + ) @staticmethod def get_answers_by_response_id(response_id): - return db.session.query(Answer) \ - .filter_by(response_id=response_id, is_active=True) \ + return ( + db.session.query(Answer) + .filter_by(response_id=response_id, is_active=True) .all() + ) @staticmethod def get_answers_by_question_id(question_id): - return db.session.query(Answer) \ - .filter_by(question_id=question_id, is_active=True) \ + return ( + db.session.query(Answer) + .filter_by(question_id=question_id, is_active=True) .all() + ) @staticmethod def get_all_for_user_application(user_id, application_form_id): - return (db.session.query(Response) - .filter(Response.application_form_id == application_form_id, Response.user_id == user_id) - .all()) + return ( + db.session.query(Response) + .filter( + Response.application_form_id == application_form_id, + Response.user_id == user_id, + ) + .all() + ) @staticmethod def get_answer_by_question_id_and_response_id(question_id, response_id): - return db.session.query(Answer) \ - .filter_by(question_id=question_id, response_id=response_id, is_active=True) \ + return ( + db.session.query(Answer) + .filter_by(question_id=question_id, response_id=response_id, is_active=True) .first() + ) @staticmethod def get_question_answers_by_section_key_and_response_id(section_key, response_id): - return db.session.query(Question, Answer) \ - .join(Question, Question.id == Answer.question_id) \ - .join(Section, Question.section_id == Section.id) \ - .filter(Answer.response_id == response_id, - Answer.is_active == True, - Section.key == section_key) \ + return ( + db.session.query(Question, Answer) + .join(Question, Question.id == Answer.question_id) + .join(Section, Question.section_id == Section.id) + .filter( + Answer.response_id == response_id, + Answer.is_active == True, + Section.key == section_key, + ) .all() + ) @staticmethod def get_answer_by_question_key_and_response_id(question_key, response_id): - return db.session.query(Answer) \ - .join(Question, Question.id == Answer.question_id) \ - .filter(Question.key == question_key, - Answer.response_id == response_id, - Answer.is_active == True) \ + return ( + db.session.query(Answer) + .join(Question, Question.id == Answer.question_id) + .filter( + Question.key == question_key, + Answer.response_id == response_id, + Answer.is_active == True, + ) .first() + ) @staticmethod def get_total_count_by_event(event_id): - return (db.session.query(Response) - .join(ApplicationForm, Response.application_form_id == ApplicationForm.id) - .filter(ApplicationForm.event_id == event_id) - .count()) + return ( + db.session.query(Response) + .join(ApplicationForm, Response.application_form_id == ApplicationForm.id) + .filter(ApplicationForm.event_id == event_id) + .count() + ) @staticmethod def get_submitted_count_by_event(event_id): - return (db.session.query(Response) - .filter(Response.is_submitted == True) - .join(ApplicationForm, Response.application_form_id == ApplicationForm.id) - .filter(ApplicationForm.event_id == event_id) - .count()) + return ( + db.session.query(Response) + .filter(Response.is_submitted == True) + .join(ApplicationForm, Response.application_form_id == ApplicationForm.id) + .filter(ApplicationForm.event_id == event_id) + .count() + ) @staticmethod def get_withdrawn_count_by_event(event_id): - return (db.session.query(Response) - .filter(Response.is_withdrawn == True) - .join(ApplicationForm, Response.application_form_id == ApplicationForm.id) - .filter(ApplicationForm.event_id == event_id) - .count()) + return ( + db.session.query(Response) + .filter(Response.is_withdrawn == True) + .join(ApplicationForm, Response.application_form_id == ApplicationForm.id) + .filter(ApplicationForm.event_id == event_id) + .count() + ) @staticmethod def get_submitted_timeseries_by_event(event_id): - return (db.session.query(cast(Response.submitted_timestamp, Date), func.count(Response.submitted_timestamp)) - .filter(Response.is_submitted == True) - .join(ApplicationForm, Response.application_form_id == ApplicationForm.id) - .filter(ApplicationForm.event_id == event_id) - .group_by(cast(Response.submitted_timestamp, Date)) - .order_by(cast(Response.submitted_timestamp, Date)) - .all()) + return ( + db.session.query( + cast(Response.submitted_timestamp, Date), + func.count(Response.submitted_timestamp), + ) + .filter(Response.is_submitted == True) + .join(ApplicationForm, Response.application_form_id == ApplicationForm.id) + .filter(ApplicationForm.event_id == event_id) + .group_by(cast(Response.submitted_timestamp, Date)) + .order_by(cast(Response.submitted_timestamp, Date)) + .all() + ) @staticmethod def get_all_for_event(event_id, submitted_only=True) -> List[Response]: @@ -133,10 +169,13 @@ def get_all_for_event(event_id, submitted_only=True) -> List[Response]: if submitted_only: query = query.filter_by(is_submitted=True) - return (query - .join(ApplicationForm, Response.application_form_id == ApplicationForm.id) - .filter_by(event_id=event_id) - .all()) + return ( + query.join( + ApplicationForm, Response.application_form_id == ApplicationForm.id + ) + .filter_by(event_id=event_id) + .all() + ) @staticmethod def tag_response(response_id, tag_id): @@ -147,16 +186,20 @@ def tag_response(response_id, tag_id): @staticmethod def remove_tag_from_response(response_id, tag_id): - (db.session.query(ResponseTag) - .filter_by(response_id=response_id, tag_id=tag_id) - .delete()) + ( + db.session.query(ResponseTag) + .filter_by(response_id=response_id, tag_id=tag_id) + .delete() + ) db.session.commit() @staticmethod def filter_ids_to_event(response_ids, event_id): - ids = (db.session.query(Response.id) - .filter(Response.id.in_(response_ids)) - .join(ApplicationForm, Response.application_form_id == ApplicationForm.id) - .filter_by(event_id=event_id) - .all()) + ids = ( + db.session.query(Response.id) + .filter(Response.id.in_(response_ids)) + .join(ApplicationForm, Response.application_form_id == ApplicationForm.id) + .filter_by(event_id=event_id) + .all() + ) return itertools.chain.from_iterable(ids) diff --git a/api/app/responses/tests.py b/api/app/responses/tests.py index 94a9d8176..95ebe2000 100644 --- a/api/app/responses/tests.py +++ b/api/app/responses/tests.py @@ -1,166 +1,223 @@ -from io import BytesIO +import collections import json -from os import write -import zipfile import tempfile +import zipfile from datetime import date, datetime -import collections +from io import BytesIO +from os import write import dateutil.parser from flask import g from app import app, db -from app.files.api import FileUploadAPI as file_upload from app.applicationModel.models import ApplicationForm, Question, Section from app.email_template.models import EmailTemplate from app.events.models import Event +from app.files.api import FileUploadAPI as file_upload from app.organisation.models import Organisation from app.responses.models import Answer, Response from app.responses.repository import ResponseRepository as response_repository from app.users.models import AppUser, Country, UserCategory -from app.utils.testing import ApiTestCase from app.utils.strings import build_response_html_answers +from app.utils.testing import ApiTestCase class ResponseApiTest(ApiTestCase): user_data_dict = { - 'email': 'something@email.com', - 'firstname': 'Some', - 'lastname': 'Thing', - 'user_title': 'Mr', - 'password': '123456', - 'policy_agreed': True, - 'language': 'en' + "email": "something@email.com", + "firstname": "Some", + "lastname": "Thing", + "user_title": "Mr", + "password": "123456", + "policy_agreed": True, + "language": "en", } def _seed_data(self): """Create dummy data for testing""" - organisation = self.add_organisation(name='Deep Learning Indaba') + organisation = self.add_organisation(name="Deep Learning Indaba") test_country = self.add_country() test_category = self.add_category() email_templates = [ - EmailTemplate('withdrawal', None, 'Withdrawal', '', 'en'), - EmailTemplate('confirmation-response', None, 'Confirmation', '{question_answer_summary}', 'en') + EmailTemplate("withdrawal", None, "Withdrawal", "", "en"), + EmailTemplate( + "confirmation-response", + None, + "Confirmation", + "{question_answer_summary}", + "en", + ), ] db.session.add_all(email_templates) db.session.commit() - self.add_email_template('verify-email') + self.add_email_template("verify-email") - response = self.app.post('/api/v1/user', data=self.user_data_dict) + response = self.app.post("/api/v1/user", data=self.user_data_dict) self.user_data = json.loads(response.data) other_user_data = self.user_data_dict.copy() - other_user_data['email'] = 'other@user.com' - response = self.app.post('/api/v1/user', data=other_user_data) + other_user_data["email"] = "other@user.com" + response = self.app.post("/api/v1/user", data=other_user_data) self.other_user_data = json.loads(response.data) self.add_n_users(10) - self.event = self.add_event({'en': 'Event Without Nomination'}, key='indaba-2025') + self.event = self.add_event( + {"en": "Event Without Nomination"}, key="indaba-2025" + ) self.form = self.create_application_form(self.event.id, True, False) self.section = self.add_section(self.form.id) - self.section_translation = self.add_section_translation(self.section.id, 'en') + self.section_translation = self.add_section_translation(self.section.id, "en") self.question = self.add_question(self.form.id, self.section.id, order=1) - self.question_translation = self.add_question_translation(self.question.id, 'en', 'Question 1') + self.question_translation = self.add_question_translation( + self.question.id, "en", "Question 1" + ) self.question2 = self.add_question(self.form.id, self.section.id, order=2) - self.question_translation2 = self.add_question_translation(self.question2.id, 'en', 'Question 2') - self.response = self.add_response(self.form.id, self.other_user_data['id'], False, False) - self.answer1 = self.add_answer(self.response.id, self.question.id, 'My Answer') + self.question_translation2 = self.add_question_translation( + self.question2.id, "en", "Question 2" + ) + self.response = self.add_response( + self.form.id, self.other_user_data["id"], False, False + ) + self.answer1 = self.add_answer(self.response.id, self.question.id, "My Answer") - self.event_with_nomination = self.add_event({'en': 'Event With Nomination'}, key='eeml-2025') - self.form_with_nomination = self.create_application_form(self.event_with_nomination.id, True, True) + self.event_with_nomination = self.add_event( + {"en": "Event With Nomination"}, key="eeml-2025" + ) + self.form_with_nomination = self.create_application_form( + self.event_with_nomination.id, True, True + ) self.section_with_nomination = self.add_section(self.event_with_nomination.id) - self.section_translation_with_nomination = self.add_section_translation(self.section_with_nomination.id, 'en') - self.question1_with_nomination = self.add_question(self.form_with_nomination.id, self.section_with_nomination.id, order=1) - self.question_translation1_with_nomination = self.add_question_translation(self.question1_with_nomination.id, 'en', 'Question 1 with nomination') - self.question2_with_nomination = self.add_question(self.form_with_nomination.id, self.section_with_nomination.id, order=2) - self.question_translation2_with_nomination = self.add_question_translation(self.question2_with_nomination.id, 'en', 'Question 1 with nomination') - self.response1_with_nomination = self.add_response(self.form_with_nomination.id, self.other_user_data['id'], True, False) - self.answer2_with_nomination = self.add_answer(self.response1_with_nomination.id, self.question2_with_nomination.id, 'Second nomination answer') - self.answer1_with_nomination = self.add_answer(self.response1_with_nomination.id, self.question1_with_nomination.id, 'Another answer') - self.response2_with_nomination = self.add_response(self.form_with_nomination.id, self.other_user_data['id'], False, True) - self.answer4_with_nomination = self.add_answer(self.response2_with_nomination.id, self.question2_with_nomination.id, 'Second answer for second nomination') - self.answer3_with_nomination = self.add_answer(self.response2_with_nomination.id, self.question1_with_nomination.id, 'First answer for second nomination') + self.section_translation_with_nomination = self.add_section_translation( + self.section_with_nomination.id, "en" + ) + self.question1_with_nomination = self.add_question( + self.form_with_nomination.id, self.section_with_nomination.id, order=1 + ) + self.question_translation1_with_nomination = self.add_question_translation( + self.question1_with_nomination.id, "en", "Question 1 with nomination" + ) + self.question2_with_nomination = self.add_question( + self.form_with_nomination.id, self.section_with_nomination.id, order=2 + ) + self.question_translation2_with_nomination = self.add_question_translation( + self.question2_with_nomination.id, "en", "Question 1 with nomination" + ) + self.response1_with_nomination = self.add_response( + self.form_with_nomination.id, self.other_user_data["id"], True, False + ) + self.answer2_with_nomination = self.add_answer( + self.response1_with_nomination.id, + self.question2_with_nomination.id, + "Second nomination answer", + ) + self.answer1_with_nomination = self.add_answer( + self.response1_with_nomination.id, + self.question1_with_nomination.id, + "Another answer", + ) + self.response2_with_nomination = self.add_response( + self.form_with_nomination.id, self.other_user_data["id"], False, True + ) + self.answer4_with_nomination = self.add_answer( + self.response2_with_nomination.id, + self.question2_with_nomination.id, + "Second answer for second nomination", + ) + self.answer3_with_nomination = self.add_answer( + self.response2_with_nomination.id, + self.question1_with_nomination.id, + "First answer for second nomination", + ) db.session.flush() def test_get_response_without_nomination(self): """Test a GET flow when the applications do not allow nominations""" - + self._seed_data() - response = self.app.get('/api/v1/response', - headers={ - 'Authorization': self.other_user_data['token']}, - query_string={'event_id': self.event.id}) + response = self.app.get( + "/api/v1/response", + headers={"Authorization": self.other_user_data["token"]}, + query_string={"event_id": self.event.id}, + ) data = json.loads(response.data) self.assertEqual(len(data), 1) data = data[0] - self.assertEqual(data['application_form_id'], self.form.id) - self.assertEqual(data['user_id'], self.other_user_data['id']) - self.assertFalse(data['is_submitted']) - self.assertIsNone(data['submitted_timestamp']) - self.assertFalse(data['is_withdrawn']) - self.assertIsNone(data['withdrawn_timestamp']) - self.assertIsNotNone(data['started_timestamp']) - self.assertTrue(data['answers']) - self.assertEqual(data['language'], 'en') - - self.assertEqual(len(data['answers']), 1) - answer = data['answers'][0] - self.assertEqual(answer['id'], self.answer1.id) - self.assertEqual(answer['value'], self.answer1.value) - self.assertEqual(answer['question_id'], 1) + self.assertEqual(data["application_form_id"], self.form.id) + self.assertEqual(data["user_id"], self.other_user_data["id"]) + self.assertFalse(data["is_submitted"]) + self.assertIsNone(data["submitted_timestamp"]) + self.assertFalse(data["is_withdrawn"]) + self.assertIsNone(data["withdrawn_timestamp"]) + self.assertIsNotNone(data["started_timestamp"]) + self.assertTrue(data["answers"]) + self.assertEqual(data["language"], "en") + + self.assertEqual(len(data["answers"]), 1) + answer = data["answers"][0] + self.assertEqual(answer["id"], self.answer1.id) + self.assertEqual(answer["value"], self.answer1.value) + self.assertEqual(answer["question_id"], 1) def test_get_response_with_nomination(self): """Test a GET flow when the applications do allow nominations""" self._seed_data() response = self.app.get( - '/api/v1/response', - headers={'Authorization': self.other_user_data['token']}, - query_string={'event_id': self.event_with_nomination.id} + "/api/v1/response", + headers={"Authorization": self.other_user_data["token"]}, + query_string={"event_id": self.event_with_nomination.id}, ) data = json.loads(response.data) self.assertEqual(len(data), 2) - self.assertEqual(data[0]['application_form_id'], self.form_with_nomination.id) - self.assertEqual(data[0]['user_id'], self.other_user_data['id']) - self.assertIsNotNone(data[0]['submitted_timestamp']) - self.assertTrue(data[0]['is_submitted']) - self.assertIsNone(data[0]['withdrawn_timestamp']) - self.assertFalse(data[0]['is_withdrawn']) - self.assertIsNotNone(data[0]['started_timestamp']) - self.assertEqual(len(data[0]['answers']), 2) - answer1 = data[0]['answers'][0] - self.assertEqual(answer1['id'], self.answer1_with_nomination.id) - self.assertEqual(answer1['value'], self.answer1_with_nomination.value) - self.assertEqual(answer1['question_id'], self.answer1_with_nomination.question_id) - answer2 = data[0]['answers'][1] - self.assertEqual(answer2['id'], self.answer2_with_nomination.id) - self.assertEqual(answer2['value'], self.answer2_with_nomination.value) - self.assertEqual(answer2['question_id'], self.answer2_with_nomination.question_id) - - self.assertEqual(data[1]['application_form_id'], self.form_with_nomination.id) - self.assertEqual(data[1]['user_id'], self.other_user_data['id']) - self.assertIsNone(data[1]['submitted_timestamp']) - self.assertFalse(data[1]['is_submitted']) - self.assertIsNotNone(data[1]['withdrawn_timestamp']) - self.assertTrue(data[1]['is_withdrawn']) - self.assertIsNotNone(data[1]['started_timestamp']) - self.assertEqual(len(data[1]['answers']), 2) - answer3 = data[1]['answers'][0] - self.assertEqual(answer3['id'], self.answer3_with_nomination.id) - self.assertEqual(answer3['value'], self.answer3_with_nomination.value) - self.assertEqual(answer3['question_id'], self.answer3_with_nomination.question_id) - answer4 = data[1]['answers'][1] - self.assertEqual(answer4['id'], self.answer4_with_nomination.id) - self.assertEqual(answer4['value'], self.answer4_with_nomination.value) - self.assertEqual(answer4['question_id'], self.answer4_with_nomination.question_id) + self.assertEqual(data[0]["application_form_id"], self.form_with_nomination.id) + self.assertEqual(data[0]["user_id"], self.other_user_data["id"]) + self.assertIsNotNone(data[0]["submitted_timestamp"]) + self.assertTrue(data[0]["is_submitted"]) + self.assertIsNone(data[0]["withdrawn_timestamp"]) + self.assertFalse(data[0]["is_withdrawn"]) + self.assertIsNotNone(data[0]["started_timestamp"]) + self.assertEqual(len(data[0]["answers"]), 2) + answer1 = data[0]["answers"][0] + self.assertEqual(answer1["id"], self.answer1_with_nomination.id) + self.assertEqual(answer1["value"], self.answer1_with_nomination.value) + self.assertEqual( + answer1["question_id"], self.answer1_with_nomination.question_id + ) + answer2 = data[0]["answers"][1] + self.assertEqual(answer2["id"], self.answer2_with_nomination.id) + self.assertEqual(answer2["value"], self.answer2_with_nomination.value) + self.assertEqual( + answer2["question_id"], self.answer2_with_nomination.question_id + ) + + self.assertEqual(data[1]["application_form_id"], self.form_with_nomination.id) + self.assertEqual(data[1]["user_id"], self.other_user_data["id"]) + self.assertIsNone(data[1]["submitted_timestamp"]) + self.assertFalse(data[1]["is_submitted"]) + self.assertIsNotNone(data[1]["withdrawn_timestamp"]) + self.assertTrue(data[1]["is_withdrawn"]) + self.assertIsNotNone(data[1]["started_timestamp"]) + self.assertEqual(len(data[1]["answers"]), 2) + answer3 = data[1]["answers"][0] + self.assertEqual(answer3["id"], self.answer3_with_nomination.id) + self.assertEqual(answer3["value"], self.answer3_with_nomination.value) + self.assertEqual( + answer3["question_id"], self.answer3_with_nomination.question_id + ) + answer4 = data[1]["answers"][1] + self.assertEqual(answer4["id"], self.answer4_with_nomination.id) + self.assertEqual(answer4["value"], self.answer4_with_nomination.value) + self.assertEqual( + answer4["question_id"], self.answer4_with_nomination.question_id + ) def test_get_event(self): """Test that we get an error if we try to get a response for an event that doesn't exist.""" @@ -168,36 +225,36 @@ def test_get_event(self): self._seed_data() response = self.app.get( - '/api/v1/response', - headers={'Authorization': self.other_user_data['token']}, - query_string={'event_id': self.event.id + 100} + "/api/v1/response", + headers={"Authorization": self.other_user_data["token"]}, + query_string={"event_id": self.event.id + 100}, ) self.assertEqual(response.status_code, 404) def test_get_missing_form(self): """Test that we get a 404 error if we try to get a response for an event with no application form.""" - + self._seed_data() - test_event2 = self.add_event({'en': 'Test Event 2'}, key='HOLLA') + test_event2 = self.add_event({"en": "Test Event 2"}, key="HOLLA") response = self.app.get( - '/api/v1/response', - headers={'Authorization': self.other_user_data['token']}, - query_string={'event_id': test_event2.id} + "/api/v1/response", + headers={"Authorization": self.other_user_data["token"]}, + query_string={"event_id": test_event2.id}, ) self.assertEqual(response.status_code, 404) def test_get_missing_response(self): """Test that we get an empty list if there is no response for the event and user combination.""" - + self._seed_data() response = self.app.get( - '/api/v1/response', - headers={'Authorization': self.user_data['token']}, - query_string={'event_id': self.event.id} + "/api/v1/response", + headers={"Authorization": self.user_data["token"]}, + query_string={"event_id": self.event.id}, ) data = json.loads(response.data) @@ -209,108 +266,101 @@ def test_post_without_nomination(self): self._seed_data() response_data = { - 'application_form_id': self.form.id, - 'is_submitted': True, - 'answers': [ + "application_form_id": self.form.id, + "is_submitted": True, + "answers": [ + {"question_id": self.question.id, "value": "Answer 1"}, { - 'question_id': self.question.id, - 'value': 'Answer 1' + "question_id": self.question2.id, + "value": "Hello world, this is the 2nd answer.", }, - { - 'question_id': self.question2.id, - 'value': 'Hello world, this is the 2nd answer.' - } - ] + ], } response = self.app.post( - '/api/v1/response', + "/api/v1/response", data=json.dumps(response_data), - content_type='application/json', - headers={'Authorization': self.user_data['token']}, - query_string={'language': 'en'}) - + content_type="application/json", + headers={"Authorization": self.user_data["token"]}, + query_string={"language": "en"}, + ) + self.assertEqual(response.status_code, 201) data = json.loads(response.data) - self.assertEqual(data['application_form_id'], self.form.id) - self.assertEqual(data['user_id'], self.user_data['id']) - self.assertIsNotNone(data['submitted_timestamp']) - self.assertTrue(data['is_submitted']) - self.assertFalse(data['is_withdrawn']) - self.assertIsNone(data['withdrawn_timestamp']) - self.assertEqual(len(data['answers']), 2) - self.assertEqual(data['language'], 'en') + self.assertEqual(data["application_form_id"], self.form.id) + self.assertEqual(data["user_id"], self.user_data["id"]) + self.assertIsNotNone(data["submitted_timestamp"]) + self.assertTrue(data["is_submitted"]) + self.assertFalse(data["is_withdrawn"]) + self.assertIsNone(data["withdrawn_timestamp"]) + self.assertEqual(len(data["answers"]), 2) + self.assertEqual(data["language"], "en") - answer = data['answers'][0] - self.assertEqual(answer['value'], 'Answer 1') - self.assertEqual(answer['question_id'], self.question.id) + answer = data["answers"][0] + self.assertEqual(answer["value"], "Answer 1") + self.assertEqual(answer["question_id"], self.question.id) - answer = data['answers'][1] - self.assertEqual( - answer['value'], 'Hello world, this is the 2nd answer.') - self.assertEqual(answer['question_id'], self.question2.id) + answer = data["answers"][1] + self.assertEqual(answer["value"], "Hello world, this is the 2nd answer.") + self.assertEqual(answer["question_id"], self.question2.id) def test_second_response_rejected_without_nomination(self): self._seed_data() response_data = { - 'application_form_id': self.form.id, - 'is_submitted': True, - 'answers': [ - { - 'question_id': self.question.id, - 'value': 'Answer 1' - } - ] + "application_form_id": self.form.id, + "is_submitted": True, + "answers": [{"question_id": self.question.id, "value": "Answer 1"}], } response = self.app.post( - '/api/v1/response', + "/api/v1/response", data=json.dumps(response_data), - content_type='application/json', - headers={'Authorization': self.user_data['token']}, - query_string={'language': 'en'}) - + content_type="application/json", + headers={"Authorization": self.user_data["token"]}, + query_string={"language": "en"}, + ) + self.assertEqual(response.status_code, 201) response = self.app.post( - '/api/v1/response', + "/api/v1/response", data=json.dumps(response_data), - content_type='application/json', - headers={'Authorization': self.user_data['token']}) - + content_type="application/json", + headers={"Authorization": self.user_data["token"]}, + ) + self.assertEqual(response.status_code, 400) def test_second_response_accepted_with_nomination(self): self._seed_data() response_data = { - 'application_form_id': self.form_with_nomination.id, - 'is_submitted': True, - 'answers': [ - { - 'question_id': self.question1_with_nomination.id, - 'value': 'Answer 1' - } - ] + "application_form_id": self.form_with_nomination.id, + "is_submitted": True, + "answers": [ + {"question_id": self.question1_with_nomination.id, "value": "Answer 1"} + ], } response = self.app.post( - '/api/v1/response', + "/api/v1/response", data=json.dumps(response_data), - content_type='application/json', - headers={'Authorization': self.user_data['token']}, - query_string={'language': 'en'}) - + content_type="application/json", + headers={"Authorization": self.user_data["token"]}, + query_string={"language": "en"}, + ) + self.assertEqual(response.status_code, 201) response = self.app.post( - '/api/v1/response', + "/api/v1/response", data=json.dumps(response_data), - content_type='application/json', - headers={'Authorization': self.user_data['token']}, - query_string={'language': 'en'}) - + content_type="application/json", + headers={"Authorization": self.user_data["token"]}, + query_string={"language": "en"}, + ) + self.assertEqual(response.status_code, 201) def test_update(self): @@ -318,220 +368,265 @@ def test_update(self): self._seed_data() update_data = { - 'id': self.response.id, - 'application_form_id': self.form.id, - 'is_submitted': True, # Set submitted - 'answers': [ + "id": self.response.id, + "application_form_id": self.form.id, + "is_submitted": True, # Set submitted + "answers": [ { - 'question_id': self.question.id, - 'value': 'Answer 1 UPDATED' # Update an existing answer + "question_id": self.question.id, + "value": "Answer 1 UPDATED", # Update an existing answer }, { - 'question_id': self.question2.id, # Add a new answer - 'value': 'This is the 2nd answer.' - } - ] + "question_id": self.question2.id, # Add a new answer + "value": "This is the 2nd answer.", + }, + ], } response = self.app.put( - '/api/v1/response', + "/api/v1/response", data=json.dumps(update_data), - content_type='application/json', - headers={'Authorization': self.other_user_data['token']}, - query_string={'language': 'fr'}) # Updating the language from English to French + content_type="application/json", + headers={"Authorization": self.other_user_data["token"]}, + query_string={"language": "fr"}, + ) # Updating the language from English to French self.assertEqual(response.status_code, 200) # Retrieve the response and check that the fields are as expected response = self.app.get( - 'api/v1/response', - headers={'Authorization': self.other_user_data['token']}, - query_string={'event_id': self.event.id}) + "api/v1/response", + headers={"Authorization": self.other_user_data["token"]}, + query_string={"event_id": self.event.id}, + ) data = json.loads(response.data)[0] - self.assertEqual(data['application_form_id'], self.form.id) - self.assertEqual(data['user_id'], self.other_user_data['id']) + self.assertEqual(data["application_form_id"], self.form.id) + self.assertEqual(data["user_id"], self.other_user_data["id"]) - parsed_submitted = dateutil.parser.parse( - data['submitted_timestamp']) + parsed_submitted = dateutil.parser.parse(data["submitted_timestamp"]) self.assertLess( - abs((datetime.now() - parsed_submitted).total_seconds()), 5*60) + abs((datetime.now() - parsed_submitted).total_seconds()), 5 * 60 + ) - self.assertTrue(data['is_submitted']) - self.assertFalse(data['is_withdrawn']) - self.assertEqual(data['language'], 'fr') - self.assertTrue(data['answers']) + self.assertTrue(data["is_submitted"]) + self.assertFalse(data["is_withdrawn"]) + self.assertEqual(data["language"], "fr") + self.assertTrue(data["answers"]) - answer = data['answers'][0] - self.assertEqual(answer['value'], 'Answer 1 UPDATED') - self.assertEqual(answer['question_id'], self.question.id) + answer = data["answers"][0] + self.assertEqual(answer["value"], "Answer 1 UPDATED") + self.assertEqual(answer["question_id"], self.question.id) - answer = data['answers'][1] - self.assertEqual(answer['value'], 'This is the 2nd answer.') - self.assertEqual(answer['question_id'], self.question2.id) + answer = data["answers"][1] + self.assertEqual(answer["value"], "This is the 2nd answer.") + self.assertEqual(answer["question_id"], self.question2.id) def test_update_missing(self): """Test that 404 is returned if we try to update a response that doesn't exist.""" - + self._seed_data() update_data = { - 'id': self.response.id + 100, - 'application_form_id': self.form.id, - 'is_submitted': True, # Set submitted - 'answers': [] + "id": self.response.id + 100, + "application_form_id": self.form.id, + "is_submitted": True, # Set submitted + "answers": [], } response = self.app.put( - '/api/v1/response', + "/api/v1/response", data=json.dumps(update_data), - content_type='application/json', - headers={'Authorization': self.other_user_data['token']}, - query_string={'language': 'en'}) + content_type="application/json", + headers={"Authorization": self.other_user_data["token"]}, + query_string={"language": "en"}, + ) self.assertEqual(response.status_code, 404) def test_update_permission(self): """Test that a user can't update another user's response.""" - + self._seed_data() update_data = { - 'id': self.response.id, - 'application_form_id': self.form.id, - 'is_submitted': True, # Set submitted - 'answers': [] + "id": self.response.id, + "application_form_id": self.form.id, + "is_submitted": True, # Set submitted + "answers": [], } response = self.app.put( - '/api/v1/response', + "/api/v1/response", data=json.dumps(update_data), - content_type='application/json', - headers={'Authorization': self.user_data['token']}, - query_string={'language': 'en'}) + content_type="application/json", + headers={"Authorization": self.user_data["token"]}, + query_string={"language": "en"}, + ) self.assertEqual(response.status_code, 401) def test_update_conflict(self): """Test that we can't update the application form id for a response.""" - + self._seed_data() update_data = { - 'id': self.response.id, - 'application_form_id': self.form.id + 100, - 'is_submitted': True, # Set submitted - 'answers': [] + "id": self.response.id, + "application_form_id": self.form.id + 100, + "is_submitted": True, # Set submitted + "answers": [], } response = self.app.put( - '/api/v1/response', + "/api/v1/response", data=json.dumps(update_data), - content_type='application/json', - headers={'Authorization': self.other_user_data['token']}, - query_string={'language': 'en'}) + content_type="application/json", + headers={"Authorization": self.other_user_data["token"]}, + query_string={"language": "en"}, + ) self.assertEqual(response.status_code, 409) def test_delete(self): """Test a typical DELETE flow.""" - + self._seed_data() response = self.app.delete( - '/api/v1/response', - headers={'Authorization': self.other_user_data['token']}, - query_string={'id': self.response.id}) + "/api/v1/response", + headers={"Authorization": self.other_user_data["token"]}, + query_string={"id": self.response.id}, + ) self.assertEqual(response.status_code, 204) # We should still be able to get the response, but it is marked unsubmitted and withdrawn response = self.app.get( - '/api/v1/response', - headers={'Authorization': self.other_user_data['token']}, - query_string={'event_id': self.event.id}) + "/api/v1/response", + headers={"Authorization": self.other_user_data["token"]}, + query_string={"event_id": self.event.id}, + ) data = json.loads(response.data)[0] - self.assertFalse(data['is_submitted']) - self.assertTrue(data['is_withdrawn']) + self.assertFalse(data["is_submitted"]) + self.assertTrue(data["is_withdrawn"]) def test_delete_missing(self): """Test that we can't delete a response that doesn't exist.""" - + self._seed_data() response = self.app.delete( - '/api/v1/response', - headers={'Authorization': self.other_user_data['token']}, - query_string={'id': self.response.id + 1000}) + "/api/v1/response", + headers={"Authorization": self.other_user_data["token"]}, + query_string={"id": self.response.id + 1000}, + ) self.assertEqual(response.status_code, 404) # Not found def test_delete_permission(self): """Test that we can't delete another user's response.""" - + self._seed_data() # test_response belongs to "other_user", check that "user" can't delete it response = self.app.delete( - '/api/v1/response', - headers={'Authorization': self.user_data['token']}, - query_string={'id': self.response.id}) + "/api/v1/response", + headers={"Authorization": self.user_data["token"]}, + query_string={"id": self.response.id}, + ) self.assertEqual(response.status_code, 401) # Unauthorized class ResponseListAPITest(ApiTestCase): def _seed_static_data(self): - self.event1 = self.add_event(key='event1') - self.event2 = self.add_event(key='event2') + self.event1 = self.add_event(key="event1") + self.event2 = self.add_event(key="event2") self.users = self.add_n_users(4) - UserName = collections.namedtuple('UserName', ['user_title', 'firstname', 'lastname']) - self.user_names = [UserName(u.user_title, u.firstname, u.lastname) for u in self.users] - - self.event1admin = self.add_user('event1admin@mail.com') - self.event2admin = self.add_user('event2admin@mail.com') - - self.event1.add_event_role('admin', self.event1admin.id) - self.event2.add_event_role('admin', self.event2admin.id) + UserName = collections.namedtuple( + "UserName", ["user_title", "firstname", "lastname"] + ) + self.user_names = [ + UserName(u.user_title, u.firstname, u.lastname) for u in self.users + ] + + self.event1admin = self.add_user("event1admin@mail.com") + self.event2admin = self.add_user("event2admin@mail.com") + + self.event1.add_event_role("admin", self.event1admin.id) + self.event2.add_event_role("admin", self.event2admin.id) db.session.commit() self.form1 = self.create_application_form(self.event1.id) self.form1_section1 = self.add_section(self.form1.id) - self.form1_question1 = self.add_question(self.form1.id, self.form1_section1.id) # ID 1 - self.add_question_translation(self.form1_question1.id, 'en', 'English Headline 1') - self.add_question_translation(self.form1_question1.id, 'fr', 'French Headline 1') - self.form1_question2 = self.add_question(self.form1.id, self.form1_section1.id) # ID 2 - self.add_question_translation(self.form1_question2.id, 'en', 'English Headline 2') - self.add_question_translation(self.form1_question2.id, 'fr', 'French Headline 2') - self.form1_question3 = self.add_question(self.form1.id, self.form1_section1.id) # ID 3 - self.add_question_translation(self.form1_question3.id, 'en', 'English Headline 3') - self.add_question_translation(self.form1_question3.id, 'fr', 'French Headline 3') + self.form1_question1 = self.add_question( + self.form1.id, self.form1_section1.id + ) # ID 1 + self.add_question_translation( + self.form1_question1.id, "en", "English Headline 1" + ) + self.add_question_translation( + self.form1_question1.id, "fr", "French Headline 1" + ) + self.form1_question2 = self.add_question( + self.form1.id, self.form1_section1.id + ) # ID 2 + self.add_question_translation( + self.form1_question2.id, "en", "English Headline 2" + ) + self.add_question_translation( + self.form1_question2.id, "fr", "French Headline 2" + ) + self.form1_question3 = self.add_question( + self.form1.id, self.form1_section1.id + ) # ID 3 + self.add_question_translation( + self.form1_question3.id, "en", "English Headline 3" + ) + self.add_question_translation( + self.form1_question3.id, "fr", "French Headline 3" + ) self.form2 = self.create_application_form(self.event2.id) self.form2_section1 = self.add_section(self.form2.id) - self.form2_question1 = self.add_question(self.form2.id, self.form2_section1.id) # ID 4 - self.add_question_translation(self.form2_question1.id, 'en', 'English Headline 3') - self.add_question_translation(self.form2_question1.id, 'fr', 'French Headline 3') + self.form2_question1 = self.add_question( + self.form2.id, self.form2_section1.id + ) # ID 4 + self.add_question_translation( + self.form2_question1.id, "en", "English Headline 3" + ) + self.add_question_translation( + self.form2_question1.id, "fr", "French Headline 3" + ) # Create responses, 3 for event 1 (2 submitted, 1 unsubmitted), 1 for event 2 - self.response1 = self.add_response(self.form1.id, self.users[0].id, is_submitted=True) + self.response1 = self.add_response( + self.form1.id, self.users[0].id, is_submitted=True + ) self.response1_started = self.response1.started_timestamp self.response1_submitted = self.response1.submitted_timestamp - self.add_answer(self.response1.id, self.form1_question1.id, 'First answer') - self.add_answer(self.response1.id, self.form1_question2.id, 'Second answer') - self.add_answer(self.response1.id, self.form1_question3.id, 'Third answer') + self.add_answer(self.response1.id, self.form1_question1.id, "First answer") + self.add_answer(self.response1.id, self.form1_question2.id, "Second answer") + self.add_answer(self.response1.id, self.form1_question3.id, "Third answer") - self.response2 = self.add_response(self.form1.id, self.users[1].id, is_submitted=True, language='fr') + self.response2 = self.add_response( + self.form1.id, self.users[1].id, is_submitted=True, language="fr" + ) self.response2_started = self.response2.started_timestamp self.response2_submitted = self.response2.submitted_timestamp - self.add_answer(self.response2.id, self.form1_question1.id, 'Forth answer') - self.add_answer(self.response2.id, self.form1_question2.id, 'Fifth answer') - self.add_answer(self.response2.id, self.form1_question3.id, 'Sixth answer') + self.add_answer(self.response2.id, self.form1_question1.id, "Forth answer") + self.add_answer(self.response2.id, self.form1_question2.id, "Fifth answer") + self.add_answer(self.response2.id, self.form1_question3.id, "Sixth answer") - self.response3 = self.add_response(self.form1.id, self.users[2].id, is_submitted=False) + self.response3 = self.add_response( + self.form1.id, self.users[2].id, is_submitted=False + ) self.response3_started = self.response3.started_timestamp - self.add_answer(self.response3.id, self.form1_question1.id, 'Seventh answer') - self.add_answer(self.response3.id, self.form1_question2.id, 'Eigth answer') - self.add_answer(self.response3.id, self.form1_question3.id, 'Ninth answer') + self.add_answer(self.response3.id, self.form1_question1.id, "Seventh answer") + self.add_answer(self.response3.id, self.form1_question2.id, "Eigth answer") + self.add_answer(self.response3.id, self.form1_question3.id, "Ninth answer") - self.response4 = self.add_response(self.form2.id, self.users[3].id, is_submitted=False) - self.add_answer(self.response4.id, self.form2_question1.id, 'Tenth answer') + self.response4 = self.add_response( + self.form2.id, self.users[3].id, is_submitted=False + ) + self.add_answer(self.response4.id, self.form2_question1.id, "Tenth answer") self.review_form1 = self.add_review_form(self.form1.id) self.review_config1 = self.add_review_config(self.review_form1.id) @@ -540,11 +635,13 @@ def _seed_static_data(self): self.add_response_reviewer(self.response1.id, self.users[2].id) self.add_response_reviewer(self.response2.id, self.users[0].id) - review_response = self.add_review_response(self.users[1].id, self.response1.id, self.review_form1.id) + review_response = self.add_review_response( + self.users[1].id, self.response1.id, self.review_form1.id + ) self.review_response_id = review_response.id tag1 = self.add_tag() - tag2 = self.add_tag(names={'en': 'Tag 2 en', 'fr': 'Tag 2 fr'}) + tag2 = self.add_tag(names={"en": "Tag 2 en", "fr": "Tag 2 fr"}) self.tag_response(self.response1.id, tag1.id) self.tag_response(self.response3.id, tag1.id) self.tag_response(self.response3.id, tag2.id) @@ -554,134 +651,155 @@ def test_no_questions_submitted(self): self._seed_static_data() params = { - 'event_id': self.event1.id, - 'language': 'en', - 'include_unsubmitted': False + "event_id": self.event1.id, + "language": "en", + "include_unsubmitted": False, } response = self.app.get( - '/api/v1/responses', - headers=self.get_auth_header_for('event1admin@mail.com'), - data=params) + "/api/v1/responses", + headers=self.get_auth_header_for("event1admin@mail.com"), + data=params, + ) data = json.loads(response.data) self.assertEqual(len(data), 2) response1 = data[0] - self.assertEqual(response1['response_id'], 1) - self.assertEqual(response1['user_title'], self.user_names[0].user_title) - self.assertEqual(response1['firstname'], self.user_names[0].firstname) - self.assertEqual(response1['lastname'], self.user_names[0].lastname) - self.assertEqual(response1['start_date'], self.response1_started.isoformat()) - self.assertEqual(response1['is_submitted'], True) - self.assertEqual(response1['is_withdrawn'], False) - self.assertEqual(response1['submitted_date'], self.response1_submitted.isoformat()) - self.assertEqual(response1['language'], 'en') - self.assertEqual(len(response1['answers']), 0) - self.assertEqual(len(response1['reviewers']), 2) - self.assertEqual(response1['reviewers'][0]['reviewer_id'], 2) - self.assertEqual(response1['reviewers'][0]['reviewer_name'], '{} {} {}'.format(*self.user_names[1])) - self.assertEqual(response1['reviewers'][0]['review_response_id'], self.review_response_id) - self.assertEqual(response1['reviewers'][1]['reviewer_id'], 3) - self.assertEqual(response1['reviewers'][1]['reviewer_name'], '{} {} {}'.format(*self.user_names[2])) - self.assertEqual(response1['reviewers'][1]['review_response_id'], None) - self.assertEqual(len(response1['tags']), 1) - self.assertEqual(response1['tags'][0]['id'], 1) - self.assertEqual(response1['tags'][0]['name'], 'Tag 1 en') + self.assertEqual(response1["response_id"], 1) + self.assertEqual(response1["user_title"], self.user_names[0].user_title) + self.assertEqual(response1["firstname"], self.user_names[0].firstname) + self.assertEqual(response1["lastname"], self.user_names[0].lastname) + self.assertEqual(response1["start_date"], self.response1_started.isoformat()) + self.assertEqual(response1["is_submitted"], True) + self.assertEqual(response1["is_withdrawn"], False) + self.assertEqual( + response1["submitted_date"], self.response1_submitted.isoformat() + ) + self.assertEqual(response1["language"], "en") + self.assertEqual(len(response1["answers"]), 0) + self.assertEqual(len(response1["reviewers"]), 2) + self.assertEqual(response1["reviewers"][0]["reviewer_id"], 2) + self.assertEqual( + response1["reviewers"][0]["reviewer_name"], + "{} {} {}".format(*self.user_names[1]), + ) + self.assertEqual( + response1["reviewers"][0]["review_response_id"], self.review_response_id + ) + self.assertEqual(response1["reviewers"][1]["reviewer_id"], 3) + self.assertEqual( + response1["reviewers"][1]["reviewer_name"], + "{} {} {}".format(*self.user_names[2]), + ) + self.assertEqual(response1["reviewers"][1]["review_response_id"], None) + self.assertEqual(len(response1["tags"]), 1) + self.assertEqual(response1["tags"][0]["id"], 1) + self.assertEqual(response1["tags"][0]["name"], "Tag 1 en") response2 = data[1] - self.assertEqual(response2['response_id'], 2) - self.assertEqual(response2['user_title'], self.user_names[1].user_title) - self.assertEqual(response2['firstname'], self.user_names[1].firstname) - self.assertEqual(response2['lastname'], self.user_names[1].lastname) - self.assertEqual(response2['start_date'], self.response2_started.isoformat()) - self.assertEqual(response2['is_submitted'], True) - self.assertEqual(response2['is_withdrawn'], False) - self.assertEqual(response2['submitted_date'], self.response2_submitted.isoformat()) - self.assertEqual(response2['language'], 'fr') - self.assertEqual(len(response2['answers']), 0) - self.assertEqual(len(response2['reviewers']), 2) - self.assertEqual(response2['reviewers'][0]['reviewer_id'], 1) - self.assertEqual(response2['reviewers'][0]['reviewer_name'], '{} {} {}'.format(*self.user_names[0])) - self.assertEqual(response2['reviewers'][0]['review_response_id'], None) - self.assertEqual(response2['reviewers'][1], None) - self.assertEqual(len(response2['tags']), 0) + self.assertEqual(response2["response_id"], 2) + self.assertEqual(response2["user_title"], self.user_names[1].user_title) + self.assertEqual(response2["firstname"], self.user_names[1].firstname) + self.assertEqual(response2["lastname"], self.user_names[1].lastname) + self.assertEqual(response2["start_date"], self.response2_started.isoformat()) + self.assertEqual(response2["is_submitted"], True) + self.assertEqual(response2["is_withdrawn"], False) + self.assertEqual( + response2["submitted_date"], self.response2_submitted.isoformat() + ) + self.assertEqual(response2["language"], "fr") + self.assertEqual(len(response2["answers"]), 0) + self.assertEqual(len(response2["reviewers"]), 2) + self.assertEqual(response2["reviewers"][0]["reviewer_id"], 1) + self.assertEqual( + response2["reviewers"][0]["reviewer_name"], + "{} {} {}".format(*self.user_names[0]), + ) + self.assertEqual(response2["reviewers"][0]["review_response_id"], None) + self.assertEqual(response2["reviewers"][1], None) + self.assertEqual(len(response2["tags"]), 0) def test_questions_unsubmitted(self): """Test response list with questions requested and unsubmitted included.""" self._seed_static_data() params = { - 'event_id': self.event1.id, - 'language': 'en', - 'include_unsubmitted': True, - 'question_ids[]': [1, 3] + "event_id": self.event1.id, + "language": "en", + "include_unsubmitted": True, + "question_ids[]": [1, 3], } response = self.app.get( - '/api/v1/responses', - headers=self.get_auth_header_for('event1admin@mail.com'), - data=params) + "/api/v1/responses", + headers=self.get_auth_header_for("event1admin@mail.com"), + data=params, + ) data = json.loads(response.data) self.assertEqual(len(data), 3) - self.assertEqual(len(data[0]['answers']), 2) + self.assertEqual(len(data[0]["answers"]), 2) - self.assertEqual(data[0]['answers'][0]['question_id'], 1) - self.assertEqual(data[0]['answers'][0]['value'], 'First answer') - self.assertEqual(data[0]['answers'][0]['type'], 'short-text') - self.assertEqual(data[0]['answers'][0]['options'], None) - self.assertEqual(data[0]['answers'][0]['headline'], 'English Headline 1') - self.assertEqual(data[0]['answers'][1]['question_id'], 3) - self.assertEqual(data[0]['answers'][1]['value'], 'Third answer') - self.assertEqual(data[0]['answers'][1]['type'], 'short-text') - self.assertEqual(data[0]['answers'][1]['options'], None) - self.assertEqual(data[0]['answers'][1]['headline'], 'English Headline 3') + self.assertEqual(data[0]["answers"][0]["question_id"], 1) + self.assertEqual(data[0]["answers"][0]["value"], "First answer") + self.assertEqual(data[0]["answers"][0]["type"], "short-text") + self.assertEqual(data[0]["answers"][0]["options"], None) + self.assertEqual(data[0]["answers"][0]["headline"], "English Headline 1") + self.assertEqual(data[0]["answers"][1]["question_id"], 3) + self.assertEqual(data[0]["answers"][1]["value"], "Third answer") + self.assertEqual(data[0]["answers"][1]["type"], "short-text") + self.assertEqual(data[0]["answers"][1]["options"], None) + self.assertEqual(data[0]["answers"][1]["headline"], "English Headline 3") response3 = data[2] - self.assertEqual(response3['response_id'], 3) - self.assertEqual(response3['user_title'], self.user_names[2].user_title) - self.assertEqual(response3['firstname'], self.user_names[2].firstname) - self.assertEqual(response3['lastname'], self.user_names[2].lastname) - self.assertEqual(response3['start_date'], self.response3_started.isoformat()) - self.assertEqual(response3['is_submitted'], False) - self.assertEqual(response3['is_withdrawn'], False) - self.assertEqual(response3['submitted_date'], None) - self.assertEqual(response3['language'], 'en') - self.assertEqual(len(response3['answers']), 2) - self.assertEqual(len(response3['reviewers']), 2) - self.assertEqual(response3['reviewers'][0], None) - self.assertEqual(response3['reviewers'][1], None) - self.assertEqual(len(response3['tags']), 2) - self.assertEqual(response3['tags'][0]['id'], 1) - self.assertEqual(response3['tags'][0]['name'], 'Tag 1 en') - self.assertEqual(response3['tags'][1]['id'], 2) - self.assertEqual(response3['tags'][1]['name'], 'Tag 2 en') + self.assertEqual(response3["response_id"], 3) + self.assertEqual(response3["user_title"], self.user_names[2].user_title) + self.assertEqual(response3["firstname"], self.user_names[2].firstname) + self.assertEqual(response3["lastname"], self.user_names[2].lastname) + self.assertEqual(response3["start_date"], self.response3_started.isoformat()) + self.assertEqual(response3["is_submitted"], False) + self.assertEqual(response3["is_withdrawn"], False) + self.assertEqual(response3["submitted_date"], None) + self.assertEqual(response3["language"], "en") + self.assertEqual(len(response3["answers"]), 2) + self.assertEqual(len(response3["reviewers"]), 2) + self.assertEqual(response3["reviewers"][0], None) + self.assertEqual(response3["reviewers"][1], None) + self.assertEqual(len(response3["tags"]), 2) + self.assertEqual(response3["tags"][0]["id"], 1) + self.assertEqual(response3["tags"][0]["name"], "Tag 1 en") + self.assertEqual(response3["tags"][1]["id"], 2) + self.assertEqual(response3["tags"][1]["name"], "Tag 2 en") class ResponseTagAPITest(ApiTestCase): def _seed_static_data(self): - self.event1 = self.add_event(key='event1') - self.event1admin = self.add_user('event1admin@mail.com') - self.event1reviewer1 = self.add_user('event1reviewer1@mail.com') - self.event1reviewer2 = self.add_user('event1reviewer2@mail.com') - self.user1 = self.add_user('user1@mail.com') - self.user2 = self.add_user('user2@mail.com') + self.event1 = self.add_event(key="event1") + self.event1admin = self.add_user("event1admin@mail.com") + self.event1reviewer1 = self.add_user("event1reviewer1@mail.com") + self.event1reviewer2 = self.add_user("event1reviewer2@mail.com") + self.user1 = self.add_user("user1@mail.com") + self.user2 = self.add_user("user2@mail.com") - self.event1.add_event_role('admin', self.event1admin.id) - self.event1.add_event_role('reviewer', self.event1reviewer1.id) - self.event1.add_event_role('reviewer', self.event1reviewer2.id) + self.event1.add_event_role("admin", self.event1admin.id) + self.event1.add_event_role("reviewer", self.event1reviewer1.id) + self.event1.add_event_role("reviewer", self.event1reviewer2.id) application_form = self.create_application_form(self.event1.id) - self.response1 = self.add_response(application_form.id, self.user1.id, is_submitted=True) + self.response1 = self.add_response( + application_form.id, self.user1.id, is_submitted=True + ) self.add_response_reviewer(self.response1.id, self.event1reviewer1.id) - self.response2 = self.add_response(application_form.id, self.user2.id, is_submitted=True) + self.response2 = self.add_response( + application_form.id, self.user2.id, is_submitted=True + ) self.add_response_reviewer(self.response2.id, self.event1reviewer2.id) self.tag1 = self.add_tag() - self.tag2 = self.add_tag(names={'en': 'Tag 2 en', 'fr': 'Tag 2 fr'}) + self.tag2 = self.add_tag(names={"en": "Tag 2 en", "fr": "Tag 2 fr"}) self.tag_response(self.response2.id, self.tag2.id) @@ -690,145 +808,154 @@ def test_tag_admin(self): self._seed_static_data() params = { - 'event_id': self.event1.id, - 'tag_id': self.tag1.id, - 'response_id': self.response1.id + "event_id": self.event1.id, + "tag_id": self.tag1.id, + "response_id": self.response1.id, } response = self.app.post( - '/api/v1/responsetag', - headers=self.get_auth_header_for('event1admin@mail.com'), - json=params) + "/api/v1/responsetag", + headers=self.get_auth_header_for("event1admin@mail.com"), + json=params, + ) self.assertEqual(response.status_code, 201) params = { - 'event_id': self.event1.id, - 'language': 'en', - 'include_unsubmitted': False + "event_id": self.event1.id, + "language": "en", + "include_unsubmitted": False, } response = self.app.get( - '/api/v1/responses', - headers=self.get_auth_header_for('event1admin@mail.com'), - json=params) + "/api/v1/responses", + headers=self.get_auth_header_for("event1admin@mail.com"), + json=params, + ) data = json.loads(response.data) - self.assertEqual(len(data[0]['tags']), 1) - self.assertEqual(data[0]['tags'][0]['id'], 1) + self.assertEqual(len(data[0]["tags"]), 1) + self.assertEqual(data[0]["tags"][0]["id"], 1) def test_tag_reviewer(self): """Test that a reviewer can add a tag.""" self._seed_static_data() params = { - 'event_id': self.event1.id, - 'tag_id': self.tag1.id, - 'response_id': self.response1.id + "event_id": self.event1.id, + "tag_id": self.tag1.id, + "response_id": self.response1.id, } response = self.app.post( - '/api/v1/responsetag', - headers=self.get_auth_header_for('event1reviewer1@mail.com'), - json=params) + "/api/v1/responsetag", + headers=self.get_auth_header_for("event1reviewer1@mail.com"), + json=params, + ) self.assertEqual(response.status_code, 201) params = { - 'event_id': self.event1.id, - 'language': 'en', - 'include_unsubmitted': False + "event_id": self.event1.id, + "language": "en", + "include_unsubmitted": False, } response = self.app.get( - '/api/v1/responses', - headers=self.get_auth_header_for('event1admin@mail.com'), - json=params) + "/api/v1/responses", + headers=self.get_auth_header_for("event1admin@mail.com"), + json=params, + ) data = json.loads(response.data) - self.assertEqual(len(data[0]['tags']), 1) - self.assertEqual(data[0]['tags'][0]['id'], 1) + self.assertEqual(len(data[0]["tags"]), 1) + self.assertEqual(data[0]["tags"][0]["id"], 1) def test_remove_tag_admin(self): """Test that an event admin can remove a tag from a response.""" self._seed_static_data() params = { - 'event_id': self.event1.id, - 'tag_id': self.tag2.id, - 'response_id': self.response2.id + "event_id": self.event1.id, + "tag_id": self.tag2.id, + "response_id": self.response2.id, } response = self.app.delete( - '/api/v1/responsetag', - headers=self.get_auth_header_for('event1admin@mail.com'), - json=params) + "/api/v1/responsetag", + headers=self.get_auth_header_for("event1admin@mail.com"), + json=params, + ) self.assertEqual(response.status_code, 200) params = { - 'event_id': self.event1.id, - 'language': 'en', - 'include_unsubmitted': False + "event_id": self.event1.id, + "language": "en", + "include_unsubmitted": False, } response = self.app.get( - '/api/v1/responses', - headers=self.get_auth_header_for('event1admin@mail.com'), - json=params) + "/api/v1/responses", + headers=self.get_auth_header_for("event1admin@mail.com"), + json=params, + ) data = json.loads(response.data) - self.assertEqual(len(data[1]['tags']), 0) + self.assertEqual(len(data[1]["tags"]), 0) def test_remove_tag_reviewer(self): """Test that a reviewer can remove a tag from a response.""" self._seed_static_data() params = { - 'event_id': self.event1.id, - 'tag_id': self.tag2.id, - 'response_id': self.response2.id + "event_id": self.event1.id, + "tag_id": self.tag2.id, + "response_id": self.response2.id, } response = self.app.delete( - '/api/v1/responsetag', - headers=self.get_auth_header_for('event1reviewer2@mail.com'), - json=params) + "/api/v1/responsetag", + headers=self.get_auth_header_for("event1reviewer2@mail.com"), + json=params, + ) self.assertEqual(response.status_code, 200) params = { - 'event_id': self.event1.id, - 'language': 'en', - 'include_unsubmitted': False + "event_id": self.event1.id, + "language": "en", + "include_unsubmitted": False, } response = self.app.get( - '/api/v1/responses', - headers=self.get_auth_header_for('event1admin@mail.com'), - json=params) + "/api/v1/responses", + headers=self.get_auth_header_for("event1admin@mail.com"), + json=params, + ) data = json.loads(response.data) - self.assertEqual(len(data[1]['tags']), 0) + self.assertEqual(len(data[1]["tags"]), 0) def test_tag_different_reviewer(self): """Test that a reviewer of a different response can't add a tag.""" self._seed_static_data() params = { - 'event_id': self.event1.id, - 'tag_id': self.tag1.id, - 'response_id': self.response1.id + "event_id": self.event1.id, + "tag_id": self.tag1.id, + "response_id": self.response1.id, } response = self.app.post( - '/api/v1/responsetag', - headers=self.get_auth_header_for('event1reviewer2@mail.com'), - json=params) + "/api/v1/responsetag", + headers=self.get_auth_header_for("event1reviewer2@mail.com"), + json=params, + ) self.assertEqual(response.status_code, 403) @@ -837,15 +964,16 @@ def test_remove_tag_different_reviewer(self): self._seed_static_data() params = { - 'event_id': self.event1.id, - 'tag_id': self.tag1.id, - 'response_id': self.response1.id + "event_id": self.event1.id, + "tag_id": self.tag1.id, + "response_id": self.response1.id, } response = self.app.delete( - '/api/v1/responsetag', - headers=self.get_auth_header_for('event1reviewer2@mail.com'), - json=params) + "/api/v1/responsetag", + headers=self.get_auth_header_for("event1reviewer2@mail.com"), + json=params, + ) self.assertEqual(response.status_code, 403) @@ -854,15 +982,16 @@ def test_tag_non_admin_non_reviewer(self): self._seed_static_data() params = { - 'event_id': self.event1.id, - 'tag_id': self.tag1.id, - 'response_id': self.response1.id + "event_id": self.event1.id, + "tag_id": self.tag1.id, + "response_id": self.response1.id, } response = self.app.post( - '/api/v1/responsetag', - headers=self.get_auth_header_for('user2@mail.com'), - json=params) + "/api/v1/responsetag", + headers=self.get_auth_header_for("user2@mail.com"), + json=params, + ) self.assertEqual(response.status_code, 403) @@ -871,47 +1000,59 @@ def test_remove_tag_non_admin_non_reviewer(self): self._seed_static_data() params = { - 'event_id': self.event1.id, - 'tag_id': self.tag1.id, - 'response_id': self.response1.id + "event_id": self.event1.id, + "tag_id": self.tag1.id, + "response_id": self.response1.id, } response = self.app.delete( - '/api/v1/responsetag', - headers=self.get_auth_header_for('user2@mail.com'), - json=params) + "/api/v1/responsetag", + headers=self.get_auth_header_for("user2@mail.com"), + json=params, + ) self.assertEqual(response.status_code, 403) + class ResponseDetailAPITest(ApiTestCase): def _seed_static_data(self): - self.event1 = self.add_event(key='event1') - self.event1admin = self.add_user('event1admin@mail.com') - self.user1 = self.add_user('user1@mail.com', user_title='Ms', firstname='Danai', lastname='Gurira') + self.event1 = self.add_event(key="event1") + self.event1admin = self.add_user("event1admin@mail.com") + self.user1 = self.add_user( + "user1@mail.com", user_title="Ms", firstname="Danai", lastname="Gurira" + ) - self.reviewer1 = self.add_user('reviewer1@mail.com', user_title='Mx', firstname='Skittles', lastname='Cat') - self.reviewer2 = self.add_user('reviewer2@mail.com', user_title='Mr', firstname='Finn', lastname='Dog') - self.reviewer3 = self.add_user('reviewer3@mail.com', user_title='Mx', firstname='Bailey', lastname='Dog') + self.reviewer1 = self.add_user( + "reviewer1@mail.com", user_title="Mx", firstname="Skittles", lastname="Cat" + ) + self.reviewer2 = self.add_user( + "reviewer2@mail.com", user_title="Mr", firstname="Finn", lastname="Dog" + ) + self.reviewer3 = self.add_user( + "reviewer3@mail.com", user_title="Mx", firstname="Bailey", lastname="Dog" + ) - self.event1.add_event_role('admin', self.event1admin.id) - self.event1.add_event_role('reviewer', self.reviewer1.id) - self.event1.add_event_role('reviewer', self.reviewer2.id) + self.event1.add_event_role("admin", self.event1admin.id) + self.event1.add_event_role("reviewer", self.reviewer1.id) + self.event1.add_event_role("reviewer", self.reviewer2.id) application_form = self.create_application_form(self.event1.id) section = self.add_section(application_form.id) question1 = self.add_question(application_form.id, section.id) - self.add_question_translation(question1.id, 'en') + self.add_question_translation(question1.id, "en") question2 = self.add_question(application_form.id, section.id) - self.add_question_translation(question2.id, 'en') + self.add_question_translation(question2.id, "en") - self.response1 = self.add_response(application_form.id, self.user1.id, is_submitted=True) + self.response1 = self.add_response( + application_form.id, self.user1.id, is_submitted=True + ) self.response1_submitted = self.response1.submitted_timestamp self.response1_started = self.response1.started_timestamp - self.add_answer(self.response1.id, question1.id, 'Answer 1') - self.add_answer(self.response1.id, question2.id, 'Answer 2') + self.add_answer(self.response1.id, question1.id, "Answer 1") + self.add_answer(self.response1.id, question2.id, "Answer 2") tag1 = self.add_tag() - tag2 = self.add_tag(names={'en': 'Tag 2 en', 'fr': 'Tag 2 fr'}) + tag2 = self.add_tag(names={"en": "Tag 2 en", "fr": "Tag 2 fr"}) self.tag_response(self.response1.id, tag1.id) self.tag_response(self.response1.id, tag2.id) @@ -924,173 +1065,189 @@ def _seed_static_data(self): self.add_response_reviewer(self.response1.id, self.reviewer3.id) self.add_review_response(self.reviewer1.id, self.response1.id, review_form.id) - self.add_review_response(self.reviewer2.id, self.response1.id, review_form.id, is_submitted=True) + self.add_review_response( + self.reviewer2.id, self.response1.id, review_form.id, is_submitted=True + ) def test_response_detail(self): """Test typical get request.""" self._seed_static_data() params = { - 'event_id': self.event1.id, - 'response_id': self.response1.id, - 'language': 'en' + "event_id": self.event1.id, + "response_id": self.response1.id, + "language": "en", } response = self.app.get( - '/api/v1/responsedetail', - headers=self.get_auth_header_for('event1admin@mail.com'), - json=params) + "/api/v1/responsedetail", + headers=self.get_auth_header_for("event1admin@mail.com"), + json=params, + ) data = json.loads(response.data) self.assertEqual(response.status_code, 200) - self.assertEqual(data['id'], 1) - self.assertEqual(data['application_form_id'], 1) - self.assertEqual(data['user_id'], 2) - self.assertEqual(data['is_submitted'], True) - self.assertEqual(data['submitted_timestamp'], self.response1_submitted.isoformat()) - self.assertEqual(data['is_withdrawn'], False) - self.assertEqual(data['withdrawn_timestamp'], None) - self.assertEqual(data['started_timestamp'], self.response1_started.isoformat()) - self.assertEqual(len(data['answers']), 2) - self.assertEqual(data['answers'][0]['value'], 'Answer 1') - self.assertEqual(data['answers'][1]['value'], 'Answer 2') - self.assertEqual(data['language'], 'en') - self.assertEqual(data['user_title'], 'Ms') - self.assertEqual(data['firstname'], 'Danai') - self.assertEqual(data['lastname'], 'Gurira') - self.assertEqual(len(data['tags']), 2) - self.assertEqual(data['tags'][0]['name'], 'Tag 1 en') - self.assertEqual(data['tags'][1]['name'], 'Tag 2 en') - self.assertEqual(len(data['reviewers']), 3) - self.assertEqual(data['reviewers'][0]['user_title'], 'Mx') - self.assertEqual(data['reviewers'][0]['firstname'], 'Skittles') - self.assertEqual(data['reviewers'][0]['lastname'], 'Cat') - self.assertEqual(data['reviewers'][0]['status'], 'started') - self.assertEqual(data['reviewers'][1]['user_title'], 'Mr') - self.assertEqual(data['reviewers'][1]['firstname'], 'Finn') - self.assertEqual(data['reviewers'][1]['lastname'], 'Dog') - self.assertEqual(data['reviewers'][1]['status'], 'completed') - self.assertEqual(data['reviewers'][2]['user_title'], 'Mx') - self.assertEqual(data['reviewers'][2]['firstname'], 'Bailey') - self.assertEqual(data['reviewers'][2]['lastname'], 'Dog') - self.assertEqual(data['reviewers'][2]['status'], 'not_started') - + self.assertEqual(data["id"], 1) + self.assertEqual(data["application_form_id"], 1) + self.assertEqual(data["user_id"], 2) + self.assertEqual(data["is_submitted"], True) + self.assertEqual( + data["submitted_timestamp"], self.response1_submitted.isoformat() + ) + self.assertEqual(data["is_withdrawn"], False) + self.assertEqual(data["withdrawn_timestamp"], None) + self.assertEqual(data["started_timestamp"], self.response1_started.isoformat()) + self.assertEqual(len(data["answers"]), 2) + self.assertEqual(data["answers"][0]["value"], "Answer 1") + self.assertEqual(data["answers"][1]["value"], "Answer 2") + self.assertEqual(data["language"], "en") + self.assertEqual(data["user_title"], "Ms") + self.assertEqual(data["firstname"], "Danai") + self.assertEqual(data["lastname"], "Gurira") + self.assertEqual(len(data["tags"]), 2) + self.assertEqual(data["tags"][0]["name"], "Tag 1 en") + self.assertEqual(data["tags"][1]["name"], "Tag 2 en") + self.assertEqual(len(data["reviewers"]), 3) + self.assertEqual(data["reviewers"][0]["user_title"], "Mx") + self.assertEqual(data["reviewers"][0]["firstname"], "Skittles") + self.assertEqual(data["reviewers"][0]["lastname"], "Cat") + self.assertEqual(data["reviewers"][0]["status"], "started") + self.assertEqual(data["reviewers"][1]["user_title"], "Mr") + self.assertEqual(data["reviewers"][1]["firstname"], "Finn") + self.assertEqual(data["reviewers"][1]["lastname"], "Dog") + self.assertEqual(data["reviewers"][1]["status"], "completed") + self.assertEqual(data["reviewers"][2]["user_title"], "Mx") + self.assertEqual(data["reviewers"][2]["firstname"], "Bailey") + self.assertEqual(data["reviewers"][2]["lastname"], "Dog") + self.assertEqual(data["reviewers"][2]["status"], "not_started") def test_response_detail_admin_only(self): """Test that a non admin can't access reponse detail.""" self._seed_static_data() params = { - 'event_id': self.event1.id, - 'response_id': self.response1.id, - 'language': 'en' + "event_id": self.event1.id, + "response_id": self.response1.id, + "language": "en", } response = self.app.get( - '/api/v1/responsedetail', - headers=self.get_auth_header_for('user1@mail.com'), - json=params) + "/api/v1/responsedetail", + headers=self.get_auth_header_for("user1@mail.com"), + json=params, + ) self.assertEqual(response.status_code, 403) + class ResponseExportAPITest(ApiTestCase): def _data_seed_static(self): - - self.event1 = self.add_event(key='event1') - self.event1admin = self.add_user('event1admin@mail.com', is_admin=True) - self.user1 = self.add_user('user1@mail.com', user_title='Ms', firstname='Danai', lastname='Gurira') + + self.event1 = self.add_event(key="event1") + self.event1admin = self.add_user("event1admin@mail.com", is_admin=True) + self.user1 = self.add_user( + "user1@mail.com", user_title="Ms", firstname="Danai", lastname="Gurira" + ) application_form = self.create_application_form(self.event1.id) # Section 1, two questions section1 = self.add_section(application_form.id) - self.add_section_translation(section1.id, 'en', name='Section1') + self.add_section_translation(section1.id, "en", name="Section1") question1 = self.add_question(application_form.id, section1.id) - self.add_question_translation(question1.id, 'en', headline='Question 1, S1') + self.add_question_translation(question1.id, "en", headline="Question 1, S1") question2 = self.add_question(application_form.id, section1.id) - self.add_question_translation(question2.id, 'en', headline='Question 2, S1') + self.add_question_translation(question2.id, "en", headline="Question 2, S1") # Supplementary file included in application upload (e.g. CV) - Section 1 - question_supp1 = self.add_question(application_form.id, section1.id, question_type = 'file') - self.add_question_translation(question_supp1.id, 'en', headline='Upload CV') + question_supp1 = self.add_question( + application_form.id, section1.id, question_type="file" + ) + self.add_question_translation(question_supp1.id, "en", headline="Upload CV") question_supp1_id = question_supp1.id # Section 2, 3 questions section2 = self.add_section(application_form.id) - self.add_section_translation(section2.id, 'en', name='Section2') + self.add_section_translation(section2.id, "en", name="Section2") question2_1 = self.add_question(application_form.id, section2.id) - self.add_question_translation(question2_1.id, 'en', headline='Question 1, S2') + self.add_question_translation(question2_1.id, "en", headline="Question 1, S2") question2_2 = self.add_question(application_form.id, section2.id) - self.add_question_translation(question2_2.id, 'en', headline='Question 2, S2') + self.add_question_translation(question2_2.id, "en", headline="Question 2, S2") question2_3 = self.add_question(application_form.id, section2.id) - self.add_question_translation(question2_3.id, 'en', headline='Question 3, S2') + self.add_question_translation(question2_3.id, "en", headline="Question 3, S2") # Section 3, 1 question section3 = self.add_section(application_form.id) - self.add_section_translation(section3.id, 'en', name='Section3') + self.add_section_translation(section3.id, "en", name="Section3") question3_1 = self.add_question(application_form.id, section3.id) - self.add_question_translation(question3_1.id, 'en', headline='Queston 1, S3') + self.add_question_translation(question3_1.id, "en", headline="Queston 1, S3") # Add a question that won't have the file attached - question_supp2 = self.add_question(application_form.id, section1.id, question_type = 'file') - self.add_question_translation(question_supp2.id, 'en', headline='Upload references') + question_supp2 = self.add_question( + application_form.id, section1.id, question_type="file" + ) + self.add_question_translation( + question_supp2.id, "en", headline="Upload references" + ) # Create response - self.response1 = self.add_response(application_form.id, self.user1.id, is_submitted=True) + self.response1 = self.add_response( + application_form.id, self.user1.id, is_submitted=True + ) self.response1_submitted = self.response1.submitted_timestamp self.response1_started = self.response1.started_timestamp self.response1_id = self.response1.id - self.add_answer(self.response1.id, question1.id, 'Section 1 Answer 1') - self.add_answer(self.response1.id, question2.id, 'Section 1 Answer 2') + self.add_answer(self.response1.id, question1.id, "Section 1 Answer 1") + self.add_answer(self.response1.id, question2.id, "Section 1 Answer 2") - self.add_answer(self.response1.id, question2_1.id, 'Section 2 Answer 1') - self.add_answer(self.response1.id, question2_2.id, 'Section 2 Answer 2') - self.add_answer(self.response1.id, question2_3.id, 'Section 2 Answer 3') + self.add_answer(self.response1.id, question2_1.id, "Section 2 Answer 1") + self.add_answer(self.response1.id, question2_2.id, "Section 2 Answer 2") + self.add_answer(self.response1.id, question2_3.id, "Section 2 Answer 3") # Add file type answer - with tempfile.NamedTemporaryFile(mode='wb') as temp: + with tempfile.NamedTemporaryFile(mode="wb") as temp: - temp.write(b'This is my CV') + temp.write(b"This is my CV") temp.flush() - buf = BytesIO(open(temp.name, 'rb').read()) - + buf = BytesIO(open(temp.name, "rb").read()) + response_reference = self.app.post( - '/api/v1/file', - data={'file': (buf, 'test_file')}, - content_type='multipart/form-data', - headers=self.get_auth_header_for('user1@mail.com') + "/api/v1/file", + data={"file": (buf, "test_file")}, + content_type="multipart/form-data", + headers=self.get_auth_header_for("user1@mail.com"), ) - file_id = json.loads(response_reference.data)['file_id'] + file_id = json.loads(response_reference.data)["file_id"] self.add_answer( self.response1_id, question_supp1_id, - json.dumps({"filename": file_id, "rename": "supplementarypdfONE.pdf"}) + json.dumps({"filename": file_id, "rename": "supplementarypdfONE.pdf"}), ) - def test_zipped_file_uncorrupted(self): """ - Tests that the zipped files' CRCs are okay. + Tests that the zipped files' CRCs are okay. """ self._data_seed_static() - params = { - 'response_id': self.response1_id, - 'language': 'en' - } - + params = {"response_id": self.response1_id, "language": "en"} + response = self.app.get( - '/api/v1/response-export', - headers=self.get_auth_header_for('event1admin@mail.com'), - json=params) + "/api/v1/response-export", + headers=self.get_auth_header_for("event1admin@mail.com"), + json=params, + ) - self.assertEqual(response.mimetype, 'application/zip') - self.assertEqual(response.headers.get('Content-Disposition'), 'attachment; filename=response_1.zip') + self.assertEqual(response.mimetype, "application/zip") + self.assertEqual( + response.headers.get("Content-Disposition"), + "attachment; filename=response_1.zip", + ) - with tempfile.NamedTemporaryFile(mode='wb') as temp: + with tempfile.NamedTemporaryFile(mode="wb") as temp: temp.write(response.data) @@ -1100,55 +1257,56 @@ def test_zipped_file_uncorrupted(self): self.assertIsNone(zip.testzip()) def test_number_files_returned_zipped_folder(self): - """ - Tests that the correct number of files are returned in the zip folder - """ - - self._data_seed_static() - - params = { - 'response_id': self.response1_id, - 'language': 'en' - } - - response = self.app.get( - '/api/v1/response-export', - headers=self.get_auth_header_for('event1admin@mail.com'), - json=params - ) + """ + Tests that the correct number of files are returned in the zip folder + """ + + self._data_seed_static() + + params = {"response_id": self.response1_id, "language": "en"} + + response = self.app.get( + "/api/v1/response-export", + headers=self.get_auth_header_for("event1admin@mail.com"), + json=params, + ) + + self.assertEqual(response.mimetype, "application/zip") + self.assertEqual( + response.headers.get("Content-Disposition"), + "attachment; filename=response_1.zip", + ) - self.assertEqual(response.mimetype,'application/zip') - self.assertEqual(response.headers.get('Content-Disposition'),'attachment; filename=response_1.zip') + with tempfile.NamedTemporaryFile(mode="wb") as temp_zip: - with tempfile.NamedTemporaryFile(mode='wb') as temp_zip: + temp_zip.write(response.data) - temp_zip.write(response.data) + temp_zip.flush() - temp_zip.flush() + with zipfile.ZipFile(temp_zip.name) as zip: + self.assertIsNone(zip.testzip()) + self.assertEqual(len(zip.namelist()), 2) - with zipfile.ZipFile(temp_zip.name) as zip: - self.assertIsNone(zip.testzip()) - self.assertEqual(len(zip.namelist()), 2) - def test_filename_renamed_correctly_in_zip_folder(self): "Tests that the files are correctly renamed in the downloaded zip folder" self._data_seed_static() - params = { - 'response_id': self.response1_id, - 'language': 'en' - } - + params = {"response_id": self.response1_id, "language": "en"} + response = self.app.get( - '/api/v1/response-export', - headers=self.get_auth_header_for('event1admin@mail.com'), - json=params) + "/api/v1/response-export", + headers=self.get_auth_header_for("event1admin@mail.com"), + json=params, + ) - assert response.mimetype == 'application/zip' - assert response.headers.get('Content-Disposition') == 'attachment; filename=response_1.zip' + assert response.mimetype == "application/zip" + assert ( + response.headers.get("Content-Disposition") + == "attachment; filename=response_1.zip" + ) - with tempfile.NamedTemporaryFile(mode='wb') as temp_zip: + with tempfile.NamedTemporaryFile(mode="wb") as temp_zip: temp_zip.write(response.data) @@ -1156,5 +1314,6 @@ def test_filename_renamed_correctly_in_zip_folder(self): with zipfile.ZipFile(temp_zip.name) as zip: self.assertIsNone(zip.testzip()) - self.assertEqual(zip.namelist(), ['response.pdf', 'supplementarypdfONE.pdf']) - \ No newline at end of file + self.assertEqual( + zip.namelist(), ["response.pdf", "supplementarypdfONE.pdf"] + ) diff --git a/api/app/reviews/api.py b/api/app/reviews/api.py index 2a5917185..007f98ad4 100644 --- a/api/app/reviews/api.py +++ b/api/app/reviews/api.py @@ -1,197 +1,239 @@ -from flask import g -import flask_restful as restful -from flask_restful import reqparse, fields, marshal_with -from math import ceil import random -from sqlalchemy.sql import func, exists - -from typing import Any, Mapping from datetime import datetime +from math import ceil +from typing import Any, Mapping -from app import db, LOGGER +import flask_restful as restful +from flask import g +from flask_restful import fields, marshal_with, reqparse +from sqlalchemy.sql import exists, func + +from app import LOGGER, db from app.applicationModel.models import ApplicationForm from app.events.models import Event, EventRole from app.events.repository import EventRepository as event_repository -from app.responses.repository import ResponseRepository as response_repository +from app.references.repository import ReferenceRequestRepository as reference_repository from app.responses.models import Response, ResponseReviewer -from app.reviews.mixins import ReviewMixin, GetReviewResponseMixin, PostReviewResponseMixin, PostReviewAssignmentMixin, \ - GetReviewAssignmentMixin, GetReviewHistoryMixin, GetReviewSummaryMixin -from app.reviews.models import ReviewForm, ReviewResponse, ReviewScore, ReviewQuestion, ReviewSection, ReviewSectionTranslation, ReviewQuestionTranslation +from app.responses.repository import ResponseRepository as response_repository +from app.reviews.mixins import ( + GetReviewAssignmentMixin, + GetReviewHistoryMixin, + GetReviewResponseMixin, + GetReviewSummaryMixin, + PostReviewAssignmentMixin, + PostReviewResponseMixin, + ReviewMixin, +) +from app.reviews.models import ( + ReviewForm, + ReviewQuestion, + ReviewQuestionTranslation, + ReviewResponse, + ReviewScore, + ReviewSection, + ReviewSectionTranslation, +) +from app.reviews.repository import ( + ReviewConfigurationRepository as review_configuration_repository, +) from app.reviews.repository import ReviewRepository as review_repository -from app.reviews.repository import ReviewConfigurationRepository as review_configuration_repository -from app.references.repository import ReferenceRequestRepository as reference_repository - from app.users.models import AppUser, Country, UserCategory from app.users.repository import UserRepository as user_repository - -from app.events.repository import EventRepository as event_repository -from app.utils.auth import auth_required - -from app.utils.auth import auth_required, event_admin_required -from app.utils.errors import EVENT_NOT_FOUND, REVIEW_RESPONSE_NOT_FOUND, FORBIDDEN, USER_NOT_FOUND, RESPONSE_NOT_FOUND, \ - REVIEW_FORM_NOT_FOUND, REVIEW_ALREADY_COMPLETED, NO_ACTIVE_REVIEW_FORM, REVIEW_FORM_FOR_STAGE_NOT_FOUND - from app.utils import misc +from app.utils.auth import auth_required, event_admin_required from app.utils.emailer import email_user - -option_fields = { - 'value': fields.String, - 'label': fields.String -} +from app.utils.errors import ( + EVENT_NOT_FOUND, + FORBIDDEN, + NO_ACTIVE_REVIEW_FORM, + RESPONSE_NOT_FOUND, + REVIEW_ALREADY_COMPLETED, + REVIEW_FORM_FOR_STAGE_NOT_FOUND, + REVIEW_FORM_NOT_FOUND, + REVIEW_RESPONSE_NOT_FOUND, + USER_NOT_FOUND, +) + +option_fields = {"value": fields.String, "label": 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') + "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)) + "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)), } user_fields = { - 'nationality_country': fields.String(attribute='nationality_country.name'), - 'residence_country': fields.String(attribute='residence_country.name'), - 'affiliation': fields.String, - 'department': fields.String, - 'user_category': fields.String(attribute='user_category.name'), - 'id': fields.Integer + "nationality_country": fields.String(attribute="nationality_country.name"), + "residence_country": fields.String(attribute="residence_country.name"), + "affiliation": fields.String, + "department": fields.String, + "user_category": fields.String(attribute="user_category.name"), + "id": fields.Integer, } -review_scores_fields = { - 'review_question_id': fields.Integer, - 'value': fields.String -} +review_scores_fields = {"review_question_id": fields.Integer, "value": fields.String} review_response_fields = { - 'id': fields.Integer, - 'review_form_id': fields.Integer, - 'response_id': fields.Integer, - 'reviewer_user_id': fields.Integer, - 'scores': fields.List(fields.Nested(review_scores_fields), attribute='review_scores'), - 'language': fields.String + "id": fields.Integer, + "review_form_id": fields.Integer, + "response_id": fields.Integer, + "reviewer_user_id": fields.Integer, + "scores": fields.List( + fields.Nested(review_scores_fields), attribute="review_scores" + ), + "language": fields.String, } reference_fields = { - 'firstname': fields.String, - 'title': fields.String, - 'lastname': fields.String, - 'relation': fields.String, - 'uploaded_document': fields.String, + "firstname": fields.String, + "title": fields.String, + "lastname": fields.String, + "relation": fields.String, + "uploaded_document": fields.String, } review_fields = { - 'review_form': fields.Raw, - 'response': fields.Nested(response_fields), - 'user': fields.Nested(user_fields), - 'reviews_remaining_count': fields.Integer, - 'review_response': fields.Nested(review_response_fields), - 'references': fields.List(fields.Nested(reference_fields)), - 'is_submitted': fields.Boolean, - 'submitted_timestamp': fields.DateTime(dt_format='iso8601') + "review_form": fields.Raw, + "response": fields.Nested(response_fields), + "user": fields.Nested(user_fields), + "reviews_remaining_count": fields.Integer, + "review_response": fields.Nested(review_response_fields), + "references": fields.List(fields.Nested(reference_fields)), + "is_submitted": fields.Boolean, + "submitted_timestamp": fields.DateTime(dt_format="iso8601"), } review_question_detail_fields = { - 'id': fields.Integer, - 'question_id': fields.Integer, - 'type': fields.String, - 'is_required': fields.Boolean, - 'order': fields.Integer, - 'weight': fields.Float, - 'headline': fields.Raw(attribute=lambda s: s.headline_translations), - 'description': fields.Raw(attribute=lambda s: s.description_translations), - 'placeholder': fields.Raw(attribute=lambda s: s.placeholder_translations), - 'options': fields.Raw(attribute=lambda s: s.options_translations), - 'validation_regex': fields.Raw(attribute=lambda s: s.validation_regex_translations), - 'validation_text': fields.Raw(attribute=lambda s: s.validation_text_translations) + "id": fields.Integer, + "question_id": fields.Integer, + "type": fields.String, + "is_required": fields.Boolean, + "order": fields.Integer, + "weight": fields.Float, + "headline": fields.Raw(attribute=lambda s: s.headline_translations), + "description": fields.Raw(attribute=lambda s: s.description_translations), + "placeholder": fields.Raw(attribute=lambda s: s.placeholder_translations), + "options": fields.Raw(attribute=lambda s: s.options_translations), + "validation_regex": fields.Raw(attribute=lambda s: s.validation_regex_translations), + "validation_text": fields.Raw(attribute=lambda s: s.validation_text_translations), } review_section_detail_fields = { - 'id': fields.Integer, - 'order': fields.Integer, - 'name': fields.Raw(attribute=lambda s: s.headline_translations), - 'description': fields.Raw(attribute=lambda s: s.description_translations), - 'questions': fields.List(fields.Nested(review_question_detail_fields), attribute='review_questions') + "id": fields.Integer, + "order": fields.Integer, + "name": fields.Raw(attribute=lambda s: s.headline_translations), + "description": fields.Raw(attribute=lambda s: s.description_translations), + "questions": fields.List( + fields.Nested(review_question_detail_fields), attribute="review_questions" + ), } review_form_detail_fields = { - 'id': fields.Integer, - 'event_id': fields.Integer, - 'application_form_id': fields.Integer, - 'is_open': fields.Boolean, - 'deadline': fields.DateTime(dt_format='iso8601'), - 'active': fields.Boolean, - 'stage': fields.Integer, - 'sections': fields.List(fields.Nested(review_section_detail_fields), attribute='review_sections') + "id": fields.Integer, + "event_id": fields.Integer, + "application_form_id": fields.Integer, + "is_open": fields.Boolean, + "deadline": fields.DateTime(dt_format="iso8601"), + "active": fields.Boolean, + "stage": fields.Integer, + "sections": fields.List( + fields.Nested(review_section_detail_fields), attribute="review_sections" + ), } + def _serialize_review_question(review_question, language): translation = review_question.get_translation(language) if translation is None: - LOGGER.warn('Missing {} translation for review review_question id {}'.format(language, review_question.id)) - translation = review_question.get_translation('en') + LOGGER.warn( + "Missing {} translation for review review_question id {}".format( + language, review_question.id + ) + ) + translation = review_question.get_translation("en") return { - 'id': review_question.id, - 'question_id': review_question.question_id, - 'description': translation.description, - 'headline': translation.headline, - 'type': review_question.type, - 'placeholder': translation.placeholder, - 'options': translation.options, - 'is_required': review_question.is_required, - 'order': review_question.order, - 'validation_regex': translation.validation_regex, - 'validation_text': translation.validation_text, - 'weight': review_question.weight + "id": review_question.id, + "question_id": review_question.question_id, + "description": translation.description, + "headline": translation.headline, + "type": review_question.type, + "placeholder": translation.placeholder, + "options": translation.options, + "is_required": review_question.is_required, + "order": review_question.order, + "validation_regex": translation.validation_regex, + "validation_text": translation.validation_text, + "weight": review_question.weight, } + def _serialize_review_form(review_form: ReviewForm, language: str) -> Mapping[str, Any]: review_sections = [] for section in review_form.review_sections: translation = section.get_translation(language) if translation is None: - LOGGER.warn('Missing {} translation for review section id {}'.format(language, section.id)) - translation = section.get_translation('en') - review_sections.append({ - 'id': section.id, - 'order': section.order, - 'headline': translation.headline, - 'description': translation.description, - 'review_questions': [_serialize_review_question(q, language) for q in section.review_questions] - }) + LOGGER.warn( + "Missing {} translation for review section id {}".format( + language, section.id + ) + ) + translation = section.get_translation("en") + review_sections.append( + { + "id": section.id, + "order": section.order, + "headline": translation.headline, + "description": translation.description, + "review_questions": [ + _serialize_review_question(q, language) + for q in section.review_questions + ], + } + ) form = { - 'id': review_form.id, - 'application_form_id': review_form.application_form_id, - 'is_open': review_form.is_open, - 'deadline': review_form.deadline.isoformat(), - 'stage': review_form.stage, - 'review_sections': review_sections + "id": review_form.id, + "application_form_id": review_form.application_form_id, + "is_open": review_form.is_open, + "deadline": review_form.deadline.isoformat(), + "stage": review_form.stage, + "review_sections": review_sections, } return form def _add_reviewer_role(user_id, event_id): - event_role = EventRole('reviewer', user_id, event_id) + event_role = EventRole("reviewer", user_id, event_id) db.session.add(event_role) db.session.commit() -class ReviewResponseUser(): - def __init__(self, review_form, response, reviews_remaining_count, language, reference_responses=None, - review_response=None): +class ReviewResponseUser: + def __init__( + self, + review_form, + response, + reviews_remaining_count, + language, + reference_responses=None, + review_response=None, + ): self.review_form = _serialize_review_form(review_form, language) self.response = response self.user = None if response is None else response.user @@ -200,7 +242,7 @@ def __init__(self, review_form, response, reviews_remaining_count, language, ref self.review_response = review_response -class ReviewResponseReference(): +class ReviewResponseReference: def __init__(self, title, firstname, lastname, relation, uploaded_document): self.title = title self.firstname = firstname @@ -210,47 +252,62 @@ def __init__(self, title, firstname, lastname, relation, uploaded_document): class ReviewAPI(ReviewMixin, restful.Resource): - @auth_required @marshal_with(review_fields) def get(self): args = self.req_parser.parse_args() - event_id = args['event_id'] + event_id = args["event_id"] review_form = review_repository.get_review_form(event_id) if review_form is None: return EVENT_NOT_FOUND - reviews_remaining_count = review_repository.get_remaining_reviews_count(g.current_user['id'], - review_form.application_form_id) - skip = self.sanitise_skip(args['skip'], reviews_remaining_count) + reviews_remaining_count = review_repository.get_remaining_reviews_count( + g.current_user["id"], review_form.application_form_id + ) + skip = self.sanitise_skip(args["skip"], reviews_remaining_count) - response = review_repository.get_response_to_review(skip, g.current_user['id'], review_form.application_form_id) + response = review_repository.get_response_to_review( + skip, g.current_user["id"], review_form.application_form_id + ) references = [] if response is not None: - reference_requests = reference_repository.get_all_by_response_id(response.id) + reference_requests = reference_repository.get_all_by_response_id( + response.id + ) for r in reference_requests: - reference = reference_repository.get_reference_by_reference_request_id(r.id) + reference = reference_repository.get_reference_by_reference_request_id( + r.id + ) if reference is not None: references.append( - ReviewResponseReference(r.title, r.firstname, r.lastname, r.relation, - reference.uploaded_document) + ReviewResponseReference( + r.title, + r.firstname, + r.lastname, + r.relation, + reference.uploaded_document, + ) ) - review_response = None if response is None else review_repository.get_review_response(review_form.id, - response.id, - g.current_user['id']) + review_response = ( + None + if response is None + else review_repository.get_review_response( + review_form.id, response.id, g.current_user["id"] + ) + ) references = None if len(references) == 0 else references return ReviewResponseUser( review_form, response, reviews_remaining_count, - args['language'], + args["language"], reference_responses=references, - review_response=review_response + review_response=review_response, ) def sanitise_skip(self, skip, reviews_remaining_count): @@ -273,69 +330,85 @@ class ResponseReviewAPI(restful.Resource): @marshal_with(review_fields) def get(self): parser = reqparse.RequestParser() - parser.add_argument('response_id', type=int, required=True) - parser.add_argument('event_id', type=int, required=True) - parser.add_argument('language', type=str, required=True) + parser.add_argument("response_id", type=int, required=True) + parser.add_argument("event_id", type=int, required=True) + parser.add_argument("language", type=str, required=True) args = parser.parse_args() - response_id = args['response_id'] - event_id = args['event_id'] + response_id = args["response_id"] + event_id = args["event_id"] review_form = review_repository.get_review_form(event_id) if review_form is None: return REVIEW_FORM_NOT_FOUND - response = review_repository.get_response_by_reviewer(response_id, g.current_user['id']) + response = review_repository.get_response_by_reviewer( + response_id, g.current_user["id"] + ) if response is None: return RESPONSE_NOT_FOUND - review_response = review_repository.get_review_response(review_form.id, response_id, g.current_user['id']) - - return ReviewResponseUser(review_form, response, 0, args['language'], review_response=review_response) + review_response = review_repository.get_review_response( + review_form.id, response_id, g.current_user["id"] + ) + return ReviewResponseUser( + review_form, response, 0, args["language"], review_response=review_response + ) -class ReviewResponseAPI(GetReviewResponseMixin, PostReviewResponseMixin, restful.Resource): +class ReviewResponseAPI( + GetReviewResponseMixin, PostReviewResponseMixin, restful.Resource +): @auth_required @marshal_with(review_fields) def get(self): args = self.get_req_parser.parse_args() - id = args['id'] - reviewer_user_id = g.current_user['id'] + id = args["id"] + reviewer_user_id = g.current_user["id"] - review_form_response = review_repository.get_review_response_with_form(id, reviewer_user_id) + review_form_response = review_repository.get_review_response_with_form( + id, reviewer_user_id + ) if review_form_response is None: return REVIEW_RESPONSE_NOT_FOUND review_form, review_response = review_form_response - response = review_repository.get_response_by_review_response(id, reviewer_user_id, - review_form.application_form_id) + response = review_repository.get_response_by_review_response( + id, reviewer_user_id, review_form.application_form_id + ) - return ReviewResponseUser(review_form, response, 0, args['language'], review_response=review_response) + return ReviewResponseUser( + review_form, response, 0, args["language"], review_response=review_response + ) @auth_required @marshal_with(review_response_fields) def post(self): args = self.post_req_parser.parse_args() - validation_result = self.validate_scores(args['scores']) + validation_result = self.validate_scores(args["scores"]) if validation_result is not None: return validation_result - response_id = args['response_id'] - review_form_id = args['review_form_id'] - reviewer_user_id = g.current_user['id'] - scores = args['scores'] - language = args['language'] - is_submitted = args['is_submitted'] + response_id = args["response_id"] + review_form_id = args["review_form_id"] + reviewer_user_id = g.current_user["id"] + scores = args["scores"] + language = args["language"] + is_submitted = args["is_submitted"] - response_reviewer = review_repository.get_response_reviewer(response_id, reviewer_user_id) + response_reviewer = review_repository.get_response_reviewer( + response_id, reviewer_user_id + ) if response_reviewer is None: return FORBIDDEN - review_response = ReviewResponse(review_form_id, reviewer_user_id, response_id, language) + review_response = ReviewResponse( + review_form_id, reviewer_user_id, response_id, language + ) review_response.review_scores = self.get_review_scores(scores) if is_submitted: review_response.submit() @@ -347,21 +420,25 @@ def post(self): @marshal_with(review_response_fields) def put(self): args = self.post_req_parser.parse_args() - validation_result = self.validate_scores(args['scores']) + validation_result = self.validate_scores(args["scores"]) if validation_result is not None: return validation_result - response_id = args['response_id'] - review_form_id = args['review_form_id'] - reviewer_user_id = g.current_user['id'] - scores = args['scores'] - is_submitted = args['is_submitted'] + response_id = args["response_id"] + review_form_id = args["review_form_id"] + reviewer_user_id = g.current_user["id"] + scores = args["scores"] + is_submitted = args["is_submitted"] - response_reviewer = review_repository.get_response_reviewer(response_id, reviewer_user_id) + response_reviewer = review_repository.get_response_reviewer( + response_id, reviewer_user_id + ) if response_reviewer is None: return FORBIDDEN - review_response = review_repository.get_review_response(review_form_id, response_id, reviewer_user_id) + review_response = review_repository.get_review_response( + review_form_id, response_id, reviewer_user_id + ) if review_response is None: return REVIEW_RESPONSE_NOT_FOUND @@ -376,23 +453,29 @@ def put(self): def get_review_scores(self, scores): review_scores = [] for score in scores: - review_score = ReviewScore(score['review_question_id'], score['value']) + review_score = ReviewScore(score["review_question_id"], score["value"]) review_scores.append(review_score) return review_scores def validate_scores(self, scores): for score in scores: - if 'review_question_id' not in score.keys(): - return self.get_error_message('review_question_id') - if 'value' not in score.keys(): - return self.get_error_message('value') + if "review_question_id" not in score.keys(): + return self.get_error_message("review_question_id") + if "value" not in score.keys(): + return self.get_error_message("value") def get_error_message(self, key): return ( - {'message': {key: 'Missing required parameter in the JSON body or the post body or the query string'}}, 400) + { + "message": { + key: "Missing required parameter in the JSON body or the post body or the query string" + } + }, + 400, + ) -class ReviewCountView(): +class ReviewCountView: def __init__(self, count): self.email = count.email self.user_title = count.user_title @@ -402,12 +485,13 @@ def __init__(self, count): self.reviews_completed = count.reviews_completed self.reviewer_user_id = count.reviewer_user_id + class ReviewSummaryAPI(GetReviewSummaryMixin, restful.Resource): @auth_required def get(self): args = self.get_req_parser.parse_args() - event_id = args['event_id'] - user_id = g.current_user['id'] + event_id = args["event_id"] + user_id = g.current_user["id"] current_user = user_repository.get_by_id(user_id) if not current_user.is_event_admin(event_id): @@ -416,43 +500,49 @@ def get(self): config = review_configuration_repository.get_configuration_for_event(event_id) return { - 'reviews_unallocated': review_repository.count_unassigned_reviews(event_id, config.num_reviews_required) + "reviews_unallocated": review_repository.count_unassigned_reviews( + event_id, config.num_reviews_required + ) } -class ReviewAssignmentAPI(GetReviewAssignmentMixin, PostReviewAssignmentMixin, restful.Resource): +class ReviewAssignmentAPI( + GetReviewAssignmentMixin, PostReviewAssignmentMixin, restful.Resource +): reviews_count_fields = { - 'reviewer_user_id': fields.Integer, - 'email': fields.String, - 'user_title': fields.String, - 'firstname': fields.String, - 'lastname': fields.String, - 'reviews_allocated': fields.Integer, - 'reviews_completed': fields.Integer + "reviewer_user_id": fields.Integer, + "email": fields.String, + "user_title": fields.String, + "firstname": fields.String, + "lastname": fields.String, + "reviews_allocated": fields.Integer, + "reviews_completed": fields.Integer, } @auth_required @marshal_with(reviews_count_fields) def get(self): args = self.get_req_parser.parse_args() - event_id = args['event_id'] - user_id = g.current_user['id'] + event_id = args["event_id"] + user_id = g.current_user["id"] current_user = user_repository.get_by_id(user_id) if not current_user.is_event_admin(event_id): return FORBIDDEN - counts = review_repository.count_reviews_allocated_and_completed_per_reviewer(event_id) + counts = review_repository.count_reviews_allocated_and_completed_per_reviewer( + event_id + ) views = [ReviewCountView(count) for count in counts] return views @auth_required def post(self): args = self.post_req_parser.parse_args() - user_id = g.current_user['id'] - event_id = args['event_id'] - reviewer_user_email = args['reviewer_user_email'] - num_reviews = args['num_reviews'] + user_id = g.current_user["id"] + event_id = args["event_id"] + reviewer_user_email = args["reviewer_user_email"] + num_reviews = args["num_reviews"] event = event_repository.get_by_id(event_id) if not event: @@ -462,7 +552,9 @@ def post(self): if not current_user.is_event_admin(event_id): return FORBIDDEN - reviewer_user = user_repository.get_by_email(reviewer_user_email, g.organisation.id) + reviewer_user = user_repository.get_by_email( + reviewer_user_email, g.organisation.id + ) if reviewer_user is None: return USER_NOT_FOUND @@ -471,32 +563,41 @@ def post(self): config = review_configuration_repository.get_configuration_for_event(event_id) - response_ids = self.get_eligible_response_ids(event_id, reviewer_user.id, num_reviews, - config.num_reviews_required) - response_reviewers = [ResponseReviewer(response_id, reviewer_user.id) for response_id in response_ids] + response_ids = self.get_eligible_response_ids( + event_id, reviewer_user.id, num_reviews, config.num_reviews_required + ) + response_reviewers = [ + ResponseReviewer(response_id, reviewer_user.id) + for response_id in response_ids + ] db.session.add_all(response_reviewers) db.session.commit() if len(response_ids) > 0: email_user( - 'reviews-assigned', + "reviews-assigned", template_parameters=dict( num_reviews=len(response_ids), baobab_host=misc.get_baobab_host(), system_name=g.organisation.system_name, - event_key=event.key + event_key=event.key, ), event=event, - user=reviewer_user) + user=reviewer_user, + ) return {}, 201 def add_reviewer_role(self, user_id, event_id): - event_role = EventRole('reviewer', user_id, event_id) + event_role = EventRole("reviewer", user_id, event_id) db.session.add(event_role) db.session.commit() - def get_eligible_response_ids(self, event_id, reviewer_user_id, num_reviews, reviews_required): - candidate_responses = review_repository.get_candidate_response_ids(event_id, reviewer_user_id, reviews_required) + def get_eligible_response_ids( + self, event_id, reviewer_user_id, num_reviews, reviews_required + ): + candidate_responses = review_repository.get_candidate_response_ids( + event_id, reviewer_user_id, reviews_required + ) candidate_response_ids = set([r.id for r in candidate_responses]) # Now remove any responses that the reviewer is already assigned to @@ -508,17 +609,17 @@ def get_eligible_response_ids(self, event_id, reviewer_user_id, num_reviews, rev _review_history_fields = { - 'response_id': fields.Integer, - 'review_response_id': fields.Integer, - 'submitted_timestamp': fields.DateTime(dt_format='iso8601'), - 'reviewed_user_id': fields.String + "response_id": fields.Integer, + "review_response_id": fields.Integer, + "submitted_timestamp": fields.DateTime(dt_format="iso8601"), + "reviewed_user_id": fields.String, } review_history_fields = { - 'reviews': fields.List(fields.Nested(_review_history_fields)), - 'num_entries': fields.Integer, - 'current_pagenumber': fields.Integer, - 'total_pages': fields.Integer + "reviews": fields.List(fields.Nested(_review_history_fields)), + "num_entries": fields.Integer, + "current_pagenumber": fields.Integer, + "total_pages": fields.Integer, } @@ -531,16 +632,15 @@ def __init__(self, review): class ReviewHistoryAPI(GetReviewHistoryMixin, restful.Resource): - @auth_required @marshal_with(review_history_fields) def get(self): args = self.get_req_parser.parse_args() - user_id = g.current_user['id'] - event_id = args['event_id'] - page_number = args['page_number'] - limit = args['limit'] - sort_column = args['sort_column'] + user_id = g.current_user["id"] + event_id = args["event_id"] + page_number = args["page_number"] + limit = args["limit"] + sort_column = args["sort_column"] reviewer = user_repository.get_by_id(user_id).is_reviewer(event_id) if not reviewer: @@ -548,10 +648,10 @@ def get(self): reviews = review_repository.get_review_history(user_id, event_id) - if sort_column == 'review_response_id': + if sort_column == "review_response_id": reviews = reviews.order_by(ReviewResponse.id) - if sort_column == 'submitted_timestamp': + if sort_column == "submitted_timestamp": reviews = reviews.order_by(ReviewResponse.submitted_timestamp) reviews = reviews.slice(page_number * limit, page_number * limit + limit).all() @@ -561,72 +661,85 @@ def get(self): total_pages = ceil(float(num_entries) / limit) reviews = [ReviewHistoryModel(review) for review in reviews] - return {'reviews': reviews, 'num_entries': num_entries, 'current_pagenumber': page_number, - 'total_pages': total_pages} + return { + "reviews": reviews, + "num_entries": num_entries, + "current_pagenumber": page_number, + "total_pages": total_pages, + } class ReviewListAPI(restful.Resource): - @staticmethod def _serialize_answer(answer, language): translation = answer.question.get_translation(language) if not translation: - translation = answer.question.get_translation('en') - LOGGER.warn('Could not find {} translation for question id {}'.format(language, answer.question.id)) - return { - 'headline': translation.headline, - 'value': answer.value_display - } + translation = answer.question.get_translation("en") + LOGGER.warn( + "Could not find {} translation for question id {}".format( + language, answer.question.id + ) + ) + return {"headline": translation.headline, "value": answer.value_display} @staticmethod def _serialize_response(response, review_response, language): info = [ ReviewListAPI._serialize_answer(answer, language) - for answer in response.answers if answer.question.key == 'review-identifier'] + for answer in response.answers + if answer.question.key == "review-identifier" + ] submitted = None if review_response and review_response.submitted_timestamp: submitted = review_response.submitted_timestamp.isoformat() return { - 'response_id': response.id, - 'language': response.language, - 'information': info, - 'started': review_response is not None, - 'submitted': submitted, - 'total_score': review_response.calculate_score() if review_response is not None else 0.0 + "response_id": response.id, + "language": response.language, + "information": info, + "started": review_response is not None, + "submitted": submitted, + "total_score": review_response.calculate_score() + if review_response is not None + else 0.0, } @auth_required def get(self): parser = reqparse.RequestParser() - parser.add_argument('event_id', type=int, required=True) - parser.add_argument('language', type=str, required=True) + parser.add_argument("event_id", type=int, required=True) + parser.add_argument("language", type=str, required=True) args = parser.parse_args() - event_id = args['event_id'] - user_id = g.current_user['id'] - language = args['language'] + event_id = args["event_id"] + user_id = g.current_user["id"] + language = args["language"] if not user_repository.get_by_id(user_id).is_reviewer(event_id): return FORBIDDEN responses_to_review = review_repository.get_review_list(user_id, event_id) - return [ReviewListAPI._serialize_response(response, review_response, language) - for response, review_response in responses_to_review] + return [ + ReviewListAPI._serialize_response(response, review_response, language) + for response, review_response in responses_to_review + ] + class ResponseReviewAssignmentAPI(restful.Resource): @event_admin_required def post(self, event_id): parser = reqparse.RequestParser() - parser.add_argument('response_ids', type=int, required=True, action='append') - parser.add_argument('reviewer_email', type=str, required=True) + parser.add_argument("response_ids", type=int, required=True, action="append") + parser.add_argument("reviewer_email", type=str, required=True) args = parser.parse_args() - response_ids = args['response_ids'] - reviewer_email = args['reviewer_email'] + response_ids = args["response_ids"] + reviewer_email = args["reviewer_email"] - filtered_response_ids = response_repository.filter_ids_to_event(response_ids, event_id) + filtered_response_ids = response_repository.filter_ids_to_event( + response_ids, event_id + ) if set(filtered_response_ids) != set(response_ids): return FORBIDDEN @@ -640,39 +753,45 @@ def post(self, event_id): if not reviewer_user.is_reviewer(event_id): _add_reviewer_role(reviewer_user.id, event_id) - response_reviewers = [ResponseReviewer(response_id, reviewer_user.id) for response_id in response_ids] + response_reviewers = [ + ResponseReviewer(response_id, reviewer_user.id) + for response_id in response_ids + ] db.session.add_all(response_reviewers) db.session.commit() if len(response_ids) > 0: email_user( - 'reviews-assigned', + "reviews-assigned", template_parameters=dict( num_reviews=len(response_ids), baobab_host=misc.get_baobab_host(), system_name=g.organisation.system_name, - event_key=event.key + event_key=event.key, ), event=event, - user=reviewer_user) + user=reviewer_user, + ) return {}, 201 @event_admin_required def delete(self, event_id): parser = reqparse.RequestParser() - parser.add_argument('response_id', type=int, required=True) - parser.add_argument('reviewer_user_id', type=int, required=True) + parser.add_argument("response_id", type=int, required=True) + parser.add_argument("reviewer_user_id", type=int, required=True) args = parser.parse_args() - response_id = args['response_id'] - reviewer_user_id = args['reviewer_user_id'] + response_id = args["response_id"] + reviewer_user_id = args["reviewer_user_id"] review_form = review_repository.get_review_form(event_id) if not review_form: return REVIEW_FORM_NOT_FOUND # If the reviewer has already completed the review, action can't be completed - review_response = review_repository.get_review_response(review_form.id, response_id, reviewer_user_id) + review_response = review_repository.get_review_response( + review_form.id, response_id, reviewer_user_id + ) if review_response: return REVIEW_ALREADY_COMPLETED @@ -680,81 +799,92 @@ def delete(self, event_id): return {}, 200 + class ReviewResponseDetailListAPI(restful.Resource): @staticmethod def _serialise_identifier(answer, language): question_translation = answer.question.get_translation(language) if question_translation is None: - question_translation = answer.question.get_translation('en') - LOGGER.warn('Could not find {} translation for question id {}'.format(language, answer.question.id)) + question_translation = answer.question.get_translation("en") + LOGGER.warn( + "Could not find {} translation for question id {}".format( + language, answer.question.id + ) + ) return { - 'headline': question_translation.headline, - 'value': answer.value_display + "headline": question_translation.headline, + "value": answer.value_display, } @staticmethod def _serialise_score(review_score, language): - review_question_translation = review_score.review_question.get_translation(language) + review_question_translation = review_score.review_question.get_translation( + language + ) if review_question_translation is None: - review_question_translation = review_score.review_question.get_translation('en') - LOGGER.warn('Could not find {} translation for review question id {}'.format(language, - review_score.review_question.id)) + review_question_translation = review_score.review_question.get_translation( + "en" + ) + LOGGER.warn( + "Could not find {} translation for review question id {}".format( + language, review_score.review_question.id + ) + ) return { - 'review_question_id': review_score.review_question.id, - 'headline': review_question_translation.headline, - 'description': review_question_translation.description, - 'type': review_score.review_question.type, - 'score': review_score.value, - 'weight': review_score.review_question.weight, + "review_question_id": review_score.review_question.id, + "headline": review_question_translation.headline, + "description": review_question_translation.description, + "type": review_score.review_question.type, + "score": review_score.value, + "weight": review_score.review_question.weight, } @staticmethod def _serialise_review_response(review_response, language): return { - 'review_response_id': review_response.id, - 'response_id': review_response.response_id, - - 'reviewer_user_title': review_response.reviewer_user.user_title, - 'reviewer_user_firstname': review_response.reviewer_user.firstname, - 'reviewer_user_lastname': review_response.reviewer_user.lastname, - - 'response_user_title': review_response.response.user.user_title, - 'response_user_firstname': review_response.response.user.firstname, - 'response_user_lastname': review_response.response.user.lastname, - - 'identifiers': [ + "review_response_id": review_response.id, + "response_id": review_response.response_id, + "reviewer_user_title": review_response.reviewer_user.user_title, + "reviewer_user_firstname": review_response.reviewer_user.firstname, + "reviewer_user_lastname": review_response.reviewer_user.lastname, + "response_user_title": review_response.response.user.user_title, + "response_user_firstname": review_response.response.user.firstname, + "response_user_lastname": review_response.response.user.lastname, + "identifiers": [ ReviewResponseDetailListAPI._serialise_identifier(answer, language) for answer in review_response.response.answers if answer.question.is_review_identifier() ], - - 'scores': [ + "scores": [ ReviewResponseDetailListAPI._serialise_score(review_score, language) for review_score in review_response.review_scores - if (review_score.review_question.type == 'multi-choice' - or review_score.review_question.type == 'long-text' - or review_score.review_question.type == 'short-text' - ) + if ( + review_score.review_question.type == "multi-choice" + or review_score.review_question.type == "long-text" + or review_score.review_question.type == "short-text" + ) ], - - 'total': review_response.calculate_score() + "total": review_response.calculate_score(), } @auth_required @event_admin_required def get(self, event_id): parser = reqparse.RequestParser() - parser.add_argument('language', type=str, required=True) + parser.add_argument("language", type=str, required=True) args = parser.parse_args() review_responses = review_repository.get_all_review_responses_by_event(event_id) return [ - ReviewResponseDetailListAPI._serialise_review_response(review_response, args['language']) + ReviewResponseDetailListAPI._serialise_review_response( + review_response, args["language"] + ) for review_response in review_responses ], 200 + class ReviewResponseSummaryListAPI(restful.Resource): @staticmethod def _serialise_response(response: Response, review_form: ReviewForm, language: str): @@ -762,12 +892,24 @@ def _serialise_response(response: Response, review_form: ReviewForm, language: s for review_section in review_form.review_sections: for review_question in review_section.review_questions: if review_question.weight > 0: - review_question_translation = review_question.get_translation(language) + review_question_translation = review_question.get_translation( + language + ) if not review_question_translation: - review_question_translation = review_question.get_translation('en') - LOGGER.warn('Could not find {} translation for review question id {}'.format(language, review_score.review_question.id)) - - average_score = review_repository.get_average_score_for_review_question(response.id, review_question.id) + review_question_translation = review_question.get_translation( + "en" + ) + LOGGER.warn( + "Could not find {} translation for review question id {}".format( + language, review_score.review_question.id + ) + ) + + average_score = ( + review_repository.get_average_score_for_review_question( + response.id, review_question.id + ) + ) score = { "review_question_id": review_question.id, @@ -775,7 +917,7 @@ def _serialise_response(response: Response, review_form: ReviewForm, language: s "description": review_question_translation.description, "type": review_question.type, "score": average_score, - "weight": review_question.weight + "weight": review_question.weight, } scores.append(score) @@ -784,15 +926,15 @@ def _serialise_response(response: Response, review_form: ReviewForm, language: s "response_user_title": response.user.user_title, "response_user_firstname": response.user.firstname, "response_user_lastname": response.user.lastname, - "identifiers": [ - ReviewResponseDetailListAPI._serialise_identifier(answer, response.language) + ReviewResponseDetailListAPI._serialise_identifier( + answer, response.language + ) for answer in response.answers if answer.question.is_review_identifier() ], - "scores": scores, - "total": sum(score['score'] * score['weight'] for score in scores) + "total": sum(score["score"] * score["weight"] for score in scores), } return response_summary @@ -800,49 +942,47 @@ def _serialise_response(response: Response, review_form: ReviewForm, language: s @event_admin_required def get(self, event_id): parser = reqparse.RequestParser() - parser.add_argument('language', type=str, required=True) + parser.add_argument("language", type=str, required=True) args = parser.parse_args() responses = response_repository.get_all_for_event(event_id) review_form = review_repository.get_review_form(event_id) return [ - ReviewResponseSummaryListAPI._serialise_response(response, review_form, args['language']) + ReviewResponseSummaryListAPI._serialise_response( + response, review_form, args["language"] + ) for response in responses ], 200 class ReviewStageAPI(restful.Resource): - @auth_required def get(self): parser = reqparse.RequestParser() - parser.add_argument('event_id', type=int, required=True) + parser.add_argument("event_id", type=int, required=True) args = parser.parse_args() - event_id = args['event_id'] + event_id = args["event_id"] review_forms = review_repository.get_all_review_forms_for_event(event_id) current_form = [r for r in review_forms if r.active] if not current_form: return NO_ACTIVE_REVIEW_FORM - - current_form = current_form[0] - return { - 'current_stage': current_form.stage, - 'total_stages': len(review_forms) - } + current_form = current_form[0] + + return {"current_stage": current_form.stage, "total_stages": len(review_forms)} @auth_required @event_admin_required def post(self, event_id): parser = reqparse.RequestParser() - parser.add_argument('stage', type=int, required=True) + parser.add_argument("stage", type=int, required=True) args = parser.parse_args() - stage = args['stage'] + stage = args["stage"] review_forms = review_repository.get_all_review_forms_for_event(event_id) selected_form = [r for r in review_forms if r.stage == stage] @@ -854,24 +994,23 @@ def post(self, event_id): for form in review_forms: form.deactivate() - + selected_form.activate() db.session.commit() - + return {}, 201 class ReviewFormDetailAPI(restful.Resource): - @event_admin_required @marshal_with(review_form_detail_fields) def get(self, event_id): req_parser = reqparse.RequestParser() - req_parser.add_argument('stage', type=int, required=False) + req_parser.add_argument("stage", type=int, required=False) args = req_parser.parse_args() - stage = args['stage'] + stage = args["stage"] review_form = review_repository.get_review_form(event_id, stage=stage) @@ -885,23 +1024,23 @@ def get(self, event_id): @event_admin_required @marshal_with(review_form_detail_fields) def post(self, event_id): - dt_format = lambda x: datetime.strptime(x, '%Y-%m-%dT%H:%M:%S') + dt_format = lambda x: datetime.strptime(x, "%Y-%m-%dT%H:%M:%S") req_parser = reqparse.RequestParser() - req_parser.add_argument('application_form_id', type=int, required=True) - req_parser.add_argument('stage', type=int, required=True) - req_parser.add_argument('is_open', type=bool, required=True) - req_parser.add_argument('deadline', type=dt_format, required=True) - req_parser.add_argument('active', type=bool, required=True) - req_parser.add_argument('sections', type=dict, required=True, action='append') + req_parser.add_argument("application_form_id", type=int, required=True) + req_parser.add_argument("stage", type=int, required=True) + req_parser.add_argument("is_open", type=bool, required=True) + req_parser.add_argument("deadline", type=dt_format, required=True) + req_parser.add_argument("active", type=bool, required=True) + req_parser.add_argument("sections", type=dict, required=True, action="append") args = req_parser.parse_args() - application_form_id = args['application_form_id'] - stage = args['stage'] - is_open = args['is_open'] - deadline = args['deadline'] - active = args['active'] - sections_data = args['sections'] + application_form_id = args["application_form_id"] + stage = args["stage"] + is_open = args["is_open"] + deadline = args["deadline"] + active = args["active"] + sections_data = args["sections"] review_form = review_repository.get_review_form(event_id, stage) if review_form is not None: @@ -910,49 +1049,53 @@ def post(self, event_id): other_review_forms = review_repository.get_review_forms(event_id) review_form = ReviewForm( - application_form_id, - deadline, - stage, + application_form_id, + deadline, + stage, # Only make this stage active if there is no other active stage. - active=not any(o.active for o in other_review_forms)) - + active=not any(o.active for o in other_review_forms), + ) + review_repository.add_model(review_form) for section_data in sections_data: - section = ReviewSection(review_form.id, section_data['order']) + section = ReviewSection(review_form.id, section_data["order"]) review_repository.add_model(section) - languages = section_data['name'].keys() + languages = section_data["name"].keys() for language in languages: section_translation = ReviewSectionTranslation( review_section_id=section.id, language=language, - headline=section_data['name'][language], - description=section_data['description'][language]) + headline=section_data["name"][language], + description=section_data["description"][language], + ) review_repository.add_model(section_translation) - for question_data in section_data['questions']: + for question_data in section_data["questions"]: question = ReviewQuestion( - review_section_id=section.id, - question_id=question_data['question_id'] or None, - type=question_data['type'], - is_required=question_data['is_required'], - order=question_data['order'], - weight=question_data['weight']) - + review_section_id=section.id, + question_id=question_data["question_id"] or None, + type=question_data["type"], + is_required=question_data["is_required"], + order=question_data["order"], + weight=question_data["weight"], + ) + review_repository.add_model(question) - languages = question_data['headline'].keys() + languages = question_data["headline"].keys() for language in languages: question_translation = ReviewQuestionTranslation( review_question_id=question.id, - language=language, - description=question_data["description"][language], - headline=question_data["headline"][language], - placeholder=question_data["placeholder"][language], - options=question_data["options"][language], - validation_regex=question_data["validation_regex"][language], - validation_text=question_data["validation_text"][language]) + language=language, + description=question_data["description"][language], + headline=question_data["headline"][language], + placeholder=question_data["placeholder"][language], + options=question_data["options"][language], + validation_regex=question_data["validation_regex"][language], + validation_text=question_data["validation_text"][language], + ) review_repository.add_model(question_translation) @@ -964,83 +1107,106 @@ def post(self, event_id): @event_admin_required @marshal_with(review_form_detail_fields) def put(self, event_id): - dt_format = lambda x: datetime.strptime(x, '%Y-%m-%dT%H:%M:%S') + dt_format = lambda x: datetime.strptime(x, "%Y-%m-%dT%H:%M:%S") req_parser = reqparse.RequestParser() - req_parser.add_argument('id', type=int, required=True) - req_parser.add_argument('application_form_id', type=int, required=True) - req_parser.add_argument('stage', type=int, required=True) - req_parser.add_argument('is_open', type=bool, required=True) - req_parser.add_argument('deadline', type=dt_format, required=True) - req_parser.add_argument('sections', type=dict, required=True, action='append') + req_parser.add_argument("id", type=int, required=True) + req_parser.add_argument("application_form_id", type=int, required=True) + req_parser.add_argument("stage", type=int, required=True) + req_parser.add_argument("is_open", type=bool, required=True) + req_parser.add_argument("deadline", type=dt_format, required=True) + req_parser.add_argument("sections", type=dict, required=True, action="append") args = req_parser.parse_args() - id = args['id'] - application_form_id = args['application_form_id'] - stage = args['stage'] - is_open = args['is_open'] - deadline = args['deadline'] - sections_data = args['sections'] + id = args["id"] + application_form_id = args["application_form_id"] + stage = args["stage"] + is_open = args["is_open"] + deadline = args["deadline"] + sections_data = args["sections"] review_form = review_repository.get_review_form_by_id(id) if not review_form: return REVIEW_FORM_NOT_FOUND - + if application_form_id != review_form.application_form_id: return UPDATE_CONFLICT if event_id != review_form.application_form.event_id: return UPDATE_CONFLICT - + review_form.stage = stage review_form.is_open = is_open review_form.deadline = deadline - + # Note: we don't update the active property which is controlled by # the ReviewStageAPI # Delete questions in the review form that no longer exist - all_question_ids = [q["id"] for s in sections_data for q in s["questions"] if "id" in q] + all_question_ids = [ + q["id"] for s in sections_data for q in s["questions"] if "id" in q + ] for section in review_form.review_sections: for question in section.review_questions: if question.id not in all_question_ids: review_repository.delete_review_question(question) - + all_section_ids = [s["id"] for s in sections_data if "id" in s] for section in review_form.review_sections: if section.id not in all_section_ids: review_repository.delete_review_section(section) for section_data in sections_data: - if 'id' in section_data: + if "id" in section_data: # If ID is populated, then update the existing section - section = next((s for s in review_form.review_sections if s.id == section_data['id']), None) # type: ReviewSection + section = next( + ( + s + for s in review_form.review_sections + if s.id == section_data["id"] + ), + None, + ) # type: ReviewSection if not section: return SECTION_NOT_FOUND - current_translations = section.translations # type: Sequence[ReviewSectionTranslation] + current_translations = ( + section.translations + ) # type: Sequence[ReviewSectionTranslation] for current_translation in current_translations: - current_translation.description = section_data['description'][current_translation.language] - current_translation.headline = section_data['name'][current_translation.language] - + current_translation.description = section_data["description"][ + current_translation.language + ] + current_translation.headline = section_data["name"][ + current_translation.language + ] + section.order = section_data["order"] else: # if not populated, then add new section section = ReviewSection(review_form.id, section_data["order"]) review_repository.add_model(section) - languages = section_data['name'].keys() + languages = section_data["name"].keys() for language in languages: section_translation = ReviewSectionTranslation( - section.id, - language, - section_data['name'][language], - section_data['description'][language]) + section.id, + language, + section_data["name"][language], + section_data["description"][language], + ) review_repository.add_model(section_translation) for question_data in section_data["questions"]: if "id" in question_data: - current_question = next((q for q in section.review_questions if q.id == question_data['id']), None) # type: ReviewQuestion + current_question = next( + ( + q + for q in section.review_questions + if q.id == question_data["id"] + ), + None, + ) # type: ReviewQuestion if not current_question: return QUESTION_NOT_FOUND @@ -1050,26 +1216,41 @@ def put(self, event_id): current_question.is_required = question_data["is_required"] current_question.weight = question_data["weight"] - current_translations = current_question.translations # type: Sequence[ReviewQuestionTranslation] + current_translations = ( + current_question.translations + ) # type: Sequence[ReviewQuestionTranslation] for current_translation in current_translations: - current_translation.description = question_data['description'][current_translation.language] - current_translation.headline = question_data['headline'][current_translation.language] - current_translation.options = question_data['options'][current_translation.language] - current_translation.placeholder = question_data['placeholder'][current_translation.language] - current_translation.validation_regex = question_data['validation_regex'][current_translation.language] - current_translation.validation_text = question_data['validation_text'][current_translation.language] + current_translation.description = question_data["description"][ + current_translation.language + ] + current_translation.headline = question_data["headline"][ + current_translation.language + ] + current_translation.options = question_data["options"][ + current_translation.language + ] + current_translation.placeholder = question_data["placeholder"][ + current_translation.language + ] + current_translation.validation_regex = question_data[ + "validation_regex" + ][current_translation.language] + current_translation.validation_text = question_data[ + "validation_text" + ][current_translation.language] else: question = ReviewQuestion( - review_section_id=section.id, + review_section_id=section.id, question_id=question_data["question_id"] or None, type=question_data["type"], is_required=question_data["is_required"], order=question_data["order"], - weight=question_data["weight"]) + weight=question_data["weight"], + ) review_repository.add_model(question) - for language in question_data['headline'].keys(): + for language in question_data["headline"].keys(): translation = ReviewQuestionTranslation( review_question_id=question.id, language=language, @@ -1077,12 +1258,14 @@ def put(self, event_id): headline=question_data["headline"][language], placeholder=question_data["placeholder"][language], options=question_data["options"][language], - validation_regex=question_data["validation_regex"][language], - validation_text=question_data["validation_text"][language]) + validation_regex=question_data["validation_regex"][ + language + ], + validation_text=question_data["validation_text"][language], + ) review_repository.add_model(translation) - db.session.commit() review_form = review_repository.get_review_form_by_id(id) diff --git a/api/app/reviews/mixins.py b/api/app/reviews/mixins.py index 22638493c..baae4c557 100644 --- a/api/app/reviews/mixins.py +++ b/api/app/reviews/mixins.py @@ -1,41 +1,48 @@ from flask_restful import reqparse + class ReviewMixin(object): req_parser = reqparse.RequestParser() - req_parser.add_argument('event_id', type=int, required=True) - req_parser.add_argument('skip', type=int, required=False) - req_parser.add_argument('language', type=str, required=True) + req_parser.add_argument("event_id", type=int, required=True) + req_parser.add_argument("skip", type=int, required=False) + req_parser.add_argument("language", type=str, required=True) + class GetReviewResponseMixin(object): get_req_parser = reqparse.RequestParser() - get_req_parser.add_argument('id', type=int, required=True) - get_req_parser.add_argument('language', type=str, required=True) + get_req_parser.add_argument("id", type=int, required=True) + get_req_parser.add_argument("language", type=str, required=True) + class PostReviewResponseMixin(object): post_req_parser = reqparse.RequestParser() - post_req_parser.add_argument('review_form_id', type=int, required=True) - post_req_parser.add_argument('response_id', type=int, required=True) - post_req_parser.add_argument('scores', type=dict, required=True, action='append') - post_req_parser.add_argument('language', type=str, required=True) - post_req_parser.add_argument('is_submitted', type=bool, required=True) + post_req_parser.add_argument("review_form_id", type=int, required=True) + post_req_parser.add_argument("response_id", type=int, required=True) + post_req_parser.add_argument("scores", type=dict, required=True, action="append") + post_req_parser.add_argument("language", type=str, required=True) + post_req_parser.add_argument("is_submitted", type=bool, required=True) + class GetReviewSummaryMixin(object): get_req_parser = reqparse.RequestParser() - get_req_parser.add_argument('event_id', type=int, required=True) + get_req_parser.add_argument("event_id", type=int, required=True) + class GetReviewAssignmentMixin(object): get_req_parser = reqparse.RequestParser() - get_req_parser.add_argument('event_id', type=int, required=True) + get_req_parser.add_argument("event_id", type=int, required=True) + class PostReviewAssignmentMixin(object): post_req_parser = reqparse.RequestParser() - post_req_parser.add_argument('event_id', type=int, required=True) - post_req_parser.add_argument('reviewer_user_email', type=str, required=True) - post_req_parser.add_argument('num_reviews', type=int, required=True) + post_req_parser.add_argument("event_id", type=int, required=True) + post_req_parser.add_argument("reviewer_user_email", type=str, required=True) + post_req_parser.add_argument("num_reviews", type=int, required=True) + class GetReviewHistoryMixin(object): get_req_parser = reqparse.RequestParser() - get_req_parser.add_argument('event_id', type = int, required = True) - get_req_parser.add_argument('page_number', type = int, required = True) - get_req_parser.add_argument('limit', type = int, required = True) - get_req_parser.add_argument('sort_column', type = str, required = True) \ No newline at end of file + get_req_parser.add_argument("event_id", type=int, required=True) + get_req_parser.add_argument("page_number", type=int, required=True) + get_req_parser.add_argument("limit", type=int, required=True) + get_req_parser.add_argument("sort_column", type=str, required=True) diff --git a/api/app/reviews/models.py b/api/app/reviews/models.py index f8fe1812c..f9701c083 100644 --- a/api/app/reviews/models.py +++ b/api/app/reviews/models.py @@ -1,20 +1,24 @@ from datetime import datetime +from typing import Any, Callable, Mapping + from app import db from app.utils import misc -from typing import Any, Callable, Mapping -from datetime import datetime class ReviewForm(db.Model): id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) is_open = db.Column(db.Boolean(), nullable=False) deadline = db.Column(db.DateTime(), nullable=False) stage = db.Column(db.Integer(), nullable=False) active = db.Column(db.Boolean(), nullable=False) - application_form = db.relationship('ApplicationForm', foreign_keys=[application_form_id]) - review_sections = db.relationship('ReviewSection', order_by='ReviewSection.order') + application_form = db.relationship( + "ApplicationForm", foreign_keys=[application_form_id] + ) + review_sections = db.relationship("ReviewSection", order_by="ReviewSection.order") def __init__(self, application_form_id, deadline, stage, active): self.application_form_id = application_form_id @@ -35,16 +39,18 @@ def activate(self): class ReviewSection(db.Model): id = db.Column(db.Integer(), primary_key=True) - review_form_id = db.Column(db.Integer(), db.ForeignKey('review_form.id'), nullable=False) + review_form_id = db.Column( + db.Integer(), db.ForeignKey("review_form.id"), nullable=False + ) order = db.Column(db.Integer(), nullable=False) - translations = db.relationship('ReviewSectionTranslation', lazy='dynamic') - review_questions = db.relationship('ReviewQuestion', order_by='ReviewQuestion.order') - review_form = db.relationship('ReviewForm', foreign_keys=[review_form_id]) + translations = db.relationship("ReviewSectionTranslation", lazy="dynamic") + review_questions = db.relationship( + "ReviewQuestion", order_by="ReviewQuestion.order" + ) + review_form = db.relationship("ReviewForm", foreign_keys=[review_form_id]) - def __init__(self, - review_form_id, - order): + def __init__(self, review_form_id, order): self.review_form_id = review_form_id self.order = order @@ -52,7 +58,9 @@ def get_translation(self, language): translation = self.translations.filter_by(language=language).first() return translation - def _translations_for_field(self, accessor_fn: Callable[['ReviewSectionTranslation'], Any]) -> Mapping[str, Any]: + def _translations_for_field( + self, accessor_fn: Callable[["ReviewSectionTranslation"], Any] + ) -> Mapping[str, Any]: translations = {} for translation in self.translations: translations[translation.language] = accessor_fn(translation) @@ -68,17 +76,25 @@ def description_translations(self): class ReviewSectionTranslation(db.Model): - __tablename__ = 'review_section_translation' - __table_args__ = tuple([db.UniqueConstraint('review_section_id', 'language', name='uq_review_section_id_language')]) + __tablename__ = "review_section_translation" + __table_args__ = tuple( + [ + db.UniqueConstraint( + "review_section_id", "language", name="uq_review_section_id_language" + ) + ] + ) id = db.Column(db.Integer(), primary_key=True) - review_section_id = db.Column(db.Integer(), db.ForeignKey('review_section.id'), nullable=False) + review_section_id = db.Column( + db.Integer(), db.ForeignKey("review_section.id"), nullable=False + ) language = db.Column(db.String(2), nullable=False) headline = db.Column(db.String(), nullable=True) description = db.Column(db.String(), nullable=True) - review_section = db.relationship('ReviewSection', foreign_keys=[review_section_id]) + review_section = db.relationship("ReviewSection", foreign_keys=[review_section_id]) def __init__(self, review_section_id, language, headline=None, description=None): self.review_section_id = review_section_id @@ -89,26 +105,24 @@ def __init__(self, review_section_id, language, headline=None, description=None) class ReviewQuestion(db.Model): id = db.Column(db.Integer, primary_key=True) - review_section_id = db.Column(db.Integer(), db.ForeignKey('review_section.id'), nullable=False) - question_id = db.Column(db.Integer(), db.ForeignKey('question.id'), nullable=True) + review_section_id = db.Column( + db.Integer(), db.ForeignKey("review_section.id"), nullable=False + ) + question_id = db.Column(db.Integer(), db.ForeignKey("question.id"), nullable=True) type = db.Column(db.String(), nullable=False) is_required = db.Column(db.Boolean(), nullable=False) order = db.Column(db.Integer(), nullable=False) weight = db.Column(db.Float(), nullable=False) - review_section = db.relationship('ReviewSection', foreign_keys=[review_section_id]) - question = db.relationship('Question', foreign_keys=[question_id]) - - translations = db.relationship('ReviewQuestionTranslation', lazy='dynamic') - - def __init__(self, - review_section_id, - question_id, - type, - is_required, - order, - weight): + review_section = db.relationship("ReviewSection", foreign_keys=[review_section_id]) + question = db.relationship("Question", foreign_keys=[question_id]) + + translations = db.relationship("ReviewQuestionTranslation", lazy="dynamic") + + def __init__( + self, review_section_id, question_id, type, is_required, order, weight + ): self.review_section_id = review_section_id self.question_id = question_id self.type = type @@ -120,7 +134,9 @@ def get_translation(self, language): translation = self.translations.filter_by(language=language).first() return translation - def _translations_for_field(self, accessor_fn: Callable[['ReviewQuestionTranslation'], Any]) -> Mapping[str, Any]: + def _translations_for_field( + self, accessor_fn: Callable[["ReviewQuestionTranslation"], Any] + ) -> Mapping[str, Any]: translations = {} for translation in self.translations: translations[translation.language] = accessor_fn(translation) @@ -152,12 +168,19 @@ def validation_text_translations(self): class ReviewQuestionTranslation(db.Model): - __tablename__ = 'review_question_translation' + __tablename__ = "review_question_translation" __table_args__ = tuple( - [db.UniqueConstraint('review_question_id', 'language', name='uq_review_question_id_language')]) + [ + db.UniqueConstraint( + "review_question_id", "language", name="uq_review_question_id_language" + ) + ] + ) id = db.Column(db.Integer(), primary_key=True) - review_question_id = db.Column(db.Integer(), db.ForeignKey('review_question.id'), nullable=False) + review_question_id = db.Column( + db.Integer(), db.ForeignKey("review_question.id"), nullable=False + ) language = db.Column(db.String(2), nullable=False) description = db.Column(db.String(), nullable=True) @@ -167,15 +190,17 @@ class ReviewQuestionTranslation(db.Model): validation_regex = db.Column(db.String(), nullable=True) validation_text = db.Column(db.String(), nullable=True) - def __init__(self, - review_question_id, - language, - description=None, - headline=None, - placeholder=None, - options=None, - validation_regex=None, - validation_text=None): + def __init__( + self, + review_question_id, + language, + description=None, + headline=None, + placeholder=None, + options=None, + validation_regex=None, + validation_text=None, + ): self.review_question_id = review_question_id self.language = language self.description = description @@ -188,26 +213,28 @@ def __init__(self, class ReviewResponse(db.Model): id = db.Column(db.Integer(), primary_key=True) - review_form_id = db.Column(db.Integer(), db.ForeignKey('review_form.id'), nullable=False) - reviewer_user_id = db.Column(db.Integer(), db.ForeignKey('app_user.id'), nullable=False) - response_id = db.Column(db.Integer(), db.ForeignKey('response.id'), nullable=False) + review_form_id = db.Column( + db.Integer(), db.ForeignKey("review_form.id"), nullable=False + ) + reviewer_user_id = db.Column( + db.Integer(), db.ForeignKey("app_user.id"), nullable=False + ) + response_id = db.Column(db.Integer(), db.ForeignKey("response.id"), nullable=False) submitted_timestamp = db.Column(db.DateTime(), nullable=False) language = db.Column(db.String(2), nullable=False) is_submitted = db.Column(db.Boolean(), nullable=False) submitted_timestamp = db.Column(db.DateTime(), nullable=True) - review_form = db.relationship('ReviewForm', foreign_keys=[review_form_id]) - reviewer_user = db.relationship('AppUser', foreign_keys=[reviewer_user_id]) - response = db.relationship('Response', foreign_keys=[response_id]) - review_scores = db.relationship('ReviewScore', - primaryjoin="and_(ReviewResponse.id==ReviewScore.review_response_id," - "ReviewScore.is_active==True)") - - def __init__(self, - review_form_id, - reviewer_user_id, - response_id, - language): + review_form = db.relationship("ReviewForm", foreign_keys=[review_form_id]) + reviewer_user = db.relationship("AppUser", foreign_keys=[reviewer_user_id]) + response = db.relationship("Response", foreign_keys=[response_id]) + review_scores = db.relationship( + "ReviewScore", + primaryjoin="and_(ReviewResponse.id==ReviewScore.review_response_id," + "ReviewScore.is_active==True)", + ) + + def __init__(self, review_form_id, reviewer_user_id, response_id, language): self.review_form_id = review_form_id self.reviewer_user_id = reviewer_user_id self.response_id = response_id @@ -219,26 +246,35 @@ def submit(self): self.submitted_timestamp = datetime.now() def calculate_score(self): - return sum([ - misc.try_parse_float(score.value) * score.review_question.weight for score in self.review_scores - if score.review_question.weight > 0 - ]) + return sum( + [ + misc.try_parse_float(score.value) * score.review_question.weight + for score in self.review_scores + if score.review_question.weight > 0 + ] + ) class ReviewScore(db.Model): id = db.Column(db.Integer(), primary_key=True) - review_response_id = db.Column(db.Integer(), db.ForeignKey('review_response.id'), nullable=False) - review_question_id = db.Column(db.Integer(), db.ForeignKey('review_question.id'), nullable=False) + review_response_id = db.Column( + db.Integer(), db.ForeignKey("review_response.id"), nullable=False + ) + review_question_id = db.Column( + db.Integer(), db.ForeignKey("review_question.id"), nullable=False + ) value = db.Column(db.String(), nullable=False) is_active = db.Column(db.Boolean(), nullable=False) created_on = db.Column(db.DateTime, nullable=False, server_default=db.func.now()) - review_response = db.relationship('ReviewResponse', foreign_keys=[review_response_id]) - review_question = db.relationship('ReviewQuestion', foreign_keys=[review_question_id]) + review_response = db.relationship( + "ReviewResponse", foreign_keys=[review_response_id] + ) + review_question = db.relationship( + "ReviewQuestion", foreign_keys=[review_question_id] + ) - def __init__(self, - review_question_id, - value): + def __init__(self, review_question_id, value): self.review_question_id = review_question_id self.value = value self.is_active = True @@ -247,11 +283,17 @@ def __init__(self, class ReviewConfiguration(db.Model): id = db.Column(db.Integer(), primary_key=True) - review_form_id = db.Column(db.Integer(), db.ForeignKey('review_form.id'), nullable=False) + review_form_id = db.Column( + db.Integer(), db.ForeignKey("review_form.id"), nullable=False + ) num_reviews_required = db.Column(db.Integer(), nullable=False) num_optional_reviews = db.Column(db.Integer(), nullable=False) - drop_optional_question_id = db.Column(db.Integer(), db.ForeignKey('review_question.id'), nullable=True) + drop_optional_question_id = db.Column( + db.Integer(), db.ForeignKey("review_question.id"), nullable=True + ) drop_optional_agreement_values = db.Column(db.String(), nullable=True) - review_form = db.relationship('ReviewForm', foreign_keys=[review_form_id]) - review_question = db.relationship('ReviewQuestion', foreign_keys=[drop_optional_question_id]) + review_form = db.relationship("ReviewForm", foreign_keys=[review_form_id]) + review_question = db.relationship( + "ReviewQuestion", foreign_keys=[drop_optional_question_id] + ) diff --git a/api/app/reviews/repository.py b/api/app/reviews/repository.py index eca16e52f..f960b6c60 100644 --- a/api/app/reviews/repository.py +++ b/api/app/reviews/repository.py @@ -1,19 +1,30 @@ +from sqlalchemy import Date, and_, cast, func, or_ from sqlalchemy.sql import exists -from sqlalchemy import and_, or_, func, cast, Date + from app import db from app.applicationModel.models import ApplicationForm +from app.events.models import EventRole +from app.references.models import Reference from app.responses.models import Response, ResponseReviewer -from app.reviews.models import ReviewForm, ReviewResponse, ReviewScore, ReviewSection, ReviewSectionTranslation, ReviewQuestion, ReviewQuestionTranslation, ReviewConfiguration +from app.reviews.models import ( + ReviewConfiguration, + ReviewForm, + ReviewQuestion, + ReviewQuestionTranslation, + ReviewResponse, + ReviewScore, + ReviewSection, + ReviewSectionTranslation, +) from app.users.models import AppUser -from app.references.models import Reference -from app.events.models import EventRole from app.utils import misc -class ReviewRepository(): +class ReviewRepository: @staticmethod def count_reviews_allocated_and_completed_per_reviewer(event_id): - return db.engine.execute(""" + return db.engine.execute( + """ ( select app_user.id as reviewer_user_id, @@ -64,49 +75,59 @@ def count_reviews_allocated_and_completed_per_reviewer(event_id): group by app_user.id ) - """.format(event_id=event_id)) + """.format( + event_id=event_id + ) + ) @staticmethod def count_unassigned_reviews(event_id, required_reviews_per_response): - responses = ( + responses = ( db.session.query(func.count(Response.id)) - .join(ApplicationForm, Response.application_form_id ==ApplicationForm.id) + .join(ApplicationForm, Response.application_form_id == ApplicationForm.id) .filter(ApplicationForm.event_id == event_id) - .filter(Response.is_submitted==True, Response.is_withdrawn==False) + .filter(Response.is_submitted == True, Response.is_withdrawn == False) .first()[0] ) reviews = ( db.session.query(func.count(ResponseReviewer.id)) - .join(Response, ResponseReviewer.response_id ==Response.id) - .join(ApplicationForm, Response.application_form_id ==ApplicationForm.id) - .filter(ApplicationForm.event_id == event_id).first()[0] + .join(Response, ResponseReviewer.response_id == Response.id) + .join(ApplicationForm, Response.application_form_id == ApplicationForm.id) + .filter(ApplicationForm.event_id == event_id) + .first()[0] ) - return responses*required_reviews_per_response - reviews + return responses * required_reviews_per_response - reviews @staticmethod def get_review_form(event_id, stage=None): query = db.session.query(ReviewForm) - + if stage: query = query.filter_by(stage=stage) else: query = query.filter_by(active=True) - review_form = (query.join(ApplicationForm, ApplicationForm.id==ReviewForm.application_form_id) - .filter_by(event_id=event_id) - .first()) + review_form = ( + query.join( + ApplicationForm, ApplicationForm.id == ReviewForm.application_form_id + ) + .filter_by(event_id=event_id) + .first() + ) return review_form @staticmethod def get_review_forms(event_id): - review_forms = (db.session.query(ReviewForm) - .join(ApplicationForm, ApplicationForm.id==ReviewForm.application_form_id) + review_forms = ( + db.session.query(ReviewForm) + .join(ApplicationForm, ApplicationForm.id == ReviewForm.application_form_id) .filter_by(event_id=event_id) - .all()) - + .all() + ) + return review_forms @staticmethod @@ -117,12 +138,24 @@ def get_review_form_by_id(id): def get_remaining_reviews_count(reviewer_user_id, application_form_id): remaining = ( db.session.query(func.count(ResponseReviewer.id)) - .filter_by(reviewer_user_id=reviewer_user_id, active=True) - .join(Response) - .filter_by(is_withdrawn=False, application_form_id=application_form_id, is_submitted=True) - .outerjoin(ReviewResponse, and_(ReviewResponse.response_id==ResponseReviewer.response_id, ReviewResponse.reviewer_user_id==reviewer_user_id)) - .filter(or_(ReviewResponse.id == None, ReviewResponse.is_submitted == False)) - .all()[0][0] + .filter_by(reviewer_user_id=reviewer_user_id, active=True) + .join(Response) + .filter_by( + is_withdrawn=False, + application_form_id=application_form_id, + is_submitted=True, + ) + .outerjoin( + ReviewResponse, + and_( + ReviewResponse.response_id == ResponseReviewer.response_id, + ReviewResponse.reviewer_user_id == reviewer_user_id, + ), + ) + .filter( + or_(ReviewResponse.id == None, ReviewResponse.is_submitted == False) + ) + .all()[0][0] ) return remaining @@ -130,14 +163,26 @@ def get_remaining_reviews_count(reviewer_user_id, application_form_id): def get_response_to_review(skip, reviewer_user_id, application_form_id): response = ( db.session.query(Response) - .filter_by(is_withdrawn=False, application_form_id=application_form_id, is_submitted=True) - .join(ResponseReviewer) - .filter_by(reviewer_user_id=reviewer_user_id, active=True) - .outerjoin(ReviewResponse, and_(ReviewResponse.response_id==ResponseReviewer.response_id, ReviewResponse.reviewer_user_id==reviewer_user_id)) - .filter(or_(ReviewResponse.id == None, ReviewResponse.is_submitted == False)) - .order_by(ResponseReviewer.response_id) - .offset(skip) - .first() + .filter_by( + is_withdrawn=False, + application_form_id=application_form_id, + is_submitted=True, + ) + .join(ResponseReviewer) + .filter_by(reviewer_user_id=reviewer_user_id, active=True) + .outerjoin( + ReviewResponse, + and_( + ReviewResponse.response_id == ResponseReviewer.response_id, + ReviewResponse.reviewer_user_id == reviewer_user_id, + ), + ) + .filter( + or_(ReviewResponse.id == None, ReviewResponse.is_submitted == False) + ) + .order_by(ResponseReviewer.response_id) + .offset(skip) + .first() ) return response @@ -145,10 +190,10 @@ def get_response_to_review(skip, reviewer_user_id, application_form_id): def get_review_response_with_form(id, reviewer_user_id): review_form_response = ( db.session.query(ReviewForm, ReviewResponse) - .filter(ReviewForm.active == True) - .join(ReviewResponse) - .filter_by(id=id, reviewer_user_id=reviewer_user_id) - .first() + .filter(ReviewForm.active == True) + .join(ReviewResponse) + .filter_by(id=id, reviewer_user_id=reviewer_user_id) + .first() ) return review_form_response @@ -156,12 +201,16 @@ def get_review_response_with_form(id, reviewer_user_id): def get_response_by_review_response(id, reviewer_user_id, application_form_id): response = ( db.session.query(Response) - .filter_by(is_withdrawn=False, application_form_id=application_form_id, is_submitted=True) - .join(ResponseReviewer) - .filter_by(reviewer_user_id=reviewer_user_id) - .join(ReviewResponse) - .filter_by(id=id) - .first() + .filter_by( + is_withdrawn=False, + application_form_id=application_form_id, + is_submitted=True, + ) + .join(ResponseReviewer) + .filter_by(reviewer_user_id=reviewer_user_id) + .join(ReviewResponse) + .filter_by(id=id) + .first() ) return response @@ -169,8 +218,8 @@ def get_response_by_review_response(id, reviewer_user_id, application_form_id): def get_response_reviewer(response_id, reviewer_user_id): response_reviewer = ( db.session.query(ResponseReviewer) - .filter_by(response_id=response_id, reviewer_user_id=reviewer_user_id) - .first() + .filter_by(response_id=response_id, reviewer_user_id=reviewer_user_id) + .first() ) return response_reviewer @@ -178,18 +227,24 @@ def get_response_reviewer(response_id, reviewer_user_id): def get_review_response(review_form_id, response_id, reviewer_user_id): review_response = ( db.session.query(ReviewResponse) - .filter_by(review_form_id=review_form_id, response_id=response_id, reviewer_user_id=reviewer_user_id) - .first() + .filter_by( + review_form_id=review_form_id, + response_id=response_id, + reviewer_user_id=reviewer_user_id, + ) + .first() ) return review_response @staticmethod def get_response_by_reviewer(response_id, reviewer_user_id): - return (db.session.query(Response) - .filter_by(id=response_id) - .join(ResponseReviewer, Response.id == ResponseReviewer.response_id) - .filter_by(reviewer_user_id=reviewer_user_id) - .first()) + return ( + db.session.query(Response) + .filter_by(id=response_id) + .join(ResponseReviewer, Response.id == ResponseReviewer.response_id) + .filter_by(reviewer_user_id=reviewer_user_id) + .first() + ) @staticmethod def add_model(model): @@ -198,89 +253,128 @@ def add_model(model): @staticmethod def get_review_history(reviewer_user_id, event_id): - reviews = (db.session.query(ReviewResponse.id, ReviewResponse.submitted_timestamp, AppUser, Response) - .filter(ReviewResponse.reviewer_user_id == reviewer_user_id) - .join(ReviewForm, ReviewForm.id == ReviewResponse.review_form_id) - .filter(ReviewForm.application_form_id == event_id, ReviewForm.active == True) - .join(Response, ReviewResponse.response_id == Response.id) - .join(AppUser, Response.user_id == AppUser.id)) + reviews = ( + db.session.query( + ReviewResponse.id, ReviewResponse.submitted_timestamp, AppUser, Response + ) + .filter(ReviewResponse.reviewer_user_id == reviewer_user_id) + .join(ReviewForm, ReviewForm.id == ReviewResponse.review_form_id) + .filter( + ReviewForm.application_form_id == event_id, ReviewForm.active == True + ) + .join(Response, ReviewResponse.response_id == Response.id) + .join(AppUser, Response.user_id == AppUser.id) + ) return reviews @staticmethod def get_review_list(reviewer_user_id, event_id): - reviews = (db.session.query(Response, ReviewResponse) - .join(ResponseReviewer, Response.id == ResponseReviewer.response_id) - .filter_by(reviewer_user_id=reviewer_user_id) - .join(ApplicationForm, Response.application_form_id == ApplicationForm.id) - .filter_by(event_id=event_id) - .outerjoin(ReviewResponse, and_(Response.id == ReviewResponse.response_id, ReviewResponse.reviewer_user_id==reviewer_user_id))) + reviews = ( + db.session.query(Response, ReviewResponse) + .join(ResponseReviewer, Response.id == ResponseReviewer.response_id) + .filter_by(reviewer_user_id=reviewer_user_id) + .join(ApplicationForm, Response.application_form_id == ApplicationForm.id) + .filter_by(event_id=event_id) + .outerjoin( + ReviewResponse, + and_( + Response.id == ReviewResponse.response_id, + ReviewResponse.reviewer_user_id == reviewer_user_id, + ), + ) + ) return reviews @staticmethod def get_review_history_count(reviewer_user_id, event_id): - count = (db.session.query(ReviewResponse) - .filter(ReviewResponse.reviewer_user_id == reviewer_user_id) - .join(ReviewForm, ReviewForm.id == ReviewResponse.review_form_id) - .filter(ReviewForm.active == True) - .join(ApplicationForm, ReviewForm.application_form_id == ApplicationForm.id) - .filter(ApplicationForm.event_id == event_id) - .count()) + count = ( + db.session.query(ReviewResponse) + .filter(ReviewResponse.reviewer_user_id == reviewer_user_id) + .join(ReviewForm, ReviewForm.id == ReviewResponse.review_form_id) + .filter(ReviewForm.active == True) + .join(ApplicationForm, ReviewForm.application_form_id == ApplicationForm.id) + .filter(ApplicationForm.event_id == event_id) + .count() + ) return count @staticmethod def get_count_reviews_completed_for_event(event_id): - count = (db.session.query(ReviewResponse) - .join(ReviewForm, ReviewForm.id == ReviewResponse.review_form_id) - .filter(ReviewForm.active == True) - .join(ApplicationForm, ReviewForm.application_form_id == ApplicationForm.id) - .filter(ApplicationForm.event_id == event_id) - .count()) + count = ( + db.session.query(ReviewResponse) + .join(ReviewForm, ReviewForm.id == ReviewResponse.review_form_id) + .filter(ReviewForm.active == True) + .join(ApplicationForm, ReviewForm.application_form_id == ApplicationForm.id) + .filter(ApplicationForm.event_id == event_id) + .count() + ) return count @staticmethod def get_count_reviews_incomplete_for_event(event_id): - count = (db.session.query(ResponseReviewer) - .join(Response, ResponseReviewer.response_id == Response.id) - .join(ApplicationForm, Response.application_form_id == ApplicationForm.id) - .filter(ApplicationForm.event_id == event_id) - .join(ReviewForm, ApplicationForm.id == ReviewForm.application_form_id) - .filter(ReviewForm.active == True) - .outerjoin(ReviewResponse, and_( - ReviewResponse.review_form_id == ReviewForm.id, - ReviewResponse.reviewer_user_id == ResponseReviewer.reviewer_user_id)) - .filter(ReviewResponse.id == None) - .count()) + count = ( + db.session.query(ResponseReviewer) + .join(Response, ResponseReviewer.response_id == Response.id) + .join(ApplicationForm, Response.application_form_id == ApplicationForm.id) + .filter(ApplicationForm.event_id == event_id) + .join(ReviewForm, ApplicationForm.id == ReviewForm.application_form_id) + .filter(ReviewForm.active == True) + .outerjoin( + ReviewResponse, + and_( + ReviewResponse.review_form_id == ReviewForm.id, + ReviewResponse.reviewer_user_id + == ResponseReviewer.reviewer_user_id, + ), + ) + .filter(ReviewResponse.id == None) + .count() + ) return count @staticmethod def get_review_complete_timeseries_by_event(event_id): - timeseries = (db.session.query(cast(ReviewResponse.submitted_timestamp, Date), func.count(ReviewResponse.submitted_timestamp)) - .join(ReviewForm, ReviewForm.id == ReviewResponse.review_form_id) - .filter(ReviewForm.active == True) - .join(ApplicationForm, ReviewForm.application_form_id == ApplicationForm.id) - .filter(ApplicationForm.event_id == event_id) - .group_by(cast(ReviewResponse.submitted_timestamp, Date)) - .order_by(cast(ReviewResponse.submitted_timestamp, Date)) - .all()) + timeseries = ( + db.session.query( + cast(ReviewResponse.submitted_timestamp, Date), + func.count(ReviewResponse.submitted_timestamp), + ) + .join(ReviewForm, ReviewForm.id == ReviewResponse.review_form_id) + .filter(ReviewForm.active == True) + .join(ApplicationForm, ReviewForm.application_form_id == ApplicationForm.id) + .filter(ApplicationForm.event_id == event_id) + .group_by(cast(ReviewResponse.submitted_timestamp, Date)) + .order_by(cast(ReviewResponse.submitted_timestamp, Date)) + .all() + ) return timeseries @staticmethod def get_already_assigned(reviewer_user_id): - already_assigned = db.session.query(ResponseReviewer.response_id) \ - .filter(ResponseReviewer.reviewer_user_id == reviewer_user_id) \ + already_assigned = ( + db.session.query(ResponseReviewer.response_id) + .filter(ResponseReviewer.reviewer_user_id == reviewer_user_id) .all() + ) return already_assigned - @staticmethod def get_form_id(event_id): - form_id = db.session.query(ApplicationForm.id).filter_by(event_id=event_id).first()[0] + form_id = ( + db.session.query(ApplicationForm.id).filter_by(event_id=event_id).first()[0] + ) return form_id @staticmethod def deactivate_review(review_response): - scores = db.session.query(ReviewScore).filter(ReviewScore.review_response_id == review_response.id, - ReviewScore.is_active == True).all() + scores = ( + db.session.query(ReviewScore) + .filter( + ReviewScore.review_response_id == review_response.id, + ReviewScore.is_active == True, + ) + .all() + ) for score in scores: score.is_active = False db.session.merge(score) @@ -293,38 +387,48 @@ def get_reference_models(response_id): @staticmethod def get_candidate_response_ids(event_id, reviewer_user_id, reviews_required): - candidate_response_ids = db.session.query(Response.id) \ - .filter(Response.user_id != reviewer_user_id, - Response.is_submitted == True, - Response.is_withdrawn == False) \ - .join(ApplicationForm, Response.application_form_id == ApplicationForm.id) \ - .filter(ApplicationForm.event_id == event_id) \ - .outerjoin(ResponseReviewer, Response.id == ResponseReviewer.response_id) \ - .group_by(Response.id) \ - .having(func.count(ResponseReviewer.reviewer_user_id) < reviews_required) \ + candidate_response_ids = ( + db.session.query(Response.id) + .filter( + Response.user_id != reviewer_user_id, + Response.is_submitted == True, + Response.is_withdrawn == False, + ) + .join(ApplicationForm, Response.application_form_id == ApplicationForm.id) + .filter(ApplicationForm.event_id == event_id) + .outerjoin(ResponseReviewer, Response.id == ResponseReviewer.response_id) + .group_by(Response.id) + .having(func.count(ResponseReviewer.reviewer_user_id) < reviews_required) .all() + ) return candidate_response_ids def get_response_reviewers_for_event(event_id): - return (db.session.query(ResponseReviewer) - .join(Response, Response.id == ResponseReviewer.response_id) - .join(ApplicationForm, Response.application_form_id == ApplicationForm.id) - .filter_by(event_id=event_id) - .order_by(ResponseReviewer.response_id) - .all()) + return ( + db.session.query(ResponseReviewer) + .join(Response, Response.id == ResponseReviewer.response_id) + .join(ApplicationForm, Response.application_form_id == ApplicationForm.id) + .filter_by(event_id=event_id) + .order_by(ResponseReviewer.response_id) + .all() + ) @staticmethod def get_review_responses_for_event(event_id): - return (db.session.query(ReviewResponse) - .join(ReviewForm, ReviewResponse.review_form_id == ReviewForm.id) - .filter_by(active=True) - .join(ApplicationForm, ReviewForm.application_form_id == ApplicationForm.id) - .filter_by(event_id=event_id) - .all()) + return ( + db.session.query(ReviewResponse) + .join(ReviewForm, ReviewResponse.review_form_id == ReviewForm.id) + .filter_by(active=True) + .join(ApplicationForm, ReviewForm.application_form_id == ApplicationForm.id) + .filter_by(event_id=event_id) + .all() + ) @staticmethod def delete_response_reviewer(response_id, reviewer_user_id): - db.session.query(ResponseReviewer).filter_by(response_id=response_id, reviewer_user_id=reviewer_user_id).delete() + db.session.query(ResponseReviewer).filter_by( + response_id=response_id, reviewer_user_id=reviewer_user_id + ).delete() db.session.commit() @staticmethod @@ -339,17 +443,25 @@ def get_all_review_responses_by_event(event_id): ) @staticmethod - def get_average_score_for_review_question(response_id: int, review_question_id: int): + def get_average_score_for_review_question( + response_id: int, review_question_id: int + ): review_score_values = ( db.session.query(ReviewScore.value) - .filter(ReviewScore.review_question_id == review_question_id, ReviewScore.is_active == True) + .filter( + ReviewScore.review_question_id == review_question_id, + ReviewScore.is_active == True, + ) .join(ReviewResponse, ReviewResponse.id == ReviewScore.review_response_id) .join(Response, Response.id == ReviewResponse.response_id) .filter_by(id=response_id) .all() ) - review_score_values = [misc.try_parse_float(review_score_value[0]) for review_score_value in review_score_values] + review_score_values = [ + misc.try_parse_float(review_score_value[0]) + for review_score_value in review_score_values + ] if len(review_score_values) == 0: average_review_score = 0 @@ -364,19 +476,24 @@ def get_all_review_forms_for_event(event_id): db.session.query(ReviewForm) .join(ApplicationForm, ApplicationForm.id == ReviewForm.application_form_id) .filter_by(event_id=event_id) - .all()) + .all() + ) return forms - + @staticmethod def delete_review_question(question_to_delete: ReviewQuestion): - db.session.query(ReviewQuestionTranslation).filter_by(review_question_id=question_to_delete.id).delete() + db.session.query(ReviewQuestionTranslation).filter_by( + review_question_id=question_to_delete.id + ).delete() db.session.query(ReviewQuestion).filter_by(id=question_to_delete.id).delete() db.session.commit() - + @staticmethod def delete_review_section(section: ReviewSection): - db.session.query(ReviewSectionTranslation).filter_by(review_section_id=section.id).delete() + db.session.query(ReviewSectionTranslation).filter_by( + review_section_id=section.id + ).delete() for question in section.review_questions: ReviewRepository.delete_review_question(question) @@ -384,22 +501,25 @@ def delete_review_section(section: ReviewSection): db.session.query(ReviewSection).filter_by(id=section.id).delete() db.session.commit() -class ReviewConfigurationRepository(): +class ReviewConfigurationRepository: @staticmethod def get_configuration_for_form(review_form_id): - config = (db.session.query(ReviewConfiguration) - .filter_by(review_form_id=review_form_id) - .first()) + config = ( + db.session.query(ReviewConfiguration) + .filter_by(review_form_id=review_form_id) + .first() + ) return config @staticmethod def get_configuration_for_event(event_id): - config = (db.session.query(ReviewConfiguration) - .join(ReviewForm, ReviewConfiguration.review_form_id == ReviewForm.id) - .filter_by(active=True) - .join(ApplicationForm, ReviewForm.application_form_id == ApplicationForm.id) - .filter(ApplicationForm.event_id == event_id) - .first()) + config = ( + db.session.query(ReviewConfiguration) + .join(ReviewForm, ReviewConfiguration.review_form_id == ReviewForm.id) + .filter_by(active=True) + .join(ApplicationForm, ReviewForm.application_form_id == ApplicationForm.id) + .filter(ApplicationForm.event_id == event_id) + .first() + ) return config - diff --git a/api/app/reviews/tests.py b/api/app/reviews/tests.py index c22ae11ea..d6f5f5b19 100644 --- a/api/app/reviews/tests.py +++ b/api/app/reviews/tests.py @@ -1,130 +1,212 @@ -from datetime import datetime -import json import copy import itertools +import json +from datetime import datetime -from app import db, LOGGER -from app.utils.testing import ApiTestCase -from app.events.models import Event, EventRole -from app.users.models import AppUser, UserCategory, Country -from app.applicationModel.models import ApplicationForm, Question, Section -from app.responses.models import Response, Answer, ResponseReviewer - -from app.references.models import ReferenceRequest, Reference -from app.references.repository import ReferenceRequestRepository as reference_request_repository -from app.reviews.models import ReviewForm, ReviewQuestion, ReviewResponse, ReviewScore, ReviewConfiguration - -from app.reviews.models import ReviewForm, ReviewQuestion, ReviewQuestionTranslation, ReviewResponse, ReviewScore, ReviewConfiguration -from app.utils.errors import REVIEW_RESPONSE_NOT_FOUND, FORBIDDEN, USER_NOT_FOUND from nose.plugins.skip import SkipTest +from parameterized import parameterized + +from app import LOGGER, db +from app.applicationModel.models import ApplicationForm, Question, Section +from app.events.models import Event, EventRole from app.organisation.models import Organisation +from app.references.models import Reference, ReferenceRequest +from app.references.repository import ( + ReferenceRequestRepository as reference_request_repository, +) +from app.responses.models import Answer, Response, ResponseReviewer +from app.reviews.models import ( + ReviewConfiguration, + ReviewForm, + ReviewQuestion, + ReviewQuestionTranslation, + ReviewResponse, + ReviewScore, +) +from app.users.models import AppUser, Country, UserCategory +from app.utils.errors import FORBIDDEN, REVIEW_RESPONSE_NOT_FOUND, USER_NOT_FOUND +from app.utils.testing import ApiTestCase -from parameterized import parameterized class ReviewsApiTest(ApiTestCase): - def seed_static_data(self): - self.add_organisation('Deep Learning Indaba 2019', 'blah.png', 'blah_big.png') - self.add_organisation('Deep Learning Indaba 2020', 'blah.png', 'blah_big.png') + self.add_organisation("Deep Learning Indaba 2019", "blah.png", "blah_big.png") + self.add_organisation("Deep Learning Indaba 2020", "blah.png", "blah_big.png") user_categories = [ - UserCategory('Honours'), - UserCategory('Student'), - UserCategory('MSc'), - UserCategory('PhD') + UserCategory("Honours"), + UserCategory("Student"), + UserCategory("MSc"), + UserCategory("PhD"), ] db.session.add_all(user_categories) db.session.commit() countries = [ - Country('Egypt'), - Country('Botswana'), - Country('Namibia'), - Country('Zimbabwe'), - Country('Mozambique'), - Country('Ghana'), - Country('Nigeria') + Country("Egypt"), + Country("Botswana"), + Country("Namibia"), + Country("Zimbabwe"), + Country("Mozambique"), + Country("Ghana"), + Country("Nigeria"), ] db.session.add_all(countries) db.session.commit() - reviewer1 = AppUser('r1@r.com', 'reviewer', '1', 'Mr', password='abc', organisation_id=1,) - reviewer2 = AppUser('r2@r.com', 'reviewer', '2', 'Ms', password='abc', organisation_id=1,) - reviewer3 = AppUser('r3@r.com', 'reviewer', '3', 'Mr', password='abc', organisation_id=1,) - reviewer4 = AppUser('r4@r.com', 'reviewer', '4', 'Ms', password='abc', organisation_id=1,) - candidate1 = AppUser('c1@c.com', 'candidate', '1', 'Mr', password='abc', organisation_id=1,) - candidate2 = AppUser('c2@c.com', 'candidate', '2', 'Ms', password='abc', organisation_id=1,) - candidate3 = AppUser('c3@c.com', 'candidate', '3', 'Mr', password='abc', organisation_id=1,) - candidate4 = AppUser('c4@c.com', 'candidate', '4', 'Ms', password='abc', organisation_id=1,) - system_admin = AppUser('sa@sa.com', 'system_admin', '1', 'Ms', password='abc', organisation_id=1, is_admin=True) - event_admin = AppUser('ea@ea.com', 'event_admin', '1', 'Ms', password='abc',organisation_id=1) - users = [reviewer1, reviewer2, reviewer3, reviewer4, candidate1, candidate2, candidate3, candidate4, system_admin, event_admin] + reviewer1 = AppUser( + "r1@r.com", + "reviewer", + "1", + "Mr", + password="abc", + organisation_id=1, + ) + reviewer2 = AppUser( + "r2@r.com", + "reviewer", + "2", + "Ms", + password="abc", + organisation_id=1, + ) + reviewer3 = AppUser( + "r3@r.com", + "reviewer", + "3", + "Mr", + password="abc", + organisation_id=1, + ) + reviewer4 = AppUser( + "r4@r.com", + "reviewer", + "4", + "Ms", + password="abc", + organisation_id=1, + ) + candidate1 = AppUser( + "c1@c.com", + "candidate", + "1", + "Mr", + password="abc", + organisation_id=1, + ) + candidate2 = AppUser( + "c2@c.com", + "candidate", + "2", + "Ms", + password="abc", + organisation_id=1, + ) + candidate3 = AppUser( + "c3@c.com", + "candidate", + "3", + "Mr", + password="abc", + organisation_id=1, + ) + candidate4 = AppUser( + "c4@c.com", + "candidate", + "4", + "Ms", + password="abc", + organisation_id=1, + ) + system_admin = AppUser( + "sa@sa.com", + "system_admin", + "1", + "Ms", + password="abc", + organisation_id=1, + is_admin=True, + ) + event_admin = AppUser( + "ea@ea.com", "event_admin", "1", "Ms", password="abc", organisation_id=1 + ) + users = [ + reviewer1, + reviewer2, + reviewer3, + reviewer4, + candidate1, + candidate2, + candidate3, + candidate4, + system_admin, + event_admin, + ] for user in users: user.verify() db.session.add_all(users) db.session.commit() events = [ - self.add_event({'en': 'indaba 2019'}, {'en': 'The Deep Learning Indaba 2019, Kenyatta University, Nairobi, Kenya '}, datetime(2019, 8, 25), datetime(2019, 8, 31), - 'KENYADABA2019'), - self.add_event({'en': 'indaba 2020'}, {'en': 'The Deep Learning Indaba 2018, Stellenbosch University, South Africa'}, datetime(2018, 9, 9), datetime(2018, 9, 15), - 'INDABA2020', 2) + self.add_event( + {"en": "indaba 2019"}, + { + "en": "The Deep Learning Indaba 2019, Kenyatta University, Nairobi, Kenya " + }, + datetime(2019, 8, 25), + datetime(2019, 8, 31), + "KENYADABA2019", + ), + self.add_event( + {"en": "indaba 2020"}, + { + "en": "The Deep Learning Indaba 2018, Stellenbosch University, South Africa" + }, + datetime(2018, 9, 9), + datetime(2018, 9, 15), + "INDABA2020", + 2, + ), ] db.session.commit() - event_roles = [ - EventRole('admin', 10, 1), - EventRole('reviewer', 3, 1) - ] + event_roles = [EventRole("admin", 10, 1), EventRole("reviewer", 3, 1)] db.session.add_all(event_roles) db.session.commit() application_forms = [ self.create_application_form(1, True, False), - self.create_application_form(2, False, False) + self.create_application_form(2, False, False), ] db.session.add_all(application_forms) db.session.commit() - sections = [ - Section(1, 1), - Section(2, 1) - ] + sections = [Section(1, 1), Section(2, 1)] db.session.add_all(sections) db.session.commit() options = [ - { - "value": "indaba-2017", - "label": "Yes, I attended the 2017 Indaba" - }, - { - "value": "indaba-2018", - "label": "Yes, I attended the 2018 Indaba" - }, - { - "value": "indaba-2017-2018", - "label": "Yes, I attended both Indabas" - }, - { - "value": "none", - "label": "No" - } + {"value": "indaba-2017", "label": "Yes, I attended the 2017 Indaba"}, + {"value": "indaba-2018", "label": "Yes, I attended the 2018 Indaba"}, + {"value": "indaba-2017-2018", "label": "Yes, I attended both Indabas"}, + {"value": "none", "label": "No"}, ] questions = [ - Question(1, 1, 1, 'long_text'), - Question(1, 1, 2, 'long_text'), - Question(2, 2, 1, 'long_text'), - Question(2, 2, 2, 'long_text'), - Question(1, 1, 3, 'multi-choice') + Question(1, 1, 1, "long_text"), + Question(1, 1, 2, "long_text"), + Question(2, 2, 1, "long_text"), + Question(2, 2, 2, "long_text"), + Question(1, 1, 3, "multi-choice"), ] db.session.add_all(questions) db.session.commit() - self.add_question_translation(1, 'en', 'Question 1') - self.add_question_translation(2, 'en', 'Question 2') - self.add_question_translation(3, 'en', 'Question 3') - self.add_question_translation(4, 'en', 'Question 4') - self.add_question_translation(5, 'en', 'Did you attend the 2017 or 2018 Indaba', options=options) + self.add_question_translation(1, "en", "Question 1") + self.add_question_translation(2, "en", "Question 2") + self.add_question_translation(3, "en", "Question 3") + self.add_question_translation(4, "en", "Question 4") + self.add_question_translation( + 5, "en", "Did you attend the 2017 or 2018 Indaba", options=options + ) closed_review = ReviewForm(2, datetime(2018, 4, 30), 1, True) closed_review.close() @@ -133,70 +215,105 @@ def seed_static_data(self): ReviewForm(1, datetime(2019, 4, 30), 1, True), ReviewForm(1, datetime(2019, 4, 30), 2, False), closed_review, - inactive_review + inactive_review, ] db.session.add_all(review_forms) db.session.commit() review_configs = [ - ReviewConfiguration(review_form_id=review_forms[0].id, num_reviews_required=3, num_optional_reviews=0), - ReviewConfiguration(review_form_id=review_forms[1].id, num_reviews_required=3, num_optional_reviews=0) + ReviewConfiguration( + review_form_id=review_forms[0].id, + num_reviews_required=3, + num_optional_reviews=0, + ), + ReviewConfiguration( + review_form_id=review_forms[1].id, + num_reviews_required=3, + num_optional_reviews=0, + ), ] db.session.add_all(review_configs) db.session.commit() review_section1 = self.add_review_section(review_forms[0].id) - self.add_review_section_translation(review_section1.id, 'en', 'Review Section 1 English', 'Review Section 1 Description English') - self.add_review_section_translation(review_section1.id, 'fr', 'Review Section 1 French', 'Review Section 1 Description French') + self.add_review_section_translation( + review_section1.id, + "en", + "Review Section 1 English", + "Review Section 1 Description English", + ) + self.add_review_section_translation( + review_section1.id, + "fr", + "Review Section 1 French", + "Review Section 1 Description French", + ) review_section2 = self.add_review_section(review_forms[1].id) - self.add_review_section_translation(review_section2.id, 'en', 'Review Section 2', 'Review Section 2 Description') + self.add_review_section_translation( + review_section2.id, "en", "Review Section 2", "Review Section 2 Description" + ) review_questions = [ - ReviewQuestion(review_section1.id, 1, 'multi-choice', True, 1, 0), - ReviewQuestion(review_section1.id, 2, 'multi-choice', True, 2, 0), - ReviewQuestion(review_section2.id, 3, 'multi-choice', True, 1, 0), - ReviewQuestion(review_section2.id, 4, 'information', False, 2, 0) + ReviewQuestion(review_section1.id, 1, "multi-choice", True, 1, 0), + ReviewQuestion(review_section1.id, 2, "multi-choice", True, 2, 0), + ReviewQuestion(review_section2.id, 3, "multi-choice", True, 1, 0), + ReviewQuestion(review_section2.id, 4, "information", False, 2, 0), ] db.session.add_all(review_questions) db.session.commit() review_question_translations = [ - ReviewQuestionTranslation(review_questions[0].id, 'en'), - ReviewQuestionTranslation(review_questions[0].id, 'fr'), - ReviewQuestionTranslation(review_questions[1].id, 'en', headline='English Headline', description='English Description', placeholder='English Placeholder', options=[{'label': 'en1', 'value': 'en'}], validation_regex='EN Regex', validation_text='EN Validation Message'), - ReviewQuestionTranslation(review_questions[1].id, 'fr', headline='French Headline', description='French Description', placeholder='French Placeholder', options=[{'label': 'fr1', 'value': 'fr'}], validation_regex='FR Regex', validation_text='FR Validation Message'), - ReviewQuestionTranslation(review_questions[2].id, 'en'), - ReviewQuestionTranslation(review_questions[2].id, 'fr'), - ReviewQuestionTranslation(review_questions[3].id, 'en'), - ReviewQuestionTranslation(review_questions[3].id, 'fr'), + ReviewQuestionTranslation(review_questions[0].id, "en"), + ReviewQuestionTranslation(review_questions[0].id, "fr"), + ReviewQuestionTranslation( + review_questions[1].id, + "en", + headline="English Headline", + description="English Description", + placeholder="English Placeholder", + options=[{"label": "en1", "value": "en"}], + validation_regex="EN Regex", + validation_text="EN Validation Message", + ), + ReviewQuestionTranslation( + review_questions[1].id, + "fr", + headline="French Headline", + description="French Description", + placeholder="French Placeholder", + options=[{"label": "fr1", "value": "fr"}], + validation_regex="FR Regex", + validation_text="FR Validation Message", + ), + ReviewQuestionTranslation(review_questions[2].id, "en"), + ReviewQuestionTranslation(review_questions[2].id, "fr"), + ReviewQuestionTranslation(review_questions[3].id, "en"), + ReviewQuestionTranslation(review_questions[3].id, "fr"), ] db.session.add_all(review_question_translations) db.session.commit() - self.add_email_template('reviews-assigned') + self.add_email_template("reviews-assigned") def get_auth_header_for(self, email): - body = { - 'email': email, - 'password': 'abc' - } - response = self.app.post('api/v1/authenticate', data=body) + body = {"email": email, "password": "abc"} + response = self.app.post("api/v1/authenticate", data=body) data = json.loads(response.data) - header = {'Authorization': data['token']} + header = {"Authorization": data["token"]} return header def setup_one_reviewer_one_candidate(self, active=True): response = self.add_response(1, 5, is_submitted=True) answers = [ - Answer(1, 1, 'I will learn alot.'), - Answer(1, 2, 'I will share by doing talks.') + Answer(1, 1, "I will learn alot."), + Answer(1, 2, "I will share by doing talks."), ] db.session.add_all(answers) db.session.commit() response_reviewers = [ - ResponseReviewer(1, 1) # assign reviewer 1 to candidate 1 response + ResponseReviewer(1, 1) # assign reviewer 1 to candidate 1 response ] if not active: response_reviewers[0].deactivate() @@ -206,46 +323,46 @@ def setup_one_reviewer_one_candidate(self, active=True): def test_one_reviewer_one_candidate(self): self.seed_static_data() self.setup_one_reviewer_one_candidate() - header = self.get_auth_header_for('r1@r.com') - params = {'event_id': 1, 'language': 'en'} + header = self.get_auth_header_for("r1@r.com") + params = {"event_id": 1, "language": "en"} - response = self.app.get('/api/v1/review', headers=header, data=params) + response = self.app.get("/api/v1/review", headers=header, data=params) data = json.loads(response.data) - self.assertEqual(data['reviews_remaining_count'], 1) + self.assertEqual(data["reviews_remaining_count"], 1) def test_one_reviewer_one_candidate_inactive(self): self.seed_static_data() self.setup_one_reviewer_one_candidate(active=False) - header = self.get_auth_header_for('r1@r.com') - params = {'event_id': 1, 'language': 'en'} + header = self.get_auth_header_for("r1@r.com") + params = {"event_id": 1, "language": "en"} - response = self.app.get('/api/v1/review', headers=header, data=params) + response = self.app.get("/api/v1/review", headers=header, data=params) data = json.loads(response.data) - self.assertEqual(data['reviews_remaining_count'], 0) - self.assertEqual(data['response']['id'], 0) + self.assertEqual(data["reviews_remaining_count"], 0) + self.assertEqual(data["response"]["id"], 0) - @parameterized.expand([ - (True,), (False,) - ]) + @parameterized.expand([(True,), (False,)]) def test_one_reviewer_one_candidate_review_summary(self, active): self.seed_static_data() self.setup_one_reviewer_one_candidate(active=active) - header = self.get_auth_header_for('ea@ea.com') - params = {'event_id': 1} + header = self.get_auth_header_for("ea@ea.com") + params = {"event_id": 1} - response = self.app.get('/api/v1/reviewassignment/summary', headers=header, data=params) + response = self.app.get( + "/api/v1/reviewassignment/summary", headers=header, data=params + ) data = json.loads(response.data) - self.assertEqual(data['reviews_unallocated'], 2) + self.assertEqual(data["reviews_unallocated"], 2) def setup_responses_and_no_reviewers(self): response = self.add_response(1, 5, is_submitted=True) answers = [ - Answer(1, 1, 'I will learn alot.'), - Answer(1, 2, 'I will share by doing talks.') + Answer(1, 1, "I will learn alot."), + Answer(1, 2, "I will share by doing talks."), ] db.session.add_all(answers) db.session.commit() @@ -253,23 +370,25 @@ def setup_responses_and_no_reviewers(self): def test_no_response_reviewers(self): self.seed_static_data() self.setup_responses_and_no_reviewers() - header = self.get_auth_header_for('r1@r.com') - params = {'event_id': 1, 'language': 'en'} + header = self.get_auth_header_for("r1@r.com") + params = {"event_id": 1, "language": "en"} - response = self.app.get('/api/v1/review', headers=header, data=params) + response = self.app.get("/api/v1/review", headers=header, data=params) data = json.loads(response.data) - self.assertEqual(data['reviews_remaining_count'], 0) + self.assertEqual(data["reviews_remaining_count"], 0) def test_no_response_reviewers_reviews_unallocated(self): self.seed_static_data() self.setup_responses_and_no_reviewers() - header = self.get_auth_header_for('ea@ea.com') - params = {'event_id': 1} - response = self.app.get('/api/v1/reviewassignment/summary', headers=header, data=params) + header = self.get_auth_header_for("ea@ea.com") + params = {"event_id": 1} + response = self.app.get( + "/api/v1/reviewassignment/summary", headers=header, data=params + ) data = json.loads(response.data) - self.assertEqual(data['reviews_unallocated'], 3) + self.assertEqual(data["reviews_unallocated"], 3) def setup_one_reviewer_three_candidates(self): self.add_response(application_form_id=1, user_id=5, is_submitted=True) @@ -277,12 +396,12 @@ def setup_one_reviewer_three_candidates(self): self.add_response(application_form_id=1, user_id=7, is_submitted=True) answers = [ - Answer(1, 1, 'I will learn alot.'), - Answer(1, 2, 'I will share by doing talks.'), - Answer(2, 1, 'I want to do a PhD.'), - Answer(2, 2, 'I will share by writing a blog.'), - Answer(3, 1, 'I want to solve new problems.'), - Answer(3, 2, 'I will share by tutoring.'), + Answer(1, 1, "I will learn alot."), + Answer(1, 2, "I will share by doing talks."), + Answer(2, 1, "I want to do a PhD."), + Answer(2, 2, "I will share by writing a blog."), + Answer(3, 1, "I want to solve new problems."), + Answer(3, 2, "I will share by tutoring."), ] db.session.add_all(answers) db.session.commit() @@ -290,7 +409,7 @@ def setup_one_reviewer_three_candidates(self): response_reviewers = [ ResponseReviewer(1, 1), ResponseReviewer(2, 1), - ResponseReviewer(3, 1) + ResponseReviewer(3, 1), ] response_reviewers[1].deactivate() @@ -300,13 +419,13 @@ def setup_one_reviewer_three_candidates(self): def test_one_reviewer_three_candidates(self): self.seed_static_data() self.setup_one_reviewer_three_candidates() - header = self.get_auth_header_for('r1@r.com') - params = {'event_id': 1, 'language': 'en'} + header = self.get_auth_header_for("r1@r.com") + params = {"event_id": 1, "language": "en"} - response = self.app.get('/api/v1/review', headers=header, data=params) + response = self.app.get("/api/v1/review", headers=header, data=params) data = json.loads(response.data) - self.assertEqual(data['reviews_remaining_count'], 2) + self.assertEqual(data["reviews_remaining_count"], 2) def setup_one_reviewer_three_candidates_and_one_completed_review(self): self.add_response(1, 5, is_submitted=True) @@ -314,12 +433,12 @@ def setup_one_reviewer_three_candidates_and_one_completed_review(self): self.add_response(1, 7, is_submitted=True) answers = [ - Answer(1, 1, 'I will learn alot.'), - Answer(1, 2, 'I will share by doing talks.'), - Answer(2, 1, 'I want to do a PhD.'), - Answer(2, 2, 'I will share by writing a blog.'), - Answer(3, 1, 'I want to solve new problems.'), - Answer(3, 2, 'I will share by tutoring.') + Answer(1, 1, "I will learn alot."), + Answer(1, 2, "I will share by doing talks."), + Answer(2, 1, "I want to do a PhD."), + Answer(2, 2, "I will share by writing a blog."), + Answer(3, 1, "I want to solve new problems."), + Answer(3, 2, "I will share by tutoring."), ] db.session.add_all(answers) db.session.commit() @@ -327,12 +446,12 @@ def setup_one_reviewer_three_candidates_and_one_completed_review(self): response_reviewers = [ ResponseReviewer(1, 1), ResponseReviewer(2, 1), - ResponseReviewer(3, 1) + ResponseReviewer(3, 1), ] db.session.add_all(response_reviewers) db.session.commit() - review_response = ReviewResponse(1, 1, 1, 'en') + review_response = ReviewResponse(1, 1, 1, "en") review_response.submit() db.session.add(review_response) db.session.commit() @@ -340,26 +459,28 @@ def setup_one_reviewer_three_candidates_and_one_completed_review(self): def test_one_reviewer_three_candidates_and_one_completed_review(self): self.seed_static_data() self.setup_one_reviewer_three_candidates_and_one_completed_review() - header = self.get_auth_header_for('r1@r.com') - params = {'event_id': 1, 'language': 'en'} + header = self.get_auth_header_for("r1@r.com") + params = {"event_id": 1, "language": "en"} - response = self.app.get('/api/v1/review', headers=header, data=params) + response = self.app.get("/api/v1/review", headers=header, data=params) data = json.loads(response.data) - self.assertEqual(data['reviews_remaining_count'], 2) + self.assertEqual(data["reviews_remaining_count"], 2) - def setup_one_reviewer_three_candidates_with_one_withdrawn_response_and_one_unsubmitted_response(self): + def setup_one_reviewer_three_candidates_with_one_withdrawn_response_and_one_unsubmitted_response( + self, + ): withdrawn_response = self.add_response(1, 5, is_withdrawn=True) submitted_response = self.add_response(1, 7, is_submitted=True) self.add_response(1, 6) answers = [ - Answer(1, 1, 'I will learn alot.'), - Answer(1, 2, 'I will share by doing talks.'), - Answer(2, 1, 'I want to do a PhD.'), - Answer(2, 2, 'I will share by writing a blog.'), - Answer(3, 1, 'I want to solve new problems.'), - Answer(3, 2, 'I will share by tutoring.') + Answer(1, 1, "I will learn alot."), + Answer(1, 2, "I will share by doing talks."), + Answer(2, 1, "I want to do a PhD."), + Answer(2, 2, "I will share by writing a blog."), + Answer(3, 1, "I want to solve new problems."), + Answer(3, 2, "I will share by tutoring."), ] db.session.add_all(answers) db.session.commit() @@ -367,37 +488,41 @@ def setup_one_reviewer_three_candidates_with_one_withdrawn_response_and_one_unsu response_reviewers = [ ResponseReviewer(1, 1), ResponseReviewer(2, 1), - ResponseReviewer(3, 1) + ResponseReviewer(3, 1), ] db.session.add_all(response_reviewers) db.session.commit() - def test_one_reviewer_three_candidates_with_one_withdrawn_response_and_one_unsubmitted_response(self): + def test_one_reviewer_three_candidates_with_one_withdrawn_response_and_one_unsubmitted_response( + self, + ): self.seed_static_data() self.setup_one_reviewer_three_candidates_with_one_withdrawn_response_and_one_unsubmitted_response() - header = self.get_auth_header_for('r1@r.com') - params = {'event_id': 1, 'language': 'en'} + header = self.get_auth_header_for("r1@r.com") + params = {"event_id": 1, "language": "en"} - response = self.app.get('/api/v1/review', headers=header, data=params) + response = self.app.get("/api/v1/review", headers=header, data=params) data = json.loads(response.data) - self.assertEqual(data['reviews_remaining_count'], 1) + self.assertEqual(data["reviews_remaining_count"], 1) - def setup_multiple_reviewers_with_different_subsets_of_candidates_and_reviews_completed(self): + def setup_multiple_reviewers_with_different_subsets_of_candidates_and_reviews_completed( + self, + ): self.add_response(1, 5, is_submitted=True) self.add_response(1, 6, is_submitted=True) self.add_response(1, 7, is_submitted=True) self.add_response(1, 8, is_submitted=True) answers = [ - Answer(1, 1, 'I will learn alot.'), - Answer(1, 2, 'I will share by doing talks.'), - Answer(2, 1, 'I want to do a PhD.'), - Answer(2, 2, 'I will share by writing a blog.'), - Answer(3, 1, 'I want to solve new problems.'), - Answer(3, 2, 'I will share by tutoring.'), - Answer(4, 1, 'I want to exchange ideas with like minded people'), - Answer(4, 2, 'I will mentor people interested in ML.') + Answer(1, 1, "I will learn alot."), + Answer(1, 2, "I will share by doing talks."), + Answer(2, 1, "I want to do a PhD."), + Answer(2, 2, "I will share by writing a blog."), + Answer(3, 1, "I want to solve new problems."), + Answer(3, 2, "I will share by tutoring."), + Answer(4, 1, "I want to exchange ideas with like minded people"), + Answer(4, 2, "I will mentor people interested in ML."), ] db.session.add_all(answers) db.session.commit() @@ -406,27 +531,24 @@ def setup_multiple_reviewers_with_different_subsets_of_candidates_and_reviews_co ResponseReviewer(1, 1), ResponseReviewer(2, 1), ResponseReviewer(3, 1), - ResponseReviewer(2, 2), ResponseReviewer(3, 2), - ResponseReviewer(1, 3), ResponseReviewer(2, 3), ResponseReviewer(3, 3), ResponseReviewer(4, 3), - ResponseReviewer(1, 4), - ResponseReviewer(2, 4) + ResponseReviewer(2, 4), ] response_reviewers[-1].deactivate() db.session.add_all(response_reviewers) db.session.commit() review_responses = [ - ReviewResponse(1, 2, 2, 'en'), - ReviewResponse(1, 3, 1, 'en'), - ReviewResponse(1, 3, 2, 'en'), - ReviewResponse(1, 4, 1, 'en') + ReviewResponse(1, 2, 2, "en"), + ReviewResponse(1, 3, 1, "en"), + ReviewResponse(1, 3, 2, "en"), + ReviewResponse(1, 4, 1, "en"), ] for rr in review_responses: rr.submit() @@ -434,52 +556,58 @@ def setup_multiple_reviewers_with_different_subsets_of_candidates_and_reviews_co db.session.add_all(review_responses) db.session.commit() - def test_multiple_reviewers_with_different_subsets_of_candidates_and_reviews_completed(self): + def test_multiple_reviewers_with_different_subsets_of_candidates_and_reviews_completed( + self, + ): self.seed_static_data() self.setup_multiple_reviewers_with_different_subsets_of_candidates_and_reviews_completed() - params = {'event_id': 1, 'language': 'en'} + params = {"event_id": 1, "language": "en"} - header = self.get_auth_header_for('r1@r.com') - response1 = self.app.get('/api/v1/review', headers=header, data=params) + header = self.get_auth_header_for("r1@r.com") + response1 = self.app.get("/api/v1/review", headers=header, data=params) data1 = json.loads(response1.data) - header = self.get_auth_header_for('r2@r.com') - response2 = self.app.get('/api/v1/review', headers=header, data=params) + header = self.get_auth_header_for("r2@r.com") + response2 = self.app.get("/api/v1/review", headers=header, data=params) data2 = json.loads(response2.data) - header = self.get_auth_header_for('r3@r.com') - response3 = self.app.get('/api/v1/review', headers=header, data=params) + header = self.get_auth_header_for("r3@r.com") + response3 = self.app.get("/api/v1/review", headers=header, data=params) data3 = json.loads(response3.data) - header = self.get_auth_header_for('r4@r.com') - response4 = self.app.get('/api/v1/review', headers=header, data=params) + header = self.get_auth_header_for("r4@r.com") + response4 = self.app.get("/api/v1/review", headers=header, data=params) data4 = json.loads(response4.data) - self.assertEqual(data1['reviews_remaining_count'], 3) - self.assertEqual(data2['reviews_remaining_count'], 1) - self.assertEqual(data3['reviews_remaining_count'], 2) - self.assertEqual(data4['reviews_remaining_count'], 0) + self.assertEqual(data1["reviews_remaining_count"], 3) + self.assertEqual(data2["reviews_remaining_count"], 1) + self.assertEqual(data3["reviews_remaining_count"], 2) + self.assertEqual(data4["reviews_remaining_count"], 0) def test_skipping(self): self.seed_static_data() self.setup_one_reviewer_three_candidates() - params = {'event_id': 1, 'skip': 1, 'language': 'en'} - header = self.get_auth_header_for('r1@r.com') + params = {"event_id": 1, "skip": 1, "language": "en"} + header = self.get_auth_header_for("r1@r.com") - response = self.app.get('/api/v1/review', headers=header, data=params) + response = self.app.get("/api/v1/review", headers=header, data=params) data = json.loads(response.data) - self.assertEqual(data['response']['user_id'], 7) - self.assertEqual(data['response']['answers'][0]['value'], 'I want to solve new problems.') + self.assertEqual(data["response"]["user_id"], 7) + self.assertEqual( + data["response"]["answers"][0]["value"], "I want to solve new problems." + ) def test_high_skip_defaults_to_last_review(self): self.seed_static_data() self.setup_one_reviewer_three_candidates() - params = {'event_id': 1, 'skip': 5, 'language': 'en'} - header = self.get_auth_header_for('r1@r.com') + params = {"event_id": 1, "skip": 5, "language": "en"} + header = self.get_auth_header_for("r1@r.com") - response = self.app.get('/api/v1/review', headers=header, data=params) + response = self.app.get("/api/v1/review", headers=header, data=params) data = json.loads(response.data) - self.assertEqual(data['response']['user_id'], 7) - self.assertEqual(data['response']['answers'][1]['value'], 'I will share by tutoring.') + self.assertEqual(data["response"]["user_id"], 7) + self.assertEqual( + data["response"]["answers"][1]["value"], "I will share by tutoring." + ) def setup_candidate_who_has_applied_to_multiple_events(self): user_id = 5 @@ -488,38 +616,37 @@ def setup_candidate_who_has_applied_to_multiple_events(self): self.add_response(application_form_id=2, user_id=user_id, is_submitted=True) answers = [ - Answer(1, 1, 'I will learn alot.'), - Answer(1, 2, 'I will share by doing talks.'), - Answer(2, 3, 'Yes I worked on a vision task.'), - Answer(2, 4, 'Yes I want the travel award.') + Answer(1, 1, "I will learn alot."), + Answer(1, 2, "I will share by doing talks."), + Answer(2, 3, "Yes I worked on a vision task."), + Answer(2, 4, "Yes I want the travel award."), ] db.session.add_all(answers) db.session.commit() - response_reviewers = [ - ResponseReviewer(1, 1), - ResponseReviewer(2, 1) - ] + response_reviewers = [ResponseReviewer(1, 1), ResponseReviewer(2, 1)] db.session.add_all(response_reviewers) db.session.commit() def test_filtering_on_event_when_candidate_has_applied_to_more_than(self): self.seed_static_data() self.setup_candidate_who_has_applied_to_multiple_events() - params = {'event_id': 2, 'language': 'en'} - header = self.get_auth_header_for('r1@r.com') + params = {"event_id": 2, "language": "en"} + header = self.get_auth_header_for("r1@r.com") - response = self.app.get('/api/v1/review', headers=header, data=params) + response = self.app.get("/api/v1/review", headers=header, data=params) data = json.loads(response.data) - self.assertEqual(data['reviews_remaining_count'], 1) - self.assertEqual(data['response']['user_id'], 5) - self.assertEqual(data['response']['answers'][0]['value'], 'Yes I worked on a vision task.') + self.assertEqual(data["reviews_remaining_count"], 1) + self.assertEqual(data["response"]["user_id"], 5) + self.assertEqual( + data["response"]["answers"][0]["value"], "Yes I worked on a vision task." + ) def setup_multi_choice_answer(self): self.add_response(1, 5, is_submitted=True) - answer = Answer(1, 5, 'indaba-2017') + answer = Answer(1, 5, "indaba-2017") db.session.add(answer) db.session.commit() @@ -530,21 +657,23 @@ def setup_multi_choice_answer(self): def test_multi_choice_answers_use_label_instead_of_value(self): self.seed_static_data() self.setup_multi_choice_answer() - params = {'event_id': 1, 'language': 'en'} - header = self.get_auth_header_for('r1@r.com') + params = {"event_id": 1, "language": "en"} + header = self.get_auth_header_for("r1@r.com") - response = self.app.get('/api/v1/review', headers=header, data=params) + response = self.app.get("/api/v1/review", headers=header, data=params) data = json.loads(response.data) print(data) - self.assertEqual(data['response']['answers'][0]['value'], 'Yes, I attended the 2017 Indaba') + self.assertEqual( + data["response"]["answers"][0]["value"], "Yes, I attended the 2017 Indaba" + ) def test_review_response_not_found(self): self.seed_static_data() - params = {'id': 55, 'language': 'en'} - header = self.get_auth_header_for('r1@r.com') + params = {"id": 55, "language": "en"} + header = self.get_auth_header_for("r1@r.com") - response = self.app.get('/api/v1/reviewresponse', headers=header, data=params) + response = self.app.get("/api/v1/reviewresponse", headers=header, data=params) data = json.loads(response.data) self.assertEqual(response.status_code, REVIEW_RESPONSE_NOT_FOUND[1]) @@ -552,13 +681,13 @@ def test_review_response_not_found(self): def setup_review_response(self): self.add_response(1, 5, is_submitted=True) - answer = Answer(1, 1, 'To learn alot') + answer = Answer(1, 1, "To learn alot") db.session.add(answer) db.session.commit() - self.review_response = ReviewResponse(1, 1, 1, 'en') - self.review_response.review_scores.append(ReviewScore(1, 'answer1')) - self.review_response.review_scores.append(ReviewScore(2, 'answer2')) + self.review_response = ReviewResponse(1, 1, 1, "en") + self.review_response.review_scores.append(ReviewScore(1, "answer1")) + self.review_response.review_scores.append(ReviewScore(2, "answer2")) db.session.add(self.review_response) db.session.commit() @@ -567,36 +696,62 @@ def setup_review_response(self): def test_review_response(self): self.seed_static_data() self.setup_review_response() - params = {'id': self.review_response.id, 'language': 'en'} - header = self.get_auth_header_for('r1@r.com') + params = {"id": self.review_response.id, "language": "en"} + header = self.get_auth_header_for("r1@r.com") - response = self.app.get('/api/v1/reviewresponse', headers=header, data=params) + response = self.app.get("/api/v1/reviewresponse", headers=header, data=params) data = json.loads(response.data) print(data) - self.assertEqual(data['review_form']['id'], 1) - self.assertEqual(data['review_response']['reviewer_user_id'], 1) - self.assertEqual(data['review_response']['response_id'], 1) - self.assertEqual(data['review_response']['scores'][0]['value'], 'answer1') - self.assertEqual(data['review_response']['scores'][1]['value'], 'answer2') + self.assertEqual(data["review_form"]["id"], 1) + self.assertEqual(data["review_response"]["reviewer_user_id"], 1) + self.assertEqual(data["review_response"]["response_id"], 1) + self.assertEqual(data["review_response"]["scores"][0]["value"], "answer1") + self.assertEqual(data["review_response"]["scores"][1]["value"], "answer2") def test_prevent_saving_review_response_reviewer_was_not_assigned_to_response(self): self.seed_static_data() - params = json.dumps({'review_form_id': 1, 'response_id': 1, 'scores': [{'review_question_id': 1, 'value': 'test_answer'}], 'language': 'en', 'is_submitted': False}) - header = self.get_auth_header_for('r1@r.com') + params = json.dumps( + { + "review_form_id": 1, + "response_id": 1, + "scores": [{"review_question_id": 1, "value": "test_answer"}], + "language": "en", + "is_submitted": False, + } + ) + header = self.get_auth_header_for("r1@r.com") - response = self.app.post('/api/v1/reviewresponse', headers=header, data=params, content_type='application/json') + response = self.app.post( + "/api/v1/reviewresponse", + headers=header, + data=params, + content_type="application/json", + ) self.assertEqual(response.status_code, FORBIDDEN[1]) def test_can_still_submit_inactive_response_reviewer(self): self.seed_static_data() self.setup_one_reviewer_three_candidates() - params = json.dumps({'review_form_id': 1, 'response_id': 2, 'scores': [{'review_question_id': 1, 'value': 'test_answer'}], 'language': 'en', 'is_submitted': True}) - header = self.get_auth_header_for('r1@r.com') + params = json.dumps( + { + "review_form_id": 1, + "response_id": 2, + "scores": [{"review_question_id": 1, "value": "test_answer"}], + "language": "en", + "is_submitted": True, + } + ) + header = self.get_auth_header_for("r1@r.com") - response = self.app.post('/api/v1/reviewresponse', headers=header, data=params, content_type='application/json') + response = self.app.post( + "/api/v1/reviewresponse", + headers=header, + data=params, + content_type="application/json", + ) self.assertEqual(response.status_code, 201) @@ -610,15 +765,32 @@ def setup_response_reviewer(self): def test_saving_review_response(self): self.seed_static_data() self.setup_response_reviewer() - params = json.dumps({'review_form_id': 1, 'response_id': 1, 'scores': [{'review_question_id': 1, 'value': 'test_answer'}], 'language': 'en', 'is_submitted': False}) - header = self.get_auth_header_for('r1@r.com') + params = json.dumps( + { + "review_form_id": 1, + "response_id": 1, + "scores": [{"review_question_id": 1, "value": "test_answer"}], + "language": "en", + "is_submitted": False, + } + ) + header = self.get_auth_header_for("r1@r.com") - response = self.app.post('/api/v1/reviewresponse', headers=header, data=params, content_type='application/json') + response = self.app.post( + "/api/v1/reviewresponse", + headers=header, + data=params, + content_type="application/json", + ) - review_scores = db.session.query(ReviewScore).filter_by(review_response_id=1, is_active=True).all() + review_scores = ( + db.session.query(ReviewScore) + .filter_by(review_response_id=1, is_active=True) + .all() + ) self.assertEqual(response.status_code, 201) self.assertEqual(len(review_scores), 1) - self.assertEqual(review_scores[0].value, 'test_answer') + self.assertEqual(review_scores[0].value, "test_answer") def setup_existing_review_response(self): self.add_response(1, 5, is_submitted=True) @@ -627,66 +799,109 @@ def setup_existing_review_response(self): db.session.add(response_reviewer) db.session.commit() - review_response = ReviewResponse(1, 1, 1, 'en') + review_response = ReviewResponse(1, 1, 1, "en") review_response.submit() - review_response.review_scores = [ReviewScore(1, 'test_answer1'), ReviewScore(2, 'test_answer2')] + review_response.review_scores = [ + ReviewScore(1, "test_answer1"), + ReviewScore(2, "test_answer2"), + ] db.session.add(review_response) db.session.commit() def test_updating_review_response(self): self.seed_static_data() self.setup_existing_review_response() - params = json.dumps({'review_form_id': 1, 'response_id': 1, 'scores': [{'review_question_id': 1, 'value': 'test_answer3'}, {'review_question_id': 2, 'value': 'test_answer4'}], 'language': 'en', 'is_submitted': True}) - header = self.get_auth_header_for('r1@r.com') + params = json.dumps( + { + "review_form_id": 1, + "response_id": 1, + "scores": [ + {"review_question_id": 1, "value": "test_answer3"}, + {"review_question_id": 2, "value": "test_answer4"}, + ], + "language": "en", + "is_submitted": True, + } + ) + header = self.get_auth_header_for("r1@r.com") - response = self.app.put('/api/v1/reviewresponse', headers=header, data=params, content_type='application/json') + response = self.app.put( + "/api/v1/reviewresponse", + headers=header, + data=params, + content_type="application/json", + ) - review_scores = db.session.query(ReviewScore).filter_by(review_response_id=1, is_active=True).order_by(ReviewScore.review_question_id).all() + review_scores = ( + db.session.query(ReviewScore) + .filter_by(review_response_id=1, is_active=True) + .order_by(ReviewScore.review_question_id) + .all() + ) self.assertEqual(response.status_code, 200) self.assertEqual(len(review_scores), 2) - self.assertEqual(review_scores[0].value, 'test_answer3') - self.assertEqual(review_scores[1].value, 'test_answer4') + self.assertEqual(review_scores[0].value, "test_answer3") + self.assertEqual(review_scores[1].value, "test_answer4") - def test_user_cant_assign_responsesreviewer_without_system_or_event_admin_role(self): + def test_user_cant_assign_responsesreviewer_without_system_or_event_admin_role( + self, + ): self.seed_static_data() - params = {'event_id': 1, 'reviewer_user_email': 'r2@r.com', 'num_reviews': 10} - header = self.get_auth_header_for('c1@c.com') + params = {"event_id": 1, "reviewer_user_email": "r2@r.com", "num_reviews": 10} + header = self.get_auth_header_for("c1@c.com") - response = self.app.post('/api/v1/reviewassignment', headers=header, data=params) + response = self.app.post( + "/api/v1/reviewassignment", headers=header, data=params + ) self.assertEqual(response.status_code, FORBIDDEN[1]) def test_reviewer_not_found(self): self.seed_static_data() - params = {'event_id': 1, 'reviewer_user_email': 'non_existent@user.com', 'num_reviews': 10} - header = self.get_auth_header_for('sa@sa.com') + params = { + "event_id": 1, + "reviewer_user_email": "non_existent@user.com", + "num_reviews": 10, + } + header = self.get_auth_header_for("sa@sa.com") - response = self.app.post('/api/v1/reviewassignment', headers=header, data=params) + response = self.app.post( + "/api/v1/reviewassignment", headers=header, data=params + ) self.assertEqual(response.status_code, USER_NOT_FOUND[1]) def test_add_reviewer_with_no_roles(self): self.seed_static_data() - params = {'event_id': 1, 'reviewer_user_email': 'r1@r.com', 'num_reviews': 10} - header = self.get_auth_header_for('ea@ea.com') + params = {"event_id": 1, "reviewer_user_email": "r1@r.com", "num_reviews": 10} + header = self.get_auth_header_for("ea@ea.com") - response = self.app.post('/api/v1/reviewassignment', headers=header, data=params) + response = self.app.post( + "/api/v1/reviewassignment", headers=header, data=params + ) event_roles = db.session.query(EventRole).filter_by(user_id=1, event_id=1).all() self.assertEqual(len(event_roles), 1) - self.assertEqual(event_roles[0].role, 'reviewer') + self.assertEqual(event_roles[0].role, "reviewer") def test_add_reviewer_with_a_role(self): self.seed_static_data() - params = {'event_id': 1, 'reviewer_user_email': 'ea@ea.com', 'num_reviews': 10} - header = self.get_auth_header_for('sa@sa.com') + params = {"event_id": 1, "reviewer_user_email": "ea@ea.com", "num_reviews": 10} + header = self.get_auth_header_for("sa@sa.com") - response = self.app.post('/api/v1/reviewassignment', headers=header, data=params) + response = self.app.post( + "/api/v1/reviewassignment", headers=header, data=params + ) - event_roles = db.session.query(EventRole).filter_by(user_id=10, event_id=1).order_by(EventRole.id).all() + event_roles = ( + db.session.query(EventRole) + .filter_by(user_id=10, event_id=1) + .order_by(EventRole.id) + .all() + ) self.assertEqual(len(event_roles), 2) - self.assertEqual(event_roles[0].role, 'admin') - self.assertEqual(event_roles[1].role, 'reviewer') + self.assertEqual(event_roles[0].role, "admin") + self.assertEqual(event_roles[1].role, "reviewer") def setup_responses_without_reviewers(self): self.add_response(1, 5, is_submitted=True) @@ -697,38 +912,50 @@ def setup_responses_without_reviewers(self): def test_adding_first_reviewer(self): self.seed_static_data() self.setup_responses_without_reviewers() - params = {'event_id': 1, 'reviewer_user_email': 'r3@r.com', 'num_reviews': 4} - header = self.get_auth_header_for('ea@ea.com') + params = {"event_id": 1, "reviewer_user_email": "r3@r.com", "num_reviews": 4} + header = self.get_auth_header_for("ea@ea.com") - response = self.app.post('/api/v1/reviewassignment', headers=header, data=params) - response_reviewers = db.session.query(ResponseReviewer).filter_by(reviewer_user_id=3).all() + response = self.app.post( + "/api/v1/reviewassignment", headers=header, data=params + ) + response_reviewers = ( + db.session.query(ResponseReviewer).filter_by(reviewer_user_id=3).all() + ) self.assertEqual(response.status_code, 201) self.assertEqual(len(response_reviewers), 4) def test_limit_of_num_reviews(self): self.seed_static_data() self.setup_responses_without_reviewers() - params = {'event_id': 1, 'reviewer_user_email': 'r3@r.com', 'num_reviews': 3} - header = self.get_auth_header_for('ea@ea.com') + params = {"event_id": 1, "reviewer_user_email": "r3@r.com", "num_reviews": 3} + header = self.get_auth_header_for("ea@ea.com") - response = self.app.post('/api/v1/reviewassignment', headers=header, data=params) + response = self.app.post( + "/api/v1/reviewassignment", headers=header, data=params + ) - response_reviewers = db.session.query(ResponseReviewer).filter_by(reviewer_user_id=3).all() + response_reviewers = ( + db.session.query(ResponseReviewer).filter_by(reviewer_user_id=3).all() + ) self.assertEqual(len(response_reviewers), 3) def setup_reviewer_with_own_response(self): - self.add_response(1, 3, is_submitted=True) # reviewer - self.add_response(1, 5, is_submitted=True) # someone else + self.add_response(1, 3, is_submitted=True) # reviewer + self.add_response(1, 5, is_submitted=True) # someone else def test_reviewer_does_not_get_assigned_to_own_response(self): self.seed_static_data() self.setup_reviewer_with_own_response() - params = {'event_id': 1, 'reviewer_user_email': 'r3@r.com', 'num_reviews': 3} - header = self.get_auth_header_for('ea@ea.com') + params = {"event_id": 1, "reviewer_user_email": "r3@r.com", "num_reviews": 3} + header = self.get_auth_header_for("ea@ea.com") - response = self.app.post('/api/v1/reviewassignment', headers=header, data=params) + response = self.app.post( + "/api/v1/reviewassignment", headers=header, data=params + ) - response_reviewers = db.session.query(ResponseReviewer).filter_by(reviewer_user_id=3).all() + response_reviewers = ( + db.session.query(ResponseReviewer).filter_by(reviewer_user_id=3).all() + ) self.assertEqual(len(response_reviewers), 1) self.assertEqual(response_reviewers[0].response_id, 2) @@ -740,12 +967,16 @@ def setup_withdrawn_and_unsubmitted_responses(self): def test_withdrawn_and_unsubmitted_responses_are_not_assigned_reviewers(self): self.seed_static_data() self.setup_withdrawn_and_unsubmitted_responses() - params = {'event_id': 1, 'reviewer_user_email': 'r3@r.com', 'num_reviews': 3} - header = self.get_auth_header_for('ea@ea.com') + params = {"event_id": 1, "reviewer_user_email": "r3@r.com", "num_reviews": 3} + header = self.get_auth_header_for("ea@ea.com") - response = self.app.post('/api/v1/reviewassignment', headers=header, data=params) + response = self.app.post( + "/api/v1/reviewassignment", headers=header, data=params + ) - response_reviewers = db.session.query(ResponseReviewer).filter_by(reviewer_user_id=3).all() + response_reviewers = ( + db.session.query(ResponseReviewer).filter_by(reviewer_user_id=3).all() + ) self.assertEqual(len(response_reviewers), 1) self.assertEqual(response_reviewers[0].response_id, 3) @@ -755,7 +986,7 @@ def setup_response_with_three_reviewers(self): response_reviewers = [ ResponseReviewer(1, 1), ResponseReviewer(1, 2), - ResponseReviewer(1, 4) + ResponseReviewer(1, 4), ] db.session.add_all(response_reviewers) db.session.commit() @@ -763,12 +994,16 @@ def setup_response_with_three_reviewers(self): def test_response_with_three_reviewers_does_not_get_assigned_another_reviewer(self): self.seed_static_data() self.setup_response_with_three_reviewers() - params = {'event_id': 1, 'reviewer_user_email': 'r3@r.com', 'num_reviews': 3} - header = self.get_auth_header_for('ea@ea.com') + params = {"event_id": 1, "reviewer_user_email": "r3@r.com", "num_reviews": 3} + header = self.get_auth_header_for("ea@ea.com") - response = self.app.post('/api/v1/reviewassignment', headers=header, data=params) + response = self.app.post( + "/api/v1/reviewassignment", headers=header, data=params + ) - response_reviewers = db.session.query(ResponseReviewer).filter_by(reviewer_user_id=3).all() + response_reviewers = ( + db.session.query(ResponseReviewer).filter_by(reviewer_user_id=3).all() + ) self.assertEqual(len(response_reviewers), 0) def setup_responsereview_with_different_reviewer(self): @@ -781,11 +1016,17 @@ def setup_responsereview_with_different_reviewer(self): def test_response_will_get_multiple_reviewers_assigned(self): self.seed_static_data() self.setup_responsereview_with_different_reviewer() - params = {'event_id': 1, 'reviewer_user_email': 'r3@r.com', 'num_reviews': 3} - header = self.get_auth_header_for('ea@ea.com') + params = {"event_id": 1, "reviewer_user_email": "r3@r.com", "num_reviews": 3} + header = self.get_auth_header_for("ea@ea.com") - response = self.app.post('/api/v1/reviewassignment', headers=header, data=params) - response_reviewers = db.session.query(ResponseReviewer).order_by(ResponseReviewer.reviewer_user_id).all() + response = self.app.post( + "/api/v1/reviewassignment", headers=header, data=params + ) + response_reviewers = ( + db.session.query(ResponseReviewer) + .order_by(ResponseReviewer.reviewer_user_id) + .all() + ) self.assertEqual(len(response_reviewers), 2) self.assertEqual(response_reviewers[0].reviewer_user_id, 1) @@ -795,19 +1036,21 @@ def setup_reviewer_is_not_assigned_to_response_more_than_once(self): self.add_response(1, 5, is_submitted=True) def setup_count_reviews_allocated_and_completed(self): - db.session.add_all([ - EventRole('reviewer', 1, 1), - EventRole('reviewer', 2, 1), - EventRole('reviewer', 3, 1), - EventRole('reviewer', 4, 1) - ]) - - self.add_response(1, 5, is_submitted=True) #1 - self.add_response(1, 6, is_submitted=True) #2 - self.add_response(1, 7, is_submitted=True) #3 - self.add_response(1, 8, is_submitted=True) #4 - self.add_response(2, 5, is_submitted=True) #5 - self.add_response(2, 6, is_submitted=True) #6 + db.session.add_all( + [ + EventRole("reviewer", 1, 1), + EventRole("reviewer", 2, 1), + EventRole("reviewer", 3, 1), + EventRole("reviewer", 4, 1), + ] + ) + + self.add_response(1, 5, is_submitted=True) # 1 + self.add_response(1, 6, is_submitted=True) # 2 + self.add_response(1, 7, is_submitted=True) # 3 + self.add_response(1, 8, is_submitted=True) # 4 + self.add_response(2, 5, is_submitted=True) # 5 + self.add_response(2, 6, is_submitted=True) # 6 response_reviewers = [ ResponseReviewer(1, 2), @@ -815,27 +1058,22 @@ def setup_count_reviews_allocated_and_completed(self): ResponseReviewer(3, 2), ResponseReviewer(4, 2), ResponseReviewer(6, 2), - ResponseReviewer(2, 3), ResponseReviewer(4, 3), - ResponseReviewer(3, 4), - ResponseReviewer(5, 1), - ] - db.session.add_all(response_reviewers) # review form, review_user_id, response_id review_responses = [ - ReviewResponse(1, 3, 2, 'en'), - ReviewResponse(1, 3, 4, 'en'), - ReviewResponse(1, 2, 1, 'en'), - ReviewResponse(1, 2, 3, 'en'), - ReviewResponse(1, 2, 4, 'en'), - ReviewResponse(2, 1, 5, 'en'), - ReviewResponse(2, 2, 6, 'en') + ReviewResponse(1, 3, 2, "en"), + ReviewResponse(1, 3, 4, "en"), + ReviewResponse(1, 2, 1, "en"), + ReviewResponse(1, 2, 3, "en"), + ReviewResponse(1, 2, 4, "en"), + ReviewResponse(2, 1, 5, "en"), + ReviewResponse(2, 2, 6, "en"), ] db.session.add_all(review_responses) @@ -862,126 +1100,157 @@ def setup_count_reviews_allocated_and_completed(self): def test_count_reviews_allocated_and_completed(self): self.seed_static_data() self.setup_count_reviews_allocated_and_completed() - header = self.get_auth_header_for('ea@ea.com') - params = {'event_id': 1} + header = self.get_auth_header_for("ea@ea.com") + params = {"event_id": 1} - response = self.app.get('/api/v1/reviewassignment', headers=header, data=params) + response = self.app.get("/api/v1/reviewassignment", headers=header, data=params) data = json.loads(response.data) - data = sorted(data, key=lambda k: k['email']) + data = sorted(data, key=lambda k: k["email"]) LOGGER.debug(data) - self.assertEqual(len(data),3) - self.assertEqual(data[0]['email'], 'r2@r.com') - self.assertEqual(data[0]['reviews_allocated'], 4) - self.assertEqual(data[0]['reviews_completed'], 3) - self.assertEqual(data[1]['email'], 'r3@r.com') - self.assertEqual(data[1]['reviews_allocated'], 2) - self.assertEqual(data[1]['reviews_completed'], 2) - self.assertEqual(data[2]['email'], 'r4@r.com') - self.assertEqual(data[2]['reviews_allocated'], 1) - self.assertEqual(data[2]['reviews_completed'], 0) + self.assertEqual(len(data), 3) + self.assertEqual(data[0]["email"], "r2@r.com") + self.assertEqual(data[0]["reviews_allocated"], 4) + self.assertEqual(data[0]["reviews_completed"], 3) + self.assertEqual(data[1]["email"], "r3@r.com") + self.assertEqual(data[1]["reviews_allocated"], 2) + self.assertEqual(data[1]["reviews_completed"], 2) + self.assertEqual(data[2]["email"], "r4@r.com") + self.assertEqual(data[2]["reviews_allocated"], 1) + self.assertEqual(data[2]["reviews_completed"], 0) def test_reviewer_is_not_assigned_to_response_more_than_once(self): self.seed_static_data() self.setup_reviewer_is_not_assigned_to_response_more_than_once() - params = {'event_id': 1, 'reviewer_user_email': 'r3@r.com', 'num_reviews': 3} - header = self.get_auth_header_for('ea@ea.com') + params = {"event_id": 1, "reviewer_user_email": "r3@r.com", "num_reviews": 3} + header = self.get_auth_header_for("ea@ea.com") - response = self.app.post('/api/v1/reviewassignment', headers=header, data=params) - response2 = self.app.post('/api/v1/reviewassignment', headers=header, data=params) + response = self.app.post( + "/api/v1/reviewassignment", headers=header, data=params + ) + response2 = self.app.post( + "/api/v1/reviewassignment", headers=header, data=params + ) response_reviewers = db.session.query(ResponseReviewer).all() self.assertEqual(len(response_reviewers), 1) - def setup_reviewer_responses_finalverdict_reviewquestion_reviewresponses_and_scores(self): - second_reviewer = EventRole('reviewer', 2, 1) + def setup_reviewer_responses_finalverdict_reviewquestion_reviewresponses_and_scores( + self, + ): + second_reviewer = EventRole("reviewer", 2, 1) db.session.add(second_reviewer) db.session.commit() - users_id = [5,6,7] + users_id = [5, 6, 7] self.add_response(application_form_id=1, user_id=users_id[0], is_submitted=True) self.add_response(application_form_id=1, user_id=users_id[1], is_submitted=True) self.add_response(application_form_id=1, user_id=users_id[2], is_submitted=True) final_verdict_options = [ - {'label': 'Yes', 'value': 2}, - {'label': 'No', 'value': 0}, - {'label': 'Maybe', 'value': 1}, + {"label": "Yes", "value": 2}, + {"label": "No", "value": 0}, + {"label": "Maybe", "value": 1}, ] - verdict_question = ReviewQuestion(1, None, 'multi-choice', True, 3, 0) + verdict_question = ReviewQuestion(1, None, "multi-choice", True, 3, 0) db.session.add(verdict_question) db.session.commit() - verdict_question_translation = ReviewQuestionTranslation(verdict_question.id, 'en', headline='Final Verdict', options=final_verdict_options) + verdict_question_translation = ReviewQuestionTranslation( + verdict_question.id, + "en", + headline="Final Verdict", + options=final_verdict_options, + ) db.session.add(verdict_question_translation) db.session.commit() review_responses = [ - ReviewResponse(1,3,1, 'en'), - ReviewResponse(1,3,2, 'en'), - ReviewResponse(1,2,1, 'en'), - ReviewResponse(1,2,2, 'en'), - ReviewResponse(1,3,3, 'en') + ReviewResponse(1, 3, 1, "en"), + ReviewResponse(1, 3, 2, "en"), + ReviewResponse(1, 2, 1, "en"), + ReviewResponse(1, 2, 2, "en"), + ReviewResponse(1, 3, 3, "en"), ] - review_responses[0].review_scores = [ReviewScore(1, '23'), ReviewScore(5, '1')] - review_responses[1].review_scores = [ReviewScore(1, '55'), ReviewScore(5, '2')] - review_responses[2].review_scores = [ReviewScore(1, '45'), ReviewScore(2, '67'), ReviewScore(5, 'No')] - review_responses[3].review_scores = [ReviewScore(1, '220'), ReviewScore(5, '2')] - review_responses[4].review_scores = [ReviewScore(1, '221'), ReviewScore(5, '1')] + review_responses[0].review_scores = [ReviewScore(1, "23"), ReviewScore(5, "1")] + review_responses[1].review_scores = [ReviewScore(1, "55"), ReviewScore(5, "2")] + review_responses[2].review_scores = [ + ReviewScore(1, "45"), + ReviewScore(2, "67"), + ReviewScore(5, "No"), + ] + review_responses[3].review_scores = [ReviewScore(1, "220"), ReviewScore(5, "2")] + review_responses[4].review_scores = [ReviewScore(1, "221"), ReviewScore(5, "1")] db.session.add_all(review_responses) db.session.commit() return users_id - def test_review_history_returned(self): self.seed_static_data() - users_id = self.setup_reviewer_responses_finalverdict_reviewquestion_reviewresponses_and_scores() + users_id = ( + self.setup_reviewer_responses_finalverdict_reviewquestion_reviewresponses_and_scores() + ) - params ={'event_id' : 1, 'page_number' : 0, 'limit' : 10, 'sort_column' : 'review_response_id'} - header = self.get_auth_header_for('r3@r.com') + params = { + "event_id": 1, + "page_number": 0, + "limit": 10, + "sort_column": "review_response_id", + } + header = self.get_auth_header_for("r3@r.com") - response = self.app.get('/api/v1/reviewhistory', headers=header, data=params) + response = self.app.get("/api/v1/reviewhistory", headers=header, data=params) data = json.loads(response.data) - self.assertEqual(len(data['reviews']), 3) - self.assertEqual(data['num_entries'], 3) + self.assertEqual(len(data["reviews"]), 3) + self.assertEqual(data["num_entries"], 3) - self.assertEqual(data['reviews'][0]['review_response_id'], 1) - self.assertEqual(data['reviews'][0]['reviewed_user_id'], str(users_id[0])) + self.assertEqual(data["reviews"][0]["review_response_id"], 1) + self.assertEqual(data["reviews"][0]["reviewed_user_id"], str(users_id[0])) - self.assertEqual(data['reviews'][1]['review_response_id'], 2) - self.assertEqual(data['reviews'][1]['reviewed_user_id'], str(users_id[1])) + self.assertEqual(data["reviews"][1]["review_response_id"], 2) + self.assertEqual(data["reviews"][1]["reviewed_user_id"], str(users_id[1])) - self.assertEqual(data['reviews'][2]['review_response_id'], 5) - self.assertEqual(data['reviews'][2]['reviewed_user_id'], str(users_id[2])) + self.assertEqual(data["reviews"][2]["review_response_id"], 5) + self.assertEqual(data["reviews"][2]["reviewed_user_id"], str(users_id[2])) def test_brings_back_only_logged_in_reviewer_reviewresponses(self): self.seed_static_data() self.setup_reviewer_responses_finalverdict_reviewquestion_reviewresponses_and_scores() - params ={'event_id' : 1, 'page_number' : 0, 'limit' : 10, 'sort_column' : 'review_response_id'} - header = self.get_auth_header_for('r2@r.com') + params = { + "event_id": 1, + "page_number": 0, + "limit": 10, + "sort_column": "review_response_id", + } + header = self.get_auth_header_for("r2@r.com") - response = self.app.get('/api/v1/reviewhistory', headers=header, data=params) + response = self.app.get("/api/v1/reviewhistory", headers=header, data=params) data = json.loads(response.data) - self.assertEqual(len(data['reviews']), 2) - self.assertEqual(data['reviews'][0]['review_response_id'], 3) - self.assertEqual(data['reviews'][1]['review_response_id'], 4) + self.assertEqual(len(data["reviews"]), 2) + self.assertEqual(data["reviews"][0]["review_response_id"], 3) + self.assertEqual(data["reviews"][1]["review_response_id"], 4) def test_logged_in_user_not_reviewer(self): self.seed_static_data() self.setup_reviewer_responses_finalverdict_reviewquestion_reviewresponses_and_scores() - params ={'event_id' : 1, 'page_number' : 0, 'limit' : 10, 'sort_column' : 'review_response_id'} - header = self.get_auth_header_for('c1@c.com') + params = { + "event_id": 1, + "page_number": 0, + "limit": 10, + "sort_column": "review_response_id", + } + header = self.get_auth_header_for("c1@c.com") - response = self.app.get('/api/v1/reviewhistory', headers=header, data=params) + response = self.app.get("/api/v1/reviewhistory", headers=header, data=params) self.assertEqual(response.status_code, FORBIDDEN[1]) def setup_reviewer_with_no_reviewresponses(self): - reviewer = EventRole('reviewer', 1, 1) + reviewer = EventRole("reviewer", 1, 1) db.session.add(reviewer) db.session.commit() @@ -990,40 +1259,55 @@ def test_reviewer_with_no_reviewresponses(self): self.setup_reviewer_responses_finalverdict_reviewquestion_reviewresponses_and_scores() self.setup_reviewer_with_no_reviewresponses() - params ={'event_id' : 1, 'page_number' : 0, 'limit' : 10, 'sort_column' : 'review_response_id'} - header = self.get_auth_header_for('r1@r.com') + params = { + "event_id": 1, + "page_number": 0, + "limit": 10, + "sort_column": "review_response_id", + } + header = self.get_auth_header_for("r1@r.com") - response = self.app.get('/api/v1/reviewhistory', headers=header, data=params) + response = self.app.get("/api/v1/reviewhistory", headers=header, data=params) data = json.loads(response.data) - self.assertEqual(data['num_entries'], 0) - self.assertEqual(data['reviews'], []) + self.assertEqual(data["num_entries"], 0) + self.assertEqual(data["reviews"], []) def test_order_by_reviewresponseid(self): self.seed_static_data() self.setup_reviewer_responses_finalverdict_reviewquestion_reviewresponses_and_scores() - params ={'event_id' : 1, 'page_number' : 0, 'limit' : 10, 'sort_column' : 'review_response_id'} - header = self.get_auth_header_for('r3@r.com') + params = { + "event_id": 1, + "page_number": 0, + "limit": 10, + "sort_column": "review_response_id", + } + header = self.get_auth_header_for("r3@r.com") - response = self.app.get('/api/v1/reviewhistory', headers=header, data=params) + response = self.app.get("/api/v1/reviewhistory", headers=header, data=params) data = json.loads(response.data) - self.assertEqual(data['reviews'][0]['review_response_id'], 1) - self.assertEqual(data['reviews'][1]['review_response_id'], 2) - self.assertEqual(data['reviews'][2]['review_response_id'], 5) + self.assertEqual(data["reviews"][0]["review_response_id"], 1) + self.assertEqual(data["reviews"][1]["review_response_id"], 2) + self.assertEqual(data["reviews"][2]["review_response_id"], 5) def setup_reviewresponses_with_unordered_timestamps(self): final_verdict_options = [ - {'label': 'Yes', 'value': 2}, - {'label': 'No', 'value': 0}, - {'label': 'Maybe', 'value': 1}, + {"label": "Yes", "value": 2}, + {"label": "No", "value": 0}, + {"label": "Maybe", "value": 1}, ] - verdict_question = ReviewQuestion(1, None, 'multi-choice', True, 3, 0) + verdict_question = ReviewQuestion(1, None, "multi-choice", True, 3, 0) db.session.add(verdict_question) db.session.commit() - verdict_question_translation = ReviewQuestionTranslation(verdict_question.id, 'en', headline='Final Verdict', options=final_verdict_options) + verdict_question_translation = ReviewQuestionTranslation( + verdict_question.id, + "en", + headline="Final Verdict", + options=final_verdict_options, + ) db.session.add(verdict_question_translation) db.session.commit() @@ -1031,16 +1315,25 @@ def setup_reviewresponses_with_unordered_timestamps(self): self.add_response(1, 6, is_submitted=True) self.add_response(1, 7, is_submitted=True) - review_response_1 = ReviewResponse(1,3,1, 'en') - review_response_2 = ReviewResponse(1,3,2, 'en') - review_response_3 = ReviewResponse(1,3,3, 'en') + review_response_1 = ReviewResponse(1, 3, 1, "en") + review_response_2 = ReviewResponse(1, 3, 2, "en") + review_response_3 = ReviewResponse(1, 3, 3, "en") review_response_1.submitted_timestamp = datetime(2019, 1, 1) review_response_2.submitted_timestamp = datetime(2018, 1, 1) review_response_3.submitted_timestamp = datetime(2018, 6, 6) review_responses = [review_response_1, review_response_2, review_response_3] - review_responses[0].review_scores = [ReviewScore(1, '67'), ReviewScore(5, 'Yes')] - review_responses[1].review_scores = [ReviewScore(1, '23'), ReviewScore(5, 'Yes')] - review_responses[2].review_scores = [ReviewScore(1, '53'), ReviewScore(5, 'Yes')] + review_responses[0].review_scores = [ + ReviewScore(1, "67"), + ReviewScore(5, "Yes"), + ] + review_responses[1].review_scores = [ + ReviewScore(1, "23"), + ReviewScore(5, "Yes"), + ] + review_responses[2].review_scores = [ + ReviewScore(1, "53"), + ReviewScore(5, "Yes"), + ] db.session.add_all(review_responses) db.session.commit() @@ -1048,15 +1341,26 @@ def test_order_by_submittedtimestamp(self): self.seed_static_data() self.setup_reviewresponses_with_unordered_timestamps() - params ={'event_id' : 1, 'page_number' : 0, 'limit' : 10, 'sort_column' : 'submitted_timestamp'} - header = self.get_auth_header_for('r3@r.com') + params = { + "event_id": 1, + "page_number": 0, + "limit": 10, + "sort_column": "submitted_timestamp", + } + header = self.get_auth_header_for("r3@r.com") - response = self.app.get('/api/v1/reviewhistory', headers=header, data=params) + response = self.app.get("/api/v1/reviewhistory", headers=header, data=params) data = json.loads(response.data) LOGGER.debug(data) - self.assertEqual(data['reviews'][0]['submitted_timestamp'], '2018-01-01T00:00:00') - self.assertEqual(data['reviews'][1]['submitted_timestamp'], '2018-06-06T00:00:00') - self.assertEqual(data['reviews'][2]['submitted_timestamp'], '2019-01-01T00:00:00') + self.assertEqual( + data["reviews"][0]["submitted_timestamp"], "2018-01-01T00:00:00" + ) + self.assertEqual( + data["reviews"][1]["submitted_timestamp"], "2018-06-06T00:00:00" + ) + self.assertEqual( + data["reviews"][2]["submitted_timestamp"], "2019-01-01T00:00:00" + ) # TODO re-add these tests once we can get the info outside of AppUser @SkipTest @@ -1064,15 +1368,20 @@ def test_order_by_nationalitycountry(self): self.seed_static_data() self.setup_reviewer_responses_finalverdict_reviewquestion_reviewresponses_and_scores() - params ={'event_id' : 1, 'page_number' : 0, 'limit' : 10, 'sort_column' : 'nationality_country'} - header = self.get_auth_header_for('r3@r.com') + params = { + "event_id": 1, + "page_number": 0, + "limit": 10, + "sort_column": "nationality_country", + } + header = self.get_auth_header_for("r3@r.com") - response = self.app.get('/api/v1/reviewhistory', headers=header, data=params) + response = self.app.get("/api/v1/reviewhistory", headers=header, data=params) data = json.loads(response.data) - self.assertEqual(data['reviews'][0]['nationality_country'], 'Botswana') - self.assertEqual(data['reviews'][1]['nationality_country'], 'South Africa') - self.assertEqual(data['reviews'][2]['nationality_country'], 'Zimbabwe') + self.assertEqual(data["reviews"][0]["nationality_country"], "Botswana") + self.assertEqual(data["reviews"][1]["nationality_country"], "South Africa") + self.assertEqual(data["reviews"][2]["nationality_country"], "Zimbabwe") # TODO re-add these tests once we can get the info outside of AppUser @SkipTest @@ -1080,15 +1389,20 @@ def test_order_by_residencecountry(self): self.seed_static_data() self.setup_reviewer_responses_finalverdict_reviewquestion_reviewresponses_and_scores() - params ={'event_id' : 1, 'page_number' : 0, 'limit' : 10, 'sort_column' : 'residence_country'} - header = self.get_auth_header_for('r3@r.com') + params = { + "event_id": 1, + "page_number": 0, + "limit": 10, + "sort_column": "residence_country", + } + header = self.get_auth_header_for("r3@r.com") - response = self.app.get('/api/v1/reviewhistory', headers=header, data=params) + response = self.app.get("/api/v1/reviewhistory", headers=header, data=params) data = json.loads(response.data) - self.assertEqual(data['reviews'][0]['residence_country'], 'Egypt') - self.assertEqual(data['reviews'][1]['residence_country'], 'Mozambique') - self.assertEqual(data['reviews'][2]['residence_country'], 'Namibia') + self.assertEqual(data["reviews"][0]["residence_country"], "Egypt") + self.assertEqual(data["reviews"][1]["residence_country"], "Mozambique") + self.assertEqual(data["reviews"][2]["residence_country"], "Namibia") # TODO re-add these tests once we can get the info outside of AppUser @SkipTest @@ -1096,15 +1410,20 @@ def test_order_by_affiliation(self): self.seed_static_data() self.setup_reviewer_responses_finalverdict_reviewquestion_reviewresponses_and_scores() - params ={'event_id' : 1, 'page_number' : 0, 'limit' : 10, 'sort_column' : 'affiliation'} - header = self.get_auth_header_for('r3@r.com') + params = { + "event_id": 1, + "page_number": 0, + "limit": 10, + "sort_column": "affiliation", + } + header = self.get_auth_header_for("r3@r.com") - response = self.app.get('/api/v1/reviewhistory', headers=header, data=params) + response = self.app.get("/api/v1/reviewhistory", headers=header, data=params) data = json.loads(response.data) - self.assertEqual(data['reviews'][0]['affiliation'], 'RU') - self.assertEqual(data['reviews'][1]['affiliation'], 'UFH') - self.assertEqual(data['reviews'][2]['affiliation'], 'UWC') + self.assertEqual(data["reviews"][0]["affiliation"], "RU") + self.assertEqual(data["reviews"][1]["affiliation"], "UFH") + self.assertEqual(data["reviews"][2]["affiliation"], "UWC") # TODO re-add these tests once we can get the info outside of AppUser @SkipTest @@ -1112,41 +1431,59 @@ def test_order_by_department(self): self.seed_static_data() self.setup_reviewer_responses_finalverdict_reviewquestion_reviewresponses_and_scores() - params ={'event_id' : 1, 'page_number' : 0, 'limit' : 10, 'sort_column' : 'department'} - header = self.get_auth_header_for('r3@r.com') + params = { + "event_id": 1, + "page_number": 0, + "limit": 10, + "sort_column": "department", + } + header = self.get_auth_header_for("r3@r.com") - response = self.app.get('/api/v1/reviewhistory', headers=header, data=params) + response = self.app.get("/api/v1/reviewhistory", headers=header, data=params) data = json.loads(response.data) - self.assertEqual(data['reviews'][0]['department'], 'CS') # ascii ordering orders capital letters before lowercase - self.assertEqual(data['reviews'][1]['department'], 'Chem') - self.assertEqual(data['reviews'][2]['department'], 'Phys') + self.assertEqual( + data["reviews"][0]["department"], "CS" + ) # ascii ordering orders capital letters before lowercase + self.assertEqual(data["reviews"][1]["department"], "Chem") + self.assertEqual(data["reviews"][2]["department"], "Phys") @SkipTest def test_order_by_usercategory(self): self.seed_static_data() self.setup_reviewer_responses_finalverdict_reviewquestion_reviewresponses_and_scores() - params ={'event_id' : 1, 'page_number' : 0, 'limit' : 10, 'sort_column' : 'user_category'} - header = self.get_auth_header_for('r3@r.com') + params = { + "event_id": 1, + "page_number": 0, + "limit": 10, + "sort_column": "user_category", + } + header = self.get_auth_header_for("r3@r.com") - response = self.app.get('/api/v1/reviewhistory', headers=header, data=params) + response = self.app.get("/api/v1/reviewhistory", headers=header, data=params) data = json.loads(response.data) - self.assertEqual(data['reviews'][0]['user_category'], 'Honours') - self.assertEqual(data['reviews'][1]['user_category'], 'MSc') - self.assertEqual(data['reviews'][2]['user_category'], 'Student') + self.assertEqual(data["reviews"][0]["user_category"], "Honours") + self.assertEqual(data["reviews"][1]["user_category"], "MSc") + self.assertEqual(data["reviews"][2]["user_category"], "Student") def setup_two_extra_responses_for_reviewer3(self): self.add_response(1, 8, is_submitted=True) self.add_response(1, 1, is_submitted=True) review_responses = [ - ReviewResponse(1,3,4, 'en'), - ReviewResponse(1,3,5, 'en') + ReviewResponse(1, 3, 4, "en"), + ReviewResponse(1, 3, 5, "en"), + ] + review_responses[0].review_scores = [ + ReviewScore(1, "89"), + ReviewScore(5, "Maybe"), + ] + review_responses[1].review_scores = [ + ReviewScore(1, "75"), + ReviewScore(5, "Yes"), ] - review_responses[0].review_scores = [ReviewScore(1, '89'), ReviewScore(5, 'Maybe')] - review_responses[1].review_scores = [ReviewScore(1, '75'), ReviewScore(5, 'Yes')] db.session.add_all(review_responses) db.session.commit() @@ -1155,208 +1492,347 @@ def test_first_page_in_pagination(self): self.setup_reviewer_responses_finalverdict_reviewquestion_reviewresponses_and_scores() self.setup_two_extra_responses_for_reviewer3() - params ={'event_id' : 1, 'page_number' : 0, 'limit' : 2, 'sort_column' : 'review_response_id'} - header = self.get_auth_header_for('r3@r.com') + params = { + "event_id": 1, + "page_number": 0, + "limit": 2, + "sort_column": "review_response_id", + } + header = self.get_auth_header_for("r3@r.com") - response = self.app.get('/api/v1/reviewhistory', headers=header, data=params) + response = self.app.get("/api/v1/reviewhistory", headers=header, data=params) data = json.loads(response.data) - self.assertEqual(len(data['reviews']), 2) - self.assertEqual(data['num_entries'], 5) + self.assertEqual(len(data["reviews"]), 2) + self.assertEqual(data["num_entries"], 5) - self.assertEqual(data['reviews'][0]['review_response_id'], 1) - self.assertEqual(data['reviews'][1]['review_response_id'], 2) + self.assertEqual(data["reviews"][0]["review_response_id"], 1) + self.assertEqual(data["reviews"][1]["review_response_id"], 2) def test_middle_page_in_pagination(self): self.seed_static_data() self.setup_reviewer_responses_finalverdict_reviewquestion_reviewresponses_and_scores() self.setup_two_extra_responses_for_reviewer3() - params ={'event_id' : 1, 'page_number' : 1, 'limit' : 2, 'sort_column' : 'review_response_id'} - header = self.get_auth_header_for('r3@r.com') + params = { + "event_id": 1, + "page_number": 1, + "limit": 2, + "sort_column": "review_response_id", + } + header = self.get_auth_header_for("r3@r.com") - response = self.app.get('/api/v1/reviewhistory', headers=header, data=params) + response = self.app.get("/api/v1/reviewhistory", headers=header, data=params) data = json.loads(response.data) - self.assertEqual(len(data['reviews']), 2) - self.assertEqual(data['num_entries'], 5) + self.assertEqual(len(data["reviews"]), 2) + self.assertEqual(data["num_entries"], 5) - self.assertEqual(data['reviews'][0]['review_response_id'], 5) - self.assertEqual(data['reviews'][1]['review_response_id'], 6) + self.assertEqual(data["reviews"][0]["review_response_id"], 5) + self.assertEqual(data["reviews"][1]["review_response_id"], 6) def test_last_page_in_pagination(self): self.seed_static_data() self.setup_reviewer_responses_finalverdict_reviewquestion_reviewresponses_and_scores() self.setup_two_extra_responses_for_reviewer3() - params ={'event_id' : 1, 'page_number' : 2, 'limit' : 2, 'sort_column' : 'review_response_id'} - header = self.get_auth_header_for('r3@r.com') + params = { + "event_id": 1, + "page_number": 2, + "limit": 2, + "sort_column": "review_response_id", + } + header = self.get_auth_header_for("r3@r.com") - response = self.app.get('/api/v1/reviewhistory', headers=header, data=params) + response = self.app.get("/api/v1/reviewhistory", headers=header, data=params) data = json.loads(response.data) - self.assertEqual(len(data['reviews']), 1) - self.assertEqual(data['num_entries'], 5) - self.assertEqual(data['reviews'][0]['review_response_id'], 7) + self.assertEqual(len(data["reviews"]), 1) + self.assertEqual(data["num_entries"], 5) + self.assertEqual(data["reviews"][0]["review_response_id"], 7) def test_total_number_of_pages_greater_than_zero(self): self.seed_static_data() self.setup_reviewer_responses_finalverdict_reviewquestion_reviewresponses_and_scores() self.setup_two_extra_responses_for_reviewer3() - params ={'event_id' : 1, 'page_number' : 2, 'limit' : 2, 'sort_column' : 'review_response_id'} - header = self.get_auth_header_for('r3@r.com') + params = { + "event_id": 1, + "page_number": 2, + "limit": 2, + "sort_column": "review_response_id", + } + header = self.get_auth_header_for("r3@r.com") - response = self.app.get('/api/v1/reviewhistory', headers=header, data=params) + response = self.app.get("/api/v1/reviewhistory", headers=header, data=params) data = json.loads(response.data) - self.assertEqual(data['total_pages'], 3) + self.assertEqual(data["total_pages"], 3) def test_total_number_of_pages_when_zero(self): self.seed_static_data() self.setup_reviewer_with_no_reviewresponses() - params ={'event_id' : 1, 'page_number' : 2, 'limit' : 2, 'sort_column' : 'review_response_id'} - header = self.get_auth_header_for('r1@r.com') + params = { + "event_id": 1, + "page_number": 2, + "limit": 2, + "sort_column": "review_response_id", + } + header = self.get_auth_header_for("r1@r.com") - response = self.app.get('/api/v1/reviewhistory', headers=header, data=params) + response = self.app.get("/api/v1/reviewhistory", headers=header, data=params) data = json.loads(response.data) - self.assertEqual(data['total_pages'], 0) - + self.assertEqual(data["total_pages"], 0) def test_review_form_language(self): """Test that the review form questions are returned in the correct language.""" self.seed_static_data() - params ={'event_id' : 1, 'language': 'en'} - header = self.get_auth_header_for('r1@r.com') + params = {"event_id": 1, "language": "en"} + header = self.get_auth_header_for("r1@r.com") - response = self.app.get('/api/v1/review', headers=header, data=params) + response = self.app.get("/api/v1/review", headers=header, data=params) data = json.loads(response.data) - print(data['review_form']['review_sections']) + print(data["review_form"]["review_sections"]) - self.assertEqual(data['review_form']['review_sections'][0]['headline'], 'Review Section 1 English') - self.assertEqual(data['review_form']['review_sections'][0]['description'], 'Review Section 1 Description English') + self.assertEqual( + data["review_form"]["review_sections"][0]["headline"], + "Review Section 1 English", + ) + self.assertEqual( + data["review_form"]["review_sections"][0]["description"], + "Review Section 1 Description English", + ) - self.assertEqual(len(data['review_form']['review_sections'][0]['review_questions']), 2) + self.assertEqual( + len(data["review_form"]["review_sections"][0]["review_questions"]), 2 + ) - self.assertEqual(data['review_form']['review_sections'][0]['review_questions'][1]['description'], 'English Description') - self.assertEqual(data['review_form']['review_sections'][0]['review_questions'][1]['headline'], 'English Headline') - self.assertEqual(data['review_form']['review_sections'][0]['review_questions'][1]['placeholder'], 'English Placeholder') - self.assertDictEqual(data['review_form']['review_sections'][0]['review_questions'][1]['options'][0], {'label': 'en1', 'value': 'en'}) - self.assertEqual(data['review_form']['review_sections'][0]['review_questions'][1]['validation_regex'], 'EN Regex') - self.assertEqual(data['review_form']['review_sections'][0]['review_questions'][1]['validation_text'], 'EN Validation Message') + self.assertEqual( + data["review_form"]["review_sections"][0]["review_questions"][1][ + "description" + ], + "English Description", + ) + self.assertEqual( + data["review_form"]["review_sections"][0]["review_questions"][1][ + "headline" + ], + "English Headline", + ) + self.assertEqual( + data["review_form"]["review_sections"][0]["review_questions"][1][ + "placeholder" + ], + "English Placeholder", + ) + self.assertDictEqual( + data["review_form"]["review_sections"][0]["review_questions"][1]["options"][ + 0 + ], + {"label": "en1", "value": "en"}, + ) + self.assertEqual( + data["review_form"]["review_sections"][0]["review_questions"][1][ + "validation_regex" + ], + "EN Regex", + ) + self.assertEqual( + data["review_form"]["review_sections"][0]["review_questions"][1][ + "validation_text" + ], + "EN Validation Message", + ) - params ={'event_id' : 1, 'language': 'fr'} + params = {"event_id": 1, "language": "fr"} - response = self.app.get('/api/v1/review', headers=header, data=params) + response = self.app.get("/api/v1/review", headers=header, data=params) data = json.loads(response.data) - self.assertEqual(data['review_form']['review_sections'][0]['headline'], 'Review Section 1 French') - self.assertEqual(data['review_form']['review_sections'][0]['description'], 'Review Section 1 Description French') + self.assertEqual( + data["review_form"]["review_sections"][0]["headline"], + "Review Section 1 French", + ) + self.assertEqual( + data["review_form"]["review_sections"][0]["description"], + "Review Section 1 Description French", + ) - self.assertEqual(len(data['review_form']['review_sections'][0]['review_questions']), 2) + self.assertEqual( + len(data["review_form"]["review_sections"][0]["review_questions"]), 2 + ) - self.assertEqual(data['review_form']['review_sections'][0]['review_questions'][1]['description'], 'French Description') - self.assertEqual(data['review_form']['review_sections'][0]['review_questions'][1]['headline'], 'French Headline') - self.assertEqual(data['review_form']['review_sections'][0]['review_questions'][1]['placeholder'], 'French Placeholder') - self.assertDictEqual(data['review_form']['review_sections'][0]['review_questions'][1]['options'][0], {'label': 'fr1', 'value': 'fr'}) - self.assertEqual(data['review_form']['review_sections'][0]['review_questions'][1]['validation_regex'], 'FR Regex') - self.assertEqual(data['review_form']['review_sections'][0]['review_questions'][1]['validation_text'], 'FR Validation Message') + self.assertEqual( + data["review_form"]["review_sections"][0]["review_questions"][1][ + "description" + ], + "French Description", + ) + self.assertEqual( + data["review_form"]["review_sections"][0]["review_questions"][1][ + "headline" + ], + "French Headline", + ) + self.assertEqual( + data["review_form"]["review_sections"][0]["review_questions"][1][ + "placeholder" + ], + "French Placeholder", + ) + self.assertDictEqual( + data["review_form"]["review_sections"][0]["review_questions"][1]["options"][ + 0 + ], + {"label": "fr1", "value": "fr"}, + ) + self.assertEqual( + data["review_form"]["review_sections"][0]["review_questions"][1][ + "validation_regex" + ], + "FR Regex", + ) + self.assertEqual( + data["review_form"]["review_sections"][0]["review_questions"][1][ + "validation_text" + ], + "FR Validation Message", + ) class ReviewListAPITest(ApiTestCase): - def seed_static_data(self): - self.event1 = self.add_event(key='event1') - self.event2 = self.add_event(key='event2') + self.event1 = self.add_event(key="event1") + self.event2 = self.add_event(key="event2") - self.reviewer1 = self.add_user('reviewer1@mail.com') - self.reviewer2 = self.add_user('reviewer2@mail.com') - self.event1.add_event_role('reviewer', self.reviewer1.id) - self.event2.add_event_role('reviewer', self.reviewer1.id) - self.event1.add_event_role('reviewer', self.reviewer2.id) - self.event2.add_event_role('reviewer', self.reviewer2.id) + self.reviewer1 = self.add_user("reviewer1@mail.com") + self.reviewer2 = self.add_user("reviewer2@mail.com") + self.event1.add_event_role("reviewer", self.reviewer1.id) + self.event2.add_event_role("reviewer", self.reviewer1.id) + self.event1.add_event_role("reviewer", self.reviewer2.id) + self.event2.add_event_role("reviewer", self.reviewer2.id) - self.user1 = self.add_user('user1@mail.com') - user2 = self.add_user('user2@mail.com') - user3 = self.add_user('user3@mail.com') - user4 = self.add_user('user4@mail.com') + self.user1 = self.add_user("user1@mail.com") + user2 = self.add_user("user2@mail.com") + user3 = self.add_user("user3@mail.com") + user4 = self.add_user("user4@mail.com") application_form1 = self.create_application_form(self.event1.id) section1 = self.add_section(application_form1.id) question1 = self.add_question(application_form1.id, section1.id) - question1.key = 'review-identifier' - self.add_question_translation(question1.id, 'en', 'Headline 1 EN') - self.add_question_translation(question1.id, 'fr', 'Headline 1 FR') + question1.key = "review-identifier" + self.add_question_translation(question1.id, "en", "Headline 1 EN") + self.add_question_translation(question1.id, "fr", "Headline 1 FR") question2 = self.add_question(application_form1.id, section1.id) - question2.key = 'review-identifier' - self.add_question_translation(question2.id, 'en', 'Headline 2 EN') - self.add_question_translation(question2.id, 'fr', 'Headline 2 FR') + question2.key = "review-identifier" + self.add_question_translation(question2.id, "en", "Headline 2 EN") + self.add_question_translation(question2.id, "fr", "Headline 2 FR") question3 = self.add_question(application_form1.id, section1.id) - self.add_question_translation(question3.id, 'en', 'Headline 3 EN') - self.add_question_translation(question3.id, 'fr', 'Headline 3 FR') + self.add_question_translation(question3.id, "en", "Headline 3 EN") + self.add_question_translation(question3.id, "fr", "Headline 3 FR") review_form1 = self.add_review_form(application_form1.id) review_form1_section1 = self.add_review_section(review_form1.id) - self.add_review_section_translation(review_form1_section1.id, 'en', 'Review Section 1 en', 'Review Section 1 en') - self.add_review_section_translation(review_form1_section1.id, 'fr', 'Review Section 1 fr', 'Review Section 1 fr') + self.add_review_section_translation( + review_form1_section1.id, "en", "Review Section 1 en", "Review Section 1 en" + ) + self.add_review_section_translation( + review_form1_section1.id, "fr", "Review Section 1 fr", "Review Section 1 fr" + ) review_form1_section2 = self.add_review_section(review_form1.id) - self.add_review_section_translation(review_form1_section2.id, 'en', 'Review Section 2 en', 'Review Section 2 en') - self.add_review_section_translation(review_form1_section2.id, 'fr', 'Review Section 2 fr', 'Review Section 2 fr') + self.add_review_section_translation( + review_form1_section2.id, "en", "Review Section 2 en", "Review Section 2 en" + ) + self.add_review_section_translation( + review_form1_section2.id, "fr", "Review Section 2 fr", "Review Section 2 fr" + ) review_form1_section2 = self.add_review_section(review_form1.id) review_q1 = self.add_review_question(review_form1_section1.id, weight=1) - review_q1_translation_en = self.add_review_question_translation(review_q1.id, 'en', headline='Heading En') - review_q1_translation_fr = self.add_review_question_translation(review_q1.id, 'fr', headline='Heading Fr') + review_q1_translation_en = self.add_review_question_translation( + review_q1.id, "en", headline="Heading En" + ) + review_q1_translation_fr = self.add_review_question_translation( + review_q1.id, "fr", headline="Heading Fr" + ) review_q2 = self.add_review_question(review_form1_section1.id, weight=0) - review_q2_translation_en = self.add_review_question_translation(review_q2.id, 'en', headline='Heading En') - review_q2_translation_fr = self.add_review_question_translation(review_q2.id, 'fr', headline='Heading Fr') + review_q2_translation_en = self.add_review_question_translation( + review_q2.id, "en", headline="Heading En" + ) + review_q2_translation_fr = self.add_review_question_translation( + review_q2.id, "fr", headline="Heading Fr" + ) review_q3 = self.add_review_question(review_form1_section2.id, weight=1) - review_q3_translation_en = self.add_review_question_translation(review_q3.id, 'en', headline='Heading En') - review_q3_translation_fr = self.add_review_question_translation(review_q3.id, 'fr', headline='Heading Fr') + review_q3_translation_en = self.add_review_question_translation( + review_q3.id, "en", headline="Heading En" + ) + review_q3_translation_fr = self.add_review_question_translation( + review_q3.id, "fr", headline="Heading Fr" + ) application_form2 = self.create_application_form(self.event2.id) review_form2 = self.add_review_form(application_form2.id) - event1_response1 = self.add_response(application_form1.id, self.user1.id, is_submitted=True) - self.add_answer(event1_response1.id, question1.id, 'First answer') - self.add_answer(event1_response1.id, question2.id, 'Second answer') - self.add_answer(event1_response1.id, question3.id, 'Third answer') + event1_response1 = self.add_response( + application_form1.id, self.user1.id, is_submitted=True + ) + self.add_answer(event1_response1.id, question1.id, "First answer") + self.add_answer(event1_response1.id, question2.id, "Second answer") + self.add_answer(event1_response1.id, question3.id, "Third answer") - event1_response2 = self.add_response(application_form1.id, user2.id, is_submitted=True, language='fr') - self.add_answer(event1_response2.id, question1.id, 'Forth answer') - self.add_answer(event1_response2.id, question2.id, 'Fifth answer') - self.add_answer(event1_response2.id, question3.id, 'Sixth answer') + event1_response2 = self.add_response( + application_form1.id, user2.id, is_submitted=True, language="fr" + ) + self.add_answer(event1_response2.id, question1.id, "Forth answer") + self.add_answer(event1_response2.id, question2.id, "Fifth answer") + self.add_answer(event1_response2.id, question3.id, "Sixth answer") - event1_response3 = self.add_response(application_form1.id, user3.id, is_submitted=True) - self.add_answer(event1_response3.id, question1.id, 'Seventh answer') - self.add_answer(event1_response3.id, question2.id, 'Eigth answer') - self.add_answer(event1_response3.id, question3.id, 'Ninth answer') + event1_response3 = self.add_response( + application_form1.id, user3.id, is_submitted=True + ) + self.add_answer(event1_response3.id, question1.id, "Seventh answer") + self.add_answer(event1_response3.id, question2.id, "Eigth answer") + self.add_answer(event1_response3.id, question3.id, "Ninth answer") - event1_response4 = self.add_response(application_form1.id, user4.id, is_submitted=True, language='zu') - self.add_answer(event1_response4.id, question1.id, 'Tenth answer') - self.add_answer(event1_response4.id, question2.id, 'Eleventh answer') - self.add_answer(event1_response4.id, question3.id, 'Twelfth answer') + event1_response4 = self.add_response( + application_form1.id, user4.id, is_submitted=True, language="zu" + ) + self.add_answer(event1_response4.id, question1.id, "Tenth answer") + self.add_answer(event1_response4.id, question2.id, "Eleventh answer") + self.add_answer(event1_response4.id, question3.id, "Twelfth answer") - event2_response1 = self.add_response(application_form2.id, self.user1.id, is_submitted=True) - event2_response2 = self.add_response(application_form2.id, user2.id, is_submitted=True, language='fr') - event2_response3 = self.add_response(application_form2.id, user3.id, is_submitted=True) + event2_response1 = self.add_response( + application_form2.id, self.user1.id, is_submitted=True + ) + event2_response2 = self.add_response( + application_form2.id, user2.id, is_submitted=True, language="fr" + ) + event2_response3 = self.add_response( + application_form2.id, user3.id, is_submitted=True + ) # Review 1 completed review self.add_response_reviewer(event1_response1.id, self.reviewer1.id) - review_response1 = self.add_review_response(self.reviewer1.id, event1_response1.id, review_form1.id) + review_response1 = self.add_review_response( + self.reviewer1.id, event1_response1.id, review_form1.id + ) review_response1.submit() self.add_review_score(review_response1.id, review_q1.id, 10.5) self.add_review_score(review_response1.id, review_q2.id, 100) - self.add_review_score(review_response1.id, review_q3.id, 'Hello world') + self.add_review_score(review_response1.id, review_q3.id, "Hello world") # Reviewer 1 incomplete review - self.review_response1_submitted = review_response1.submitted_timestamp.isoformat() + self.review_response1_submitted = ( + review_response1.submitted_timestamp.isoformat() + ) self.add_response_reviewer(event1_response2.id, self.reviewer1.id) - review_response2 = self.add_review_response(self.reviewer1.id, event1_response2.id, review_form1.id) + review_response2 = self.add_review_response( + self.reviewer1.id, event1_response2.id, review_form1.id + ) self.add_review_score(review_response2.id, review_q1.id, 13) # Reviewer 1 not started review @@ -1370,92 +1846,110 @@ def seed_static_data(self): def test_review_list(self): self.seed_static_data() - params ={'event_id' : 1, 'language': 'en'} + params = {"event_id": 1, "language": "en"} response = self.app.get( - '/api/v1/reviewlist', - headers=self.get_auth_header_for('reviewer1@mail.com'), - data=params) + "/api/v1/reviewlist", + headers=self.get_auth_header_for("reviewer1@mail.com"), + data=params, + ) data = json.loads(response.data) self.assertEqual(len(data), 3) - self.assertEqual(data[0]['response_id'], 1) - self.assertEqual(data[0]['language'], 'en') - self.assertEqual(len(data[0]['information']), 2) - self.assertEqual(data[0]['information'][0]['headline'], 'Headline 1 EN') - self.assertEqual(data[0]['information'][0]['value'], 'First answer') - self.assertEqual(data[0]['information'][1]['headline'], 'Headline 2 EN') - self.assertEqual(data[0]['information'][1]['value'], 'Second answer') - self.assertTrue(data[0]['started']) - self.assertEqual(data[0]['submitted'], self.review_response1_submitted) - self.assertEqual(data[0]['total_score'], 10.5) - - self.assertEqual(data[1]['response_id'], 2) - self.assertEqual(data[1]['language'], 'fr') - self.assertEqual(len(data[1]['information']), 2) - self.assertEqual(data[1]['information'][0]['headline'], 'Headline 1 EN') - self.assertEqual(data[1]['information'][0]['value'], 'Forth answer') - self.assertEqual(data[1]['information'][1]['headline'], 'Headline 2 EN') - self.assertEqual(data[1]['information'][1]['value'], 'Fifth answer') - self.assertTrue(data[1]['started']) - self.assertIsNone(data[1]['submitted']) - self.assertEqual(data[1]['total_score'], 13) - - self.assertEqual(data[2]['response_id'], 3) - self.assertEqual(data[2]['language'], 'en') - self.assertEqual(len(data[2]['information']), 2) - self.assertEqual(data[2]['information'][0]['headline'], 'Headline 1 EN') - self.assertEqual(data[2]['information'][0]['value'], 'Seventh answer') - self.assertEqual(data[2]['information'][1]['headline'], 'Headline 2 EN') - self.assertEqual(data[2]['information'][1]['value'], 'Eigth answer') - self.assertFalse(data[2]['started']) - self.assertIsNone(data[2]['submitted']) - self.assertEqual(data[2]['total_score'], 0.0) + self.assertEqual(data[0]["response_id"], 1) + self.assertEqual(data[0]["language"], "en") + self.assertEqual(len(data[0]["information"]), 2) + self.assertEqual(data[0]["information"][0]["headline"], "Headline 1 EN") + self.assertEqual(data[0]["information"][0]["value"], "First answer") + self.assertEqual(data[0]["information"][1]["headline"], "Headline 2 EN") + self.assertEqual(data[0]["information"][1]["value"], "Second answer") + self.assertTrue(data[0]["started"]) + self.assertEqual(data[0]["submitted"], self.review_response1_submitted) + self.assertEqual(data[0]["total_score"], 10.5) + + self.assertEqual(data[1]["response_id"], 2) + self.assertEqual(data[1]["language"], "fr") + self.assertEqual(len(data[1]["information"]), 2) + self.assertEqual(data[1]["information"][0]["headline"], "Headline 1 EN") + self.assertEqual(data[1]["information"][0]["value"], "Forth answer") + self.assertEqual(data[1]["information"][1]["headline"], "Headline 2 EN") + self.assertEqual(data[1]["information"][1]["value"], "Fifth answer") + self.assertTrue(data[1]["started"]) + self.assertIsNone(data[1]["submitted"]) + self.assertEqual(data[1]["total_score"], 13) + + self.assertEqual(data[2]["response_id"], 3) + self.assertEqual(data[2]["language"], "en") + self.assertEqual(len(data[2]["information"]), 2) + self.assertEqual(data[2]["information"][0]["headline"], "Headline 1 EN") + self.assertEqual(data[2]["information"][0]["value"], "Seventh answer") + self.assertEqual(data[2]["information"][1]["headline"], "Headline 2 EN") + self.assertEqual(data[2]["information"][1]["value"], "Eigth answer") + self.assertFalse(data[2]["started"]) + self.assertIsNone(data[2]["submitted"]) + self.assertEqual(data[2]["total_score"], 0.0) + class ResponseReviewerAssignmentApiTest(ApiTestCase): def seed_static_data(self): - self.event = self.add_event(key='event1') - self.event2 = self.add_event(key='event2') - self.event_admin = self.add_user('eventadmin@mail.com') - self.reviewer = self.add_user('reviewer@mail.com') + self.event = self.add_event(key="event1") + self.event2 = self.add_event(key="event2") + self.event_admin = self.add_user("eventadmin@mail.com") + self.reviewer = self.add_user("reviewer@mail.com") self.reviewer_user_id = self.reviewer.id - self.user1 = self.add_user('user1@mail.com') - self.user2 = self.add_user('user2@mail.com') - self.user3 = self.add_user('user3@mail.com') + self.user1 = self.add_user("user1@mail.com") + self.user2 = self.add_user("user2@mail.com") + self.user3 = self.add_user("user3@mail.com") - self.event.add_event_role('admin', self.event_admin.id) + self.event.add_event_role("admin", self.event_admin.id) application_form = self.create_application_form(self.event.id) application_form2 = self.create_application_form(self.event2.id) - self.response1 = self.add_response(application_form.id, self.user1.id, is_submitted=True) - self.response2 = self.add_response(application_form.id, self.user2.id, is_submitted=True) - self.response3 = self.add_response(application_form.id, self.user3.id, is_submitted=True) + self.response1 = self.add_response( + application_form.id, self.user1.id, is_submitted=True + ) + self.response2 = self.add_response( + application_form.id, self.user2.id, is_submitted=True + ) + self.response3 = self.add_response( + application_form.id, self.user3.id, is_submitted=True + ) self.add_review_form(application_form.id) self.add_review_form(application_form2.id) - self.event2_response_id = self.add_response(application_form2.id, self.user1.id, is_submitted=True).id + self.event2_response_id = self.add_response( + application_form2.id, self.user1.id, is_submitted=True + ).id - self.add_email_template('reviews-assigned') + self.add_email_template("reviews-assigned") def test_responses_assigned(self): self.seed_static_data() - params = {'event_id' : 1, 'response_ids': [1, 2], 'reviewer_email': 'reviewer@mail.com'} + params = { + "event_id": 1, + "response_ids": [1, 2], + "reviewer_email": "reviewer@mail.com", + } response = self.app.post( - '/api/v1/assignresponsereviewer', - headers=self.get_auth_header_for('eventadmin@mail.com'), - data=params) + "/api/v1/assignresponsereviewer", + headers=self.get_auth_header_for("eventadmin@mail.com"), + data=params, + ) self.assertEqual(response.status_code, 201) - response_reviewers = (db.session.query(ResponseReviewer) - .join(Response, ResponseReviewer.response_id == Response.id) - .filter_by(application_form_id=1).all()) + response_reviewers = ( + db.session.query(ResponseReviewer) + .join(Response, ResponseReviewer.response_id == Response.id) + .filter_by(application_form_id=1) + .all() + ) self.assertEqual(len(response_reviewers), 2) @@ -1465,12 +1959,17 @@ def test_responses_assigned(self): def test_response_for_different_event_forbidden(self): self.seed_static_data() - params = {'event_id' : 1, 'response_ids': [1, 2, self.event2_response_id], 'reviewer_email': 'reviewer@mail.com'} + params = { + "event_id": 1, + "response_ids": [1, 2, self.event2_response_id], + "reviewer_email": "reviewer@mail.com", + } response = self.app.post( - '/api/v1/assignresponsereviewer', - headers=self.get_auth_header_for('eventadmin@mail.com'), - data=params) + "/api/v1/assignresponsereviewer", + headers=self.get_auth_header_for("eventadmin@mail.com"), + data=params, + ) self.assertEqual(response.status_code, 403) @@ -1482,17 +1981,26 @@ def test_delete(self): response_id = self.response1.id self.add_response_reviewer(response_id, self.reviewer_user_id) - params = {'event_id' : 1, 'response_id': self.response1.id, 'reviewer_user_id': self.reviewer_user_id} + params = { + "event_id": 1, + "response_id": self.response1.id, + "reviewer_user_id": self.reviewer_user_id, + } response = self.app.delete( - '/api/v1/assignresponsereviewer', - headers=self.get_auth_header_for('eventadmin@mail.com'), - data=params) + "/api/v1/assignresponsereviewer", + headers=self.get_auth_header_for("eventadmin@mail.com"), + data=params, + ) self.assertEqual(response.status_code, 200) # Check that it was actually deleted - response_reviewer = db.session.query(ResponseReviewer).filter_by(response_id=response_id, reviewer_user_id=self.reviewer_user_id).first() + response_reviewer = ( + db.session.query(ResponseReviewer) + .filter_by(response_id=response_id, reviewer_user_id=self.reviewer_user_id) + .first() + ) self.assertIsNone(response_reviewer) def test_delete_not_allowed_if_completed(self): @@ -1504,12 +2012,17 @@ def test_delete_not_allowed_if_completed(self): self.add_response_reviewer(response_id, self.reviewer_user_id) self.add_review_response(self.reviewer_user_id, response_id) - params = {'event_id' : 1, 'response_id': self.response1.id, 'reviewer_user_id': self.reviewer_user_id} + params = { + "event_id": 1, + "response_id": self.response1.id, + "reviewer_user_id": self.reviewer_user_id, + } response = self.app.delete( - '/api/v1/assignresponsereviewer', - headers=self.get_auth_header_for('eventadmin@mail.com'), - data=params) + "/api/v1/assignresponsereviewer", + headers=self.get_auth_header_for("eventadmin@mail.com"), + data=params, + ) self.assertEqual(response.status_code, 400) @@ -1517,15 +2030,15 @@ def test_delete_not_allowed_if_completed(self): class ReferenceReviewRequest(ApiTestCase): def static_seed_data(self): # User, country and organisation is set up by ApiTestCase - self.first_user = self.add_user('firstuser@mail.com', 'First', 'User', 'Mx') + self.first_user = self.add_user("firstuser@mail.com", "First", "User", "Mx") self.event = self.add_event() - self.system_admin = self.add_user('sa@sa.com', is_admin=True) + self.system_admin = self.add_user("sa@sa.com", is_admin=True) non_admin_users = self.add_n_users(3) users = non_admin_users.append(self.system_admin) - self.event.add_event_role('admin', 4), - self.event.add_event_role('reviewer', 1), + self.event.add_event_role("admin", 4), + self.event.add_event_role("reviewer", 1), db.session.commit() @@ -1540,14 +2053,18 @@ def static_seed_data(self): ReviewForm(1, datetime(2019, 4, 30), 1, True), ReviewForm(1, datetime(2019, 4, 30), 2, False), closed_review, - inactive_review + inactive_review, ] db.session.add_all(review_forms) db.session.commit() - self.test_response = self.add_response(1, self.first_user.id, language='en', is_submitted=True) + self.test_response = self.add_response( + 1, self.first_user.id, language="en", is_submitted=True + ) - self.response_review = self.add_response_reviewer(self.test_response.id, self.first_user.id) + self.response_review = self.add_response_reviewer( + self.test_response.id, self.first_user.id + ) self.first_headers = self.get_auth_header_for("firstuser@mail.com") @@ -1555,179 +2072,236 @@ def static_seed_data(self): def test_get_reference_request_by_event_id(self): self.static_seed_data() - reference_req = ReferenceRequest(1, 'Mr', 'John', 'Snow', 'Supervisor', 'common@email.com') + reference_req = ReferenceRequest( + 1, "Mr", "John", "Snow", "Supervisor", "common@email.com" + ) reference_request_repository.create(reference_req) REFERENCE_DETAIL = { - 'token': reference_req.token, - 'uploaded_document': 'DOCT-UPLOAD-78999', + "token": reference_req.token, + "uploaded_document": "DOCT-UPLOAD-78999", } response = self.app.post( - '/api/v1/reference', data=REFERENCE_DETAIL, headers=self.first_headers) + "/api/v1/reference", data=REFERENCE_DETAIL, headers=self.first_headers + ) self.assertEqual(response.status_code, 201) - params = {'event_id': 1, 'language': 'en'} - response = self.app.get('/api/v1/review', headers=self.get_auth_header_for('firstuser@mail.com'), data=params) + params = {"event_id": 1, "language": "en"} + response = self.app.get( + "/api/v1/review", + headers=self.get_auth_header_for("firstuser@mail.com"), + data=params, + ) data = json.loads(response.data) self.assertEqual(response.status_code, 200) self.assertEqual(len(data), 8) - self.assertEqual(len(data['references']), 1) - self.assertDictEqual(data['references'][0], { - u'title': u'Mr', - u'firstname': u'John', - u'lastname': u'Snow', - u'relation': u'Supervisor', - u'uploaded_document': u'DOCT-UPLOAD-78999', - }) + self.assertEqual(len(data["references"]), 1) + self.assertDictEqual( + data["references"][0], + { + "title": "Mr", + "firstname": "John", + "lastname": "Snow", + "relation": "Supervisor", + "uploaded_document": "DOCT-UPLOAD-78999", + }, + ) def test_get_reference_request_with_two_references(self): """ In this test, there are two references requested and submitted """ self.static_seed_data() - reference_req = ReferenceRequest(1, 'Mr', 'John', 'Snow', 'Supervisor', 'common@email.com') - reference_req2 = ReferenceRequest(1, 'Mrs', 'Jane', 'Jones', 'Manager', 'jane@email.com') + reference_req = ReferenceRequest( + 1, "Mr", "John", "Snow", "Supervisor", "common@email.com" + ) + reference_req2 = ReferenceRequest( + 1, "Mrs", "Jane", "Jones", "Manager", "jane@email.com" + ) reference_request_repository.create(reference_req) reference_request_repository.create(reference_req2) REFERENCE_DETAIL = { - 'token': reference_req.token, - 'uploaded_document': 'DOCT-UPLOAD-78999', + "token": reference_req.token, + "uploaded_document": "DOCT-UPLOAD-78999", } REFERENCE_DETAIL_2 = { - 'token': reference_req2.token, - 'uploaded_document': 'DOCT-UPLOAD-78979', + "token": reference_req2.token, + "uploaded_document": "DOCT-UPLOAD-78979", } response = self.app.post( - '/api/v1/reference', data=REFERENCE_DETAIL, headers=self.first_headers) + "/api/v1/reference", data=REFERENCE_DETAIL, headers=self.first_headers + ) self.assertEqual(response.status_code, 201) response = self.app.post( - '/api/v1/reference', data=REFERENCE_DETAIL_2, headers=self.first_headers) + "/api/v1/reference", data=REFERENCE_DETAIL_2, headers=self.first_headers + ) self.assertEqual(response.status_code, 201) - params = {'event_id': 1, 'language': 'en'} - response = self.app.get('/api/v1/review', headers=self.get_auth_header_for('firstuser@mail.com'), data=params) + params = {"event_id": 1, "language": "en"} + response = self.app.get( + "/api/v1/review", + headers=self.get_auth_header_for("firstuser@mail.com"), + data=params, + ) data = json.loads(response.data) self.assertEqual(response.status_code, 200) self.assertEqual(len(data), 8) - self.assertEqual(len(data['references']), 2) - self.assertDictEqual(data['references'][0], { - u'title': u'Mr', - u'firstname': u'John', - u'lastname': u'Snow', - u'relation': u'Supervisor', - u'uploaded_document': u'DOCT-UPLOAD-78999', - }) - - self.assertDictEqual(data['references'][1], { - u'title': u'Mrs', - u'firstname': u'Jane', - u'lastname': u'Jones', - u'relation': u'Manager', - u'uploaded_document': u'DOCT-UPLOAD-78979', - }) + self.assertEqual(len(data["references"]), 2) + self.assertDictEqual( + data["references"][0], + { + "title": "Mr", + "firstname": "John", + "lastname": "Snow", + "relation": "Supervisor", + "uploaded_document": "DOCT-UPLOAD-78999", + }, + ) + + self.assertDictEqual( + data["references"][1], + { + "title": "Mrs", + "firstname": "Jane", + "lastname": "Jones", + "relation": "Manager", + "uploaded_document": "DOCT-UPLOAD-78979", + }, + ) def test_get_reference_not_yet_submitted(self): """ In this test, a reference has been requested, but not yet submitted by the referee (id exists, but no reference yet) """ self.static_seed_data() - reference_req = ReferenceRequest(1, 'Mr', 'John', 'Snow', 'Supervisor', 'common@email.com') + reference_req = ReferenceRequest( + 1, "Mr", "John", "Snow", "Supervisor", "common@email.com" + ) reference_request_repository.create(reference_req) - params = {'event_id': 1, 'language': 'en'} - response = self.app.get('/api/v1/review', headers=self.get_auth_header_for('firstuser@mail.com'), data=params) + params = {"event_id": 1, "language": "en"} + response = self.app.get( + "/api/v1/review", + headers=self.get_auth_header_for("firstuser@mail.com"), + data=params, + ) data = json.loads(response.data) self.assertEqual(response.status_code, 200) self.assertEqual(len(data), 8) - self.assertEqual(data.get('references', None), None) + self.assertEqual(data.get("references", None), None) def test_get_reference_submitted_later(self): """ In this test, a reference has been requested, but the reference is only submitted after the second check """ self.static_seed_data() - reference_req = ReferenceRequest(1, 'Mr', 'John', 'Snow', 'Supervisor', 'common@email.com') + reference_req = ReferenceRequest( + 1, "Mr", "John", "Snow", "Supervisor", "common@email.com" + ) reference_request_repository.create(reference_req) - params = {'event_id': 1, 'language': 'en'} + params = {"event_id": 1, "language": "en"} REFERENCE_DETAIL = { - 'token': reference_req.token, - 'uploaded_document': 'DOCT-UPLOAD-78999', + "token": reference_req.token, + "uploaded_document": "DOCT-UPLOAD-78999", } - response = self.app.get('/api/v1/review', headers=self.get_auth_header_for('firstuser@mail.com'), - data=params) + response = self.app.get( + "/api/v1/review", + headers=self.get_auth_header_for("firstuser@mail.com"), + data=params, + ) data = json.loads(response.data) self.assertEqual(response.status_code, 200) self.assertEqual(len(data), 8) - self.assertEqual(data.get('references', None), None) + self.assertEqual(data.get("references", None), None) response = self.app.post( - '/api/v1/reference', data=REFERENCE_DETAIL, headers=self.first_headers) + "/api/v1/reference", data=REFERENCE_DETAIL, headers=self.first_headers + ) self.assertEqual(response.status_code, 201) - params = {'event_id': 1, 'language': 'en'} - response = self.app.get('/api/v1/review', headers=self.get_auth_header_for('firstuser@mail.com'), data=params) + params = {"event_id": 1, "language": "en"} + response = self.app.get( + "/api/v1/review", + headers=self.get_auth_header_for("firstuser@mail.com"), + data=params, + ) data = json.loads(response.data) self.assertEqual(response.status_code, 200) self.assertEqual(len(data), 8) - self.assertEqual(len(data['references']), 1) - self.assertDictEqual(data['references'][0], { - u'title': u'Mr', - u'firstname': u'John', - u'lastname': u'Snow', - u'relation': u'Supervisor', - u'uploaded_document': u'DOCT-UPLOAD-78999', - }) + self.assertEqual(len(data["references"]), 1) + self.assertDictEqual( + data["references"][0], + { + "title": "Mr", + "firstname": "John", + "lastname": "Snow", + "relation": "Supervisor", + "uploaded_document": "DOCT-UPLOAD-78999", + }, + ) def test_two_references_only_one_submitted(self): """ In this test, two references have been requested, but only one submitted (two ids exists, one reference submitted) """ self.static_seed_data() - reference_req = ReferenceRequest(1, 'Mr', 'John', 'Snow', 'Supervisor', 'common@email.com') - reference_req2 = ReferenceRequest(1, 'Mrs', 'Jane', 'Jones', 'Manager', 'jane@email.com') + reference_req = ReferenceRequest( + 1, "Mr", "John", "Snow", "Supervisor", "common@email.com" + ) + reference_req2 = ReferenceRequest( + 1, "Mrs", "Jane", "Jones", "Manager", "jane@email.com" + ) reference_request_repository.create(reference_req) reference_request_repository.create(reference_req2) REFERENCE_DETAIL_2 = { - 'token': reference_req2.token, - 'uploaded_document': 'DOCT-UPLOAD-78979', + "token": reference_req2.token, + "uploaded_document": "DOCT-UPLOAD-78979", } response = self.app.post( - '/api/v1/reference', data=REFERENCE_DETAIL_2, headers=self.first_headers) + "/api/v1/reference", data=REFERENCE_DETAIL_2, headers=self.first_headers + ) self.assertEqual(response.status_code, 201) - params = {'event_id': 1, 'language': 'en'} - response = self.app.get('/api/v1/review', headers=self.get_auth_header_for('firstuser@mail.com'), data=params) + params = {"event_id": 1, "language": "en"} + response = self.app.get( + "/api/v1/review", + headers=self.get_auth_header_for("firstuser@mail.com"), + data=params, + ) data = json.loads(response.data) self.assertEqual(response.status_code, 200) self.assertEqual(len(data), 8) - self.assertEqual(len(data['references']), 1) + self.assertEqual(len(data["references"]), 1) - self.assertDictEqual(data['references'][0], { - u'title': u'Mrs', - u'firstname': u'Jane', - u'lastname': u'Jones', - u'relation': u'Manager', - u'uploaded_document': u'DOCT-UPLOAD-78979', - }) + self.assertDictEqual( + data["references"][0], + { + "title": "Mrs", + "firstname": "Jane", + "lastname": "Jones", + "relation": "Manager", + "uploaded_document": "DOCT-UPLOAD-78979", + }, + ) def test_get_review_no_reference(self): """ @@ -1735,144 +2309,167 @@ def test_get_review_no_reference(self): """ self.static_seed_data() - params = {'event_id': 1, 'language': 'en'} - response = self.app.get('/api/v1/review', headers=self.get_auth_header_for('firstuser@mail.com'), data=params) + params = {"event_id": 1, "language": "en"} + response = self.app.get( + "/api/v1/review", + headers=self.get_auth_header_for("firstuser@mail.com"), + data=params, + ) data = json.loads(response.data) self.assertEqual(response.status_code, 200) self.assertEqual(len(data), 8) - self.assertEqual(data.get('references', None), None) + self.assertEqual(data.get("references", None), None) class ReviewResponseDetailListApiTest(ApiTestCase): def seed_static_data(self): - self.user1 = self.add_user('user1@mail.com', 'Jane', 'Bloggs', 'Ms') - self.user2 = self.add_user('user2@mail.com', 'Alex', 'Person', 'Dr') - self.reviewer = self.add_user('reviewer@mail.com', 'Joe', 'Soap', 'Mr') - self.reviewer2 = self.add_user('reviewer2@mail.com', 'Jenny', 'Sharp', 'Ms') - self.event_admin = self.add_user('event_admin@mail.com') + self.user1 = self.add_user("user1@mail.com", "Jane", "Bloggs", "Ms") + self.user2 = self.add_user("user2@mail.com", "Alex", "Person", "Dr") + self.reviewer = self.add_user("reviewer@mail.com", "Joe", "Soap", "Mr") + self.reviewer2 = self.add_user("reviewer2@mail.com", "Jenny", "Sharp", "Ms") + self.event_admin = self.add_user("event_admin@mail.com") self.event = self.add_event() - self.event2 = self.add_event(key='Empty') - self.event.add_event_role('admin', self.event_admin.id) - self.event2.add_event_role('admin', self.event_admin.id) + self.event2 = self.add_event(key="Empty") + self.event.add_event_role("admin", self.event_admin.id) + self.event2.add_event_role("admin", self.event_admin.id) self.application_form = self.create_application_form(self.event.id) self.section = self.add_section(self.application_form.id) self.question1 = self.add_question( - self.application_form.id, - self.section.id, - order=1, - key='review-identifier') + self.application_form.id, self.section.id, order=1, key="review-identifier" + ) self.question_translation1 = self.add_question_translation( - self.question1.id, - 'en', - 'Organisation') + self.question1.id, "en", "Organisation" + ) self.question2 = self.add_question( - self.application_form.id, - self.section.id, - order=2, - key='review-identifier') + self.application_form.id, self.section.id, order=2, key="review-identifier" + ) self.question_translation2 = self.add_question_translation( - self.question2.id, - 'en', - 'Country') + self.question2.id, "en", "Country" + ) self.question3 = self.add_question( self.application_form.id, self.section.id, order=3, ) self.question_translation3 = self.add_question_translation( - self.question3.id, - 'en', - 'Non-review question' + self.question3.id, "en", "Non-review question" ) self.response1 = self.add_response( - self.application_form.id, - self.user1.id, - is_submitted=True) - self.answer1 = self.add_answer(self.response1.id, self.question1.id, 'Pets R Us') - self.answer2 = self.add_answer(self.response1.id, self.question2.id, 'Nigeria') - self.answer3 = self.add_answer(self.response1.id, self.question3.id, 'Non-review answer') + self.application_form.id, self.user1.id, is_submitted=True + ) + self.answer1 = self.add_answer( + self.response1.id, self.question1.id, "Pets R Us" + ) + self.answer2 = self.add_answer(self.response1.id, self.question2.id, "Nigeria") + self.answer3 = self.add_answer( + self.response1.id, self.question3.id, "Non-review answer" + ) self.response2 = self.add_response( - self.application_form.id, - self.user2.id, - is_submitted=True) - self.answer4 = self.add_answer(self.response2.id, self.question1.id, 'Nokia') - self.answer5 = self.add_answer(self.response2.id, self.question2.id, 'South Africa') - self.answer6 = self.add_answer(self.response2.id, self.question3.id, 'Non-review answer 2') + self.application_form.id, self.user2.id, is_submitted=True + ) + self.answer4 = self.add_answer(self.response2.id, self.question1.id, "Nokia") + self.answer5 = self.add_answer( + self.response2.id, self.question2.id, "South Africa" + ) + self.answer6 = self.add_answer( + self.response2.id, self.question3.id, "Non-review answer 2" + ) self.review_form = self.add_review_form(self.application_form.id) self.review_section1 = self.add_review_section(self.review_form.id) self.review_section1_translation1 = self.add_review_section_translation( - self.review_section1.id, - 'en') + self.review_section1.id, "en" + ) self.review_section2 = self.add_review_section(self.review_form.id) self.review_section2_translation1 = self.add_review_section_translation( self.review_section2.id, - 'en' - 'Review Section 2', - 'Review Section 2 Description') - self.review_question1 = self.add_review_question(self.review_section1.id, type='short-text', weight=1) + "en" "Review Section 2", + "Review Section 2 Description", + ) + self.review_question1 = self.add_review_question( + self.review_section1.id, type="short-text", weight=1 + ) self.review_question_translation1 = self.add_review_question_translation( self.review_question1.id, - 'en', - headline='Ethical Considerations', - description="How ethical is the candidate's proposal from 1 to 5?") - self.review_question2 = self.add_review_question(self.review_section1.id, type='long-text', weight=0) + "en", + headline="Ethical Considerations", + description="How ethical is the candidate's proposal from 1 to 5?", + ) + self.review_question2 = self.add_review_question( + self.review_section1.id, type="long-text", weight=0 + ) self.review_question_translation2 = self.add_review_question_translation( self.review_question2.id, - 'en', + "en", headline=None, - description='Comments for the candidate' + description="Comments for the candidate", + ) + self.review_question3 = self.add_review_question( + self.review_section2.id, type="multi-choice", weight=2 ) - self.review_question3 = self.add_review_question(self.review_section2.id, type='multi-choice', weight=2) self.review_question_translation3 = self.add_review_question_translation( self.review_question3.id, - 'en', + "en", headline=None, - description='What is your overall rating?' + description="What is your overall rating?", + ) + self.review_question4 = self.add_review_question( + self.review_section2.id, type="checkbox", weight=0 ) - self.review_question4 = self.add_review_question(self.review_section2.id, type='checkbox', weight=0) self.review_question_translation4 = self.add_review_question_translation( - self.review_question4.id, - 'en', - headline='Yes/No Question' + self.review_question4.id, "en", headline="Yes/No Question" ) self.review_response1 = self.add_review_response( - self.reviewer.id, - self.response1.id, - self.review_form.id, - is_submitted=True) - self.review_score1 = self.add_review_score(self.review_response1.id, self.review_question1.id, '4') - self.review_score2 = self.add_review_score(self.review_response1.id, self.review_question2.id, 'This is a very good proposal') - self.review_score3 = self.add_review_score(self.review_response1.id, self.review_question3.id, '5') - self.review_score4 = self.add_review_score(self.review_response1.id, self.review_question4.id, 'Yes') + self.reviewer.id, self.response1.id, self.review_form.id, is_submitted=True + ) + self.review_score1 = self.add_review_score( + self.review_response1.id, self.review_question1.id, "4" + ) + self.review_score2 = self.add_review_score( + self.review_response1.id, + self.review_question2.id, + "This is a very good proposal", + ) + self.review_score3 = self.add_review_score( + self.review_response1.id, self.review_question3.id, "5" + ) + self.review_score4 = self.add_review_score( + self.review_response1.id, self.review_question4.id, "Yes" + ) self.review_response2 = self.add_review_response( - self.reviewer2.id, - self.response2.id, - self.review_form.id, - is_submitted=True) - self.review_score5 = self.add_review_score(self.review_response2.id, self.review_question1.id, '3') - self.review_score6 = self.add_review_score(self.review_response2.id, self.review_question2.id, 'Not bad!') - self.review_score7 = self.add_review_score(self.review_response2.id, self.review_question3.id, '3') - self.review_score8 = self.add_review_score(self.review_response2.id, self.review_question4.id, 'No') + self.reviewer2.id, self.response2.id, self.review_form.id, is_submitted=True + ) + self.review_score5 = self.add_review_score( + self.review_response2.id, self.review_question1.id, "3" + ) + self.review_score6 = self.add_review_score( + self.review_response2.id, self.review_question2.id, "Not bad!" + ) + self.review_score7 = self.add_review_score( + self.review_response2.id, self.review_question3.id, "3" + ) + self.review_score8 = self.add_review_score( + self.review_response2.id, self.review_question4.id, "No" + ) def test_not_authed(self): - response = self.app.get('/api/v1/reviewresponsedetaillist') + response = self.app.get("/api/v1/reviewresponsedetaillist") self.assertEqual(response.status_code, 401) def test_not_event_admin(self): self.seed_static_data() response = self.app.get( - '/api/v1/reviewresponsedetaillist', - headers=self.get_auth_header_for('user1@mail.com'), - data={'event_id': 1} + "/api/v1/reviewresponsedetaillist", + headers=self.get_auth_header_for("user1@mail.com"), + data={"event_id": 1}, ) self.assertEqual(response.status_code, 403) @@ -1881,9 +2478,9 @@ def test_no_review_responses(self): self.seed_static_data() response = self.app.get( - '/api/v1/reviewresponsedetaillist', - headers=self.get_auth_header_for('event_admin@mail.com'), - data={'event_id': 2, 'language': 'en'} + "/api/v1/reviewresponsedetaillist", + headers=self.get_auth_header_for("event_admin@mail.com"), + data={"event_id": 2, "language": "en"}, ) data = json.loads(response.data) @@ -1895,246 +2492,292 @@ def test_review_responses_for_event(self): self.seed_static_data() response = self.app.get( - '/api/v1/reviewresponsedetaillist', - headers=self.get_auth_header_for('event_admin@mail.com'), - data={'event_id': 1, 'language': 'en'} + "/api/v1/reviewresponsedetaillist", + headers=self.get_auth_header_for("event_admin@mail.com"), + data={"event_id": 1, "language": "en"}, ) data = json.loads(response.data) self.assertEqual(len(data), 2) - self.assertEqual(data[0]['review_response_id'], 1) - self.assertEqual(data[0]['response_id'], 1) - - self.assertEqual(data[0]['reviewer_user_title'], 'Mr') - self.assertEqual(data[0]['reviewer_user_firstname'], 'Joe') - self.assertEqual(data[0]['reviewer_user_lastname'], 'Soap') - - self.assertEqual(data[0]['response_user_title'], 'Ms') - self.assertEqual(data[0]['response_user_firstname'], 'Jane') - self.assertEqual(data[0]['response_user_lastname'], 'Bloggs') - - self.assertEqual(len(data[0]['identifiers']), 2) - self.assertEqual(data[0]['identifiers'][0]['headline'], 'Organisation') - self.assertEqual(data[0]['identifiers'][0]['value'], 'Pets R Us') - self.assertEqual(data[0]['identifiers'][1]['headline'], 'Country') - self.assertEqual(data[0]['identifiers'][1]['value'], 'Nigeria') + self.assertEqual(data[0]["review_response_id"], 1) + self.assertEqual(data[0]["response_id"], 1) - self.assertEqual(len(data[0]['scores']), 3) - 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]['weight'], 1) + self.assertEqual(data[0]["reviewer_user_title"], "Mr") + self.assertEqual(data[0]["reviewer_user_firstname"], "Joe") + self.assertEqual(data[0]["reviewer_user_lastname"], "Soap") - 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]['weight'], 0) + self.assertEqual(data[0]["response_user_title"], "Ms") + self.assertEqual(data[0]["response_user_firstname"], "Jane") + self.assertEqual(data[0]["response_user_lastname"], "Bloggs") - 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]['weight'], 2) + self.assertEqual(len(data[0]["identifiers"]), 2) + self.assertEqual(data[0]["identifiers"][0]["headline"], "Organisation") + self.assertEqual(data[0]["identifiers"][0]["value"], "Pets R Us") + self.assertEqual(data[0]["identifiers"][1]["headline"], "Country") + self.assertEqual(data[0]["identifiers"][1]["value"], "Nigeria") - self.assertEqual(data[0]['total'], 14) - - - self.assertEqual(data[1]['review_response_id'], 2) - self.assertEqual(data[1]['response_id'], 2) - - self.assertEqual(data[1]['reviewer_user_title'], 'Ms') - self.assertEqual(data[1]['reviewer_user_firstname'], 'Jenny') - self.assertEqual(data[1]['reviewer_user_lastname'], 'Sharp') - - self.assertEqual(data[1]['response_user_title'], 'Dr') - self.assertEqual(data[1]['response_user_firstname'], 'Alex') - self.assertEqual(data[1]['response_user_lastname'], 'Person') + self.assertEqual(len(data[0]["scores"]), 3) + 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]["weight"], 1) - self.assertEqual(len(data[1]['identifiers']), 2) - self.assertEqual(data[1]['identifiers'][0]['headline'], 'Organisation') - self.assertEqual(data[1]['identifiers'][0]['value'], 'Nokia') - self.assertEqual(data[1]['identifiers'][1]['headline'], 'Country') - self.assertEqual(data[1]['identifiers'][1]['value'], 'South Africa') + 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]["weight"], 0) - self.assertEqual(len(data[1]['scores']), 3) - 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]['weight'], 1) + 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]["weight"], 2) + + self.assertEqual(data[0]["total"], 14) + + self.assertEqual(data[1]["review_response_id"], 2) + self.assertEqual(data[1]["response_id"], 2) + + self.assertEqual(data[1]["reviewer_user_title"], "Ms") + self.assertEqual(data[1]["reviewer_user_firstname"], "Jenny") + self.assertEqual(data[1]["reviewer_user_lastname"], "Sharp") + + self.assertEqual(data[1]["response_user_title"], "Dr") + self.assertEqual(data[1]["response_user_firstname"], "Alex") + self.assertEqual(data[1]["response_user_lastname"], "Person") + + self.assertEqual(len(data[1]["identifiers"]), 2) + self.assertEqual(data[1]["identifiers"][0]["headline"], "Organisation") + self.assertEqual(data[1]["identifiers"][0]["value"], "Nokia") + self.assertEqual(data[1]["identifiers"][1]["headline"], "Country") + self.assertEqual(data[1]["identifiers"][1]["value"], "South Africa") + + self.assertEqual(len(data[1]["scores"]), 3) + 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]["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]['weight'], 0) + 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]["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]['weight'], 2) + 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]["weight"], 2) - self.assertEqual(data[1]['total'], 9) + self.assertEqual(data[1]["total"], 9) class ReviewResponseSummaryListApiTest(ApiTestCase): def seed_static_data(self): - self.user1 = self.add_user('user1@mail.com', 'Jane', 'Bloggs', 'Ms') - self.user2 = self.add_user('user2@mail.com', 'Alex', 'Person', 'Dr') - self.reviewer = self.add_user('reviewer@mail.com', 'Joe', 'Soap', 'Mr') - self.reviewer2 = self.add_user('reviewer2@mail.com', 'Jenny', 'Sharp', 'Ms') - self.event_admin = self.add_user('event_admin@mail.com') + self.user1 = self.add_user("user1@mail.com", "Jane", "Bloggs", "Ms") + self.user2 = self.add_user("user2@mail.com", "Alex", "Person", "Dr") + self.reviewer = self.add_user("reviewer@mail.com", "Joe", "Soap", "Mr") + self.reviewer2 = self.add_user("reviewer2@mail.com", "Jenny", "Sharp", "Ms") + self.event_admin = self.add_user("event_admin@mail.com") self.event = self.add_event() - self.event2 = self.add_event(key='Empty') - self.event.add_event_role('admin', self.event_admin.id) - self.event2.add_event_role('admin', self.event_admin.id) + self.event2 = self.add_event(key="Empty") + self.event.add_event_role("admin", self.event_admin.id) + self.event2.add_event_role("admin", self.event_admin.id) self.application_form = self.create_application_form(self.event.id) self.section = self.add_section(self.application_form.id) self.question1 = self.add_question( - self.application_form.id, - self.section.id, - order=1, - key='review-identifier') + self.application_form.id, self.section.id, order=1, key="review-identifier" + ) self.question_translation1 = self.add_question_translation( - self.question1.id, - 'en', - 'Organisation') + self.question1.id, "en", "Organisation" + ) self.question2 = self.add_question( - self.application_form.id, - self.section.id, - order=2, - key='review-identifier') + self.application_form.id, self.section.id, order=2, key="review-identifier" + ) self.question_translation2 = self.add_question_translation( - self.question2.id, - 'en', - 'Country') + self.question2.id, "en", "Country" + ) self.question3 = self.add_question( self.application_form.id, self.section.id, order=3, ) self.question_translation3 = self.add_question_translation( - self.question3.id, - 'en', - 'Non-review question' + self.question3.id, "en", "Non-review question" ) self.response1 = self.add_response( - self.application_form.id, - self.user1.id, - is_submitted=True) - self.answer1 = self.add_answer(self.response1.id, self.question1.id, 'Pets R Us') - self.answer2 = self.add_answer(self.response1.id, self.question2.id, 'Nigeria') - self.answer3 = self.add_answer(self.response1.id, self.question3.id, 'Non-review answer') + self.application_form.id, self.user1.id, is_submitted=True + ) + self.answer1 = self.add_answer( + self.response1.id, self.question1.id, "Pets R Us" + ) + self.answer2 = self.add_answer(self.response1.id, self.question2.id, "Nigeria") + self.answer3 = self.add_answer( + self.response1.id, self.question3.id, "Non-review answer" + ) self.response2 = self.add_response( - self.application_form.id, - self.user2.id, - is_submitted=True) - self.answer4 = self.add_answer(self.response2.id, self.question1.id, 'Nokia') - self.answer5 = self.add_answer(self.response2.id, self.question2.id, 'South Africa') - self.answer6 = self.add_answer(self.response2.id, self.question3.id, 'Non-review answer 2') + self.application_form.id, self.user2.id, is_submitted=True + ) + self.answer4 = self.add_answer(self.response2.id, self.question1.id, "Nokia") + self.answer5 = self.add_answer( + self.response2.id, self.question2.id, "South Africa" + ) + self.answer6 = self.add_answer( + self.response2.id, self.question3.id, "Non-review answer 2" + ) self.review_form = self.add_review_form(self.application_form.id) self.review_section1 = self.add_review_section(self.review_form.id) self.review_section1_translation1 = self.add_review_section_translation( - self.review_section1.id, - 'en') + self.review_section1.id, "en" + ) self.review_section2 = self.add_review_section(self.review_form.id) self.review_section2_translation1 = self.add_review_section_translation( self.review_section2.id, - 'en' - 'Review Section 2', - 'Review Section 2 Description') - self.review_question1 = self.add_review_question(self.review_section1.id, type='short-text', weight=1) + "en" "Review Section 2", + "Review Section 2 Description", + ) + self.review_question1 = self.add_review_question( + self.review_section1.id, type="short-text", weight=1 + ) self.review_question_translation1 = self.add_review_question_translation( self.review_question1.id, - 'en', - headline='Ethical Considerations', - description="How ethical is the candidate's proposal from 1 to 5?") - self.review_question2 = self.add_review_question(self.review_section1.id, type='long-text', weight=0) + "en", + headline="Ethical Considerations", + description="How ethical is the candidate's proposal from 1 to 5?", + ) + self.review_question2 = self.add_review_question( + self.review_section1.id, type="long-text", weight=0 + ) self.review_question_translation2 = self.add_review_question_translation( self.review_question2.id, - 'en', + "en", headline=None, - description='Comments for the candidate' + description="Comments for the candidate", + ) + self.review_question3 = self.add_review_question( + self.review_section2.id, type="multi-choice", weight=2 ) - self.review_question3 = self.add_review_question(self.review_section2.id, type='multi-choice', weight=2) self.review_question_translation3 = self.add_review_question_translation( self.review_question3.id, - 'en', + "en", headline=None, - description='What is your overall rating?' + description="What is your overall rating?", + ) + self.review_question4 = self.add_review_question( + self.review_section2.id, type="checkbox", weight=0 ) - self.review_question4 = self.add_review_question(self.review_section2.id, type='checkbox', weight=0) self.review_question_translation4 = self.add_review_question_translation( - self.review_question4.id, - 'en', - headline='Yes/No Question' + self.review_question4.id, "en", headline="Yes/No Question" ) # reviewer 1 for first response self.review_response1 = self.add_review_response( - self.reviewer.id, - self.response1.id, - self.review_form.id, - is_submitted=True) - self.review_score1 = self.add_review_score(self.review_response1.id, self.review_question1.id, '4') - self.review_score2 = self.add_review_score(self.review_response1.id, self.review_question2.id, 'This is a very good proposal') - self.review_score3 = self.add_review_score(self.review_response1.id, self.review_question3.id, '5') - self.review_score4 = self.add_review_score(self.review_response1.id, self.review_question4.id, 'Yes') + self.reviewer.id, self.response1.id, self.review_form.id, is_submitted=True + ) + self.review_score1 = self.add_review_score( + self.review_response1.id, self.review_question1.id, "4" + ) + self.review_score2 = self.add_review_score( + self.review_response1.id, + self.review_question2.id, + "This is a very good proposal", + ) + self.review_score3 = self.add_review_score( + self.review_response1.id, self.review_question3.id, "5" + ) + self.review_score4 = self.add_review_score( + self.review_response1.id, self.review_question4.id, "Yes" + ) # reviewer 2 for first response self.review_response3 = self.add_review_response( - self.reviewer2.id, - self.response1.id, - self.review_form.id, - is_submitted=True) - self.review_score9 = self.add_review_score(self.review_response3.id, self.review_question1.id, '3') - self.review_score10 = self.add_review_score(self.review_response3.id, self.review_question2.id, 'This is a good proposal') - self.review_score11 = self.add_review_score(self.review_response3.id, self.review_question3.id, '4') - self.review_score12 = self.add_review_score(self.review_response3.id, self.review_question4.id, 'Yes') + self.reviewer2.id, self.response1.id, self.review_form.id, is_submitted=True + ) + self.review_score9 = self.add_review_score( + self.review_response3.id, self.review_question1.id, "3" + ) + self.review_score10 = self.add_review_score( + self.review_response3.id, + self.review_question2.id, + "This is a good proposal", + ) + self.review_score11 = self.add_review_score( + self.review_response3.id, self.review_question3.id, "4" + ) + self.review_score12 = self.add_review_score( + self.review_response3.id, self.review_question4.id, "Yes" + ) # reviewer 2 for second response self.review_response2 = self.add_review_response( - self.reviewer2.id, - self.response2.id, - self.review_form.id, - is_submitted=True) - self.review_score5 = self.add_review_score(self.review_response2.id, self.review_question1.id, '3') - self.review_score6 = self.add_review_score(self.review_response2.id, self.review_question2.id, 'Close to bad!') - self.review_score7 = self.add_review_score(self.review_response2.id, self.review_question3.id, '2') - self.review_score8 = self.add_review_score(self.review_response2.id, self.review_question4.id, 'No') + self.reviewer2.id, self.response2.id, self.review_form.id, is_submitted=True + ) + self.review_score5 = self.add_review_score( + self.review_response2.id, self.review_question1.id, "3" + ) + self.review_score6 = self.add_review_score( + self.review_response2.id, self.review_question2.id, "Close to bad!" + ) + self.review_score7 = self.add_review_score( + self.review_response2.id, self.review_question3.id, "2" + ) + self.review_score8 = self.add_review_score( + self.review_response2.id, self.review_question4.id, "No" + ) # reviewer 1 for second response self.review_response4 = self.add_review_response( - self.reviewer.id, - self.response2.id, - self.review_form.id, - is_submitted=True) - self.review_score13 = self.add_review_score(self.review_response4.id, self.review_question1.id, '2') - self.review_score14 = self.add_review_score(self.review_response4.id, self.review_question2.id, 'Bad!') - self.review_score15 = self.add_review_score(self.review_response4.id, self.review_question3.id, '1') - self.review_score16 = self.add_review_score(self.review_response4.id, self.review_question4.id, 'No') + self.reviewer.id, self.response2.id, self.review_form.id, is_submitted=True + ) + self.review_score13 = self.add_review_score( + self.review_response4.id, self.review_question1.id, "2" + ) + self.review_score14 = self.add_review_score( + self.review_response4.id, self.review_question2.id, "Bad!" + ) + self.review_score15 = self.add_review_score( + self.review_response4.id, self.review_question3.id, "1" + ) + self.review_score16 = self.add_review_score( + self.review_response4.id, self.review_question4.id, "No" + ) def test_not_authed(self): - response = self.app.get('/api/v1/reviewresponsesummarylist') + response = self.app.get("/api/v1/reviewresponsesummarylist") self.assertEqual(response.status_code, 401) def test_not_event_admin(self): self.seed_static_data() response = self.app.get( - '/api/v1/reviewresponsesummarylist', - headers=self.get_auth_header_for('user1@mail.com'), - data={'event_id': 1} + "/api/v1/reviewresponsesummarylist", + headers=self.get_auth_header_for("user1@mail.com"), + data={"event_id": 1}, ) self.assertEqual(response.status_code, 403) @@ -2143,67 +2786,76 @@ def test_review_response_summary_for_event(self): self.seed_static_data() response = self.app.get( - '/api/v1/reviewresponsesummarylist', - headers=self.get_auth_header_for('event_admin@mail.com'), - data={'event_id': 1, 'language': 'en'} + "/api/v1/reviewresponsesummarylist", + headers=self.get_auth_header_for("event_admin@mail.com"), + data={"event_id": 1, "language": "en"}, ) data = json.loads(response.data) self.assertEqual(len(data), 2) - self.assertEqual(data[0]['response_id'], 1) - self.assertEqual(data[0]['response_user_title'], 'Ms') - self.assertEqual(data[0]['response_user_firstname'], 'Jane') - self.assertEqual(data[0]['response_user_lastname'], 'Bloggs') - - self.assertEqual(len(data[0]['identifiers']), 2) - self.assertEqual(data[0]['identifiers'][0]['headline'], 'Organisation') - self.assertEqual(data[0]['identifiers'][0]['value'], 'Pets R Us') - self.assertEqual(data[0]['identifiers'][1]['headline'], 'Country') - self.assertEqual(data[0]['identifiers'][1]['value'], 'Nigeria') - - self.assertEqual(len(data[0]['scores']), 2) - 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'], 3.5) - self.assertEqual(data[0]['scores'][0]['weight'], 1) - - self.assertEqual(data[0]['scores'][1]['headline'], None) - self.assertEqual(data[0]['scores'][1]['description'], 'What is your overall rating?') - self.assertEqual(data[0]['scores'][1]['type'], 'multi-choice') - self.assertEqual(data[0]['scores'][1]['score'], 4.5) - self.assertEqual(data[0]['scores'][1]['weight'], 2) - - self.assertEqual(data[0]['total'], 12.5) - - - self.assertEqual(data[1]['response_id'], 2) - self.assertEqual(data[1]['response_user_title'], 'Dr') - self.assertEqual(data[1]['response_user_firstname'], 'Alex') - self.assertEqual(data[1]['response_user_lastname'], 'Person') - - self.assertEqual(len(data[1]['identifiers']), 2) - self.assertEqual(data[1]['identifiers'][0]['headline'], 'Organisation') - self.assertEqual(data[1]['identifiers'][0]['value'], 'Nokia') - self.assertEqual(data[1]['identifiers'][1]['headline'], 'Country') - self.assertEqual(data[1]['identifiers'][1]['value'], 'South Africa') + self.assertEqual(data[0]["response_id"], 1) + self.assertEqual(data[0]["response_user_title"], "Ms") + self.assertEqual(data[0]["response_user_firstname"], "Jane") + self.assertEqual(data[0]["response_user_lastname"], "Bloggs") + + self.assertEqual(len(data[0]["identifiers"]), 2) + self.assertEqual(data[0]["identifiers"][0]["headline"], "Organisation") + self.assertEqual(data[0]["identifiers"][0]["value"], "Pets R Us") + self.assertEqual(data[0]["identifiers"][1]["headline"], "Country") + self.assertEqual(data[0]["identifiers"][1]["value"], "Nigeria") + + self.assertEqual(len(data[0]["scores"]), 2) + 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"], 3.5) + self.assertEqual(data[0]["scores"][0]["weight"], 1) - self.assertEqual(len(data[1]['scores']), 2) - 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'], 2.5) - self.assertEqual(data[1]['scores'][0]['weight'], 1) + self.assertEqual(data[0]["scores"][1]["headline"], None) + self.assertEqual( + data[0]["scores"][1]["description"], "What is your overall rating?" + ) + self.assertEqual(data[0]["scores"][1]["type"], "multi-choice") + self.assertEqual(data[0]["scores"][1]["score"], 4.5) + self.assertEqual(data[0]["scores"][1]["weight"], 2) + + self.assertEqual(data[0]["total"], 12.5) + + self.assertEqual(data[1]["response_id"], 2) + self.assertEqual(data[1]["response_user_title"], "Dr") + self.assertEqual(data[1]["response_user_firstname"], "Alex") + self.assertEqual(data[1]["response_user_lastname"], "Person") + + self.assertEqual(len(data[1]["identifiers"]), 2) + self.assertEqual(data[1]["identifiers"][0]["headline"], "Organisation") + self.assertEqual(data[1]["identifiers"][0]["value"], "Nokia") + self.assertEqual(data[1]["identifiers"][1]["headline"], "Country") + self.assertEqual(data[1]["identifiers"][1]["value"], "South Africa") + + self.assertEqual(len(data[1]["scores"]), 2) + 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"], 2.5) + self.assertEqual(data[1]["scores"][0]["weight"], 1) - self.assertEqual(data[1]['scores'][1]['headline'], None) - self.assertEqual(data[1]['scores'][1]['description'], 'What is your overall rating?') - self.assertEqual(data[1]['scores'][1]['type'], 'multi-choice') - self.assertEqual(data[1]['scores'][1]['score'], 1.5) - self.assertEqual(data[1]['scores'][1]['weight'], 2) + self.assertEqual(data[1]["scores"][1]["headline"], None) + self.assertEqual( + data[1]["scores"][1]["description"], "What is your overall rating?" + ) + self.assertEqual(data[1]["scores"][1]["type"], "multi-choice") + self.assertEqual(data[1]["scores"][1]["score"], 1.5) + self.assertEqual(data[1]["scores"][1]["weight"], 2) - self.assertEqual(data[1]['total'], 5.5) + self.assertEqual(data[1]["total"], 5.5) class ReviewStageAPITest(ApiTestCase): @@ -2212,7 +2864,7 @@ def seed_static_data(self, activate_second_stage=True): self.create_application_form() event_admin = self.add_user("event@admin.com") - first_event.add_event_role('admin', event_admin.id) + first_event.add_event_role("admin", event_admin.id) self.add_review_form(stage=1, active=False) self.add_review_form(stage=2, active=activate_second_stage) @@ -2221,7 +2873,7 @@ def seed_static_data(self, activate_second_stage=True): # An event with no review form no_form_event = self.add_event(key="no_form_event") self.no_form_event_id = no_form_event.id - no_form_event.add_event_role('admin', event_admin.id) + no_form_event.add_event_role("admin", event_admin.id) db.session.commit() self.event_admin_headers = self.get_auth_header_for("event@admin.com") @@ -2231,24 +2883,24 @@ def test_get(self): self.seed_static_data() response = self.app.get( - '/api/v1/reviewstage', + "/api/v1/reviewstage", headers=self.event_admin_headers, - data={'event_id': 1} + data={"event_id": 1}, ) data = json.loads(response.data) - self.assertEqual(data['current_stage'], 2) - self.assertEqual(data['total_stages'], 3) + self.assertEqual(data["current_stage"], 2) + self.assertEqual(data["total_stages"], 3) def test_get_no_active(self): """Test that the get method returns a 404 when ther is no active review form.""" self.seed_static_data(activate_second_stage=False) response = self.app.get( - '/api/v1/reviewstage', + "/api/v1/reviewstage", headers=self.event_admin_headers, - data={'event_id': 1} + data={"event_id": 1}, ) self.assertEqual(response.status_code, 404) @@ -2258,9 +2910,9 @@ def test_get_no_form(self): self.seed_static_data() response = self.app.get( - '/api/v1/reviewstage', + "/api/v1/reviewstage", headers=self.event_admin_headers, - data={'event_id': self.no_form_event_id} + data={"event_id": self.no_form_event_id}, ) self.assertEqual(response.status_code, 404) @@ -2270,32 +2922,32 @@ def test_post(self): self.seed_static_data() response = self.app.post( - '/api/v1/reviewstage', + "/api/v1/reviewstage", headers=self.event_admin_headers, - data={'event_id': 1, 'stage': 3} + data={"event_id": 1, "stage": 3}, ) self.assertEqual(response.status_code, 201) response = self.app.get( - '/api/v1/reviewstage', + "/api/v1/reviewstage", headers=self.event_admin_headers, - data={'event_id': 1} + data={"event_id": 1}, ) data = json.loads(response.data) - self.assertEqual(data['current_stage'], 3) - self.assertEqual(data['total_stages'], 3) + self.assertEqual(data["current_stage"], 3) + self.assertEqual(data["total_stages"], 3) def test_post_no_form(self): """Test that the post method returns a 404 when there are no review forms for the event.""" self.seed_static_data() response = self.app.post( - '/api/v1/reviewstage', + "/api/v1/reviewstage", headers=self.event_admin_headers, - data={'event_id': self.no_form_event_id, 'stage': 1} + data={"event_id": self.no_form_event_id, "stage": 1}, ) self.assertEqual(response.status_code, 404) @@ -2305,298 +2957,223 @@ def test_post_no_form(self): self.seed_static_data() response = self.app.post( - '/api/v1/reviewstage', + "/api/v1/reviewstage", headers=self.event_admin_headers, - data={'event_id': 1, 'stage': 4} + data={"event_id": 1, "stage": 4}, ) self.assertEqual(response.status_code, 404) - + REVIEW_FORM_POST = { - "event_id": 1, - "application_form_id": 1, - "stage": 2, - "is_open": True, - "deadline": datetime(2024, 6, 1).strftime('%Y-%m-%dT%H:%M:%S'), - "active": True, - "sections": [ - { - "order": 1, - "name": { - "en": "Section 1", - "fr": "Section 1" - }, - "description": { - "en": "This is the first section", - "fr": "Ceci est la première section" - }, - "questions": [ - { - "type": "short-text", - "is_required": False, - "order": 1, - "weight": 0.0, - "question_id": 0, - "headline": { - "en": "Question 1", - "fr": "Question 1" - }, - "description": { - "en": "This is the first question", - "fr": "C'est la première question" - }, - "placeholder": { - "en": "Enter something interesting", - "fr": "Entrez quelque chose d'intéressant" - }, - "options": { - "en": None, - "fr": None - }, - "validation_regex": { - "en": "(a|b|c|d)", - "fr": "(a|b|c|d)" - }, - "validation_text": { - "en": "You must enter a, b, c or d", - "fr": "Vous devez entrer a, b, c ou d" - } - }, - { - "type": "radio", - "is_required": True, - "order": 2, - "weight": 1.5, - "question_id": 0, - "headline": { - "en": "Question 2", - "fr": "Question 2" - }, - "description": { - "en": "This is the second question", - "fr": "C'est la deuxième question" - }, - "placeholder": { - "en": None, - "fr": None - }, - "options": { - "en": [ - {"value": "0", "label": "Bad"}, - {"value": "1", "label": "Average"}, - {"value": "2", "label": "Good"}, - ], - "fr": [ - {"value": "0", "label": "Mal"}, - {"value": "1", "label": "Moyenne"}, - {"value": "2", "label": "Bien"}, - ], - }, - "validation_regex": { - "en": None, - "fr": None - }, - "validation_text": { - "en": None, - "fr": None - } - } - ] - }, - { - "order": 2, - "name": { - "en": "Section 2", - "fr": "Section 2" - }, - "description": { - "en": "This is the first section", - "fr": "Ceci est la deuxième section" - }, - "questions": [ - { - "type": "information", - "is_required": False, - "order": 1, - "weight": 0.0, - "question_id": 1, - "headline": { - "en": "Question 1 from application form", - "fr": "Question 1 from application form" - }, - "description": { - "en": "This is the first question from the application form", - "fr": "C'est la première question du formulaire de candidature" - }, - "placeholder": { - "en": None, - "fr": None - }, - "options": { - "en": None, - "fr": None - }, - "validation_regex": { - "en": None, - "fr": None - }, - "validation_text": { - "en": None, - "fr": None - } - } - ] - } - ] + "event_id": 1, + "application_form_id": 1, + "stage": 2, + "is_open": True, + "deadline": datetime(2024, 6, 1).strftime("%Y-%m-%dT%H:%M:%S"), + "active": True, + "sections": [ + { + "order": 1, + "name": {"en": "Section 1", "fr": "Section 1"}, + "description": { + "en": "This is the first section", + "fr": "Ceci est la première section", + }, + "questions": [ + { + "type": "short-text", + "is_required": False, + "order": 1, + "weight": 0.0, + "question_id": 0, + "headline": {"en": "Question 1", "fr": "Question 1"}, + "description": { + "en": "This is the first question", + "fr": "C'est la première question", + }, + "placeholder": { + "en": "Enter something interesting", + "fr": "Entrez quelque chose d'intéressant", + }, + "options": {"en": None, "fr": None}, + "validation_regex": {"en": "(a|b|c|d)", "fr": "(a|b|c|d)"}, + "validation_text": { + "en": "You must enter a, b, c or d", + "fr": "Vous devez entrer a, b, c ou d", + }, + }, + { + "type": "radio", + "is_required": True, + "order": 2, + "weight": 1.5, + "question_id": 0, + "headline": {"en": "Question 2", "fr": "Question 2"}, + "description": { + "en": "This is the second question", + "fr": "C'est la deuxième question", + }, + "placeholder": {"en": None, "fr": None}, + "options": { + "en": [ + {"value": "0", "label": "Bad"}, + {"value": "1", "label": "Average"}, + {"value": "2", "label": "Good"}, + ], + "fr": [ + {"value": "0", "label": "Mal"}, + {"value": "1", "label": "Moyenne"}, + {"value": "2", "label": "Bien"}, + ], + }, + "validation_regex": {"en": None, "fr": None}, + "validation_text": {"en": None, "fr": None}, + }, + ], + }, + { + "order": 2, + "name": {"en": "Section 2", "fr": "Section 2"}, + "description": { + "en": "This is the first section", + "fr": "Ceci est la deuxième section", + }, + "questions": [ + { + "type": "information", + "is_required": False, + "order": 1, + "weight": 0.0, + "question_id": 1, + "headline": { + "en": "Question 1 from application form", + "fr": "Question 1 from application form", + }, + "description": { + "en": "This is the first question from the application form", + "fr": "C'est la première question du formulaire de candidature", + }, + "placeholder": {"en": None, "fr": None}, + "options": {"en": None, "fr": None}, + "validation_regex": {"en": None, "fr": None}, + "validation_text": {"en": None, "fr": None}, + } + ], + }, + ], } REVIEW_FORM_PUT = { - "id": 1, - "event_id": 1, - "application_form_id": 1, - "stage": 3, # Updated - "is_open": True, - "deadline": datetime(2023, 2, 2).strftime('%Y-%m-%dT%H:%M:%S'), # Updated - "active": True, - "sections": [ - { # Updated first section - "id": 1, - "order": 2, # Updated - "name": { - "en": "Section 1 Updated", - "fr": "Section 1 Updated" - }, - "description": { - "en": "This is the first section updated", - "fr": "Ceci est la première section updated" - }, - "questions": [ - { - "id": 1, # Updated - "type": "long-text", # Updated - "is_required": True, - "order": 10, - "weight": 1.0, - "question_id": 0, - "headline": { - "en": "Question 1 Updated", - "fr": "Question 1 Updated" - }, - "description": { - "en": "This is the first question Updated", - "fr": "C'est la première question Updated" - }, - "placeholder": { - "en": "Enter something interesting updated", - "fr": "Entrez quelque chose d'intéressant updated" - }, - "options": { - "en": {"blah": "blah"}, - "fr": {"blah": "blah"} - }, - "validation_regex": { - "en": "(a|b|c|d|e)", - "fr": "(a|b|c|d|e)" - }, - "validation_text": { - "en": "You must enter a, b, c or d, or e", - "fr": "Vous devez entrer a, b, c, d, ou e" - } - }, - # Deleted question 2 - { # New question - "type": "radio", - "is_required": True, - "order": 20, - "weight": 2.5, - "question_id": 0, - "headline": { - "en": "Question 3", - "fr": "Question 3" - }, - "description": { - "en": "This is the third question", - "fr": "C'est la deuxième question" - }, - "placeholder": { - "en": "hello", - "fr": "world" - }, - "options": { - "en": [ - {"value": "0", "label": "Bad"}, - {"value": "1", "label": "Average"}, - {"value": "2", "label": "Good"}, - ], - "fr": [ - {"value": "0", "label": "Mal"}, - {"value": "1", "label": "Moyenne"}, - {"value": "2", "label": "Bien"}, - ], - }, - "validation_regex": { - "en": None, - "fr": None - }, - "validation_text": { - "en": None, - "fr": None - } - } - ] - }, - # Deleted section 2 - { # New section - "order": 3, - "name": { - "en": "Section 2 New", - "fr": "Section 2 New" - }, - "description": { - "en": "This is the new second section", - "fr": "Ceci est la neue deuxième section" - }, - "questions": [ - { - "type": "information", - "is_required": False, - "order": 1, - "weight": 0.0, - "question_id": 1, - "headline": { - "en": "Question 1 from application form", - "fr": "Question 1 from application form" - }, - "description": { - "en": "This is the first question from the application form", - "fr": "C'est la première question du formulaire de candidature" - }, - "placeholder": { - "en": None, - "fr": None - }, - "options": { - "en": None, - "fr": None - }, - "validation_regex": { - "en": None, - "fr": None - }, - "validation_text": { - "en": None, - "fr": None - } - } - ] - } - ] + "id": 1, + "event_id": 1, + "application_form_id": 1, + "stage": 3, # Updated + "is_open": True, + "deadline": datetime(2023, 2, 2).strftime("%Y-%m-%dT%H:%M:%S"), # Updated + "active": True, + "sections": [ + { # Updated first section + "id": 1, + "order": 2, # Updated + "name": {"en": "Section 1 Updated", "fr": "Section 1 Updated"}, + "description": { + "en": "This is the first section updated", + "fr": "Ceci est la première section updated", + }, + "questions": [ + { + "id": 1, # Updated + "type": "long-text", # Updated + "is_required": True, + "order": 10, + "weight": 1.0, + "question_id": 0, + "headline": { + "en": "Question 1 Updated", + "fr": "Question 1 Updated", + }, + "description": { + "en": "This is the first question Updated", + "fr": "C'est la première question Updated", + }, + "placeholder": { + "en": "Enter something interesting updated", + "fr": "Entrez quelque chose d'intéressant updated", + }, + "options": {"en": {"blah": "blah"}, "fr": {"blah": "blah"}}, + "validation_regex": {"en": "(a|b|c|d|e)", "fr": "(a|b|c|d|e)"}, + "validation_text": { + "en": "You must enter a, b, c or d, or e", + "fr": "Vous devez entrer a, b, c, d, ou e", + }, + }, + # Deleted question 2 + { # New question + "type": "radio", + "is_required": True, + "order": 20, + "weight": 2.5, + "question_id": 0, + "headline": {"en": "Question 3", "fr": "Question 3"}, + "description": { + "en": "This is the third question", + "fr": "C'est la deuxième question", + }, + "placeholder": {"en": "hello", "fr": "world"}, + "options": { + "en": [ + {"value": "0", "label": "Bad"}, + {"value": "1", "label": "Average"}, + {"value": "2", "label": "Good"}, + ], + "fr": [ + {"value": "0", "label": "Mal"}, + {"value": "1", "label": "Moyenne"}, + {"value": "2", "label": "Bien"}, + ], + }, + "validation_regex": {"en": None, "fr": None}, + "validation_text": {"en": None, "fr": None}, + }, + ], + }, + # Deleted section 2 + { # New section + "order": 3, + "name": {"en": "Section 2 New", "fr": "Section 2 New"}, + "description": { + "en": "This is the new second section", + "fr": "Ceci est la neue deuxième section", + }, + "questions": [ + { + "type": "information", + "is_required": False, + "order": 1, + "weight": 0.0, + "question_id": 1, + "headline": { + "en": "Question 1 from application form", + "fr": "Question 1 from application form", + }, + "description": { + "en": "This is the first question from the application form", + "fr": "C'est la première question du formulaire de candidature", + }, + "placeholder": {"en": None, "fr": None}, + "options": {"en": None, "fr": None}, + "validation_regex": {"en": None, "fr": None}, + "validation_text": {"en": None, "fr": None}, + } + ], + }, + ], } -class ReviewFormDetailAPITest(ApiTestCase): +class ReviewFormDetailAPITest(ApiTestCase): def seed_static_data(self): first_event = self.add_event() self.first_event_id = first_event.id @@ -2609,108 +3186,158 @@ def seed_static_data(self): self.question = self.add_question(app_form.id, section.id) event_admin = self.add_user("event@admin.com") - first_event.add_event_role('admin', event_admin.id) - second_event.add_event_role('admin', event_admin.id) + first_event.add_event_role("admin", event_admin.id) + second_event.add_event_role("admin", event_admin.id) db.session.commit() def add_review_forms(self): # First stage review form - form1 = self.add_review_form(stage=1, active=False, deadline=datetime(2021, 6, 1)) + form1 = self.add_review_form( + stage=1, active=False, deadline=datetime(2021, 6, 1) + ) form1_section1 = self.add_review_section(form1.id) - self.add_review_section_translation(form1_section1.id, 'en', 'Form 1 Section 1 EN', 'Form 1 Section 1 Description EN') - self.add_review_section_translation(form1_section1.id, 'fr', 'Form 1 Section 1 FR', 'Form 1 Section 1 Description FR') + self.add_review_section_translation( + form1_section1.id, + "en", + "Form 1 Section 1 EN", + "Form 1 Section 1 Description EN", + ) + self.add_review_section_translation( + form1_section1.id, + "fr", + "Form 1 Section 1 FR", + "Form 1 Section 1 Description FR", + ) - form1_section1_question1 = self.add_review_question(form1_section1.id, weight=1, type='numeric-text') + form1_section1_question1 = self.add_review_question( + form1_section1.id, weight=1, type="numeric-text" + ) self.add_review_question_translation( form1_section1_question1.id, - language='en', - headline='Review Question 1 EN', - description='Review Question 1 Description EN', - placeholder='Placeholder Question 1 EN', - validation_regex='(a|b|c|d)', - validation_text='Validation text question 1 EN') + language="en", + headline="Review Question 1 EN", + description="Review Question 1 Description EN", + placeholder="Placeholder Question 1 EN", + validation_regex="(a|b|c|d)", + validation_text="Validation text question 1 EN", + ) self.add_review_question_translation( form1_section1_question1.id, - language='fr', - headline='Review Question 1 FR', - description='Review Question 1 Description FR', - placeholder='Placeholder Question 1 FR', - validation_regex='(a|b|c|d)', - validation_text='Validation text question 1 FR') - - form1_section1_question2 = self.add_review_question(form1_section1.id, type='information', order=2, question_id=self.question.id) + language="fr", + headline="Review Question 1 FR", + description="Review Question 1 Description FR", + placeholder="Placeholder Question 1 FR", + validation_regex="(a|b|c|d)", + validation_text="Validation text question 1 FR", + ) + + form1_section1_question2 = self.add_review_question( + form1_section1.id, type="information", order=2, question_id=self.question.id + ) self.add_review_question_translation( form1_section1_question2.id, - language='en', - headline='Review Question 2 EN', - description='Review Question 2 Description EN') + language="en", + headline="Review Question 2 EN", + description="Review Question 2 Description EN", + ) self.add_review_question_translation( form1_section1_question2.id, - language='fr', - headline='Review Question 2 FR', - description='Review Question 2 Description FR') + language="fr", + headline="Review Question 2 FR", + description="Review Question 2 Description FR", + ) form1_section2 = self.add_review_section(form1.id, order=2) - self.add_review_section_translation(form1_section2.id, 'en', 'Form 1 Section 2 EN', 'Form 1 Section 2 Description EN') - self.add_review_section_translation(form1_section2.id, 'fr', 'Form 1 Section 2 FR', 'Form 1 Section 2 Description FR') + self.add_review_section_translation( + form1_section2.id, + "en", + "Form 1 Section 2 EN", + "Form 1 Section 2 Description EN", + ) + self.add_review_section_translation( + form1_section2.id, + "fr", + "Form 1 Section 2 FR", + "Form 1 Section 2 Description FR", + ) - form1_section2_question1 = self.add_review_question(form1_section2.id, weight=1, type='radio') + form1_section2_question1 = self.add_review_question( + form1_section2.id, weight=1, type="radio" + ) self.add_review_question_translation( form1_section2_question1.id, - language='en', - headline='Review Question 3 EN', - description='Review Question 3 Description EN', + language="en", + headline="Review Question 3 EN", + description="Review Question 3 Description EN", options=[ - {'value': '0', 'label': 'Bad EN'}, - {'value': '1', 'label': 'Average EN'}, - {'value': '2', 'label': 'Good EN'} - ]) + {"value": "0", "label": "Bad EN"}, + {"value": "1", "label": "Average EN"}, + {"value": "2", "label": "Good EN"}, + ], + ) self.add_review_question_translation( form1_section2_question1.id, - language='fr', - headline='Review Question 3 FR', - description='Review Question 3 Description FR', + language="fr", + headline="Review Question 3 FR", + description="Review Question 3 Description FR", options=[ - {'value': '0', 'label': 'Bad FR'}, - {'value': '1', 'label': 'Average FR'}, - {'value': '2', 'label': 'Good FR'} - ]) + {"value": "0", "label": "Bad FR"}, + {"value": "1", "label": "Average FR"}, + {"value": "2", "label": "Good FR"}, + ], + ) # Second stage review form - form2 = self.add_review_form(stage=2, active=True, deadline=datetime(2021, 8, 1)) + form2 = self.add_review_form( + stage=2, active=True, deadline=datetime(2021, 8, 1) + ) form2_section1 = self.add_review_section(form2.id) - self.add_review_section_translation(form2_section1.id, 'en', 'Form 2 Section 1 EN', 'Form 2 Section 1 Description EN') - self.add_review_section_translation(form2_section1.id, 'fr', 'Form 2 Section 1 FR', 'Form 2 Section 1 Description FR') + self.add_review_section_translation( + form2_section1.id, + "en", + "Form 2 Section 1 EN", + "Form 2 Section 1 Description EN", + ) + self.add_review_section_translation( + form2_section1.id, + "fr", + "Form 2 Section 1 FR", + "Form 2 Section 1 Description FR", + ) - form2_section1_question1 = self.add_review_question(form2_section1.id, type='information', question_id=self.question.id) + form2_section1_question1 = self.add_review_question( + form2_section1.id, type="information", question_id=self.question.id + ) self.add_review_question_translation( form2_section1_question1.id, - language='en', - headline='Stage 2 Review Question 1 EN', - description='Stage 2 Review Question 1 Description EN') + language="en", + headline="Stage 2 Review Question 1 EN", + description="Stage 2 Review Question 1 Description EN", + ) self.add_review_question_translation( form2_section1_question1.id, - language='fr', - headline='Stage 2 Review Question 1 FR', - description='Stage 2 Review Question 1 Description FR') + language="fr", + headline="Stage 2 Review Question 1 FR", + description="Stage 2 Review Question 1 Description FR", + ) db.session.commit() - + def test_get_without_stage(self): """Test that the get method returns the correct stage when a stage parameter isn't provided.""" self.seed_static_data() self.add_review_forms() response = self.app.get( - '/api/v1/review-form-detail', + "/api/v1/review-form-detail", headers=self.get_auth_header_for("event@admin.com"), - data={'event_id': self.first_event_id} + data={"event_id": self.first_event_id}, ) data = json.loads(response.data) - self.assertEqual(data['stage'], 2) + self.assertEqual(data["stage"], 2) def test_get_first_stage(self): """Check that the get method returns the form for stage 1.""" @@ -2718,91 +3345,115 @@ def test_get_first_stage(self): self.add_review_forms() response = self.app.get( - '/api/v1/review-form-detail', + "/api/v1/review-form-detail", headers=self.get_auth_header_for("event@admin.com"), - data={'event_id': self.first_event_id, 'stage': 1} + data={"event_id": self.first_event_id, "stage": 1}, ) data = json.loads(response.data) - self.assertEqual(data['id'], 1) - self.assertEqual(data['event_id'], self.first_event_id) - self.assertEqual(data['application_form_id'], 1) - self.assertEqual(data['is_open'], True) - self.assertEqual(data['deadline'], "2021-06-01T00:00:00") - self.assertEqual(data['active'], False) - self.assertEqual(data['stage'], 1) - self.assertEqual(len(data['sections']), 2) - - section1 = data['sections'][0] - self.assertEqual(section1['order'], 1) - self.assertEqual(section1['name']['en'], 'Form 1 Section 1 EN') - self.assertEqual(section1['name']['fr'], 'Form 1 Section 1 FR') - self.assertEqual(section1['description']['en'], 'Form 1 Section 1 Description EN') - self.assertEqual(section1['description']['fr'], 'Form 1 Section 1 Description FR') - self.assertEqual(len(section1['questions']), 2) - - question1 = section1['questions'][0] - self.assertEqual(question1['order'], 1) - self.assertEqual(question1['weight'], 1) - self.assertEqual(question1['type'], 'numeric-text') - self.assertEqual(question1['question_id'], 0) - self.assertEqual(question1['headline']['en'], 'Review Question 1 EN') - self.assertEqual(question1['headline']['fr'], 'Review Question 1 FR') - self.assertEqual(question1['description']['en'], 'Review Question 1 Description EN') - self.assertEqual(question1['description']['fr'], 'Review Question 1 Description FR') - self.assertEqual(question1['placeholder']['en'], 'Placeholder Question 1 EN') - self.assertEqual(question1['placeholder']['fr'], 'Placeholder Question 1 FR') - self.assertEqual(question1['validation_regex']['en'], '(a|b|c|d)') - self.assertEqual(question1['validation_regex']['fr'], '(a|b|c|d)') - self.assertEqual(question1['validation_text']['en'], 'Validation text question 1 EN') - self.assertEqual(question1['validation_text']['fr'], 'Validation text question 1 FR') - self.assertEqual(question1['options']['en'], None) - self.assertEqual(question1['options']['fr'], None) - - question2 = section1['questions'][1] - self.assertEqual(question2['order'], 2) - self.assertEqual(question2['weight'], 0) - self.assertEqual(question2['type'], 'information') - self.assertEqual(question2['question_id'], 1) - self.assertEqual(question2['headline']['en'], 'Review Question 2 EN') - self.assertEqual(question2['headline']['fr'], 'Review Question 2 FR') - self.assertEqual(question2['description']['en'], 'Review Question 2 Description EN') - self.assertEqual(question2['description']['fr'], 'Review Question 2 Description FR') - self.assertEqual(question2['placeholder']['en'], None) - self.assertEqual(question2['placeholder']['fr'], None) - self.assertEqual(question2['validation_regex']['en'], None) - self.assertEqual(question2['validation_regex']['fr'], None) - self.assertEqual(question2['validation_text']['en'], None) - self.assertEqual(question2['validation_text']['fr'], None) - self.assertEqual(question2['options']['en'], None) - self.assertEqual(question2['options']['fr'], None) - - section2 = data['sections'][1] - self.assertEqual(section2['order'], 2) - self.assertEqual(section2['name']['en'], 'Form 1 Section 2 EN') - self.assertEqual(section2['name']['fr'], 'Form 1 Section 2 FR') - self.assertEqual(section2['description']['en'], 'Form 1 Section 2 Description EN') - self.assertEqual(section2['description']['fr'], 'Form 1 Section 2 Description FR') - self.assertEqual(len(section2['questions']), 1) - - question3 = section2['questions'][0] - self.assertEqual(question3['order'], 1) - self.assertEqual(question3['weight'], 1) - self.assertEqual(question3['type'], 'radio') - self.assertEqual(question3['question_id'], 0) - self.assertEqual(question3['headline']['en'], 'Review Question 3 EN') - self.assertEqual(question3['headline']['fr'], 'Review Question 3 FR') - self.assertEqual(question3['description']['en'], 'Review Question 3 Description EN') - self.assertEqual(question3['description']['fr'], 'Review Question 3 Description FR') - self.assertEqual(question3['placeholder']['en'], None) - self.assertEqual(question3['placeholder']['fr'], None) - self.assertEqual(question3['validation_regex']['en'], None) - self.assertEqual(question3['validation_regex']['fr'], None) - self.assertEqual(question3['validation_text']['en'], None) - self.assertEqual(question3['validation_text']['fr'], None) - self.assertEqual(len(question3['options']['en']), 3) - self.assertEqual(len(question3['options']['fr']), 3) + self.assertEqual(data["id"], 1) + self.assertEqual(data["event_id"], self.first_event_id) + self.assertEqual(data["application_form_id"], 1) + self.assertEqual(data["is_open"], True) + self.assertEqual(data["deadline"], "2021-06-01T00:00:00") + self.assertEqual(data["active"], False) + self.assertEqual(data["stage"], 1) + self.assertEqual(len(data["sections"]), 2) + + section1 = data["sections"][0] + self.assertEqual(section1["order"], 1) + self.assertEqual(section1["name"]["en"], "Form 1 Section 1 EN") + self.assertEqual(section1["name"]["fr"], "Form 1 Section 1 FR") + self.assertEqual( + section1["description"]["en"], "Form 1 Section 1 Description EN" + ) + self.assertEqual( + section1["description"]["fr"], "Form 1 Section 1 Description FR" + ) + self.assertEqual(len(section1["questions"]), 2) + + question1 = section1["questions"][0] + self.assertEqual(question1["order"], 1) + self.assertEqual(question1["weight"], 1) + self.assertEqual(question1["type"], "numeric-text") + self.assertEqual(question1["question_id"], 0) + self.assertEqual(question1["headline"]["en"], "Review Question 1 EN") + self.assertEqual(question1["headline"]["fr"], "Review Question 1 FR") + self.assertEqual( + question1["description"]["en"], "Review Question 1 Description EN" + ) + self.assertEqual( + question1["description"]["fr"], "Review Question 1 Description FR" + ) + self.assertEqual(question1["placeholder"]["en"], "Placeholder Question 1 EN") + self.assertEqual(question1["placeholder"]["fr"], "Placeholder Question 1 FR") + self.assertEqual(question1["validation_regex"]["en"], "(a|b|c|d)") + self.assertEqual(question1["validation_regex"]["fr"], "(a|b|c|d)") + self.assertEqual( + question1["validation_text"]["en"], "Validation text question 1 EN" + ) + self.assertEqual( + question1["validation_text"]["fr"], "Validation text question 1 FR" + ) + self.assertEqual(question1["options"]["en"], None) + self.assertEqual(question1["options"]["fr"], None) + + question2 = section1["questions"][1] + self.assertEqual(question2["order"], 2) + self.assertEqual(question2["weight"], 0) + self.assertEqual(question2["type"], "information") + self.assertEqual(question2["question_id"], 1) + self.assertEqual(question2["headline"]["en"], "Review Question 2 EN") + self.assertEqual(question2["headline"]["fr"], "Review Question 2 FR") + self.assertEqual( + question2["description"]["en"], "Review Question 2 Description EN" + ) + self.assertEqual( + question2["description"]["fr"], "Review Question 2 Description FR" + ) + self.assertEqual(question2["placeholder"]["en"], None) + self.assertEqual(question2["placeholder"]["fr"], None) + self.assertEqual(question2["validation_regex"]["en"], None) + self.assertEqual(question2["validation_regex"]["fr"], None) + self.assertEqual(question2["validation_text"]["en"], None) + self.assertEqual(question2["validation_text"]["fr"], None) + self.assertEqual(question2["options"]["en"], None) + self.assertEqual(question2["options"]["fr"], None) + + section2 = data["sections"][1] + self.assertEqual(section2["order"], 2) + self.assertEqual(section2["name"]["en"], "Form 1 Section 2 EN") + self.assertEqual(section2["name"]["fr"], "Form 1 Section 2 FR") + self.assertEqual( + section2["description"]["en"], "Form 1 Section 2 Description EN" + ) + self.assertEqual( + section2["description"]["fr"], "Form 1 Section 2 Description FR" + ) + self.assertEqual(len(section2["questions"]), 1) + + question3 = section2["questions"][0] + self.assertEqual(question3["order"], 1) + self.assertEqual(question3["weight"], 1) + self.assertEqual(question3["type"], "radio") + self.assertEqual(question3["question_id"], 0) + self.assertEqual(question3["headline"]["en"], "Review Question 3 EN") + self.assertEqual(question3["headline"]["fr"], "Review Question 3 FR") + self.assertEqual( + question3["description"]["en"], "Review Question 3 Description EN" + ) + self.assertEqual( + question3["description"]["fr"], "Review Question 3 Description FR" + ) + self.assertEqual(question3["placeholder"]["en"], None) + self.assertEqual(question3["placeholder"]["fr"], None) + self.assertEqual(question3["validation_regex"]["en"], None) + self.assertEqual(question3["validation_regex"]["fr"], None) + self.assertEqual(question3["validation_text"]["en"], None) + self.assertEqual(question3["validation_text"]["fr"], None) + self.assertEqual(len(question3["options"]["en"]), 3) + self.assertEqual(len(question3["options"]["fr"]), 3) def test_get_second_stage(self): """Check that the get method returns the form for stage 2.""" @@ -2810,47 +3461,55 @@ def test_get_second_stage(self): self.add_review_forms() response = self.app.get( - '/api/v1/review-form-detail', + "/api/v1/review-form-detail", headers=self.get_auth_header_for("event@admin.com"), - data={'event_id': self.first_event_id, 'stage': 2} + data={"event_id": self.first_event_id, "stage": 2}, ) data = json.loads(response.data) - self.assertEqual(data['id'], 2) - self.assertEqual(data['event_id'], self.first_event_id) - self.assertEqual(data['application_form_id'], 1) - self.assertEqual(data['is_open'], True) - self.assertEqual(data['deadline'], "2021-08-01T00:00:00") - self.assertEqual(data['active'], True) - self.assertEqual(data['stage'], 2) - self.assertEqual(len(data['sections']), 1) - - section1 = data['sections'][0] - self.assertEqual(section1['order'], 1) - self.assertEqual(section1['name']['en'], 'Form 2 Section 1 EN') - self.assertEqual(section1['name']['fr'], 'Form 2 Section 1 FR') - self.assertEqual(section1['description']['en'], 'Form 2 Section 1 Description EN') - self.assertEqual(section1['description']['fr'], 'Form 2 Section 1 Description FR') - self.assertEqual(len(section1['questions']), 1) - - question1 = section1['questions'][0] - self.assertEqual(question1['order'], 1) - self.assertEqual(question1['weight'], 0) - self.assertEqual(question1['type'], 'information') - self.assertEqual(question1['question_id'], 1) - self.assertEqual(question1['headline']['en'], 'Stage 2 Review Question 1 EN') - self.assertEqual(question1['headline']['fr'], 'Stage 2 Review Question 1 FR') - self.assertEqual(question1['description']['en'], 'Stage 2 Review Question 1 Description EN') - self.assertEqual(question1['description']['fr'], 'Stage 2 Review Question 1 Description FR') - self.assertEqual(question1['placeholder']['en'], None) - self.assertEqual(question1['placeholder']['fr'], None) - self.assertEqual(question1['validation_regex']['en'], None) - self.assertEqual(question1['validation_regex']['fr'], None) - self.assertEqual(question1['validation_text']['en'], None) - self.assertEqual(question1['validation_text']['fr'], None) - self.assertEqual(question1['options']['en'], None) - self.assertEqual(question1['options']['fr'], None) + self.assertEqual(data["id"], 2) + self.assertEqual(data["event_id"], self.first_event_id) + self.assertEqual(data["application_form_id"], 1) + self.assertEqual(data["is_open"], True) + self.assertEqual(data["deadline"], "2021-08-01T00:00:00") + self.assertEqual(data["active"], True) + self.assertEqual(data["stage"], 2) + self.assertEqual(len(data["sections"]), 1) + + section1 = data["sections"][0] + self.assertEqual(section1["order"], 1) + self.assertEqual(section1["name"]["en"], "Form 2 Section 1 EN") + self.assertEqual(section1["name"]["fr"], "Form 2 Section 1 FR") + self.assertEqual( + section1["description"]["en"], "Form 2 Section 1 Description EN" + ) + self.assertEqual( + section1["description"]["fr"], "Form 2 Section 1 Description FR" + ) + self.assertEqual(len(section1["questions"]), 1) + + question1 = section1["questions"][0] + self.assertEqual(question1["order"], 1) + self.assertEqual(question1["weight"], 0) + self.assertEqual(question1["type"], "information") + self.assertEqual(question1["question_id"], 1) + self.assertEqual(question1["headline"]["en"], "Stage 2 Review Question 1 EN") + self.assertEqual(question1["headline"]["fr"], "Stage 2 Review Question 1 FR") + self.assertEqual( + question1["description"]["en"], "Stage 2 Review Question 1 Description EN" + ) + self.assertEqual( + question1["description"]["fr"], "Stage 2 Review Question 1 Description FR" + ) + self.assertEqual(question1["placeholder"]["en"], None) + self.assertEqual(question1["placeholder"]["fr"], None) + self.assertEqual(question1["validation_regex"]["en"], None) + self.assertEqual(question1["validation_regex"]["fr"], None) + self.assertEqual(question1["validation_text"]["en"], None) + self.assertEqual(question1["validation_text"]["fr"], None) + self.assertEqual(question1["options"]["en"], None) + self.assertEqual(question1["options"]["fr"], None) def test_get_new_stage(self): """Check that the get method returns not found for a stage that doesn't exist.""" @@ -2858,9 +3517,9 @@ def test_get_new_stage(self): self.add_review_forms() response = self.app.get( - '/api/v1/review-form-detail', + "/api/v1/review-form-detail", headers=self.get_auth_header_for("event@admin.com"), - data={'event_id': self.first_event_id, 'stage': 3} + data={"event_id": self.first_event_id, "stage": 3}, ) self.assertEqual(response.status_code, 404) @@ -2871,9 +3530,9 @@ def test_get_no_form(self): self.add_review_forms() response = self.app.get( - '/api/v1/review-form-detail', + "/api/v1/review-form-detail", headers=self.get_auth_header_for("event@admin.com"), - data={'event_id': self.no_form_event_id, 'stage': 1} + data={"event_id": self.no_form_event_id, "stage": 1}, ) self.assertEqual(response.status_code, 404) @@ -2883,17 +3542,21 @@ def test_post(self): self.seed_static_data() response = self.app.post( - '/api/v1/review-form-detail', + "/api/v1/review-form-detail", headers=self.get_auth_header_for("event@admin.com"), data=json.dumps(REVIEW_FORM_POST), - content_type='application/json',) + content_type="application/json", + ) self.assertEqual(response.status_code, 201) response = self.app.get( - '/api/v1/review-form-detail', + "/api/v1/review-form-detail", headers=self.get_auth_header_for("event@admin.com"), - data={'event_id': REVIEW_FORM_POST["event_id"], 'stage': REVIEW_FORM_POST["stage"]} + data={ + "event_id": REVIEW_FORM_POST["event_id"], + "stage": REVIEW_FORM_POST["stage"], + }, ) data = json.loads(response.data) @@ -2901,21 +3564,34 @@ def test_post(self): self.assertTrue("id" in data and data["id"] is not None) del data["id"] - self.assertTrue("id" in data["sections"][0] and data["sections"][0]["id"] is not None) + self.assertTrue( + "id" in data["sections"][0] and data["sections"][0]["id"] is not None + ) del data["sections"][0]["id"] - self.assertTrue("id" in data["sections"][1] and data["sections"][1]["id"] is not None) + self.assertTrue( + "id" in data["sections"][1] and data["sections"][1]["id"] is not None + ) del data["sections"][1]["id"] - self.assertTrue("id" in data["sections"][0]["questions"][0] and data["sections"][0]["questions"][0]["id"] is not None) + self.assertTrue( + "id" in data["sections"][0]["questions"][0] + and data["sections"][0]["questions"][0]["id"] is not None + ) del data["sections"][0]["questions"][0]["id"] - self.assertTrue("id" in data["sections"][0]["questions"][1] and data["sections"][0]["questions"][1]["id"] is not None) + self.assertTrue( + "id" in data["sections"][0]["questions"][1] + and data["sections"][0]["questions"][1]["id"] is not None + ) del data["sections"][0]["questions"][1]["id"] - self.assertTrue("id" in data["sections"][1]["questions"][0] and data["sections"][1]["questions"][0]["id"] is not None) + self.assertTrue( + "id" in data["sections"][1]["questions"][0] + and data["sections"][1]["questions"][0]["id"] is not None + ) del data["sections"][1]["questions"][0]["id"] - + self.maxDiff = 6000 self.assertEqual(data, REVIEW_FORM_POST) @@ -2924,19 +3600,20 @@ def test_put(self): # First create the review form response = self.app.post( - '/api/v1/review-form-detail', + "/api/v1/review-form-detail", headers=self.get_auth_header_for("event@admin.com"), data=json.dumps(REVIEW_FORM_POST), - content_type='application/json',) + content_type="application/json", + ) self.assertEqual(response.status_code, 201) - # Now update it + # Now update it response = self.app.put( - '/api/v1/review-form-detail', + "/api/v1/review-form-detail", headers=self.get_auth_header_for("event@admin.com"), data=json.dumps(REVIEW_FORM_PUT), - content_type='application/json' + content_type="application/json", ) data = json.loads(response.data) diff --git a/api/app/routes.py b/api/app/routes.py index 411980d08..a4c507441 100644 --- a/api/app/routes.py +++ b/api/app/routes.py @@ -1,130 +1,143 @@ from app import rest_api + from .applicationModel import api as form_api -from .users import api as users_api -from .responses import api as responses_api +from .attendance import api as attendance_api from .content import api as content_api -from .files import api as files_api from .events import api as events_api -from .reviews import api as reviews_api -from .invitedGuest import api as invitedGuest_api -from .references import api as reference_api -from .registration import api as registration_api -from .registrationResponse import api as registration_response +from .files import api as files_api from .guestRegistrations import api as guest_registration +from .integration_tests import api as integration_tests_api from .invitationletter import api as invitation_letter_api -from .attendance import api as attendance_api +from .invitedGuest import api as invitedGuest_api from .organisation import api as organisation_api -from .integration_tests import api as integration_tests_api from .outcome import api as outcome_api +from .references import api as reference_api +from .registration import api as registration_api +from .registrationResponse import api as registration_response +from .responses import api as responses_api +from .reviews import api as reviews_api from .tags import api as tag_api +from .users import api as users_api -rest_api.add_resource(users_api.UserAPI, '/api/v1/user') -rest_api.add_resource(users_api.UserCommentAPI, '/api/v1/user-comment') -rest_api.add_resource(users_api.AuthenticationAPI, '/api/v1/authenticate') -rest_api.add_resource(users_api.AuthenticationRefreshAPI, '/api/v1/authenticationrefresh') -rest_api.add_resource(users_api.PasswordResetRequestAPI, - '/api/v1/password-reset/request') -rest_api.add_resource(users_api.PasswordResetConfirmAPI, - '/api/v1/password-reset/confirm') -rest_api.add_resource(users_api.AdminOnlyAPI, '/api/v1/admin') -rest_api.add_resource(users_api.EmailerAPI, - '/api/v1/admin/emailer') -rest_api.add_resource(form_api.ApplicationFormAPI, '/api/v1/application-form') -rest_api.add_resource(form_api.ApplicationFormDetailAPI, '/api/v1/application-form-detail') -rest_api.add_resource(responses_api.ResponseAPI, '/api/v1/response') -rest_api.add_resource(responses_api.ResponseExportAPI, '/api/v1/response-export') -rest_api.add_resource(content_api.CountryContentAPI, - '/api/v1/content/countries') -rest_api.add_resource(files_api.FileUploadAPI, '/api/v1/file') -rest_api.add_resource(content_api.CategoryContentAPI, - '/api/v1/content/categories') -rest_api.add_resource(content_api.EthnicityContentAPI, - '/api/v1/content/ethnicity') -rest_api.add_resource(content_api.TitleContentAPI, - '/api/v1/content/title'), -rest_api.add_resource(content_api.DisabilityContentAPI, - '/api/v1/content/disability'), -rest_api.add_resource(content_api.GenderContentAPI, - '/api/v1/content/gender'), -rest_api.add_resource(events_api.EventsAPI, - '/api/v1/events'), -rest_api.add_resource(events_api.EventAPI, - '/api/v1/event'), -rest_api.add_resource(events_api.EventStatsAPI, - '/api/v1/eventstats'), -rest_api.add_resource(events_api.EventsByKeyAPI, - '/api/v1/event-by-key'), -rest_api.add_resource(users_api.VerifyEmailAPI, - '/api/v1/verify-email'), -rest_api.add_resource(users_api.ResendVerificationEmailAPI, - '/api/v1/resend-verification-email'), -rest_api.add_resource(reviews_api.ReviewAPI, '/api/v1/review') -rest_api.add_resource(reviews_api.ReviewResponseAPI, '/api/v1/reviewresponse') -rest_api.add_resource(reviews_api.ReviewAssignmentAPI, - '/api/v1/reviewassignment') -rest_api.add_resource(reviews_api.ReviewSummaryAPI, - '/api/v1/reviewassignment/summary') -rest_api.add_resource(events_api.NotSubmittedReminderAPI, - '/api/v1/reminder-unsubmitted') -rest_api.add_resource(events_api.NotStartedReminderAPI, - '/api/v1/reminder-not-started') -rest_api.add_resource(reviews_api.ReviewHistoryAPI, '/api/v1/reviewhistory') -rest_api.add_resource(users_api.UserProfileList, '/api/v1/userprofilelist') -rest_api.add_resource(users_api.UserProfile, '/api/v1/userprofile') -rest_api.add_resource(invitedGuest_api.InvitedGuestAPI, '/api/v1/invitedGuest') -rest_api.add_resource(invitedGuest_api.CreateUser, - '/api/v1/invitedGuest/create') -rest_api.add_resource(invitedGuest_api.InvitedGuestList, - '/api/v1/invitedGuestList') -rest_api.add_resource(invitedGuest_api.CheckIfInvitedGuest, - '/api/v1/checkIfInvitedGuest') -rest_api.add_resource(reference_api.ReferenceRequestListAPI, - '/api/v1/reference-request/list') -rest_api.add_resource(reference_api.ReferenceRequestDetailAPI, - '/api/v1/reference-request/detail') -rest_api.add_resource(reference_api.ReferenceRequestAPI, - '/api/v1/reference-request') -rest_api.add_resource(reference_api.ReferenceAPI, - '/api/v1/reference') -rest_api.add_resource(registration_api.OfferAPI, '/api/v1/offer') -rest_api.add_resource(registration_api.RegistrationFormAPI, - '/api/v1/registration-form') -rest_api.add_resource(registration_api.RegistrationSectionAPI, - '/api/v1/registration-section') -rest_api.add_resource(registration_api.RegistrationQuestionAPI, - '/api/v1/registration-question') -rest_api.add_resource(registration_response.RegistrationApi, - '/api/v1/registration-response') -rest_api.add_resource(guest_registration.GuestRegistrationApi, - '/api/v1/guest-registration') -rest_api.add_resource(guest_registration.GuestRegistrationFormAPI, - '/api/v1/guest-registration-form') -rest_api.add_resource(registration_response.RegistrationUnconfirmedAPI, - '/api/v1/registration/unconfirmed') -rest_api.add_resource(registration_response.RegistrationConfirmedAPI, - '/api/v1/registration/confirmed') -rest_api.add_resource(registration_response.RegistrationConfirmAPI, - '/api/v1/registration/confirm') -rest_api.add_resource(invitation_letter_api.InvitationLetterAPI, - '/api/v1/invitation-letter') -rest_api.add_resource(attendance_api.AttendanceAPI, '/api/v1/attendance') -rest_api.add_resource(organisation_api.OrganisationApi, '/api/v1/organisation') -rest_api.add_resource(users_api.PrivacyPolicyAPI, '/api/v1/privacypolicy') -rest_api.add_resource(integration_tests_api.CreateIntegrationUser, '/api/v1/integration-tests/createUser') -rest_api.add_resource(integration_tests_api.DeleteIntegrationUser, '/api/v1/integration-tests/deleteUser') -rest_api.add_resource(outcome_api.OutcomeAPI, '/api/v1/outcome') -rest_api.add_resource(outcome_api.OutcomeListAPI, '/api/v1/outcome-list') -rest_api.add_resource(users_api.EventAttendeeAPI, '/api/v1/validate-user-event-attendee') -rest_api.add_resource(responses_api.ResponseListAPI, '/api/v1/responses') -rest_api.add_resource(form_api.QuestionListApi, '/api/v1/questions') -rest_api.add_resource(tag_api.TagAPI, '/api/v1/tag') -rest_api.add_resource(tag_api.TagListAPI, '/api/v1/tags') -rest_api.add_resource(responses_api.ResponseTagAPI, '/api/v1/responsetag') -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.ResponseReviewAssignmentAPI, '/api/v1/assignresponsereviewer') -rest_api.add_resource(reviews_api.ReviewResponseDetailListAPI, '/api/v1/reviewresponsedetaillist') -rest_api.add_resource(reviews_api.ReviewResponseSummaryListAPI, '/api/v1/reviewresponsesummarylist') -rest_api.add_resource(reviews_api.ReviewStageAPI, '/api/v1/reviewstage') -rest_api.add_resource(reviews_api.ReviewFormDetailAPI, '/api/v1/review-form-detail') +rest_api.add_resource(users_api.UserAPI, "/api/v1/user") +rest_api.add_resource(users_api.UserCommentAPI, "/api/v1/user-comment") +rest_api.add_resource(users_api.AuthenticationAPI, "/api/v1/authenticate") +rest_api.add_resource( + users_api.AuthenticationRefreshAPI, "/api/v1/authenticationrefresh" +) +rest_api.add_resource( + users_api.PasswordResetRequestAPI, "/api/v1/password-reset/request" +) +rest_api.add_resource( + users_api.PasswordResetConfirmAPI, "/api/v1/password-reset/confirm" +) +rest_api.add_resource(users_api.AdminOnlyAPI, "/api/v1/admin") +rest_api.add_resource(users_api.EmailerAPI, "/api/v1/admin/emailer") +rest_api.add_resource(form_api.ApplicationFormAPI, "/api/v1/application-form") +rest_api.add_resource( + form_api.ApplicationFormDetailAPI, "/api/v1/application-form-detail" +) +rest_api.add_resource(responses_api.ResponseAPI, "/api/v1/response") +rest_api.add_resource(responses_api.ResponseExportAPI, "/api/v1/response-export") +rest_api.add_resource(content_api.CountryContentAPI, "/api/v1/content/countries") +rest_api.add_resource(files_api.FileUploadAPI, "/api/v1/file") +rest_api.add_resource(content_api.CategoryContentAPI, "/api/v1/content/categories") +rest_api.add_resource(content_api.EthnicityContentAPI, "/api/v1/content/ethnicity") +rest_api.add_resource(content_api.TitleContentAPI, "/api/v1/content/title"), +rest_api.add_resource(content_api.DisabilityContentAPI, "/api/v1/content/disability"), +rest_api.add_resource(content_api.GenderContentAPI, "/api/v1/content/gender"), +rest_api.add_resource(events_api.EventsAPI, "/api/v1/events"), +rest_api.add_resource(events_api.EventAPI, "/api/v1/event"), +rest_api.add_resource(events_api.EventStatsAPI, "/api/v1/eventstats"), +rest_api.add_resource(events_api.EventsByKeyAPI, "/api/v1/event-by-key"), +rest_api.add_resource(users_api.VerifyEmailAPI, "/api/v1/verify-email"), +rest_api.add_resource( + users_api.ResendVerificationEmailAPI, "/api/v1/resend-verification-email" +), +rest_api.add_resource(reviews_api.ReviewAPI, "/api/v1/review") +rest_api.add_resource(reviews_api.ReviewResponseAPI, "/api/v1/reviewresponse") +rest_api.add_resource(reviews_api.ReviewAssignmentAPI, "/api/v1/reviewassignment") +rest_api.add_resource(reviews_api.ReviewSummaryAPI, "/api/v1/reviewassignment/summary") +rest_api.add_resource( + events_api.NotSubmittedReminderAPI, "/api/v1/reminder-unsubmitted" +) +rest_api.add_resource(events_api.NotStartedReminderAPI, "/api/v1/reminder-not-started") +rest_api.add_resource(reviews_api.ReviewHistoryAPI, "/api/v1/reviewhistory") +rest_api.add_resource(users_api.UserProfileList, "/api/v1/userprofilelist") +rest_api.add_resource(users_api.UserProfile, "/api/v1/userprofile") +rest_api.add_resource(invitedGuest_api.InvitedGuestAPI, "/api/v1/invitedGuest") +rest_api.add_resource(invitedGuest_api.CreateUser, "/api/v1/invitedGuest/create") +rest_api.add_resource(invitedGuest_api.InvitedGuestList, "/api/v1/invitedGuestList") +rest_api.add_resource( + invitedGuest_api.CheckIfInvitedGuest, "/api/v1/checkIfInvitedGuest" +) +rest_api.add_resource( + reference_api.ReferenceRequestListAPI, "/api/v1/reference-request/list" +) +rest_api.add_resource( + reference_api.ReferenceRequestDetailAPI, "/api/v1/reference-request/detail" +) +rest_api.add_resource(reference_api.ReferenceRequestAPI, "/api/v1/reference-request") +rest_api.add_resource(reference_api.ReferenceAPI, "/api/v1/reference") +rest_api.add_resource(registration_api.OfferAPI, "/api/v1/offer") +rest_api.add_resource(registration_api.RegistrationFormAPI, "/api/v1/registration-form") +rest_api.add_resource( + registration_api.RegistrationSectionAPI, "/api/v1/registration-section" +) +rest_api.add_resource( + registration_api.RegistrationQuestionAPI, "/api/v1/registration-question" +) +rest_api.add_resource( + registration_response.RegistrationApi, "/api/v1/registration-response" +) +rest_api.add_resource( + guest_registration.GuestRegistrationApi, "/api/v1/guest-registration" +) +rest_api.add_resource( + guest_registration.GuestRegistrationFormAPI, "/api/v1/guest-registration-form" +) +rest_api.add_resource( + registration_response.RegistrationUnconfirmedAPI, "/api/v1/registration/unconfirmed" +) +rest_api.add_resource( + registration_response.RegistrationConfirmedAPI, "/api/v1/registration/confirmed" +) +rest_api.add_resource( + registration_response.RegistrationConfirmAPI, "/api/v1/registration/confirm" +) +rest_api.add_resource( + invitation_letter_api.InvitationLetterAPI, "/api/v1/invitation-letter" +) +rest_api.add_resource(attendance_api.AttendanceAPI, "/api/v1/attendance") +rest_api.add_resource(organisation_api.OrganisationApi, "/api/v1/organisation") +rest_api.add_resource(users_api.PrivacyPolicyAPI, "/api/v1/privacypolicy") +rest_api.add_resource( + integration_tests_api.CreateIntegrationUser, "/api/v1/integration-tests/createUser" +) +rest_api.add_resource( + integration_tests_api.DeleteIntegrationUser, "/api/v1/integration-tests/deleteUser" +) +rest_api.add_resource(outcome_api.OutcomeAPI, "/api/v1/outcome") +rest_api.add_resource(outcome_api.OutcomeListAPI, "/api/v1/outcome-list") +rest_api.add_resource( + users_api.EventAttendeeAPI, "/api/v1/validate-user-event-attendee" +) +rest_api.add_resource(responses_api.ResponseListAPI, "/api/v1/responses") +rest_api.add_resource(form_api.QuestionListApi, "/api/v1/questions") +rest_api.add_resource(tag_api.TagAPI, "/api/v1/tag") +rest_api.add_resource(tag_api.TagListAPI, "/api/v1/tags") +rest_api.add_resource(responses_api.ResponseTagAPI, "/api/v1/responsetag") +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.ResponseReviewAssignmentAPI, "/api/v1/assignresponsereviewer" +) +rest_api.add_resource( + reviews_api.ReviewResponseDetailListAPI, "/api/v1/reviewresponsedetaillist" +) +rest_api.add_resource( + reviews_api.ReviewResponseSummaryListAPI, "/api/v1/reviewresponsesummarylist" +) +rest_api.add_resource(reviews_api.ReviewStageAPI, "/api/v1/reviewstage") +rest_api.add_resource(reviews_api.ReviewFormDetailAPI, "/api/v1/review-form-detail") diff --git a/api/app/tags/api.py b/api/app/tags/api.py index be578c96a..e6fa0beaa 100644 --- a/api/app/tags/api.py +++ b/api/app/tags/api.py @@ -1,20 +1,20 @@ -from flask import g import flask_restful as restful -from flask_restful import reqparse, fields, marshal_with -from app.utils.auth import auth_required, event_admin_required +from flask import g +from flask_restful import fields, marshal_with, reqparse + +from app import LOGGER +from app.tags.models import Tag, TagTranslation from app.tags.repository import TagRepository as tag_repository from app.utils import errors -from app.tags.models import Tag, TagTranslation -from app import LOGGER +from app.utils.auth import auth_required, event_admin_required + def _serialize_tag_detail(tag): """Serializes a tag with all of its translations.""" result = { - 'id': tag.id, - 'event_id': tag.event_id, - 'name': { - t.language: t.name for t in tag.translations - } + "id": tag.id, + "event_id": tag.event_id, + "name": {t.language: t.name for t in tag.translations}, } return result @@ -23,22 +23,22 @@ def _serialize_tag(tag, language): """Serialize a tag in a specific language.""" translation = tag.get_translation(language) if translation is None: - LOGGER.warn('Could not find translation for language {} for tag id {}'.format(language, tag.id)) - translation = tag.get_translation('en') - return { - 'id': tag.id, - 'event_id': tag.event_id, - 'name': translation.name - } + LOGGER.warn( + "Could not find translation for language {} for tag id {}".format( + language, tag.id + ) + ) + translation = tag.get_translation("en") + return {"id": tag.id, "event_id": tag.event_id, "name": translation.name} -class TagAPI(restful.Resource): +class TagAPI(restful.Resource): @event_admin_required def get(self, event_id): req_parser = reqparse.RequestParser() - req_parser.add_argument('id', type=int, required=True) + req_parser.add_argument("id", type=int, required=True) args = req_parser.parse_args() - id = args['id'] + id = args["id"] tag = tag_repository.get_by_id(id) if tag.event_id != event_id: @@ -49,9 +49,9 @@ def get(self, event_id): @event_admin_required def post(self, event_id): req_parser = reqparse.RequestParser() - req_parser.add_argument('name', type=dict, required=True) + req_parser.add_argument("name", type=dict, required=True) args = req_parser.parse_args() - name_translations = args['name'] + name_translations = args["name"] tag = Tag(event_id) for language, name in name_translations.items(): @@ -63,11 +63,11 @@ def post(self, event_id): @event_admin_required def put(self, event_id): req_parser = reqparse.RequestParser() - req_parser.add_argument('id', type=int, required=True) - req_parser.add_argument('name', type=dict, required=True) + req_parser.add_argument("id", type=int, required=True) + req_parser.add_argument("name", type=dict, required=True) args = req_parser.parse_args() - id = args['id'] - name_translations = args['name'] + id = args["id"] + name_translations = args["name"] tag = tag_repository.get_by_id(id) if not tag or tag.event_id != event_id: @@ -93,9 +93,9 @@ class TagListAPI(restful.Resource): @event_admin_required def get(self, event_id): req_parser = reqparse.RequestParser() - req_parser.add_argument('language', type=str, required=True) + req_parser.add_argument("language", type=str, required=True) args = req_parser.parse_args() - language = args['language'] + language = args["language"] tags = tag_repository.get_all_for_event(event_id) return [_serialize_tag(t, language) for t in tags] diff --git a/api/app/tags/models.py b/api/app/tags/models.py index 39ccad2e0..21366e292 100644 --- a/api/app/tags/models.py +++ b/api/app/tags/models.py @@ -1,27 +1,31 @@ from datetime import datetime + from app import db from app.events.models import Event + class Tag(db.Model): - __tablename__ = 'tag' + __tablename__ = "tag" id = db.Column(db.Integer(), primary_key=True) - event_id = db.Column(db.Integer(), db.ForeignKey('event.id'), nullable=False) + event_id = db.Column(db.Integer(), db.ForeignKey("event.id"), nullable=False) - translations = db.relationship('TagTranslation', lazy='dynamic') + translations = db.relationship("TagTranslation", lazy="dynamic") def __init__(self, event_id): self.event_id = event_id - + def get_translation(self, language): translation = self.translations.filter_by(language=language).first() return translation class TagTranslation(db.Model): - __tablename__ = 'tag_translation' - __table_args__ = tuple([db.UniqueConstraint('tag_id', 'language', name='uq_tag_id_language')]) + __tablename__ = "tag_translation" + __table_args__ = tuple( + [db.UniqueConstraint("tag_id", "language", name="uq_tag_id_language")] + ) id = db.Column(db.Integer(), primary_key=True) - tag_id = db.Column(db.Integer(), db.ForeignKey('tag.id'), nullable=False) + tag_id = db.Column(db.Integer(), db.ForeignKey("tag.id"), nullable=False) language = db.Column(db.String(2), nullable=False) name = db.Column(db.String(), nullable=False) diff --git a/api/app/tags/repository.py b/api/app/tags/repository.py index de85b49c2..4b6b98149 100644 --- a/api/app/tags/repository.py +++ b/api/app/tags/repository.py @@ -1,8 +1,8 @@ from app import db from app.tags.models import Tag, TagTranslation -class TagRepository(): +class TagRepository: @staticmethod def get_by_id(id): return db.session.query(Tag).get(id) diff --git a/api/app/tags/tests.py b/api/app/tags/tests.py index cfabdc562..3ab15cc3c 100644 --- a/api/app/tags/tests.py +++ b/api/app/tags/tests.py @@ -1,197 +1,187 @@ import json -from app.utils.testing import ApiTestCase -from app.tags.models import Tag, TagTranslation + from app import db +from app.tags.models import Tag, TagTranslation +from app.utils.testing import ApiTestCase + class ReviewsApiTest(ApiTestCase): def seed_static_data(self): - self.event1 = self.add_event(key='event1') - self.event2 = self.add_event(key='event2') - self.user1 = self.add_user('event1admin@mail.com') - self.user2 = self.add_user('event2admin@mail.com') - self.user3 = self.add_user('user@mail.com') + self.event1 = self.add_event(key="event1") + self.event2 = self.add_event(key="event2") + self.user1 = self.add_user("event1admin@mail.com") + self.user2 = self.add_user("event2admin@mail.com") + self.user3 = self.add_user("user@mail.com") - self.event1.add_event_role('admin', self.user1.id) - self.event2.add_event_role('admin', self.user2.id) + self.event1.add_event_role("admin", self.user1.id) + self.event2.add_event_role("admin", self.user2.id) db.session.commit() - self.tags = [ - Tag(self.event1.id), - Tag(self.event1.id), - Tag(self.event2.id) - ] + self.tags = [Tag(self.event1.id), Tag(self.event1.id), Tag(self.event2.id)] db.session.add_all(self.tags) db.session.commit() tag_translations = [ - TagTranslation(self.tags[0].id, 'en', 'English Tag 1 Event 1'), - TagTranslation(self.tags[0].id, 'fr', 'French Tag 1 Event 1'), - TagTranslation(self.tags[1].id, 'en', 'English Tag 2 Event 1'), - TagTranslation(self.tags[1].id, 'fr', 'French Tag 2 Event 1'), - TagTranslation(self.tags[2].id, 'en', 'English Tag 1 Event 2') + TagTranslation(self.tags[0].id, "en", "English Tag 1 Event 1"), + TagTranslation(self.tags[0].id, "fr", "French Tag 1 Event 1"), + TagTranslation(self.tags[1].id, "en", "English Tag 2 Event 1"), + TagTranslation(self.tags[1].id, "fr", "French Tag 2 Event 1"), + TagTranslation(self.tags[2].id, "en", "English Tag 1 Event 2"), ] db.session.add_all(tag_translations) db.session.commit() - self.user1_headers = self.get_auth_header_for('event1admin@mail.com') - self.user2_headers = self.get_auth_header_for('event2admin@mail.com') - self.user3_headers = self.get_auth_header_for('user@mail.com') + self.user1_headers = self.get_auth_header_for("event1admin@mail.com") + self.user2_headers = self.get_auth_header_for("event2admin@mail.com") + self.user3_headers = self.get_auth_header_for("user@mail.com") - def test_get_tag(self): """Test typical get request.""" self.seed_static_data() - params = {'id': 1, 'event_id': 1} - response = self.app.get('/api/v1/tag', headers=self.user1_headers, data=params) + params = {"id": 1, "event_id": 1} + response = self.app.get("/api/v1/tag", headers=self.user1_headers, data=params) data = json.loads(response.data) - self.assertEqual(data['id'], 1) - self.assertEqual(data['event_id'], 1) - self.assertDictEqual(data['name'], { - 'en': 'English Tag 1 Event 1', - 'fr': 'French Tag 1 Event 1' - }) + self.assertEqual(data["id"], 1) + self.assertEqual(data["event_id"], 1) + self.assertDictEqual( + data["name"], {"en": "English Tag 1 Event 1", "fr": "French Tag 1 Event 1"} + ) def test_get_event_admin(self): """Check a non event admin can't get a tag.""" self.seed_static_data() - params = {'id': 1, 'event_id': 1} - response = self.app.get('/api/v1/tag', headers=self.user3_headers, data=params) + params = {"id": 1, "event_id": 1} + response = self.app.get("/api/v1/tag", headers=self.user3_headers, data=params) self.assertEqual(response.status_code, 403) def test_get_event_admin_correct_event(self): """Check that an event admin for a different event can't get a tag.""" self.seed_static_data() - params = {'id': 1, 'event_id': 1} - response = self.app.get('/api/v1/tag', headers=self.user2_headers, data=params) + params = {"id": 1, "event_id": 1} + response = self.app.get("/api/v1/tag", headers=self.user2_headers, data=params) self.assertEqual(response.status_code, 403) def test_typical_post(self): """Test a typical post request.""" self.seed_static_data() params = { - 'event_id': 2, - 'name': { - 'en': 'English Tag 2 Event 2', - 'fr': 'French Tag 2 Event 2', - } + "event_id": 2, + "name": { + "en": "English Tag 2 Event 2", + "fr": "French Tag 2 Event 2", + }, } response = self.app.post( - '/api/v1/tag', - headers=self.user2_headers, + "/api/v1/tag", + headers=self.user2_headers, data=json.dumps(params), - content_type='application/json') + content_type="application/json", + ) self.assertEqual(response.status_code, 201) data = json.loads(response.data) - new_id = data['id'] + new_id = data["id"] - response = self.app.get('/api/v1/tag', headers=self.user2_headers, data={'id': new_id, 'event_id': 2}) + response = self.app.get( + "/api/v1/tag", + headers=self.user2_headers, + data={"id": new_id, "event_id": 2}, + ) data = json.loads(response.data) - self.assertEqual(data['id'], new_id) - self.assertEqual(data['event_id'], 2) - self.assertDictEqual(data['name'], { - 'en': 'English Tag 2 Event 2', - 'fr': 'French Tag 2 Event 2' - }) + self.assertEqual(data["id"], new_id) + self.assertEqual(data["event_id"], 2) + self.assertDictEqual( + data["name"], {"en": "English Tag 2 Event 2", "fr": "French Tag 2 Event 2"} + ) def test_post_event_admin(self): """Test that a non-event admin can't post a new tag.""" self.seed_static_data() params = { - 'event_id': 2, - 'name': { - 'en': 'English Tag 2 Event 2', - 'fr': 'French Tag 2 Event 2', - } + "event_id": 2, + "name": { + "en": "English Tag 2 Event 2", + "fr": "French Tag 2 Event 2", + }, } # User 1 is not an event admin for event 2 response = self.app.post( - '/api/v1/tag', - headers=self.user1_headers, - data=json.dumps(params), - content_type='application/json') + "/api/v1/tag", + headers=self.user1_headers, + data=json.dumps(params), + content_type="application/json", + ) self.assertEqual(response.status_code, 403) - def test_put(self): """Test typcial put request.""" self.seed_static_data() params = { - 'id': 2, - 'event_id': 1, - 'name': { - 'en': 'Renamed English Name', # Rename - 'zu': 'Zulu Name' - } + "id": 2, + "event_id": 1, + "name": {"en": "Renamed English Name", "zu": "Zulu Name"}, # Rename } response = self.app.put( - '/api/v1/tag', - headers=self.user1_headers, + "/api/v1/tag", + headers=self.user1_headers, data=json.dumps(params), - content_type='application/json') + content_type="application/json", + ) self.assertEqual(response.status_code, 200) - response = self.app.get('/api/v1/tag', headers=self.user1_headers, data={'id': 2, 'event_id': 1}) + response = self.app.get( + "/api/v1/tag", headers=self.user1_headers, data={"id": 2, "event_id": 1} + ) data = json.loads(response.data) - self.assertEqual(data['id'], 2) - self.assertEqual(data['event_id'], 1) - self.assertDictEqual(data['name'], { - 'en': 'Renamed English Name', - 'zu': 'Zulu Name' - }) + self.assertEqual(data["id"], 2) + self.assertEqual(data["event_id"], 1) + self.assertDictEqual( + data["name"], {"en": "Renamed English Name", "zu": "Zulu Name"} + ) def test_tag_list(self): """Test that a list of tags can be retrieved in the correct language.""" self.seed_static_data() - params = { - 'event_id': 1, - 'language': 'en' - } + params = {"event_id": 1, "language": "en"} - response = self.app.get('/api/v1/tags', headers=self.user1_headers, data=params) + response = self.app.get("/api/v1/tags", headers=self.user1_headers, data=params) data = json.loads(response.data) self.assertEqual(len(data), 2) - self.assertEqual(data[0]['id'], 1) - self.assertEqual(data[0]['event_id'], 1) - self.assertEqual(data[0]['name'], 'English Tag 1 Event 1') - self.assertEqual(data[1]['id'], 2) - self.assertEqual(data[1]['event_id'], 1) - self.assertEqual(data[1]['name'], 'English Tag 2 Event 1') + self.assertEqual(data[0]["id"], 1) + self.assertEqual(data[0]["event_id"], 1) + self.assertEqual(data[0]["name"], "English Tag 1 Event 1") + self.assertEqual(data[1]["id"], 2) + self.assertEqual(data[1]["event_id"], 1) + self.assertEqual(data[1]["name"], "English Tag 2 Event 1") - params = { - 'event_id': 1, - 'language': 'fr' - } + params = {"event_id": 1, "language": "fr"} - response = self.app.get('/api/v1/tags', headers=self.user1_headers, data=params) + response = self.app.get("/api/v1/tags", headers=self.user1_headers, data=params) data = json.loads(response.data) self.assertEqual(len(data), 2) - self.assertEqual(data[0]['id'], 1) - self.assertEqual(data[0]['event_id'], 1) - self.assertEqual(data[0]['name'], 'French Tag 1 Event 1') - self.assertEqual(data[1]['id'], 2) - self.assertEqual(data[1]['event_id'], 1) - self.assertEqual(data[1]['name'], 'French Tag 2 Event 1') + self.assertEqual(data[0]["id"], 1) + self.assertEqual(data[0]["event_id"], 1) + self.assertEqual(data[0]["name"], "French Tag 1 Event 1") + self.assertEqual(data[1]["id"], 2) + self.assertEqual(data[1]["event_id"], 1) + self.assertEqual(data[1]["name"], "French Tag 2 Event 1") def test_tag_list_default_language(self): """Test that the language defaults to English when not found.""" self.seed_static_data() - params = { - 'event_id': 2, - 'language': 'zu' - } + params = {"event_id": 2, "language": "zu"} - response = self.app.get('/api/v1/tags', headers=self.user2_headers, data=params) + response = self.app.get("/api/v1/tags", headers=self.user2_headers, data=params) data = json.loads(response.data) self.assertEqual(len(data), 1) - self.assertEqual(data[0]['id'], 3) - self.assertEqual(data[0]['event_id'], 2) - self.assertEqual(data[0]['name'], 'English Tag 1 Event 2') + self.assertEqual(data[0]["id"], 3) + self.assertEqual(data[0]["event_id"], 2) + self.assertEqual(data[0]["name"], "English Tag 1 Event 2") diff --git a/api/app/tests/test_email.py b/api/app/tests/test_email.py index 27c554534..7237ace09 100644 --- a/api/app/tests/test_email.py +++ b/api/app/tests/test_email.py @@ -1,16 +1,24 @@ import unittest -from app.utils.emailer import send_mail + from app import LOGGER +from app.utils.emailer import send_mail + class TestMailer(unittest.TestCase): def test_send_email(self): try: LOGGER.info("Testing mailer") - send_mail(recipient='nischal.hp@gmail.com', subject='TestSMTPEmail', body_text='Hello world from Amazon SES') + send_mail( + recipient="nischal.hp@gmail.com", + subject="TestSMTPEmail", + body_text="Hello world from Amazon SES", + ) return True except Exception: return False -''' + + +""" if __name__ == '__main__': unittest.main() -''' +""" diff --git a/api/app/tests/test_logger.py b/api/app/tests/test_logger.py index 0019351d5..cfe8ecde7 100644 --- a/api/app/tests/test_logger.py +++ b/api/app/tests/test_logger.py @@ -1,14 +1,15 @@ +import io +import sys import unittest + from app.utils.logger import Logger -import sys -import io class TestLogger(unittest.TestCase): def test_normal_logger(self): out = io.StringIO() LOGGER = Logger().get_logger() - test_string = 'Hello World' + test_string = "Hello World" LOGGER.info(test_string) output = out.getvalue().strip() if test_string in output: @@ -22,7 +23,7 @@ def test_logger_with_exception(self): out = io.StringIO() a = 100 b = 0 - a/b + a / b except Exception: LOGGER.error("Exception has occured", exc_info=True) output = out.getvalue().strip() diff --git a/api/app/users/api.py b/api/app/users/api.py index e99685425..96aad3a7d 100644 --- a/api/app/users/api.py +++ b/api/app/users/api.py @@ -1,7 +1,7 @@ +import copy import random import string from datetime import datetime -import copy import flask_restful as restful from flask import g, request @@ -9,93 +9,114 @@ from sqlalchemy import func from sqlalchemy.exc import IntegrityError +import app.events.status as event_status from app import LOGGER, bcrypt, db from app.events.models import EventRole -import app.events.status as event_status -from app.users.mixins import (AuthenticateMixin, PrivacyPolicyMixin, - SignupMixin, UserProfileListMixin, - UserProfileMixin, EventAttendeeMixin) +from app.users.mixins import ( + AuthenticateMixin, + EventAttendeeMixin, + PrivacyPolicyMixin, + SignupMixin, + UserProfileListMixin, + UserProfileMixin, +) from app.users.models import AppUser, PasswordReset, UserComment from app.users.repository import UserRepository as user_repository from app.utils import errors, misc -from app.utils.auth import admin_required, auth_required, generate_token, get_user_from_request +from app.utils.auth import ( + admin_required, + auth_required, + generate_token, + get_user_from_request, +) from app.utils.emailer import email_user, send_mail -from app.utils.errors import (ADD_VERIFY_TOKEN_FAILED, BAD_CREDENTIALS, - EMAIL_IN_USE, EMAIL_NOT_VERIFIED, - EMAIL_VERIFY_CODE_NOT_VALID, - ERROR_UPDATING_USER_PROFILE, FORBIDDEN, - MISSING_PASSWORD, POLICY_ALREADY_AGREED, - POLICY_NOT_AGREED, RESET_PASSWORD_CODE_EXPIRED, - RESET_PASSWORD_CODE_NOT_VALID, USER_DELETED, - USER_NOT_FOUND, VERIFY_EMAIL_INVITED_GUEST) +from app.utils.errors import ( + ADD_VERIFY_TOKEN_FAILED, + BAD_CREDENTIALS, + EMAIL_IN_USE, + EMAIL_NOT_VERIFIED, + EMAIL_VERIFY_CODE_NOT_VALID, + ERROR_UPDATING_USER_PROFILE, + FORBIDDEN, + MISSING_PASSWORD, + POLICY_ALREADY_AGREED, + POLICY_NOT_AGREED, + RESET_PASSWORD_CODE_EXPIRED, + RESET_PASSWORD_CODE_NOT_VALID, + UNAUTHORIZED, + USER_DELETED, + USER_NOT_FOUND, + VERIFY_EMAIL_INVITED_GUEST, +) from app.utils.misc import make_code -from app.utils.errors import UNAUTHORIZED - user_fields = { - 'id': fields.Integer, - 'email': fields.String, - 'firstname': fields.String, - 'lastname': fields.String, - 'user_title': fields.String, + "id": fields.Integer, + "email": fields.String, + "firstname": fields.String, + "lastname": fields.String, + "user_title": fields.String, } user_comment_fields = { - 'id': fields.Integer, - 'event_id': fields.Integer, - 'user_id': fields.Integer, - 'comment_by_user_firstname': fields.String(attribute='comment_by_user.firstname'), - 'comment_by_user_lastname': fields.String(attribute='comment_by_user.lastname'), - 'timestamp': fields.DateTime('iso8601'), - 'comment': fields.String + "id": fields.Integer, + "event_id": fields.Integer, + "user_id": fields.Integer, + "comment_by_user_firstname": fields.String(attribute="comment_by_user.firstname"), + "comment_by_user_lastname": fields.String(attribute="comment_by_user.lastname"), + "timestamp": fields.DateTime("iso8601"), + "comment": fields.String, } def user_info(user, roles): return { - 'id': user.id, - 'token': generate_token(user), - 'firstname': user.firstname, - 'lastname': user.lastname, - 'email': user.email, - 'title': user.user_title, - 'is_admin': user.is_admin, - 'primary_language': user.user_primaryLanguage, - 'roles': [{'event_id': event_role.event_id, 'role': event_role.role} for event_role in roles] + "id": user.id, + "token": generate_token(user), + "firstname": user.firstname, + "lastname": user.lastname, + "email": user.email, + "title": user.user_title, + "is_admin": user.is_admin, + "primary_language": user.user_primaryLanguage, + "roles": [ + {"event_id": event_role.event_id, "role": event_role.role} + for event_role in roles + ], } class UserAPI(SignupMixin, restful.Resource): - def randomPassword(self, stringLength=10): letters = string.ascii_lowercase - return ''.join(random.choice(letters) for i in range(stringLength)) + return "".join(random.choice(letters) for i in range(stringLength)) @auth_required @marshal_with(user_fields) def get(self): - user = db.session.query(AppUser).filter( - AppUser.id == g.current_user['id']).first() + user = ( + db.session.query(AppUser).filter(AppUser.id == g.current_user["id"]).first() + ) return user def post(self, invitedGuest=False): args = self.req_parser.parse_args() - email = args['email'] - firstname = args['firstname'] - lastname = args['lastname'] - user_title = args['user_title'] - policy_agreed = args['policy_agreed'] - user_primaryLanguage = args['language'] - - if(invitedGuest): + email = args["email"] + firstname = args["firstname"] + lastname = args["lastname"] + user_title = args["user_title"] + policy_agreed = args["policy_agreed"] + user_primaryLanguage = args["language"] + + if invitedGuest: password = self.randomPassword() else: - password = args['password'] + password = args["password"] - if(password is None): + if password is None: return MISSING_PASSWORD - + if not policy_agreed: return POLICY_NOT_AGREED @@ -107,7 +128,8 @@ def post(self, invitedGuest=False): lastname=lastname, user_title=user_title, password=password, - organisation_id=g.organisation.id) + organisation_id=g.organisation.id, + ) user.user_primaryLanguage = user_primaryLanguage db.session.add(user) @@ -118,17 +140,18 @@ def post(self, invitedGuest=False): LOGGER.error("email: {} already in use".format(email)) return EMAIL_IN_USE - if(not invitedGuest): + if not invitedGuest: email_user( - 'verify-email', + "verify-email", template_parameters=dict( system=g.organisation.system_name, organisation=g.organisation.name, host=misc.get_baobab_host(), - token=user.verify_token + token=user.verify_token, ), user=user, - subject_parameters=dict(system=g.organisation.system_name)) + subject_parameters=dict(system=g.organisation.system_name), + ) LOGGER.debug("Sent verification email to {}".format(user.email)) else: @@ -145,14 +168,15 @@ def post(self, invitedGuest=False): def put(self): args = self.put_req_parser.parse_args() - firstname = args['firstname'] - lastname = args['lastname'] - user_title = args['user_title'] - email = args['email'] - user_primaryLanguage = args['language'] + firstname = args["firstname"] + lastname = args["lastname"] + user_title = args["user_title"] + email = args["email"] + user_primaryLanguage = args["language"] - user = db.session.query(AppUser).filter( - AppUser.id == g.current_user['id']).first() + user = ( + db.session.query(AppUser).filter(AppUser.id == g.current_user["id"]).first() + ) if user.email != email: user.update_email(email) @@ -170,40 +194,40 @@ def put(self): if not user.verified_email: email_user( - 'verify-email', + "verify-email", template_parameters=dict( system=g.organisation.system_name, organisation=g.organisation.name, host=misc.get_baobab_host(), - token=user.verify_token + token=user.verify_token, ), user=user, - subject_parameters=dict(system=g.organisation.system_name)) + subject_parameters=dict(system=g.organisation.system_name), + ) LOGGER.debug("Sent re-verification email to {}".format(user.email)) - roles = db.session.query(EventRole).filter( - EventRole.user_id == user.id).all() + roles = db.session.query(EventRole).filter(EventRole.user_id == user.id).all() return user_info(user, roles), 200 @auth_required def delete(self): - ''' + """ The function that lets the user delete the account - ''' + """ - LOGGER.debug("Deleting user: {}".format(g.current_user['id'])) + LOGGER.debug("Deleting user: {}".format(g.current_user["id"])) - user = db.session.query(AppUser).filter( - AppUser.id == g.current_user['id']).first() + user = ( + db.session.query(AppUser).filter(AppUser.id == g.current_user["id"]).first() + ) if user: user.is_deleted = True db.session.commit() - LOGGER.debug("Successfully deleted user {}".format( - g.current_user['id'])) + LOGGER.debug("Successfully deleted user {}".format(g.current_user["id"])) else: - LOGGER.debug("No user for id {}".format(g.current_user['id'])) + LOGGER.debug("No user for id {}".format(g.current_user["id"])) return {}, 200 @@ -243,51 +267,51 @@ def __init__(self, user_response): self.is_withdrawn = None self.withdrawn_timestamp = None - self.type = 'Invited Guest' if user_response.InvitedGuest else 'Candidate' + self.type = "Invited Guest" if user_response.InvitedGuest else "Candidate" user_profile_fields = { - 'user_id': fields.Integer, - 'email': fields.String, - 'firstname': fields.String, - 'lastname': fields.String, - 'user_title': fields.String, - 'response_id': fields.Integer, - 'is_submitted': fields.Boolean, - 'submitted_timestamp': fields.DateTime('iso8601'), - 'is_withdrawn': fields.Boolean, - 'withdrawn_timestamp': fields.DateTime('iso8601') + "user_id": fields.Integer, + "email": fields.String, + "firstname": fields.String, + "lastname": fields.String, + "user_title": fields.String, + "response_id": fields.Integer, + "is_submitted": fields.Boolean, + "submitted_timestamp": fields.DateTime("iso8601"), + "is_withdrawn": fields.Boolean, + "withdrawn_timestamp": fields.DateTime("iso8601"), } user_profile_list_fields = copy.deepcopy(user_profile_fields) -user_profile_list_fields['type'] = fields.String +user_profile_list_fields["type"] = fields.String class UserProfileList(UserProfileListMixin, restful.Resource): - @marshal_with(user_profile_list_fields) @auth_required def get(self): args = self.req_parser.parse_args() - event_id = args['event_id'] - current_user_id = g.current_user['id'] + event_id = args["event_id"] + current_user_id = g.current_user["id"] current_user = user_repository.get_by_id(current_user_id) if not current_user.is_event_admin(event_id): return FORBIDDEN - user_responses = user_repository.get_all_with_responses_or_invited_guests_for(event_id) + user_responses = user_repository.get_all_with_responses_or_invited_guests_for( + event_id + ) views = [UserProfileListView(user_response) for user_response in user_responses] return views class UserProfile(UserProfileMixin, restful.Resource): - @auth_required def get(self): args = self.req_parser.parse_args() - user_id = args['user_id'] - current_user_id = g.current_user['id'] + user_id = args["user_id"] + current_user_id = g.current_user["id"] current_user = user_repository.get_by_id(current_user_id) @@ -304,43 +328,49 @@ def get(self): class AuthenticationAPI(AuthenticateMixin, restful.Resource): - def post(self): args = self.req_parser.parse_args() - user = user_repository.get_by_email(args['email'], g.organisation.id) + user = user_repository.get_by_email(args["email"], g.organisation.id) - LOGGER.debug("Authenticating user: {}".format(args['email'])) + LOGGER.debug("Authenticating user: {}".format(args["email"])) if user: if user.is_deleted: LOGGER.debug( - "Failed to authenticate, user {} deleted".format(args['email'])) + "Failed to authenticate, user {} deleted".format(args["email"]) + ) return USER_DELETED if not user.verified_email: LOGGER.debug( - "Failed to authenticate, email {} not verified".format(args['email'])) + "Failed to authenticate, email {} not verified".format( + args["email"] + ) + ) return EMAIL_NOT_VERIFIED - if bcrypt.check_password_hash(user.password, args['password']): + if bcrypt.check_password_hash(user.password, args["password"]): LOGGER.debug( - "Successful authentication for email: {}".format(args['email'])) - roles = db.session.query(EventRole).filter( - EventRole.user_id == user.id).all() + "Successful authentication for email: {}".format(args["email"]) + ) + roles = ( + db.session.query(EventRole) + .filter(EventRole.user_id == user.id) + .all() + ) return user_info(user, roles) else: - LOGGER.debug("User not found for {}".format(args['email'])) + LOGGER.debug("User not found for {}".format(args["email"])) return BAD_CREDENTIALS class AuthenticationRefreshAPI(restful.Resource): - @auth_required def get(self): - user_id = g.current_user['id'] + user_id = g.current_user["id"] user = user_repository.get_by_id(user_id) if not user: return UNAUTHORIZED @@ -350,20 +380,26 @@ def get(self): class PasswordResetRequestAPI(restful.Resource): - def post(self): req_parser = reqparse.RequestParser() - req_parser.add_argument('email', type=str, required=True) + req_parser.add_argument("email", type=str, required=True) args = req_parser.parse_args() LOGGER.debug( - "Requesting password reset for email {} and organisation {}".format(args['email'], g.organisation.name)) + "Requesting password reset for email {} and organisation {}".format( + args["email"], g.organisation.name + ) + ) - user = user_repository.get_by_email(args['email'], g.organisation.id) + user = user_repository.get_by_email(args["email"], g.organisation.id) if not user: - LOGGER.debug("No user found for email {} and organisation {}".format(args['email'], g.organisation.name)) + LOGGER.debug( + "No user found for email {} and organisation {}".format( + args["email"], g.organisation.name + ) + ) return USER_NOT_FOUND password_reset = PasswordReset(user=user) @@ -371,45 +407,45 @@ def post(self): db.session.commit() email_user( - 'password-reset', + "password-reset", template_parameters=dict( - system_name=g.organisation.system_name, + system_name=g.organisation.system_name, organisation=g.organisation.name, - host=misc.get_baobab_host(), - token=password_reset.code + host=misc.get_baobab_host(), + token=password_reset.code, ), subject_parameters=dict(system_name=g.organisation.system_name), - user=user) + user=user, + ) return {}, 201 class PasswordResetConfirmAPI(restful.Resource): - def post(self): req_parser = reqparse.RequestParser() - req_parser.add_argument('code', type=str, required=True) - req_parser.add_argument('password', type=str, required=True) + req_parser.add_argument("code", type=str, required=True) + req_parser.add_argument("password", type=str, required=True) args = req_parser.parse_args() - LOGGER.debug( - "Confirming password reset for code: {}".format(args['code'])) + LOGGER.debug("Confirming password reset for code: {}".format(args["code"])) - password_reset = db.session.query(PasswordReset).filter( - PasswordReset.code == args['code']).first() + password_reset = ( + db.session.query(PasswordReset) + .filter(PasswordReset.code == args["code"]) + .first() + ) if not password_reset: - LOGGER.debug( - "Reset password code not valid: {}".format(args['code'])) + LOGGER.debug("Reset password code not valid: {}".format(args["code"])) return RESET_PASSWORD_CODE_NOT_VALID if password_reset.date < datetime.now(): - LOGGER.debug( - "Reset code expired for code: {}".format(args['code'])) + LOGGER.debug("Reset code expired for code: {}".format(args["code"])) return RESET_PASSWORD_CODE_EXPIRED - password_reset.user.set_password(args['password']) + password_reset.user.set_password(args["password"]) db.session.delete(password_reset) db.session.commit() @@ -419,15 +455,13 @@ def post(self): class VerifyEmailAPI(restful.Resource): - def get(self): - token = request.args.get('token') + token = request.args.get("token") LOGGER.debug("Verifying email for token: {}".format(token)) - user = db.session.query(AppUser).filter( - AppUser.verify_token == token).first() + user = db.session.query(AppUser).filter(AppUser.verify_token == token).first() if not user: LOGGER.debug("No user found for token: {}".format(token)) @@ -444,14 +478,18 @@ def get(self): class ResendVerificationEmailAPI(restful.Resource): def get(self): - email = request.args.get('email') + email = request.args.get("email") LOGGER.debug("Resending verification email to: {}".format(email)) - + user = user_repository.get_by_email(email, g.organisation.id) if not user: - LOGGER.debug("User not found for email: {} in organisation: {}".format(email, g.organisation.name)) + LOGGER.debug( + "User not found for email: {} in organisation: {}".format( + email, g.organisation.name + ) + ) return USER_NOT_FOUND if user.verify_token is None: @@ -464,15 +502,16 @@ def get(self): return ADD_VERIFY_TOKEN_FAILED email_user( - 'verify-email', + "verify-email", template_parameters=dict( system=g.organisation.system_name, organisation=g.organisation.name, host=misc.get_baobab_host(), - token=user.verify_token + token=user.verify_token, ), user=user, - subject_parameters=dict(system=g.organisation.system_name)) + subject_parameters=dict(system=g.organisation.system_name), + ) LOGGER.debug("Resent email verification to: {}".format(email)) @@ -480,25 +519,28 @@ def get(self): class AdminOnlyAPI(restful.Resource): - @admin_required def get(self): return {}, 200 class UserCommentAPI(restful.Resource): - @auth_required def post(self): req_parser = reqparse.RequestParser() - req_parser.add_argument('event_id', type=int, required=False) - req_parser.add_argument('user_id', type=int, required=False) - req_parser.add_argument('comment', type=str, required=False) + req_parser.add_argument("event_id", type=int, required=False) + req_parser.add_argument("user_id", type=int, required=False) + req_parser.add_argument("comment", type=str, required=False) args = req_parser.parse_args() - current_user_id = g.current_user['id'] - comment = UserComment(args['event_id'], args['user_id'], - current_user_id, datetime.now(), args['comment']) + current_user_id = g.current_user["id"] + comment = UserComment( + args["event_id"], + args["user_id"], + current_user_id, + datetime.now(), + args["comment"], + ) db.session.add(comment) db.session.commit() @@ -509,17 +551,22 @@ def post(self): @marshal_with(user_comment_fields) def get(self): req_parser = reqparse.RequestParser() - req_parser.add_argument('event_id', type=int, required=True) - req_parser.add_argument('user_id', type=int, required=True) + req_parser.add_argument("event_id", type=int, required=True) + req_parser.add_argument("user_id", type=int, required=True) args = req_parser.parse_args() - current_user = user_repository.get_by_id(g.current_user['id']) - if not current_user.is_event_admin(args['event_id']): + current_user = user_repository.get_by_id(g.current_user["id"]) + if not current_user.is_event_admin(args["event_id"]): return FORBIDDEN - comments = db.session.query(UserComment).filter( - UserComment.event_id == args['event_id'], - UserComment.user_id == args['user_id']).all() + comments = ( + db.session.query(UserComment) + .filter( + UserComment.event_id == args["event_id"], + UserComment.user_id == args["user_id"], + ) + .all() + ) return comments @@ -531,32 +578,32 @@ def get(self): class EmailerAPI(restful.Resource): - @admin_required def post(self): req_parser = reqparse.RequestParser() - req_parser.add_argument('user_id', type=int, required=True) - req_parser.add_argument('email_subject', type=str, required=True) - req_parser.add_argument('email_body', type=str, required=True) + req_parser.add_argument("user_id", type=int, required=True) + req_parser.add_argument("email_subject", type=str, required=True) + req_parser.add_argument("email_body", type=str, required=True) args = req_parser.parse_args() - user = user_repository.get_by_id(args['user_id']) + user = user_repository.get_by_id(args["user_id"]) if user is None: return errors.USER_NOT_FOUND try: - send_mail(recipient=user.email, - sender_name=g.organisation.name, - sender_email=g.organisation.email_from, - subject=args['email_subject'], - body_text=GENERIC_EMAIL_TEMPLATE.format( - user_title=user.user_title, - user_firstname=user.firstname, - user_lastname=user.lastname, - body=args['email_body'], - ) - ) + send_mail( + recipient=user.email, + sender_name=g.organisation.name, + sender_email=g.organisation.email_from, + subject=args["email_subject"], + body_text=GENERIC_EMAIL_TEMPLATE.format( + user_title=user.user_title, + user_firstname=user.firstname, + user_lastname=user.lastname, + body=args["email_body"], + ), + ) except Exception as e: - LOGGER.error('Error sending email: {}'.format(e)) + LOGGER.error("Error sending email: {}".format(e)) return errors.EMAIL_NOT_SENT @@ -564,12 +611,12 @@ class PrivacyPolicyAPI(PrivacyPolicyMixin, restful.Resource): @auth_required def put(self): args = self.req_parser.parse_args() - policy_agreed = args['policy_agreed'] + policy_agreed = args["policy_agreed"] if not policy_agreed: return POLICY_NOT_AGREED - - current_user_id = g.current_user['id'] + + current_user_id = g.current_user["id"] current_user = user_repository.get_by_id(current_user_id) if current_user.has_agreed(): @@ -586,11 +633,11 @@ class EventAttendeeAPI(EventAttendeeMixin, restful.Resource): @auth_required def post(self): args = self.req_parser.parse_args() - event_id = args['event_id'] + event_id = args["event_id"] user = get_user_from_request() - status = event_status.get_event_status(event_id, user['id']) - - if(not status.is_event_attendee): + status = event_status.get_event_status(event_id, user["id"]) + + if not status.is_event_attendee: return UNAUTHORIZED - return 200 \ No newline at end of file + return 200 diff --git a/api/app/users/mixins.py b/api/app/users/mixins.py index 459fcbbcd..dfb71d70c 100644 --- a/api/app/users/mixins.py +++ b/api/app/users/mixins.py @@ -5,48 +5,49 @@ class SignupMixin(object): req_parser = reqparse.RequestParser() - req_parser.add_argument('email', type=str, required=True) - req_parser.add_argument('firstname', type=str, required=True) - req_parser.add_argument('lastname', type=str, required=True) - req_parser.add_argument('user_title', type=str, required=True) - req_parser.add_argument('password', type=str, required=False) - req_parser.add_argument('policy_agreed', type=boolean, required=True) - req_parser.add_argument('language', type=str, required=True) + req_parser.add_argument("email", type=str, required=True) + req_parser.add_argument("firstname", type=str, required=True) + req_parser.add_argument("lastname", type=str, required=True) + req_parser.add_argument("user_title", type=str, required=True) + req_parser.add_argument("password", type=str, required=False) + req_parser.add_argument("policy_agreed", type=boolean, required=True) + req_parser.add_argument("language", type=str, required=True) put_req_parser = reqparse.RequestParser() - put_req_parser.add_argument('email', type=str, required=True) - put_req_parser.add_argument('firstname', type=str, required=True) - put_req_parser.add_argument('lastname', type=str, required=True) - put_req_parser.add_argument('user_title', type=str, required=True) - put_req_parser.add_argument('language', type=str, required=True) - put_req_parser.add_argument('password', type=str, required=False) + put_req_parser.add_argument("email", type=str, required=True) + put_req_parser.add_argument("firstname", type=str, required=True) + put_req_parser.add_argument("lastname", type=str, required=True) + put_req_parser.add_argument("user_title", type=str, required=True) + put_req_parser.add_argument("language", type=str, required=True) + put_req_parser.add_argument("password", type=str, required=False) class AuthenticateMixin(object): req_parser = reqparse.RequestParser() - req_parser.add_argument('email', type=str, required=True) - req_parser.add_argument('password', type=str, required=True) + req_parser.add_argument("email", type=str, required=True) + req_parser.add_argument("password", type=str, required=True) class UserProfileListMixin(object): req_parser = reqparse.RequestParser() - req_parser.add_argument('event_id', type=int, required=True) + req_parser.add_argument("event_id", type=int, required=True) class UserProfileMixin(object): req_parser = reqparse.RequestParser() - req_parser.add_argument('user_id', type=int, required=True) + req_parser.add_argument("user_id", type=int, required=True) class PrivacyPolicyMixin(object): req_parser = reqparse.RequestParser() - req_parser.add_argument('policy_agreed', type=boolean, required=True) + req_parser.add_argument("policy_agreed", type=boolean, required=True) + class EventAttendeeMixin(object): req_parser = reqparse.RequestParser() - req_parser.add_argument('event_id', type=int, required=True) + req_parser.add_argument("event_id", type=int, required=True) diff --git a/api/app/users/models.py b/api/app/users/models.py index fe72d0326..a8b3db9fc 100644 --- a/api/app/users/models.py +++ b/api/app/users/models.py @@ -1,10 +1,12 @@ from datetime import datetime, timedelta -from app import db, bcrypt, LOGGER -from app.utils.misc import make_code from flask_login import UserMixin from sqlalchemy.schema import UniqueConstraint +from app import LOGGER, bcrypt, db +from app.utils.misc import make_code + + def expiration_date(): return datetime.now() + timedelta(days=1) @@ -16,13 +18,19 @@ class AppUser(db.Model, UserMixin): firstname = db.Column(db.String(100), nullable=False) lastname = db.Column(db.String(100), nullable=False) user_title = db.Column(db.String(20), nullable=False) - nationality_country_id = db.Column(db.Integer(), db.ForeignKey('country.id'), nullable=True) - residence_country_id = db.Column(db.Integer(), db.ForeignKey('country.id'), nullable=True) + nationality_country_id = db.Column( + db.Integer(), db.ForeignKey("country.id"), nullable=True + ) + residence_country_id = db.Column( + db.Integer(), db.ForeignKey("country.id"), nullable=True + ) user_gender = db.Column(db.String(20), nullable=True) affiliation = db.Column(db.String(255), nullable=True) department = db.Column(db.String(255), nullable=True) user_disability = db.Column(db.String(255), nullable=True) - user_category_id = db.Column(db.Integer(), db.ForeignKey('user_category.id'), nullable=True) + user_category_id = db.Column( + db.Integer(), db.ForeignKey("user_category.id"), nullable=True + ) user_dateOfBirth = db.Column(db.DateTime(), nullable=True) user_primaryLanguage = db.Column(db.String(255), nullable=True) password = db.Column(db.String(255), nullable=False) @@ -31,25 +39,35 @@ class AppUser(db.Model, UserMixin): is_deleted = db.Column(db.Boolean(), nullable=False) deleted_datetime_utc = db.Column(db.DateTime(), nullable=True) verified_email = db.Column(db.Boolean(), nullable=True) - verify_token = db.Column(db.String(255), nullable=True, unique=True, default=make_code) + verify_token = db.Column( + db.String(255), nullable=True, unique=True, default=make_code + ) policy_agreed_datetime = db.Column(db.DateTime(), nullable=True) - organisation_id = db.Column(db.Integer(), db.ForeignKey('organisation.id'), nullable=False) - - __table_args__ = (UniqueConstraint('email', 'organisation_id', name='org_email_unique'),) - - nationality_country = db.relationship('Country', foreign_keys=[nationality_country_id]) - residence_country = db.relationship('Country', foreign_keys=[residence_country_id]) - user_category = db.relationship('UserCategory') - event_roles = db.relationship('EventRole') - - def __init__(self, - email, - firstname, - lastname, - user_title, - password, - organisation_id, - is_admin=False): + organisation_id = db.Column( + db.Integer(), db.ForeignKey("organisation.id"), nullable=False + ) + + __table_args__ = ( + UniqueConstraint("email", "organisation_id", name="org_email_unique"), + ) + + nationality_country = db.relationship( + "Country", foreign_keys=[nationality_country_id] + ) + residence_country = db.relationship("Country", foreign_keys=[residence_country_id]) + user_category = db.relationship("UserCategory") + event_roles = db.relationship("EventRole") + + def __init__( + self, + email, + firstname, + lastname, + user_title, + password, + organisation_id, + is_admin=False, + ): self.email = email self.firstname = firstname self.lastname = lastname @@ -68,13 +86,13 @@ def set_password(self, password): def deactivate(self): self.active = False - + def verify(self): self.verified_email = True def agree_to_policy(self): self.policy_agreed_datetime = datetime.now() - + def has_agreed(self): return self.policy_agreed_datetime is not None @@ -82,37 +100,39 @@ def update_email(self, new_email): self.verified_email = False self.verify_token = make_code() self.email = new_email - + def delete(self): self.is_deleted = True self.deleted_datetime_utc = datetime.now() - + def _has_admin_role(self, event_id, admin_role_name): if self.is_admin: return True - + if self.event_roles is None: return False for event_role in self.event_roles: if event_role.event_id == event_id and event_role.role == admin_role_name: return True - + return False def is_event_admin(self, event_id): - return self._has_admin_role(event_id, 'admin') + return self._has_admin_role(event_id, "admin") def is_registration_admin(self, event_id): # An event admin is also a registration admin - return self._has_admin_role(event_id, 'registration-admin') or self._has_admin_role(event_id, 'admin') - + return self._has_admin_role( + event_id, "registration-admin" + ) or self._has_admin_role(event_id, "admin") + def is_reviewer(self, event_id): if self.event_roles is None: return False for event_role in self.event_roles: - if event_role.event_id == event_id and event_role.role == 'reviewer': + if event_role.event_id == event_id and event_role.role == "reviewer": return True return False @@ -120,21 +140,22 @@ def is_reviewer(self, event_id): def is_registration_volunteer(self, event_id): # An event admin is also a registration admin return ( - self._has_admin_role(event_id, 'registration-admin') - or self._has_admin_role(event_id, 'admin') - or self._has_admin_role(event_id,'registration-volunteer') + self._has_admin_role(event_id, "registration-admin") + or self._has_admin_role(event_id, "admin") + or self._has_admin_role(event_id, "registration-volunteer") ) - + + class PasswordReset(db.Model): id = db.Column(db.Integer(), primary_key=True) - user_id = db.Column(db.Integer(), db.ForeignKey('app_user.id')) + user_id = db.Column(db.Integer(), db.ForeignKey("app_user.id")) code = db.Column(db.String(255), unique=True, default=make_code) date = db.Column(db.DateTime(), default=expiration_date) user = db.relationship(AppUser) - db.UniqueConstraint('user_id', 'code', name='uni_user_code') + db.UniqueConstraint("user_id", "code", name="uni_user_code") def __init__(self, user): self.user = user @@ -165,15 +186,17 @@ def __init__(self, name, description=None, group=None): class UserComment(db.Model): id = db.Column(db.Integer(), primary_key=True) - event_id = db.Column(db.Integer(), db.ForeignKey('event.id'), nullable=False) - user_id = db.Column(db.Integer(), db.ForeignKey('app_user.id'), nullable=False) - comment_by_user_id = db.Column(db.Integer(), db.ForeignKey('app_user.id'), nullable=False) + event_id = db.Column(db.Integer(), db.ForeignKey("event.id"), nullable=False) + user_id = db.Column(db.Integer(), db.ForeignKey("app_user.id"), nullable=False) + comment_by_user_id = db.Column( + db.Integer(), db.ForeignKey("app_user.id"), nullable=False + ) timestamp = db.Column(db.DateTime(), nullable=False) comment = db.Column(db.String(2000)) - event = db.relationship('Event') - user = db.relationship('AppUser', foreign_keys=[user_id]) - comment_by_user = db.relationship('AppUser', foreign_keys=[comment_by_user_id]) + event = db.relationship("Event") + user = db.relationship("AppUser", foreign_keys=[user_id]) + comment_by_user = db.relationship("AppUser", foreign_keys=[comment_by_user_id]) def __init__(self, event_id, user_id, comment_by_user_id, timestamp, comment): self.event_id = event_id diff --git a/api/app/users/repository.py b/api/app/users/repository.py index 205dcca1f..28a348929 100644 --- a/api/app/users/repository.py +++ b/api/app/users/repository.py @@ -1,80 +1,101 @@ +from sqlalchemy import func, or_ + from app import db from app.applicationModel.models import ApplicationForm from app.events.models import EventRole -from app.responses.models import Response -from app.users.models import AppUser from app.invitedGuest.models import InvitedGuest from app.organisation.models import Organisation -from sqlalchemy import func -from sqlalchemy import or_ +from app.responses.models import Response +from app.users.models import AppUser -class UserRepository(): +class UserRepository: @staticmethod def get_by_id(user_id): return db.session.query(AppUser).get(user_id) @staticmethod def get_by_id_with_response(user_id): - return db.session.query(AppUser, Response)\ - .filter_by(id=user_id)\ - .join(Response, Response.user_id==AppUser.id)\ - .first() + return ( + db.session.query(AppUser, Response) + .filter_by(id=user_id) + .join(Response, Response.user_id == AppUser.id) + .first() + ) @staticmethod def get_by_email(email, organisation_id): - return db.session.query(AppUser)\ - .filter(func.lower(AppUser.email) == func.lower(email))\ - .filter_by(organisation_id=organisation_id).first() + return ( + db.session.query(AppUser) + .filter(func.lower(AppUser.email) == func.lower(email)) + .filter_by(organisation_id=organisation_id) + .first() + ) @staticmethod def get_by_event_admin(user_id, event_admin_user_id): - return db.session.query(AppUser, Response)\ - .filter_by(id=user_id)\ - .join(Response, Response.user_id==AppUser.id)\ - .join(ApplicationForm, ApplicationForm.id==Response.application_form_id)\ - .join(EventRole, EventRole.event_id==ApplicationForm.event_id)\ - .filter_by(user_id=event_admin_user_id, role='admin')\ - .first() + return ( + db.session.query(AppUser, Response) + .filter_by(id=user_id) + .join(Response, Response.user_id == AppUser.id) + .join(ApplicationForm, ApplicationForm.id == Response.application_form_id) + .join(EventRole, EventRole.event_id == ApplicationForm.event_id) + .filter_by(user_id=event_admin_user_id, role="admin") + .first() + ) @staticmethod def get_all_with_unsubmitted_response(): # TODO: Filter this to event - return db.session.query(AppUser)\ - .filter_by(active=True, is_deleted=False)\ - .join(Response, Response.user_id==AppUser.id)\ - .filter_by(is_submitted=False, is_withdrawn=False)\ - .all() - + return ( + db.session.query(AppUser) + .filter_by(active=True, is_deleted=False) + .join(Response, Response.user_id == AppUser.id) + .filter_by(is_submitted=False, is_withdrawn=False) + .all() + ) + @staticmethod def get_all_without_responses(): # TODO: Filter this to event - return db.session.query(AppUser)\ - .filter_by(active=True, is_deleted=False)\ - .outerjoin(Response, Response.user_id==AppUser.id)\ - .filter_by(id=None)\ - .all() - + return ( + db.session.query(AppUser) + .filter_by(active=True, is_deleted=False) + .outerjoin(Response, Response.user_id == AppUser.id) + .filter_by(id=None) + .all() + ) + @staticmethod def get_all_with_responses_for(event_id): - return db.session.query(AppUser, Response)\ - .filter_by(active=True, is_deleted=False)\ - .join(Response, Response.user_id==AppUser.id)\ - .join(ApplicationForm, ApplicationForm.id==Response.application_form_id)\ - .filter_by(event_id=event_id)\ - .all() + return ( + db.session.query(AppUser, Response) + .filter_by(active=True, is_deleted=False) + .join(Response, Response.user_id == AppUser.id) + .join(ApplicationForm, ApplicationForm.id == Response.application_form_id) + .filter_by(event_id=event_id) + .all() + ) @staticmethod def get_all_with_responses_or_invited_guests_for(event_id): return ( db.session.query(AppUser, Response, InvitedGuest) - .filter_by(active=True, is_deleted=False) - .join(Response, Response.user_id==AppUser.id, isouter=True) - .join(InvitedGuest, InvitedGuest.user_id==AppUser.id, isouter=True) - .filter(or_(InvitedGuest != None, Response != None)) - .join(ApplicationForm, ApplicationForm.id==Response.application_form_id, isouter=True) - - # Since both InvitedGuest and ApplicationForm have an event_id we need to include - # both in the filter. - .filter(or_(InvitedGuest.event_id==event_id, ApplicationForm.event_id==event_id)) + .filter_by(active=True, is_deleted=False) + .join(Response, Response.user_id == AppUser.id, isouter=True) + .join(InvitedGuest, InvitedGuest.user_id == AppUser.id, isouter=True) + .filter(or_(InvitedGuest != None, Response != None)) + .join( + ApplicationForm, + ApplicationForm.id == Response.application_form_id, + isouter=True, + ) + # Since both InvitedGuest and ApplicationForm have an event_id we need to include + # both in the filter. + .filter( + or_( + InvitedGuest.event_id == event_id, + ApplicationForm.event_id == event_id, + ) + ) ).all() diff --git a/api/app/users/tests.py b/api/app/users/tests.py index 49ac8cefd..b72ac9351 100644 --- a/api/app/users/tests.py +++ b/api/app/users/tests.py @@ -1,6 +1,6 @@ +import copy import json from datetime import datetime, timedelta -import copy from app import app, db from app.applicationModel.models import ApplicationForm @@ -8,316 +8,324 @@ from app.invitedGuest.models import InvitedGuest from app.organisation.models import Organisation from app.responses.models import Response -from app.users.models import (AppUser, Country, PasswordReset, UserCategory, - UserComment) +from app.users.models import AppUser, Country, PasswordReset, UserCategory, UserComment from app.utils.errors import POLICY_ALREADY_AGREED, POLICY_NOT_AGREED from app.utils.testing import ApiTestCase USER_DATA = { - 'email': 'something@email.com', - 'firstname': 'Some', - 'lastname': 'Thing', - 'user_title': 'Mr', - 'password': '123456', - 'language': 'en', - 'policy_agreed': True - } - -AUTH_DATA = { - 'email': 'something@email.com', - 'password': '123456' - } + "email": "something@email.com", + "firstname": "Some", + "lastname": "Thing", + "user_title": "Mr", + "password": "123456", + "language": "en", + "policy_agreed": True, +} + +AUTH_DATA = {"email": "something@email.com", "password": "123456"} -class UserApiTest(ApiTestCase): +class UserApiTest(ApiTestCase): def seed_static_data(self): - self.add_organisation('Deep Learning Indaba') - self.add_organisation('Deep Learning IndabaX') - db.session.add(UserCategory('Postdoc')) - db.session.add(Country('South Africa')) - self.event1 = self.add_event({'en': 'Indaba'}, {'en': 'Indaba Event'}, - datetime.now(), datetime.now(), - 'SOUTHAFRI2019') - self.event2 = self.add_event({'en': 'IndabaX'}, {'en': 'IndabaX Sudan'}, - datetime.now(), datetime.now(), - 'SUDANMO', 2) + self.add_organisation("Deep Learning Indaba") + self.add_organisation("Deep Learning IndabaX") + db.session.add(UserCategory("Postdoc")) + db.session.add(Country("South Africa")) + self.event1 = self.add_event( + {"en": "Indaba"}, + {"en": "Indaba Event"}, + datetime.now(), + datetime.now(), + "SOUTHAFRI2019", + ) + self.event2 = self.add_event( + {"en": "IndabaX"}, + {"en": "IndabaX Sudan"}, + datetime.now(), + datetime.now(), + "SUDANMO", + 2, + ) db.session.commit() self.event1_id = self.event1.id self.event2_id = self.event2.id - self.add_email_template('verify-email') - self.add_email_template('password-reset') + self.add_email_template("verify-email") + self.add_email_template("password-reset") db.session.flush() def get_auth_header_for(self, email): - body = { - 'email': email, - 'password': 'abc' - } - response = self.app.post('api/v1/authenticate', data=body) + body = {"email": email, "password": "abc"} + response = self.app.post("api/v1/authenticate", data=body) data = json.loads(response.data) - header = {'Authorization': data['token']} + header = {"Authorization": data["token"]} return header def test_registration(self): self.seed_static_data() - response = self.app.post('/api/v1/user', data=USER_DATA) + response = self.app.post("/api/v1/user", data=USER_DATA) data = json.loads(response.data) - assert data['id'] == 1 - assert len(data['token']) > 10 + assert data["id"] == 1 + assert len(data["token"]) > 10 def test_duplicate_registration(self): self.seed_static_data() - response = self.app.post('/api/v1/user', data=USER_DATA) + response = self.app.post("/api/v1/user", data=USER_DATA) assert response.status_code == 201 - response = self.app.post('/api/v1/user', data=USER_DATA) + response = self.app.post("/api/v1/user", data=USER_DATA) assert response.status_code == 409 def test_policy_not_agreed(self): self.seed_static_data() user_data = copy.deepcopy(USER_DATA) - user_data['policy_agreed'] = False - - response = self.app.post('/api/v1/user', data=user_data) + user_data["policy_agreed"] = False + + response = self.app.post("/api/v1/user", data=user_data) self.assertEqual(response.status_code, 400) def test_get_user(self): self.seed_static_data() - response = self.app.post('/api/v1/user', data=USER_DATA) + response = self.app.post("/api/v1/user", data=USER_DATA) data = json.loads(response.data) - headers = {'Authorization': data['token']} + headers = {"Authorization": data["token"]} - response = self.app.get('/api/v1/user', headers=headers) + response = self.app.get("/api/v1/user", headers=headers) data = json.loads(response.data) - assert data['email'] == 'something@email.com' - assert data['firstname'] == 'Some' - assert data['lastname'] == 'Thing' - assert data['user_title'] == 'Mr' + assert data["email"] == "something@email.com" + assert data["firstname"] == "Some" + assert data["lastname"] == "Thing" + assert data["user_title"] == "Mr" def test_update_user(self): self.seed_static_data() - response = self.app.post('/api/v1/user', data=USER_DATA) + response = self.app.post("/api/v1/user", data=USER_DATA) data = json.loads(response.data) self.assertEqual(response.status_code, 201) - headers = {'Authorization': data['token']} - - response = self.app.put('/api/v1/user', headers=headers, data={ - 'email': 'something@email.com', - 'firstname': 'Updated', - 'lastname': 'Updated', - 'user_title': 'Mrs', - 'language': 'zu', - 'password': '' - }) + headers = {"Authorization": data["token"]} + + response = self.app.put( + "/api/v1/user", + headers=headers, + data={ + "email": "something@email.com", + "firstname": "Updated", + "lastname": "Updated", + "user_title": "Mrs", + "language": "zu", + "password": "", + }, + ) self.assertEqual(response.status_code, 200) data = json.loads(response.data) - self.assertEqual(data['primary_language'], 'zu') + self.assertEqual(data["primary_language"], "zu") - response = self.app.get('/api/v1/user', headers=headers) + response = self.app.get("/api/v1/user", headers=headers) data = json.loads(response.data) - self.assertEqual(data['email'], 'something@email.com') - self.assertEqual(data['firstname'], 'Updated') - self.assertEqual(data['lastname'], 'Updated') - self.assertEqual(data['user_title'], 'Mrs') + self.assertEqual(data["email"], "something@email.com") + self.assertEqual(data["firstname"], "Updated") + self.assertEqual(data["lastname"], "Updated") + self.assertEqual(data["user_title"], "Mrs") def test_authentication_deleted(self): self.seed_static_data() - response = self.app.post('/api/v1/user', data=USER_DATA) + response = self.app.post("/api/v1/user", data=USER_DATA) data = json.loads(response.data) assert response.status_code == 201 - headers = {'Authorization': data['token']} + headers = {"Authorization": data["token"]} - response = self.app.delete('api/v1/user', headers=headers) + response = self.app.delete("api/v1/user", headers=headers) assert response.status_code == 200 - response = self.app.post('/api/v1/authenticate', data=AUTH_DATA) + response = self.app.post("/api/v1/authenticate", data=AUTH_DATA) assert response.status_code == 404 def test_authentication_unverified_email(self): self.seed_static_data() - response = self.app.post('/api/v1/user', data=USER_DATA) + response = self.app.post("/api/v1/user", data=USER_DATA) assert response.status_code == 201 - response = self.app.post('/api/v1/authenticate', data=AUTH_DATA) + response = self.app.post("/api/v1/authenticate", data=AUTH_DATA) assert response.status_code == 422 def test_authentication_wrong_password(self): self.seed_static_data() - response = self.app.post('/api/v1/user', data=USER_DATA) + response = self.app.post("/api/v1/user", data=USER_DATA) data = json.loads(response.data) assert response.status_code == 201 - user = db.session.query(AppUser).filter( - AppUser.id == data['id']).first() + user = db.session.query(AppUser).filter(AppUser.id == data["id"]).first() - response = self.app.get( - '/api/v1/verify-email?token='+user.verify_token) + response = self.app.get("/api/v1/verify-email?token=" + user.verify_token) assert response.status_code == 201 - response = self.app.post('/api/v1/authenticate', data={ - 'email': 'something@email.com', - 'password': 'wrong' - }) + response = self.app.post( + "/api/v1/authenticate", + data={"email": "something@email.com", "password": "wrong"}, + ) assert response.status_code == 401 def test_authentication(self): self.seed_static_data() - response = self.app.post('/api/v1/user', data=USER_DATA) + response = self.app.post("/api/v1/user", data=USER_DATA) data = json.loads(response.data) assert response.status_code == 201 - user = db.session.query(AppUser).filter( - AppUser.id == data['id']).first() + user = db.session.query(AppUser).filter(AppUser.id == data["id"]).first() - response = self.app.get( - '/api/v1/verify-email?token='+user.verify_token) + response = self.app.get("/api/v1/verify-email?token=" + user.verify_token) assert response.status_code == 201 - response = self.app.post('/api/v1/authenticate', data=AUTH_DATA) + response = self.app.post("/api/v1/authenticate", data=AUTH_DATA) assert response.status_code == 200 def test_authentication_response(self): self.seed_static_data() - response = self.app.post('/api/v1/user', data=USER_DATA) + response = self.app.post("/api/v1/user", data=USER_DATA) data = json.loads(response.data) - user = db.session.query(AppUser).filter( - AppUser.id == data['id']).first() + user = db.session.query(AppUser).filter(AppUser.id == data["id"]).first() - response = self.app.get( - '/api/v1/verify-email?token='+user.verify_token) + response = self.app.get("/api/v1/verify-email?token=" + user.verify_token) - role1 = EventRole('admin', data['id'], self.event1_id) - role2 = EventRole('reviewer', data['id'], self.event2_id) + role1 = EventRole("admin", data["id"], self.event1_id) + role2 = EventRole("reviewer", data["id"], self.event2_id) db.session.add(role1) db.session.add(role2) db.session.commit() db.session.flush() - response = self.app.post('/api/v1/authenticate', data=AUTH_DATA) + response = self.app.post("/api/v1/authenticate", data=AUTH_DATA) data = json.loads(response.data) - self.assertEqual(data['firstname'], USER_DATA['firstname']) - self.assertEqual(data['lastname'], USER_DATA['lastname']) - self.assertEqual(data['title'], USER_DATA['user_title']) - self.assertEqual(data['roles'], [ - {'event_id': self.event1_id, 'role': 'admin'}, - {'event_id': self.event2_id, 'role': 'reviewer'}, - ]) + self.assertEqual(data["firstname"], USER_DATA["firstname"]) + self.assertEqual(data["lastname"], USER_DATA["lastname"]) + self.assertEqual(data["title"], USER_DATA["user_title"]) + self.assertEqual( + data["roles"], + [ + {"event_id": self.event1_id, "role": "admin"}, + {"event_id": self.event2_id, "role": "reviewer"}, + ], + ) def test_password_reset_user_does_not_exist(self): - response = self.app.post('/api/v1/password-reset/request', data={ - 'email': 'something@email.com' - }) + response = self.app.post( + "/api/v1/password-reset/request", data={"email": "something@email.com"} + ) assert response.status_code == 404 def test_password_reset_expired(self): self.seed_static_data() - response = self.app.post('/api/v1/user', data=USER_DATA) + response = self.app.post("/api/v1/user", data=USER_DATA) data = json.loads(response.data) assert response.status_code == 201 - user = db.session.query(AppUser).filter( - AppUser.id == data['id']).first() + user = db.session.query(AppUser).filter(AppUser.id == data["id"]).first() - response = self.app.get( - '/api/v1/verify-email?token='+user.verify_token) + response = self.app.get("/api/v1/verify-email?token=" + user.verify_token) assert response.status_code == 201 - response = self.app.post('/api/v1/password-reset/request', data={ - 'email': 'something@email.com' - }) + response = self.app.post( + "/api/v1/password-reset/request", data={"email": "something@email.com"} + ) assert response.status_code == 201 pw_reset = db.session.query(PasswordReset).first() pw_reset.date = datetime.now() - timedelta(days=2) db.session.commit() - response = self.app.post('/api/v1/password-reset/confirm', data={ - 'code': pw_reset.code, - 'password': 'abc123' - }) + response = self.app.post( + "/api/v1/password-reset/confirm", + data={"code": pw_reset.code, "password": "abc123"}, + ) assert response.status_code == 400 def test_password_reset(self): self.seed_static_data() - response = self.app.post('/api/v1/user', data=USER_DATA) + response = self.app.post("/api/v1/user", data=USER_DATA) data = json.loads(response.data) assert response.status_code == 201 - user = db.session.query(AppUser).filter( - AppUser.id == data['id']).first() + user = db.session.query(AppUser).filter(AppUser.id == data["id"]).first() - response = self.app.get( - '/api/v1/verify-email?token='+user.verify_token) + response = self.app.get("/api/v1/verify-email?token=" + user.verify_token) assert response.status_code == 201 - response = self.app.post('/api/v1/password-reset/request', data={ - 'email': 'something@email.com' - }) + response = self.app.post( + "/api/v1/password-reset/request", data={"email": "something@email.com"} + ) assert response.status_code == 201 pw_reset = db.session.query(PasswordReset).first() - response = self.app.post('/api/v1/password-reset/confirm', data={ - 'code': "bad code", - 'password': 'abc123' - }) + response = self.app.post( + "/api/v1/password-reset/confirm", + data={"code": "bad code", "password": "abc123"}, + ) assert response.status_code == 418 - response = self.app.post('/api/v1/password-reset/confirm', data={ - 'code': pw_reset.code, - 'password': 'abc123' - }) + response = self.app.post( + "/api/v1/password-reset/confirm", + data={"code": pw_reset.code, "password": "abc123"}, + ) assert response.status_code == 201 - response = self.app.post('/api/v1/authenticate', data={ - 'email': 'something@email.com', - 'password': 'abc123' - }) + response = self.app.post( + "/api/v1/authenticate", + data={"email": "something@email.com", "password": "abc123"}, + ) assert response.status_code == 200 def test_deletion(self): self.seed_static_data() - response = self.app.post('/api/v1/user', data=USER_DATA) + response = self.app.post("/api/v1/user", data=USER_DATA) data = json.loads(response.data) - user_id = data['id'] - headers = {'Authorization': data['token']} - response = self.app.delete('/api/v1/user', headers=headers) + user_id = data["id"] + headers = {"Authorization": data["token"]} + response = self.app.delete("/api/v1/user", headers=headers) assert response.status_code == 200 user = db.session.query(AppUser).filter(AppUser.id == user_id).first() - assert user.email == 'something@email.com' + assert user.email == "something@email.com" assert user.is_deleted == True def test_resend_verification_email(self): self.seed_static_data() - self.app.post('/api/v1/user', data=USER_DATA) + self.app.post("/api/v1/user", data=USER_DATA) response = self.app.get( - '/api/v1/resend-verification-email?email={}'.format(USER_DATA['email'])) + "/api/v1/resend-verification-email?email={}".format(USER_DATA["email"]) + ) assert response.status_code == 201 def test_resend_verification_email_no_user(self): self.seed_static_data() response = self.app.get( - '/api/v1/resend-verification-email?email={}'.format('nonexistant@dummy.com')) + "/api/v1/resend-verification-email?email={}".format("nonexistant@dummy.com") + ) assert response.status_code == 404 def setup_verified_user(self): - user = AppUser(email='something@email.com', firstname='Some', lastname='Thing', user_title='Mr',password= '123456', organisation_id=1) - user.verify_token = 'existing token' + user = AppUser( + email="something@email.com", + firstname="Some", + lastname="Thing", + user_title="Mr", + password="123456", + organisation_id=1, + ) + user.verify_token = "existing token" user.verify() db.session.add(user) db.session.commit() @@ -325,20 +333,48 @@ def setup_verified_user(self): def setup_responses(self): application_forms = [ self.create_application_form(1, True, False), - self.create_application_form(2, False, False) + self.create_application_form(2, False, False), ] db.session.add_all(application_forms) - candidate1 = AppUser(email='c1@c.com', firstname='candidate', lastname='1', user_title='Mr', password='abc', organisation_id=1) - candidate2 = AppUser(email='c2@c.com', firstname='candidate', lastname='2', user_title='Ms', password='abc', organisation_id=1) - candidate3 = AppUser(email='c3@c.com', firstname='candidate', lastname='3', user_title='Mr', password='abc', organisation_id=1) - event_admin = AppUser(email='ea@ea.com',firstname='event_admin', lastname='1', user_title='Ms', password='abc', organisation_id=1) + candidate1 = AppUser( + email="c1@c.com", + firstname="candidate", + lastname="1", + user_title="Mr", + password="abc", + organisation_id=1, + ) + candidate2 = AppUser( + email="c2@c.com", + firstname="candidate", + lastname="2", + user_title="Ms", + password="abc", + organisation_id=1, + ) + candidate3 = AppUser( + email="c3@c.com", + firstname="candidate", + lastname="3", + user_title="Mr", + password="abc", + organisation_id=1, + ) + event_admin = AppUser( + email="ea@ea.com", + firstname="event_admin", + lastname="1", + user_title="Ms", + password="abc", + organisation_id=1, + ) users = [candidate1, candidate2, candidate3, event_admin] for user in users: user.verify() db.session.add_all(users) - event_role = EventRole('admin', 4, 1) + event_role = EventRole("admin", 4, 1) db.session.add(event_role) self.add_response(1, 1, is_submitted=True) @@ -348,91 +384,120 @@ def setup_responses(self): def test_email_change_gets_new_token_and_is_unverified(self): self.seed_static_data() self.setup_verified_user() - response = self.app.post('/api/v1/authenticate', data=AUTH_DATA) + response = self.app.post("/api/v1/authenticate", data=AUTH_DATA) data = json.loads(response.data) - headers = {'Authorization': data['token']} - - response = self.app.put('/api/v1/user', headers=headers, data={ - 'email': 'somethingnew@email.com', - 'firstname': 'Some', - 'lastname': 'Thing', - 'user_title': 'Mr', - 'language': 'en', - 'password':'' - }) + headers = {"Authorization": data["token"]} + + response = self.app.put( + "/api/v1/user", + headers=headers, + data={ + "email": "somethingnew@email.com", + "firstname": "Some", + "lastname": "Thing", + "user_title": "Mr", + "language": "en", + "password": "", + }, + ) - self.assertEqual(response.status_code, 200) + self.assertEqual(response.status_code, 200) user = db.session.query(AppUser).get(1) - self.assertEqual(user.email, 'somethingnew@email.com') - self.assertEqual(user.firstname, 'Some') - self.assertEqual(user.lastname, 'Thing') - self.assertEqual(user.user_title, 'Mr') + self.assertEqual(user.email, "somethingnew@email.com") + self.assertEqual(user.firstname, "Some") + self.assertEqual(user.lastname, "Thing") + self.assertEqual(user.user_title, "Mr") self.assertEqual(user.verified_email, False) - self.assertNotEqual(user.verify_token, 'existing token') + self.assertNotEqual(user.verify_token, "existing token") def test_user_profile_list(self): self.seed_static_data() self.setup_responses() - header = self.get_auth_header_for('ea@ea.com') - params = {'event_id': 1} + header = self.get_auth_header_for("ea@ea.com") + params = {"event_id": 1} - response = self.app.get('/api/v1/userprofilelist', headers=header, data=params) + response = self.app.get("/api/v1/userprofilelist", headers=header, data=params) data = json.loads(response.data) print(data) - data = sorted(data, key=lambda k: k['user_id']) + data = sorted(data, key=lambda k: k["user_id"]) self.assertEqual(len(data), 2) - self.assertEqual(data[0]['user_id'], 1) - self.assertEqual(data[0]['is_submitted'], True) - self.assertEqual(data[0]['is_withdrawn'], False) - self.assertEqual(data[0]['withdrawn_timestamp'], None) + self.assertEqual(data[0]["user_id"], 1) + self.assertEqual(data[0]["is_submitted"], True) + self.assertEqual(data[0]["is_withdrawn"], False) + self.assertEqual(data[0]["withdrawn_timestamp"], None) - self.assertEqual(data[1]['user_id'], 2) - self.assertEqual(data[1]['is_submitted'], False) - self.assertEqual(data[1]['submitted_timestamp'], None) - self.assertEqual(data[1]['is_withdrawn'], True) + self.assertEqual(data[1]["user_id"], 2) + self.assertEqual(data[1]["is_submitted"], False) + self.assertEqual(data[1]["submitted_timestamp"], None) + self.assertEqual(data[1]["is_withdrawn"], True) class UserCommentAPITest(ApiTestCase): - def seed_static_data(self): - self.add_organisation('Deep Learning Indaba', 'blah.png', 'blah_big.png') - db.session.add(UserCategory('Postdoc')) - db.session.add(Country('South Africa')) - self.event1 = self.add_event({'en': 'Indaba'}, {'en': 'Indaba Event'}, - datetime.now(), datetime.now(), - 'NAGSOLVER') + self.add_organisation("Deep Learning Indaba", "blah.png", "blah_big.png") + db.session.add(UserCategory("Postdoc")) + db.session.add(Country("South Africa")) + self.event1 = self.add_event( + {"en": "Indaba"}, + {"en": "Indaba Event"}, + datetime.now(), + datetime.now(), + "NAGSOLVER", + ) db.session.add(self.event1) db.session.commit() self.event1_id = self.event1.id - - self.add_email_template('verify-email') - self.add_email_template('password-reset') + + self.add_email_template("verify-email") + self.add_email_template("password-reset") user_data1 = USER_DATA.copy() - response = self.app.post('/api/v1/user', data=user_data1) + response = self.app.post("/api/v1/user", data=user_data1) self.user1 = json.loads(response.data) user_data2 = USER_DATA.copy() - user_data2['email'] = 'person2@person.com' - user_data2['firstname'] = 'Person' - user_data2['lastname'] = 'Two' - response = self.app.post('/api/v1/user', data=user_data2) + user_data2["email"] = "person2@person.com" + user_data2["firstname"] = "Person" + user_data2["lastname"] = "Two" + response = self.app.post("/api/v1/user", data=user_data2) self.user2 = json.loads(response.data) - user2 = db.session.query(AppUser).filter(AppUser.email == 'person2@person.com').first() + user2 = ( + db.session.query(AppUser) + .filter(AppUser.email == "person2@person.com") + .first() + ) user2.is_admin = True db.session.flush() def seed_comments(self): - self.comment1 = UserComment(self.event1_id, self.user1['id'], self.user2['id'], datetime.now(), 'Comment 1') - self.comment2 = UserComment(self.event1_id, self.user1['id'], self.user2['id'], datetime.now(), 'Comment 2') - self.comment3 = UserComment(self.event1_id, self.user2['id'], self.user1['id'], datetime.now(), 'Comment 3') + self.comment1 = UserComment( + self.event1_id, + self.user1["id"], + self.user2["id"], + datetime.now(), + "Comment 1", + ) + self.comment2 = UserComment( + self.event1_id, + self.user1["id"], + self.user2["id"], + datetime.now(), + "Comment 2", + ) + self.comment3 = UserComment( + self.event1_id, + self.user2["id"], + self.user1["id"], + datetime.now(), + "Comment 3", + ) db.session.add_all([self.comment1, self.comment2, self.comment3]) db.session.flush() @@ -441,9 +506,18 @@ def test_post_comment(self): with app.app_context(): self.seed_static_data() - params = {'event_id': self.event1_id, 'user_id': self.user2['id'], 'comment': 'Comment1'} - print(('Sending params: ', params)) - response = self.app.post('/api/v1/user-comment', headers={'Authorization': self.user1['token']}, data=json.dumps(params), content_type='application/json') + params = { + "event_id": self.event1_id, + "user_id": self.user2["id"], + "comment": "Comment1", + } + print(("Sending params: ", params)) + response = self.app.post( + "/api/v1/user-comment", + headers={"Authorization": self.user1["token"]}, + data=json.dumps(params), + content_type="application/json", + ) data = json.loads(response.data) self.assertEqual(response.status_code, 201) @@ -453,8 +527,12 @@ def test_get_forbidden(self): self.seed_static_data() self.seed_comments() - params = {'event_id': self.event1_id, 'user_id': self.user2['id']} - response = self.app.get('/api/v1/user-comment', headers={'Authorization': self.user1['token']}, query_string=params) + params = {"event_id": self.event1_id, "user_id": self.user2["id"]} + response = self.app.get( + "/api/v1/user-comment", + headers={"Authorization": self.user1["token"]}, + query_string=params, + ) self.assertEqual(response.status_code, 403) @@ -463,61 +541,77 @@ def test_get_comments(self): self.seed_static_data() self.seed_comments() - params = {'event_id': self.event1_id, 'user_id': self.user1['id']} - response = self.app.get('/api/v1/user-comment', headers={'Authorization': self.user2['token']}, query_string=params) + params = {"event_id": self.event1_id, "user_id": self.user1["id"]} + response = self.app.get( + "/api/v1/user-comment", + headers={"Authorization": self.user2["token"]}, + query_string=params, + ) comment_list = json.loads(response.data) self.assertEqual(len(comment_list), 2) - self.assertEqual(comment_list[0]['event_id'], self.comment1.event_id) - self.assertEqual(comment_list[0]['user_id'], self.comment1.user_id) - self.assertEqual(comment_list[0]['comment_by_user_firstname'], self.user2['firstname']) - self.assertEqual(comment_list[0]['comment_by_user_lastname'], self.user2['lastname']) - self.assertEqual(comment_list[0]['comment'], self.comment1.comment) - - self.assertEqual(comment_list[1]['event_id'], self.comment2.event_id) - self.assertEqual(comment_list[1]['user_id'], self.comment2.user_id) - self.assertEqual(comment_list[1]['comment_by_user_firstname'], self.user2['firstname']) - self.assertEqual(comment_list[1]['comment_by_user_lastname'], self.user2['lastname']) - self.assertEqual(comment_list[1]['comment'], self.comment2.comment) + self.assertEqual(comment_list[0]["event_id"], self.comment1.event_id) + self.assertEqual(comment_list[0]["user_id"], self.comment1.user_id) + self.assertEqual( + comment_list[0]["comment_by_user_firstname"], self.user2["firstname"] + ) + self.assertEqual( + comment_list[0]["comment_by_user_lastname"], self.user2["lastname"] + ) + self.assertEqual(comment_list[0]["comment"], self.comment1.comment) + + self.assertEqual(comment_list[1]["event_id"], self.comment2.event_id) + self.assertEqual(comment_list[1]["user_id"], self.comment2.user_id) + self.assertEqual( + comment_list[1]["comment_by_user_firstname"], self.user2["firstname"] + ) + self.assertEqual( + comment_list[1]["comment_by_user_lastname"], self.user2["lastname"] + ) + self.assertEqual(comment_list[1]["comment"], self.comment2.comment) class UserProfileListApiTest(ApiTestCase): def seed_static_data(self): self.event1 = self.add_event( - {'en': 'Indaba'}, - {'en': 'Indaba Event'}, + {"en": "Indaba"}, + {"en": "Indaba Event"}, datetime.now(), datetime.now(), - 'SOUTHAFRI2019', - self.dummy_org_id + "SOUTHAFRI2019", + self.dummy_org_id, ) self.event2 = self.add_event( - {'en': 'IndabaX'}, - {'en': 'IndabaX Sudan'}, + {"en": "IndabaX"}, + {"en": "IndabaX Sudan"}, datetime.now(), datetime.now(), - 'SUDANMO', - self.dummy_org_id + "SUDANMO", + self.dummy_org_id, ) db.session.flush() # Setup the event admins - self.event1_admin = self.add_user('ea1@ea.com') - self.event2_admin = self.add_user('ea2@ea.com') + self.event1_admin = self.add_user("ea1@ea.com") + self.event2_admin = self.add_user("ea2@ea.com") - event1_role = self.event1.add_event_role('admin', self.event1_admin.id) - event2_role = self.event2.add_event_role('admin', self.event2_admin.id) + event1_role = self.event1.add_event_role("admin", self.event1_admin.id) + event2_role = self.event2.add_event_role("admin", self.event2_admin.id) db.session.commit() # Set up the application forms - self.event1_application_form = self.create_application_form(self.event1.id, True, False) - self.event2_application_form = self.create_application_form(self.event2.id, True, False) + self.event1_application_form = self.create_application_form( + self.event1.id, True, False + ) + self.event2_application_form = self.create_application_form( + self.event2.id, True, False + ) db.session.add_all([self.event1_application_form, self.event2_application_form]) db.session.flush() self.event1_id = self.event1.id - self.event2_id = self.event2.id #TODO: add comment about why we do this + self.event2_id = self.event2.id # TODO: add comment about why we do this def test_event_does_not_exist(self): """ @@ -528,9 +622,9 @@ def test_event_does_not_exist(self): # Make the request header = self.get_auth_header_for(self.event1_admin.email) - params = {'event_id': 1337} + params = {"event_id": 1337} - response = self.app.get('/api/v1/userprofilelist', headers=header, data=params) + response = self.app.get("/api/v1/userprofilelist", headers=header, data=params) self.assertEqual(response.status_code, 403) @@ -552,13 +646,13 @@ def test_is_not_active_or_deleted(self): self.add_response(self.event1_application_form.id, candidates[0].id), self.add_response(self.event1_application_form.id, candidates[1].id), - self.add_response(self.event1_application_form.id, candidates[2].id) + self.add_response(self.event1_application_form.id, candidates[2].id) # Make the request header = self.get_auth_header_for(self.event1_admin.email) - params = {'event_id': self.event1_id} + params = {"event_id": self.event1_id} - response = self.app.get('/api/v1/userprofilelist', headers=header, data=params) + response = self.app.get("/api/v1/userprofilelist", headers=header, data=params) # Assert stuff self.assertEqual(response.status_code, 200) @@ -576,9 +670,9 @@ def test_no_response_and_not_invited(self): # Make the request header = self.get_auth_header_for(self.event1_admin.email) - params = {'event_id': self.event1_id} + params = {"event_id": self.event1_id} - response = self.app.get('/api/v1/userprofilelist', headers=header, data=params) + response = self.app.get("/api/v1/userprofilelist", headers=header, data=params) # Assert that request succeeds and no users are returned. self.assertEqual(response.status_code, 200) @@ -595,9 +689,7 @@ def test_no_response_and_invited(self): user_id = candidates[0].id invitation = InvitedGuest( - event_id=self.event1_id, - user_id=user_id, - role='EveryRole' + event_id=self.event1_id, user_id=user_id, role="EveryRole" ) db.session.add(invitation) @@ -605,15 +697,15 @@ def test_no_response_and_invited(self): # Make the request header = self.get_auth_header_for(self.event1_admin.email) - params = {'event_id': self.event1_id} + params = {"event_id": self.event1_id} - response = self.app.get('/api/v1/userprofilelist', headers=header, data=params) + response = self.app.get("/api/v1/userprofilelist", headers=header, data=params) # Assert that request succeeds and no users of the correct type are returned. self.assertEqual(response.status_code, 200) self.assertEqual(len(response.json), 1) - self.assertEqual(response.json[0]['user_id'], user_id) - self.assertEqual(response.json[0]['type'], 'Invited Guest') + self.assertEqual(response.json[0]["user_id"], user_id) + self.assertEqual(response.json[0]["type"], "Invited Guest") def test_response_and_invited(self): """ @@ -626,26 +718,26 @@ def test_response_and_invited(self): db.session.add_all(candidates) invitation = InvitedGuest( - event_id=self.event1_id, - user_id=candidates[0].id, - role='EveryRole' + event_id=self.event1_id, user_id=candidates[0].id, role="EveryRole" ) db.session.add(invitation) db.session.commit() - application = self.add_response(self.event1_application_form.id, candidates[0].id) + application = self.add_response( + self.event1_application_form.id, candidates[0].id + ) # Make the request header = self.get_auth_header_for(self.event1_admin.email) - params = {'event_id': self.event1_id} + params = {"event_id": self.event1_id} + + response = self.app.get("/api/v1/userprofilelist", headers=header, data=params) - response = self.app.get('/api/v1/userprofilelist', headers=header, data=params) - # Assert that request succeeds and Invited Guests are returned. self.assertEqual(response.status_code, 200) self.assertEqual(len(response.json), 1) - self.assertEqual(response.json[0]['type'], 'Invited Guest') + self.assertEqual(response.json[0]["type"], "Invited Guest") def test_response_and_candidate(self): """ @@ -656,17 +748,19 @@ def test_response_and_candidate(self): candidates = self.add_n_users(3, organisation_id=self.dummy_org_id) db.session.add_all(candidates) - application = self.add_response(self.event1_application_form.id, candidates[0].id) + application = self.add_response( + self.event1_application_form.id, candidates[0].id + ) # Make the request header = self.get_auth_header_for(self.event1_admin.email) - params = {'event_id': self.event1_id} + params = {"event_id": self.event1_id} - response = self.app.get('/api/v1/userprofilelist', headers=header, data=params) + response = self.app.get("/api/v1/userprofilelist", headers=header, data=params) self.assertEqual(response.status_code, 200) self.assertEqual(len(response.json), 1) - self.assertEqual(response.json[0]['type'], 'Candidate') + self.assertEqual(response.json[0]["type"], "Candidate") def test_response_relevant_event(self): """ @@ -677,13 +771,15 @@ def test_response_relevant_event(self): candidates = self.add_n_users(3, organisation_id=self.dummy_org_id) db.session.add_all(candidates) - application = self.add_response(self.event2_application_form.id, candidates[0].id) + application = self.add_response( + self.event2_application_form.id, candidates[0].id + ) # Make the request header = self.get_auth_header_for(self.event1_admin.email) - params = {'event_id': self.event1_id} # Not event2 as with the application + params = {"event_id": self.event1_id} # Not event2 as with the application - response = self.app.get('/api/v1/userprofilelist', headers=header, data=params) + response = self.app.get("/api/v1/userprofilelist", headers=header, data=params) print(response.json) # Assert that request succeeds as they applied to the same event. self.assertEqual(response.status_code, 200) @@ -699,9 +795,7 @@ def test_invited_different_event(self): db.session.add_all(candidates) invitation = InvitedGuest( - event_id=self.event2_id, - user_id=candidates[0].id, - role='EveryRole' + event_id=self.event2_id, user_id=candidates[0].id, role="EveryRole" ) db.session.add(invitation) @@ -709,9 +803,9 @@ def test_invited_different_event(self): # Make the request header = self.get_auth_header_for(self.event1_admin.email) - params = {'event_id': self.event1_id} # Not event2 as with the application + params = {"event_id": self.event1_id} # Not event2 as with the application - response = self.app.get('/api/v1/userprofilelist', headers=header, data=params) + response = self.app.get("/api/v1/userprofilelist", headers=header, data=params) print(response.json) # Assert that request succeeds as they applied to the same event. self.assertEqual(response.status_code, 200) @@ -720,98 +814,164 @@ def test_invited_different_event(self): class UserProfileApiTest(ApiTestCase): def setup_static_data(self): - self.add_organisation('Deep Learning Indaba', 'blah.png', 'blah_big.png') - self.add_organisation('Deep Learning IndabaX', 'blah.png', 'blah_big.png') - db.session.add(UserCategory('Postdoc')) - db.session.add(Country('South Africa')) + self.add_organisation("Deep Learning Indaba", "blah.png", "blah_big.png") + self.add_organisation("Deep Learning IndabaX", "blah.png", "blah_big.png") + db.session.add(UserCategory("Postdoc")) + db.session.add(Country("South Africa")) db.session.commit() events = [ - self.add_event({'en': 'Indaba'}, {'en': 'Indaba Event'}, datetime.now(), datetime.now(), 'ADAMOPTIM'), - self.add_event({'en': 'Indaba2'}, {'en': 'Indaba Event 2'}, datetime.now(), datetime.now(), 'HACFTET', 2) + self.add_event( + {"en": "Indaba"}, + {"en": "Indaba Event"}, + datetime.now(), + datetime.now(), + "ADAMOPTIM", + ), + self.add_event( + {"en": "Indaba2"}, + {"en": "Indaba Event 2"}, + datetime.now(), + datetime.now(), + "HACFTET", + 2, + ), ] application_forms = [ self.create_application_form(1, True, False), - self.create_application_form(2, False, False) + self.create_application_form(2, False, False), ] db.session.add_all(application_forms) - candidate1 = AppUser(email='c1@c.com', firstname='candidate', lastname='1', user_title='Mr',password= 'abc', organisation_id=1) - candidate2 = AppUser(email='c2@c.com', firstname='candidate', lastname='2', user_title='Ms', password='abc', organisation_id=1) - system_admin = AppUser(email='system_admin@sa.com', firstname='system_admin', lastname='1', user_title='Mr',password='abc', organisation_id=1, is_admin=True) - event_admin = AppUser(email='event_admin@ea.com', firstname='event_admin', lastname='1', user_title='Ms', password='abc',organisation_id=1) - reviewer = AppUser(email='reviewer@r.com', firstname='reviewer', lastname='1', user_title='Ms', password='abc', organisation_id=1) + candidate1 = AppUser( + email="c1@c.com", + firstname="candidate", + lastname="1", + user_title="Mr", + password="abc", + organisation_id=1, + ) + candidate2 = AppUser( + email="c2@c.com", + firstname="candidate", + lastname="2", + user_title="Ms", + password="abc", + organisation_id=1, + ) + system_admin = AppUser( + email="system_admin@sa.com", + firstname="system_admin", + lastname="1", + user_title="Mr", + password="abc", + organisation_id=1, + is_admin=True, + ) + event_admin = AppUser( + email="event_admin@ea.com", + firstname="event_admin", + lastname="1", + user_title="Ms", + password="abc", + organisation_id=1, + ) + reviewer = AppUser( + email="reviewer@r.com", + firstname="reviewer", + lastname="1", + user_title="Ms", + password="abc", + organisation_id=1, + ) users = [candidate1, candidate2, system_admin, event_admin, reviewer] for user in users: user.verify() db.session.add_all(users) - event_roles = [ - EventRole('admin', 4, 1), - EventRole('reviwer', 5, 1) - ] + event_roles = [EventRole("admin", 4, 1), EventRole("reviwer", 5, 1)] db.session.add_all(event_roles) db.session.commit() self.add_response(1, 1, is_submitted=True) self.add_response(2, 2, is_submitted=True) - + def get_auth_header_for(self, email): - body = { - 'email': email, - 'password': 'abc' - } - response = self.app.post('api/v1/authenticate', data=body) + body = {"email": email, "password": "abc"} + response = self.app.post("api/v1/authenticate", data=body) data = json.loads(response.data) - header = {'Authorization': data['token']} + header = {"Authorization": data["token"]} return header def test_user_not_found_when_not_event_admin(self): self.setup_static_data() - header = self.get_auth_header_for('reviewer@r.com') - params = {'user_id': 1} + header = self.get_auth_header_for("reviewer@r.com") + params = {"user_id": 1} - response = self.app.get('/api/v1/userprofile', headers=header, data=params) + response = self.app.get("/api/v1/userprofile", headers=header, data=params) self.assertEqual(response.status_code, 404) - + def test_user_not_found_when_does_not_exist(self): self.setup_static_data() - header = self.get_auth_header_for('system_admin@sa.com') - params = {'user_id': 99} + header = self.get_auth_header_for("system_admin@sa.com") + params = {"user_id": 99} - response = self.app.get('/api/v1/userprofile', headers=header, data=params) + response = self.app.get("/api/v1/userprofile", headers=header, data=params) self.assertEqual(response.status_code, 404) def test_get_user_when_system_admin(self): self.setup_static_data() - header = self.get_auth_header_for('system_admin@sa.com') - params = {'user_id': 2} + header = self.get_auth_header_for("system_admin@sa.com") + params = {"user_id": 2} - response = self.app.get('/api/v1/userprofile', headers=header, data=params) + response = self.app.get("/api/v1/userprofile", headers=header, data=params) data = json.loads(response.data) - self.assertEqual(data['email'], 'c2@c.com') + self.assertEqual(data["email"], "c2@c.com") def test_get_user_when_event_admin(self): self.setup_static_data() - header = self.get_auth_header_for('event_admin@ea.com') - params = {'user_id': 1} + header = self.get_auth_header_for("event_admin@ea.com") + params = {"user_id": 1} - response = self.app.get('/api/v1/userprofile', headers=header, data=params) + response = self.app.get("/api/v1/userprofile", headers=header, data=params) data = json.loads(response.data) - self.assertEqual(data['email'], 'c1@c.com') + self.assertEqual(data["email"], "c1@c.com") + class EmailerAPITest(ApiTestCase): def setup_static_data(self): - db.session.add(UserCategory('Postdoc')) - db.session.add(Country('South Africa')) - - self.candidate1 = AppUser(email='c1@c.com', firstname='candidate', lastname='1', user_title='Mr', password='abc', organisation_id=1) - self.candidate2 = AppUser(email='c2@c.com', firstname='candidate', lastname='2', user_title='Ms', password='abc', organisation_id=1) - self.system_admin = AppUser(email='system_admin@sa.com', firstname='system_admin', lastname='1', user_title='Mr', password='abc', organisation_id=1,is_admin=True) + db.session.add(UserCategory("Postdoc")) + db.session.add(Country("South Africa")) + + self.candidate1 = AppUser( + email="c1@c.com", + firstname="candidate", + lastname="1", + user_title="Mr", + password="abc", + organisation_id=1, + ) + self.candidate2 = AppUser( + email="c2@c.com", + firstname="candidate", + lastname="2", + user_title="Ms", + password="abc", + organisation_id=1, + ) + self.system_admin = AppUser( + email="system_admin@sa.com", + firstname="system_admin", + lastname="1", + user_title="Mr", + password="abc", + organisation_id=1, + is_admin=True, + ) users = [self.candidate1, self.candidate2, self.system_admin] for user in users: @@ -819,153 +979,190 @@ def setup_static_data(self): db.session.add_all(users) db.session.commit() - + def get_auth_header_for(self, email): - body = { - 'email': email, - 'password': 'abc' - } - response = self.app.post('api/v1/authenticate', data=body) + body = {"email": email, "password": "abc"} + response = self.app.post("api/v1/authenticate", data=body) data = json.loads(response.data) - header = {'Authorization': data['token']} + header = {"Authorization": data["token"]} return header def test_forbidden_when_not_admin(self): with app.app_context(): self.setup_static_data() - header = self.get_auth_header_for('c1@c.com') + header = self.get_auth_header_for("c1@c.com") params = { - 'user_id': self.candidate2.id, - 'email_subject': 'This is a test email', - 'email_body': 'Hello world, this is a test email.' + "user_id": self.candidate2.id, + "email_subject": "This is a test email", + "email_body": "Hello world, this is a test email.", } - response = self.app.post('/api/v1/admin/emailer', headers=header, data=params) + response = self.app.post( + "/api/v1/admin/emailer", headers=header, data=params + ) self.assertEqual(response.status_code, 403) - + def test_email(self): with app.app_context(): self.setup_static_data() - header = self.get_auth_header_for('system_admin@sa.com') + header = self.get_auth_header_for("system_admin@sa.com") params = { - 'user_id': self.candidate2.id, - 'email_subject': 'This is a test email', - 'email_body': 'Hello world, this is a test email.' + "user_id": self.candidate2.id, + "email_subject": "This is a test email", + "email_body": "Hello world, this is a test email.", } - response = self.app.post('/api/v1/admin/emailer', headers=header, data=params) + response = self.app.post( + "/api/v1/admin/emailer", headers=header, data=params + ) self.assertEqual(response.status_code, 200) + class OrganisationUserTest(ApiTestCase): """Test that users are correctly linked to organisations""" + def seed_static_data(self): - db.session.add(UserCategory('Postdoc')) - db.session.add(Country('South Africa')) - self.add_organisation('Deep Learning Indaba', 'blah.png', 'blah_big.png', domain='indaba') + db.session.add(UserCategory("Postdoc")) + db.session.add(Country("South Africa")) + self.add_organisation( + "Deep Learning Indaba", "blah.png", "blah_big.png", domain="indaba" + ) - self.user1_org1 = AppUser('first_user@c.com', 'candidate', '1', 'Mr', password="abc",organisation_id=1) - self.user1_org2 = AppUser('first_user@c.com', 'candidate', '2', 'Ms', password="abc",organisation_id=2) - self.user2_org1 = AppUser('second_user@c.com', 'system_admin', '1', 'Mr', password="abc",organisation_id=1, is_admin=True) + self.user1_org1 = AppUser( + "first_user@c.com", + "candidate", + "1", + "Mr", + password="abc", + organisation_id=1, + ) + self.user1_org2 = AppUser( + "first_user@c.com", + "candidate", + "2", + "Ms", + password="abc", + organisation_id=2, + ) + self.user2_org1 = AppUser( + "second_user@c.com", + "system_admin", + "1", + "Mr", + password="abc", + organisation_id=1, + is_admin=True, + ) users = [self.user1_org1, self.user1_org2, self.user2_org1] for user in users: user.verify() db.session.add_all(users) - self.add_email_template('verify-email') - self.add_email_template('password-reset') + self.add_email_template("verify-email") + self.add_email_template("password-reset") db.session.commit() def get_auth_header_for(self, email, domain): - body = { - 'email': email, - 'password': 'abc' - } + body = {"email": email, "password": "abc"} - response = self.app.post('api/v1/authenticate', data=body, headers={'Origin': domain}) + response = self.app.post( + "api/v1/authenticate", data=body, headers={"Origin": domain} + ) data = json.loads(response.data) - header = { - 'Authorization': data['token'], - 'Origin': domain - } + header = {"Authorization": data["token"], "Origin": domain} return header def test_auth_org(self): """Test that authentication is linked to organisation.""" with app.app_context(): self.seed_static_data() - headers = self.get_auth_header_for('first_user@c.com', 'www.indaba.net/blah') - response = self.app.get('/api/v1/user', headers=headers) + headers = self.get_auth_header_for( + "first_user@c.com", "www.indaba.net/blah" + ) + response = self.app.get("/api/v1/user", headers=headers) data = json.loads(response.data) - self.assertEqual(data['firstname'], 'candidate') - self.assertEqual(data['lastname'], '2') + self.assertEqual(data["firstname"], "candidate") + self.assertEqual(data["lastname"], "2") - headers = self.get_auth_header_for('first_user@c.com', 'www.org.net/blah') - response = self.app.get('/api/v1/user', headers=headers) + headers = self.get_auth_header_for("first_user@c.com", "www.org.net/blah") + response = self.app.get("/api/v1/user", headers=headers) data = json.loads(response.data) - self.assertEqual(data['firstname'], 'candidate') - self.assertEqual(data['lastname'], '1') + self.assertEqual(data["firstname"], "candidate") + self.assertEqual(data["lastname"], "1") def test_cant_auth_outside_org(self): with app.app_context(): self.seed_static_data() - body = { - 'email': 'second_user@c.com', - 'password': 'abc' - } + body = {"email": "second_user@c.com", "password": "abc"} - response = self.app.post('api/v1/authenticate', data=body, headers={'Origin': 'www.indaba.net/blah'}) + response = self.app.post( + "api/v1/authenticate", + data=body, + headers={"Origin": "www.indaba.net/blah"}, + ) self.assertEqual(response.status_code, 401) def allow_same_email_different_org(self): - db.session.add(UserCategory('Postdoc')) - db.session.add(Country('South Africa')) - self.add_organisation('Deep Learning Indaba', 'blah.png', 'blah_big.png') + db.session.add(UserCategory("Postdoc")) + db.session.add(Country("South Africa")) + self.add_organisation("Deep Learning Indaba", "blah.png", "blah_big.png") - response = self.app.post('/api/v1/user', data=USER_DATA, headers={'Origin': 'www.myorg.net'}) + response = self.app.post( + "/api/v1/user", data=USER_DATA, headers={"Origin": "www.myorg.net"} + ) self.assertEqual(response.status_code, 201) - response = self.app.post('/api/v1/user', data=USER_DATA, headers={'Origin': 'www.indaba.net'}) + response = self.app.post( + "/api/v1/user", data=USER_DATA, headers={"Origin": "www.indaba.net"} + ) self.assertEqual(response.status_code, 201) - response = self.app.post('/api/v1/user', data=USER_DATA, headers={'Origin': 'www.indaba.net'}) + response = self.app.post( + "/api/v1/user", data=USER_DATA, headers={"Origin": "www.indaba.net"} + ) self.assertEqual(response.status_code, 409) class PrivacyPolicyApiTest(ApiTestCase): - def test_policy_agreed(self): user = self.add_user() user.policy_agreed_datetime = None db.session.commit() - headers = self.get_auth_header_for('user@user.com') - response = self.app.put('/api/v1/privacypolicy', headers=headers, data={'policy_agreed': True}) + headers = self.get_auth_header_for("user@user.com") + response = self.app.put( + "/api/v1/privacypolicy", headers=headers, data={"policy_agreed": True} + ) self.assertEqual(response.status_code, 200) def test_policy_already_agreed(self): user = self.add_user() - headers = self.get_auth_header_for('user@user.com') - response = self.app.put('/api/v1/privacypolicy', headers=headers, data={'policy_agreed': True}) + headers = self.get_auth_header_for("user@user.com") + response = self.app.put( + "/api/v1/privacypolicy", headers=headers, data={"policy_agreed": True} + ) data = json.loads(response.data) self.assertEqual(response.status_code, 400) - self.assertEqual(data['message'], POLICY_ALREADY_AGREED[0]['message']) - + self.assertEqual(data["message"], POLICY_ALREADY_AGREED[0]["message"]) + def test_policy_not_agreed(self): user = self.add_user() - headers = self.get_auth_header_for('user@user.com') - response = self.app.put('/api/v1/privacypolicy', headers=headers, data={'policy_agreed': False}) + headers = self.get_auth_header_for("user@user.com") + response = self.app.put( + "/api/v1/privacypolicy", headers=headers, data={"policy_agreed": False} + ) data = json.loads(response.data) self.assertEqual(response.status_code, 400) - self.assertEqual(data['message'], POLICY_NOT_AGREED[0]['message']) + self.assertEqual(data["message"], POLICY_NOT_AGREED[0]["message"]) class EventAttendeeAPITest(ApiTestCase): @@ -975,22 +1172,24 @@ def seed_static_data(self): self.app_form = self.create_application_form() self.response = self.add_response(self.app_form.id, self.user1.id, True, False) - self.add_email_template('verify-email') - self.add_email_template('password-reset') - + self.add_email_template("verify-email") + self.add_email_template("password-reset") + def test_auth_required(self): """Check that response is 401 when no auth token provided.""" self.seed_static_data() - response = self.app.post('/api/v1/validate-user-event-attendee', data={'event_id': 1}) + response = self.app.post( + "/api/v1/validate-user-event-attendee", data={"event_id": 1} + ) self.assertEqual(response.status_code, 401) def test_non_attendee(self): """Check that response is 401 when user is not an attendee.""" self.seed_static_data() response = self.app.post( - '/api/v1/validate-user-event-attendee', + "/api/v1/validate-user-event-attendee", headers=self.get_auth_header_for(self.user1.email), - data={'event_id': 1} + data={"event_id": 1}, ) self.assertEqual(response.status_code, 401) @@ -999,18 +1198,18 @@ def test_non_attendee_with_offer(self): self.seed_static_data() offer = self.add_offer(self.user1.id) response = self.app.post( - '/api/v1/validate-user-event-attendee', - headers=self.get_auth_header_for(self.user1.email), - data={'event_id': 1} + "/api/v1/validate-user-event-attendee", + headers=self.get_auth_header_for(self.user1.email), + data={"event_id": 1}, ) self.assertEqual(response.status_code, 401) offer.candidate_response = False db.session.commit() response = self.app.post( - '/api/v1/validate-user-event-attendee', + "/api/v1/validate-user-event-attendee", headers=self.get_auth_header_for(self.user1.email), - data={'event_id': 1} + data={"event_id": 1}, ) self.assertEqual(response.status_code, 401) @@ -1022,9 +1221,9 @@ def test_attendee_offer(self): db.session.commit() response = self.app.post( - '/api/v1/validate-user-event-attendee', - headers=self.get_auth_header_for(self.user1.email), - data={'event_id': 1} + "/api/v1/validate-user-event-attendee", + headers=self.get_auth_header_for(self.user1.email), + data={"event_id": 1}, ) self.assertEqual(response.status_code, 200) @@ -1033,8 +1232,8 @@ def test_invited_guest(self): self.seed_static_data() self.add_invited_guest(self.user1.id) response = self.app.post( - '/api/v1/validate-user-event-attendee', - headers=self.get_auth_header_for(self.user1.email), - data={'event_id': 1} + "/api/v1/validate-user-event-attendee", + headers=self.get_auth_header_for(self.user1.email), + data={"event_id": 1}, ) self.assertEqual(response.status_code, 200) diff --git a/api/app/utils/auth.py b/api/app/utils/auth.py index d51bc71e4..9975a8a26 100644 --- a/api/app/utils/auth.py +++ b/api/app/utils/auth.py @@ -1,29 +1,26 @@ from functools import wraps -from flask import request, g +from flask import g, request from flask_restful import reqparse +from itsdangerous import BadSignature, SignatureExpired from itsdangerous import TimedJSONWebSignatureSerializer as Serializer -from itsdangerous import SignatureExpired, BadSignature from app import app from app.users.repository import UserRepository as user_repository -from app.utils.errors import UNAUTHORIZED, FORBIDDEN - +from app.utils.errors import FORBIDDEN, UNAUTHORIZED TWO_WEEKS = 1209600 def generate_token(user, expiration=TWO_WEEKS): - s = Serializer(app.config['SECRET_KEY'], expires_in=expiration) - return s.dumps({ - 'id': user.id, - 'email': user.email, - 'is_admin': user.is_admin - }).decode('utf-8') + s = Serializer(app.config["SECRET_KEY"], expires_in=expiration) + return s.dumps( + {"id": user.id, "email": user.email, "is_admin": user.is_admin} + ).decode("utf-8") def verify_token(token): - s = Serializer(app.config['SECRET_KEY']) + s = Serializer(app.config["SECRET_KEY"]) try: data = s.loads(token) except (SignatureExpired, BadSignature): @@ -32,7 +29,7 @@ def verify_token(token): def get_user_from_request(): - token = request.headers.get('Authorization', '') + token = request.headers.get("Authorization", "") if token: user = verify_token(token) return user @@ -47,6 +44,7 @@ def wrapper(*args, **kwargs): g.current_user = user return func(*args, **kwargs) return UNAUTHORIZED + return wrapper @@ -58,6 +56,7 @@ def wrapper(*args, **kwargs): g.current_user = user return func(*args, **kwargs) + return wrapper @@ -65,10 +64,11 @@ def admin_required(func): @wraps(func) def wrapper(*args, **kwargs): user = get_user_from_request() - if user and user['is_admin']: + if user and user["is_admin"]: g.current_user = user return func(*args, **kwargs) return FORBIDDEN + return wrapper @@ -76,16 +76,16 @@ def event_admin_required(func): @wraps(func) def wrapper(*args, **kwargs): req_parser = reqparse.RequestParser() - req_parser.add_argument('event_id', type=int, required=True) + req_parser.add_argument("event_id", type=int, required=True) args = req_parser.parse_args() user = get_user_from_request() if user: - user_info = user_repository.get_by_id(user['id']) - if user_info.is_event_admin(args['event_id']): + user_info = user_repository.get_by_id(user["id"]) + if user_info.is_event_admin(args["event_id"]): g.current_user = user - return func(*args, event_id=args['event_id'], **kwargs) - + return func(*args, event_id=args["event_id"], **kwargs) + return FORBIDDEN - return wrapper \ No newline at end of file + return wrapper diff --git a/api/app/utils/emailer.py b/api/app/utils/emailer.py index f9d9d14a9..9437f1246 100644 --- a/api/app/utils/emailer.py +++ b/api/app/utils/emailer.py @@ -1,59 +1,98 @@ -import traceback -from app import LOGGER -from config import SMTP_USERNAME, SMTP_PASSWORD, SMTP_SENDER_NAME, SMTP_SENDER_EMAIL, SMTP_HOST, SMTP_PORT, DEBUG -import smtplib import email.utils +import smtplib +import traceback +from email import encoders +from email.mime.base import MIMEBase from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText -from email.mime.base import MIMEBase -from email import encoders + from flask import g, request + +from app import LOGGER from app.email_template.repository import EmailRepository as email_repository -from app.users.repository import UserRepository as user_repository from app.events.repository import EventRepository as event_repository +from app.users.repository import UserRepository as user_repository +from config import ( + DEBUG, + SMTP_HOST, + SMTP_PASSWORD, + SMTP_PORT, + SMTP_SENDER_EMAIL, + SMTP_SENDER_NAME, + SMTP_USERNAME, +) + def email_user( - email_template_key, - user, - template_parameters=None, + email_template_key, + user, + template_parameters=None, event=None, - subject_parameters=None, - file_name='', - file_path='' + subject_parameters=None, + file_name="", + file_path="", ): """Send an email to a specified user using an email template. Handles resolving the correct language.""" if user is None: - raise ValueError('You must specify a user!') + raise ValueError("You must specify a user!") language = user.user_primaryLanguage - email_template = email_repository.get(None if event is None else event.id, email_template_key, language) + email_template = email_repository.get( + None if event is None else event.id, email_template_key, language + ) if email_template is None: - raise ValueError('Could not find email template with key {}'.format(email_template_key)) - + raise ValueError( + "Could not find email template with key {}".format(email_template_key) + ) + subject_parameters = subject_parameters or {} - if event is not None and 'event_name' not in subject_parameters: - subject_parameters['event_name'] = event.get_name(language) if event.has_specific_translation(language) else event.get_name('en') + if event is not None and "event_name" not in subject_parameters: + subject_parameters["event_name"] = ( + event.get_name(language) + if event.has_specific_translation(language) + else event.get_name("en") + ) subject = email_template.subject.format(**subject_parameters) template_parameters = template_parameters or {} - if 'title' not in template_parameters: - template_parameters['title'] = user.user_title - if 'firstname' not in template_parameters: - template_parameters['firstname'] = user.firstname - if 'lastname' not in template_parameters: - template_parameters['lastname'] = user.lastname - if event is not None and 'event_name' not in template_parameters: - template_parameters['event_name'] = event.get_name(language) if event.has_specific_translation(language) else event.get_name('en') + if "title" not in template_parameters: + template_parameters["title"] = user.user_title + if "firstname" not in template_parameters: + template_parameters["firstname"] = user.firstname + if "lastname" not in template_parameters: + template_parameters["lastname"] = user.lastname + if event is not None and "event_name" not in template_parameters: + template_parameters["event_name"] = ( + event.get_name(language) + if event.has_specific_translation(language) + else event.get_name("en") + ) body_text = email_template.template.format(**template_parameters) - send_mail(recipient=user.email, subject=subject, body_text=body_text, file_name=file_name, file_path=file_path) - - -def send_mail(recipient, subject, body_text='', body_html='', charset='UTF-8', mail_type='AMZ', file_name='', - file_path='', sender_name=None, sender_email=None): - '''[summary] + send_mail( + recipient=user.email, + subject=subject, + body_text=body_text, + file_name=file_name, + file_path=file_path, + ) + + +def send_mail( + recipient, + subject, + body_text="", + body_html="", + charset="UTF-8", + mail_type="AMZ", + file_name="", + file_path="", + sender_name=None, + sender_email=None, +): + """[summary] Arguments: recipient {[type]} -- [description] @@ -66,30 +105,31 @@ def send_mail(recipient, subject, body_text='', body_html='', charset='UTF-8', m Raises: e -- [description] - ''' + """ sender_name = sender_name or g.organisation.name sender_email = sender_email or g.organisation.email_from - if (not DEBUG): - if mail_type == 'AMZ': + if not DEBUG: + if mail_type == "AMZ": try: msg = MIMEMultipart() - msg['Subject'] = subject - msg['From'] = email.utils.formataddr( - (sender_name, sender_email)) - msg['To'] = recipient + msg["Subject"] = subject + msg["From"] = email.utils.formataddr((sender_name, sender_email)) + msg["To"] = recipient - body_part1 = MIMEText(body_text, 'plain', _charset=charset) - body_part2 = MIMEText(body_html, 'html', _charset=charset) + body_part1 = MIMEText(body_text, "plain", _charset=charset) + body_part2 = MIMEText(body_html, "html", _charset=charset) if file_name != "" and file_path != "": attachment = open(file_path, "rb") - part = MIMEBase('application', 'octet-stream') + part = MIMEBase("application", "octet-stream") part.set_payload(attachment.read()) encoders.encode_base64(part) - part.add_header('Content-Disposition', "attachment; filename= %s" % file_name) + part.add_header( + "Content-Disposition", "attachment; filename= %s" % file_name + ) msg.attach(part) msg.attach(body_part1) @@ -103,13 +143,17 @@ def send_mail(recipient, subject, body_text='', body_html='', charset='UTF-8', m server.sendmail(sender_email, recipient, msg.as_string()) server.close() except Exception as e: - LOGGER.error("Exception {} while trying to send email: {}".format(e, traceback.format_exc())) + LOGGER.error( + "Exception {} while trying to send email: {}".format( + e, traceback.format_exc() + ) + ) raise e else: - LOGGER.debug('Sender Name: {sender_name}'.format(sender_name=sender_name)) - LOGGER.debug('Sender Email: {sender_email}'.format(sender_email=sender_email)) - LOGGER.debug('Recipient : {recipient}'.format(recipient=recipient)) - LOGGER.debug('Subject : {subject}'.format(subject=subject)) - LOGGER.debug('Body Text : {body}'.format(body=body_text)) - LOGGER.debug('Body HTML : {body}'.format(body=body_html)) + LOGGER.debug("Sender Name: {sender_name}".format(sender_name=sender_name)) + LOGGER.debug("Sender Email: {sender_email}".format(sender_email=sender_email)) + LOGGER.debug("Recipient : {recipient}".format(recipient=recipient)) + LOGGER.debug("Subject : {subject}".format(subject=subject)) + LOGGER.debug("Body Text : {body}".format(body=body_text)) + LOGGER.debug("Body HTML : {body}".format(body=body_html)) diff --git a/api/app/utils/errors.py b/api/app/utils/errors.py index 02c3e9ad0..ba45185f8 100644 --- a/api/app/utils/errors.py +++ b/api/app/utils/errors.py @@ -1,128 +1,202 @@ # Define custom error messages here -EMAIL_IN_USE = ({'message': 'User with that email already exists'}, 409) +EMAIL_IN_USE = ({"message": "User with that email already exists"}, 409) UNAUTHORIZED = ( - {'message': 'Authentication is required to access this resource', 'type': 'UNAUTHORIZED'}, 401) + { + "message": "Authentication is required to access this resource", + "type": "UNAUTHORIZED", + }, + 401, +) BAD_CREDENTIALS = ( - {'message': 'Incorrect username or password', 'type': 'BAD_CREDENTIALS'}, 401) -FORBIDDEN = ({'message': 'Access to this resource is forbidden'}, 403) + {"message": "Incorrect username or password", "type": "BAD_CREDENTIALS"}, + 401, +) +FORBIDDEN = ({"message": "Access to this resource is forbidden"}, 403) RESET_PASSWORD_CODE_NOT_VALID = ( - {'message': 'Valid code is required to reset a password'}, 418) -TOO_MANY_REQUESTS = ({'message': 'Too many requests'}, 429) -EVENT_NOT_FOUND = ({'message': 'No event exists with that ID'}, 404) -EVENT_WITH_KEY_NOT_FOUND = ({'message': 'No event exists with that KEY'}, 404) -EVENT_WITH_TRANSLATION_NOT_FOUND = ({'message': 'Translation for event not found'}, 404) -EVENT_MUST_CONTAIN_TRANSLATION = ({'message': "Event must contain at least one translation for 'name' and 'description'"}, 400) -EVENT_TRANSLATION_MISMATCH = ({'message': "Event must contain same translations for 'name' and 'description'"}, 400) -REFRERENCE_REQUEST_WITH_TOKEN_NOT_FOUND = ({'message': 'No Reference Request exists with that Token'}, 404) -DUPLICATE_REFERENCE_SUBMISSION = ({'message': 'Reference Already submitted for this Request '}, 409) -EVENT_KEY_IN_USE = ({'message': 'Event with that KEY already exists'}, 409) -SECTION_NOT_FOUND = ({'message': 'No section exists with the given ID'}, 404) -QUESTION_NOT_FOUND = ( - {'message': 'No question exists with that ID'}, 404) -FORM_NOT_FOUND = ({'message': 'No form exists with that Event ID'}, 404) -FORM_NOT_FOUND_BY_ID = ({'message': 'No application form exists with that Application Form ID'}, 404) -APPLICATION_FORM_EXISTS = ({'message': 'An application form exists with that event ID'}, 403) + {"message": "Valid code is required to reset a password"}, + 418, +) +TOO_MANY_REQUESTS = ({"message": "Too many requests"}, 429) +EVENT_NOT_FOUND = ({"message": "No event exists with that ID"}, 404) +EVENT_WITH_KEY_NOT_FOUND = ({"message": "No event exists with that KEY"}, 404) +EVENT_WITH_TRANSLATION_NOT_FOUND = ({"message": "Translation for event not found"}, 404) +EVENT_MUST_CONTAIN_TRANSLATION = ( + { + "message": "Event must contain at least one translation for 'name' and 'description'" + }, + 400, +) +EVENT_TRANSLATION_MISMATCH = ( + {"message": "Event must contain same translations for 'name' and 'description'"}, + 400, +) +REFRERENCE_REQUEST_WITH_TOKEN_NOT_FOUND = ( + {"message": "No Reference Request exists with that Token"}, + 404, +) +DUPLICATE_REFERENCE_SUBMISSION = ( + {"message": "Reference Already submitted for this Request "}, + 409, +) +EVENT_KEY_IN_USE = ({"message": "Event with that KEY already exists"}, 409) +SECTION_NOT_FOUND = ({"message": "No section exists with the given ID"}, 404) +QUESTION_NOT_FOUND = ({"message": "No question exists with that ID"}, 404) +FORM_NOT_FOUND = ({"message": "No form exists with that Event ID"}, 404) +FORM_NOT_FOUND_BY_ID = ( + {"message": "No application form exists with that Application Form ID"}, + 404, +) +APPLICATION_FORM_EXISTS = ( + {"message": "An application form exists with that event ID"}, + 403, +) RESPONSE_NOT_FOUND = ( - {'message': 'No response found for the given event and user'}, 404) -RESPONSE_ALREADY_SUBMITTED = ({'message': 'A response has already been submitted'}, 400) + {"message": "No response found for the given event and user"}, + 404, +) +RESPONSE_ALREADY_SUBMITTED = ({"message": "A response has already been submitted"}, 400) UPDATE_CONFLICT = ( - {'message': 'The requested update conflicts with the existing resource'}, 409) -DB_NOT_AVAILABLE = ({'message': 'Unable to access the database'}, 500) -EMAIL_NOT_VERIFIED = ({'message': 'The email address is not verified'}, 422) + {"message": "The requested update conflicts with the existing resource"}, + 409, +) +DB_NOT_AVAILABLE = ({"message": "Unable to access the database"}, 500) +EMAIL_NOT_VERIFIED = ({"message": "The email address is not verified"}, 422) EMAIL_VERIFY_CODE_NOT_VALID = ( - {'message': 'Valid code is required to verify email'}, 419) -USER_NOT_FOUND = ({'message': 'No user exists with that email'}, 404) + {"message": "Valid code is required to verify email"}, + 419, +) +USER_NOT_FOUND = ({"message": "No user exists with that email"}, 404) RESET_PASSWORD_CODE_EXPIRED = ( - {'message': 'The password reset request has expired'}, 400) -FILE_SIZE_EXCEEDED = ({'message': 'File size exceeded'}, 400) -USER_DELETED = ({'message': 'This account has been deleted'}, 404) -REVIEW_RESPONSE_NOT_FOUND = ({'message': 'No review response found.'}, 404) -ADD_VERIFY_TOKEN_FAILED = ( - {'message': 'Unable to add verification token.'}, 500) -ADD_INVITED_GUEST_FAILED = ( - {'message': 'Unable to add invited guest.'}, 500) + {"message": "The password reset request has expired"}, + 400, +) +FILE_SIZE_EXCEEDED = ({"message": "File size exceeded"}, 400) +USER_DELETED = ({"message": "This account has been deleted"}, 404) +REVIEW_RESPONSE_NOT_FOUND = ({"message": "No review response found."}, 404) +ADD_VERIFY_TOKEN_FAILED = ({"message": "Unable to add verification token."}, 500) +ADD_INVITED_GUEST_FAILED = ({"message": "Unable to add invited guest."}, 500) INVITED_GUEST_FOR_EVENT_EXISTS = ( - {'message': 'Invited guest already exists for this event.'}, 409) + {"message": "Invited guest already exists for this event."}, + 409, +) VERIFY_EMAIL_INVITED_GUEST = ( - {'message': 'Unable to verify email of invited guest.'}, 500) + {"message": "Unable to verify email of invited guest."}, + 500, +) -VERIFY_EMAIL_OFFER = ( - {'message': 'Unable to verify email of an offer.'}, 500) +VERIFY_EMAIL_OFFER = ({"message": "Unable to verify email of an offer."}, 500) MISSING_PASSWORD = ( - {'message': 'Password not provided', 'type': 'MISSING_CREDENTIALS'}, 400) -OFFER_EXPIRED = ({'message': 'Your offer has expired'}, 403) -ADD_OFFER_FAILED = ( - {'message': 'Unable to add an offer.'}, 500) -OFFER_NOT_FOUND = ( - {'message': 'No offer found for the given id'}, 404) + {"message": "Password not provided", "type": "MISSING_CREDENTIALS"}, + 400, +) +OFFER_EXPIRED = ({"message": "Your offer has expired"}, 403) +ADD_OFFER_FAILED = ({"message": "Unable to add an offer."}, 500) +OFFER_NOT_FOUND = ({"message": "No offer found for the given id"}, 404) REGISTRATION_FORM_NOT_FOUND = ( - {'message': 'No registration form found for the given event and offer'}, 404) + {"message": "No registration form found for the given event and offer"}, + 404, +) REGISTRATION_SECTION_NOT_FOUND = ( - {'message': 'No registration section found for the given id'}, 404) + {"message": "No registration section found for the given id"}, + 404, +) REGISTRATION_QUESTION_NOT_FOUND = ( - {'message': 'No registration question found for the given id'}, 404) -ADD_REGISTRATION_FORM_FAILED = ( - {'message': 'Unable to add registration form.'}, 500) + {"message": "No registration question found for the given id"}, + 404, +) +ADD_REGISTRATION_FORM_FAILED = ({"message": "Unable to add registration form."}, 500) ADD_REGISTRATION_SECTION_FAILED = ( - {'message': 'Unable to add registration section.'}, 500) + {"message": "Unable to add registration section."}, + 500, +) ADD_REGISTRATION_QUESTION_FAILED = ( - {'message': 'Unable to add registration question.'}, 500) + {"message": "Unable to add registration question."}, + 500, +) ADD_INVITATION_REQUEST_FAILED = ( - {'message': 'Unable to add invitation letter request.'}, 500) -TEMPLATE_NOT_FOUND = ( - {'message': 'No template found for the given parameters'}, 404) -OFFER_NOT_ACCEPTED = ( - {'message': 'Offer has not been accepted'}, 409) -APPLICATIONS_CLOSED = ( - {'message': 'Applications are now closed'}, 403) + {"message": "Unable to add invitation letter request."}, + 500, +) +TEMPLATE_NOT_FOUND = ({"message": "No template found for the given parameters"}, 404) +OFFER_NOT_ACCEPTED = ({"message": "Offer has not been accepted"}, 409) +APPLICATIONS_CLOSED = ({"message": "Applications are now closed"}, 403) DUPLICATE_OFFER = ( - {'message': 'An offer already exists for the user_id and event_id'}, 409) -CREATING_INVITATION_FAILED = ( - {'message': 'Invitation Letter creation failed'}, 502) -SENDING_INVITATION_FAILED = ( - {'message': 'Invitation Letter failed to send'}, 502) -EVENT_ID_NOT_FOUND = ( - {'message': 'Event ID not found.'}, 404) + {"message": "An offer already exists for the user_id and event_id"}, + 409, +) +CREATING_INVITATION_FAILED = ({"message": "Invitation Letter creation failed"}, 502) +SENDING_INVITATION_FAILED = ({"message": "Invitation Letter failed to send"}, 502) +EVENT_ID_NOT_FOUND = ({"message": "Event ID not found."}, 404) MISSING_DATE_OF_BIRTH = ( - {'message': 'Missing date of birth. Please update your user profile.'}, 400) + {"message": "Missing date of birth. Please update your user profile."}, + 400, +) REGISTRATION_NOT_FOUND = ( - {'message': 'Registration not found. Please register first.'}, 404) -EMAIL_NOT_SENT = ( - {'message': 'Email failed to send'}, 500) + {"message": "Registration not found. Please register first."}, + 404, +) +EMAIL_NOT_SENT = ({"message": "Email failed to send"}, 500) INVITED_GUEST_EMAIL_FAILED = ( - {'message': 'The invited guest was added added to the database, but the email failed to send. You may want to contact them manually.'}, 500) + { + "message": "The invited guest was added added to the database, but the email failed to send. You may want to contact them manually." + }, + 500, +) ATTENDANCE_ALREADY_CONFIRMED = ( - {'message': 'Attendance has already been confirmed for this user and event.'}, 400) -ATTENDANCE_NOT_FOUND = ( - {'message': 'Attendance not found.'}, 404) -ERROR_UPDATING_USER_PROFILE = ( - {'message': 'Exception updating user profile.'}, 500) + {"message": "Attendance has already been confirmed for this user and event."}, + 400, +) +ATTENDANCE_NOT_FOUND = ({"message": "Attendance not found."}, 404) +ERROR_UPDATING_USER_PROFILE = ({"message": "Exception updating user profile."}, 500) ADD_EVENT_ROLE_FAILED = ( - {'message': 'Unable to add event role for the user_id and event_id.'}, 500) + {"message": "Unable to add event role for the user_id and event_id."}, + 500, +) POLICY_NOT_AGREED = ( - {'message': 'Privacy policy must be agreed to before continuing.'}, 400) -POLICY_ALREADY_AGREED = ( - {'message': 'Privacy policy has already been agreed to.'}, 400) + {"message": "Privacy policy must be agreed to before continuing."}, + 400, +) +POLICY_ALREADY_AGREED = ({"message": "Privacy policy has already been agreed to."}, 400) REFERENCE_REQUEST_NOT_FOUND = ( - {'message': 'No response found for the given event and user'}, 404) -OUTCOME_NOT_FOUND = ( - {'message': 'No outcome found for the given event'}, 404) -OUTCOME_STATUS_NOT_VALID = ( - {'message': 'Invalid outcome status specified'}, 400) + {"message": "No response found for the given event and user"}, + 404, +) +OUTCOME_NOT_FOUND = ({"message": "No outcome found for the given event"}, 404) +OUTCOME_STATUS_NOT_VALID = ({"message": "Invalid outcome status specified"}, 400) CANDIDATE_REJECTED = ( - {'message': 'The candidate has already been rejected for the event'}, 400) + {"message": "The candidate has already been rejected for the event"}, + 400, +) FAILED_CREATE_INTEGRATION_TEST_USER = ( - {'message': 'Failed to create integration test user.'}, 500) + {"message": "Failed to create integration test user."}, + 500, +) FAILED_DELETE_INTEGRATION_TEST_USER = ( - {'message': 'Failed to delete integration test user'}, 500) -DUPLICATE_RESPONSE = ({'message': 'A response has already been submitted for this application form'}, 409) -BAD_CONFIGURATION = ({'message': 'There is an error with the form configuration'}, 500) -TAG_NOT_FOUND = ( - {'message': 'No tag found with the given id'}, 404) -REVIEW_FORM_NOT_FOUND = ({'message': "No review form found for the event"}, 404) -REVIEW_ALREADY_COMPLETED = ({'message': "Can't delete reviewer, the review has already been completed"}, 400) -NO_ACTIVE_REVIEW_FORM = ({'message': "There is no active review form for the event"}, 404) -REVIEW_FORM_FOR_STAGE_NOT_FOUND = ({'message': "There is no review form for the given stage"}, 404) -REVIEW_FORM_EXISTS = ({'message': "A review form for this event and stage already exists"}, 403) + {"message": "Failed to delete integration test user"}, + 500, +) +DUPLICATE_RESPONSE = ( + {"message": "A response has already been submitted for this application form"}, + 409, +) +BAD_CONFIGURATION = ({"message": "There is an error with the form configuration"}, 500) +TAG_NOT_FOUND = ({"message": "No tag found with the given id"}, 404) +REVIEW_FORM_NOT_FOUND = ({"message": "No review form found for the event"}, 404) +REVIEW_ALREADY_COMPLETED = ( + {"message": "Can't delete reviewer, the review has already been completed"}, + 400, +) +NO_ACTIVE_REVIEW_FORM = ( + {"message": "There is no active review form for the event"}, + 404, +) +REVIEW_FORM_FOR_STAGE_NOT_FOUND = ( + {"message": "There is no review form for the given stage"}, + 404, +) +REVIEW_FORM_EXISTS = ( + {"message": "A review form for this event and stage already exists"}, + 403, +) diff --git a/api/app/utils/logger.py b/api/app/utils/logger.py index 944be1428..d7f512f29 100644 --- a/api/app/utils/logger.py +++ b/api/app/utils/logger.py @@ -1,45 +1,38 @@ import logging.config LOGGING_APPLICATION_CONF = { - 'version': 1, # required - 'disable_existing_loggers': True, # this config overrides all other loggers - 'formatters': { - 'simple': { - 'format': '%(asctime)s %(levelname)s -- %(message)s' + "version": 1, # required + "disable_existing_loggers": True, # this config overrides all other loggers + "formatters": { + "simple": {"format": "%(asctime)s %(levelname)s -- %(message)s"}, + "sysout": { + "format": "%(asctime)s\t%(levelname)s -- %(filename)s:%(lineno)s -- %(message)s", }, - 'sysout': { - 'format': '%(asctime)s\t%(levelname)s -- %(filename)s:%(lineno)s -- %(message)s', - } }, - 'handlers': { - 'console': { - 'level': 'DEBUG', - 'class': 'logging.StreamHandler', - 'formatter': 'sysout' + "handlers": { + "console": { + "level": "DEBUG", + "class": "logging.StreamHandler", + "formatter": "sysout", } }, - 'loggers': { - 'app': { # 'root' logge - 'level': 'DEBUG', - 'handlers': ['console'] - } - } + "loggers": {"app": {"level": "DEBUG", "handlers": ["console"]}}, # 'root' logge } class Logger: - ''' + """ This class contains methods to use standard loggers - ''' + """ def get_logger(self): - '''This method sets the configuration for the logger + """This method sets the configuration for the logger Returns: [Logger] -- An instance of Logging with the right handlers set - ''' + """ logging.config.dictConfig(LOGGING_APPLICATION_CONF) - logger = logging.getLogger('app') + logger = logging.getLogger("app") handlers = logger.handlers handler = logging.StreamHandler() logger.handlers = [] diff --git a/api/app/utils/misc.py b/api/app/utils/misc.py index c099a1253..f0a197ea6 100644 --- a/api/app/utils/misc.py +++ b/api/app/utils/misc.py @@ -1,4 +1,5 @@ import uuid + from flask import g diff --git a/api/app/utils/pdfconvertor.py b/api/app/utils/pdfconvertor.py index dab3d77b0..7cae4cc74 100644 --- a/api/app/utils/pdfconvertor.py +++ b/api/app/utils/pdfconvertor.py @@ -1,35 +1,44 @@ -import sys -import subprocess -import os import io +import os +import subprocess +import sys import tempfile -from app import LOGGER -from config import GCP_CREDENTIALS_DICT - +from apiclient.discovery import build +from google.oauth2.credentials import Credentials from googleapiclient.discovery import build from googleapiclient.http import MediaIoBaseDownload, MediaIoBaseUpload -from google.oauth2.credentials import Credentials - -from apiclient.discovery import build from oauth2client.service_account import ServiceAccountCredentials +from app import LOGGER +from config import GCP_CREDENTIALS_DICT + def convert_to(folder, source, output): - LOGGER.debug('...beginning conversion to pdf...') - args = [libreoffice_exec(), '--headless', '--convert-to', 'pdf', '--outdir', folder, source] + LOGGER.debug("...beginning conversion to pdf...") + args = [ + libreoffice_exec(), + "--headless", + "--convert-to", + "pdf", + "--outdir", + folder, + source, + ] process = subprocess.call(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) if os.path.exists(output): - LOGGER.debug('Successfully converted to pdf...') + LOGGER.debug("Successfully converted to pdf...") return True - LOGGER.debug('Did not successfully convert to pdf...') + LOGGER.debug("Did not successfully convert to pdf...") return False + def libreoffice_exec(): - if sys.platform == 'linux': - return '/lib/libreoffice/program/soffice' - return 'libreoffice' + if sys.platform == "linux": + return "/lib/libreoffice/program/soffice" + return "libreoffice" + class LibreOfficeError(Exception): def __init__(self, output): @@ -39,41 +48,44 @@ def __init__(self, output): def html_to_pdf( file_name, html_string, - ): +): - LOGGER.debug('Using Google Drive to convert PDF') + LOGGER.debug("Using Google Drive to convert PDF") # If no credentials, return a dummy string for testing. - if GCP_CREDENTIALS_DICT['private_key'] == 'dummy': + if GCP_CREDENTIALS_DICT["private_key"] == "dummy": pdf_bytes = io.BytesIO(b"Hello World, This is not a real PDF") return pdf_bytes credentials = ServiceAccountCredentials.from_json_keyfile_dict(GCP_CREDENTIALS_DICT) - drive_service = build('drive', 'v3', credentials=credentials) + drive_service = build("drive", "v3", credentials=credentials) # Set the upload to be a HTML file media = MediaIoBaseUpload( - io.BytesIO(bytes(html_string, encoding='utf8')), + io.BytesIO(bytes(html_string, encoding="utf8")), mimetype="text/html", - resumable=True + resumable=True, ) # Create the file, and initiate the conversion to a Docs document # This enables the conversion to PDF - file = drive_service.files().create( - body={ - "name": file_name, - "mimeType": "application/vnd.google-apps.document", + file = ( + drive_service.files() + .create( + body={ + "name": file_name, + "mimeType": "application/vnd.google-apps.document", }, - media_body=media, - fields='id' - ).execute() + media_body=media, + fields="id", + ) + .execute() + ) # Download as a PDF file request = drive_service.files().export_media( - fileId=file.get('id'), - mimeType='application/pdf' + fileId=file.get("id"), mimeType="application/pdf" ) buffer = io.BytesIO() @@ -83,6 +95,6 @@ def html_to_pdf( _, done = downloader.next_chunk() # Delete the Docs document afterwards - drive_service.files().delete(fileId=file.get('id')).execute() + drive_service.files().delete(fileId=file.get("id")).execute() - return buffer \ No newline at end of file + return buffer diff --git a/api/app/utils/rate_limit.py b/api/app/utils/rate_limit.py index dd8a36c3c..e0c93cec5 100644 --- a/api/app/utils/rate_limit.py +++ b/api/app/utils/rate_limit.py @@ -1,8 +1,8 @@ from functools import wraps - -from flask import request, g from time import time +from flask import g, request + from app import app, redis from app.utils.errors import TOO_MANY_REQUESTS @@ -24,13 +24,15 @@ def wrapper(*args, **kwargs): redis.expire(key, window) expires_in = window - g.rate_limits = (limit, remaining-1, time()+expires_in) + g.rate_limits = (limit, remaining - 1, time() + expires_in) if remaining > 0: redis.incr(key, 1) return func(*args, **kwargs) return TOO_MANY_REQUESTS + return wrapper + return decorator @@ -41,7 +43,7 @@ def add_rate_limit_headers(response): except (AttributeError, ValueError): return response else: - response.headers.add('X-RateLimit-Remaining', remaining) - response.headers.add('X-RateLimit-Limit', limit) - response.headers.add('X-RateLimit-Reset', expires) + response.headers.add("X-RateLimit-Remaining", remaining) + response.headers.add("X-RateLimit-Limit", limit) + response.headers.add("X-RateLimit-Reset", expires) return response diff --git a/api/app/utils/storage.py b/api/app/utils/storage.py index 5c6af8f48..29e94be92 100644 --- a/api/app/utils/storage.py +++ b/api/app/utils/storage.py @@ -1,29 +1,36 @@ """Utilities for interaction with Google Cloud Storage, and a mock version of it for loal development.""" -from google.cloud import storage -from google.oauth2 import service_account -from google.auth.credentials import AnonymousCredentials -from google.api_core.client_options import ClientOptions -from config import GCP_CREDENTIALS_DICT, GCP_PROJECT_NAME, GCP_BUCKET_NAME +import os import requests import urllib3 -import os +from google.api_core.client_options import ClientOptions +from google.auth.credentials import AnonymousCredentials +from google.cloud import storage +from google.oauth2 import service_account from app import LOGGER +from config import GCP_BUCKET_NAME, GCP_CREDENTIALS_DICT, GCP_PROJECT_NAME + def _create_dummy_storage_client(): fake_host = "storage" - external_url = 'https://{}:4443'.format(fake_host) - storage.blob._API_ACCESS_ENDPOINT = 'https://storage.gcs.{}.nip.io:4443'.format(fake_host) + external_url = "https://{}:4443".format(fake_host) + storage.blob._API_ACCESS_ENDPOINT = "https://storage.gcs.{}.nip.io:4443".format( + fake_host + ) storage.blob._DOWNLOAD_URL_TEMPLATE = ( "%s/download/storage/v1{path}?alt=media" % external_url ) storage.blob._BASE_UPLOAD_TEMPLATE = ( "%s/upload/storage/v1{bucket_path}/o?uploadType=" % external_url ) - storage.blob._MULTIPART_URL_TEMPLATE = storage.blob._BASE_UPLOAD_TEMPLATE + "multipart" - storage.blob._RESUMABLE_URL_TEMPLATE = storage.blob._BASE_UPLOAD_TEMPLATE + "resumable" + storage.blob._MULTIPART_URL_TEMPLATE = ( + storage.blob._BASE_UPLOAD_TEMPLATE + "multipart" + ) + storage.blob._RESUMABLE_URL_TEMPLATE = ( + storage.blob._BASE_UPLOAD_TEMPLATE + "resumable" + ) my_http = requests.Session() my_http.verify = False # disable SSL validation urllib3.disable_warnings( @@ -32,9 +39,10 @@ def _create_dummy_storage_client(): storage_client = storage.Client( credentials=AnonymousCredentials(), - project='test', + project="test", _http=my_http, - client_options=ClientOptions(api_endpoint=external_url)) + client_options=ClientOptions(api_endpoint=external_url), + ) if len(list(storage_client.list_buckets())) == 0: bucket = storage_client.create_bucket(_get_bucket_name()) @@ -43,7 +51,7 @@ def _create_dummy_storage_client(): def _create_real_storage_client(): - if GCP_CREDENTIALS_DICT['private_key'] == 'dummy': + if GCP_CREDENTIALS_DICT["private_key"] == "dummy": # Running on GCP, so no credentials needed storage_client = storage.Client(project=GCP_PROJECT_NAME) else: @@ -51,7 +59,9 @@ def _create_real_storage_client(): credentials = service_account.Credentials.from_service_account_info( GCP_CREDENTIALS_DICT ) - storage_client = storage.Client(credentials=credentials, project=GCP_PROJECT_NAME) + storage_client = storage.Client( + credentials=credentials, project=GCP_PROJECT_NAME + ) return storage_client @@ -59,12 +69,13 @@ def _get_bucket_name(): # return 'LocalBucket' if GCP_CREDENTIALS_DICT['private_key'] == 'dummy' else GCP_BUCKET_NAME return GCP_BUCKET_NAME + def _get_storage_bucket(storage_client): return storage_client.get_bucket(_get_bucket_name()) def get_storage_bucket(): - LOGGER.debug('Setting GCP storage client') + LOGGER.debug("Setting GCP storage client") storage_client = _create_real_storage_client() return _get_storage_bucket(storage_client) diff --git a/api/app/utils/strings.py b/api/app/utils/strings.py index 1a61b4319..8f8a4d967 100644 --- a/api/app/utils/strings.py +++ b/api/app/utils/strings.py @@ -1,36 +1,41 @@ # -*- coding: utf-8 -*- -from app import LOGGER import json +from app import LOGGER + + def _get_answer_value(answer, question, question_translation): if answer is None: - if question_translation.language == 'fr': # TODO: Add proper language support for back-end text - return 'Aucune réponse fournie' + if ( + question_translation.language == "fr" + ): # TODO: Add proper language support for back-end text + return "Aucune réponse fournie" else: - return 'No answer provided' + return "No answer provided" - if question.type == 'multi-choice' and question_translation.options is not None: - value = [o for o in question_translation.options if o['value'] == answer.value] + if question.type == "multi-choice" and question_translation.options is not None: + value = [o for o in question_translation.options if o["value"] == answer.value] if not value: return answer.value - return value[0]['label'] - - if question.type == 'file' and answer.value: - if question_translation.language == 'fr': - return 'Fichier téléchargé' + return value[0]["label"] + + if question.type == "file" and answer.value: + if question_translation.language == "fr": + return "Fichier téléchargé" else: - return 'Uploaded File' + return "Uploaded File" - if question.type == 'multi-file' and answer.value: + if question.type == "multi-file" and answer.value: file_info = json.loads(answer.value) - return "\n".join([f['name'] for f in file_info]) + return "\n".join([f["name"] for f in file_info]) - if question.type == 'information': + if question.type == "information": return "" return answer.value + def _find_answer(question, answers): if question == None: return None @@ -41,6 +46,7 @@ def _find_answer(question, answers): else: return None + def _find_question(question_id, questions): question = [q for q in questions if q.id == question_id] if question: @@ -48,37 +54,55 @@ def _find_question(question_id, questions): else: return None + def build_response_email_body(answers, language, application_form): - #stringifying the dictionary summary, with linebreaks between question/answer pairs + # stringifying the dictionary summary, with linebreaks between question/answer pairs stringified_summary = "" - allQuestions = [q for section in application_form.sections for q in section.questions] + allQuestions = [ + q for section in application_form.sections for q in section.questions + ] for section in application_form.sections: if not section.questions: continue section_translation = section.get_translation(language) if section_translation is None: - LOGGER.error('Missing {} translation for section {}.'.format(language, section.id)) - section_translation = section.get_translation('en') - stringified_summary += section_translation.name + '\n' + '-' * 20 + '\n\n' + LOGGER.error( + "Missing {} translation for section {}.".format(language, section.id) + ) + section_translation = section.get_translation("en") + stringified_summary += section_translation.name + "\n" + "-" * 20 + "\n\n" for question in section.questions: question_translation = question.get_translation(language) if question_translation is None: - LOGGER.error('Missing {} translation for question {}.'.format(language, question.id)) - question_translation = question.get_translation('en') + LOGGER.error( + "Missing {} translation for question {}.".format( + language, question.id + ) + ) + question_translation = question.get_translation("en") if question.depends_on_question_id and question_translation.show_for_values: - dependency_question = _find_question(question.depends_on_question_id, allQuestions) + dependency_question = _find_question( + question.depends_on_question_id, allQuestions + ) dependency_answer = _find_answer(dependency_question, answers) - if dependency_answer and dependency_answer not in question_translation.show_for_values: + if ( + dependency_answer + and dependency_answer not in question_translation.show_for_values + ): continue answer = _find_answer(question, answers) if answer: - answer_value = _get_answer_value(answer, answer.question, question_translation) - stringified_summary += '{question}\n{answer}\n\n'.format(question=question_translation.headline, answer=answer_value) + answer_value = _get_answer_value( + answer, answer.question, question_translation + ) + stringified_summary += "{question}\n{answer}\n\n".format( + question=question_translation.headline, answer=answer_value + ) return stringified_summary @@ -88,7 +112,7 @@ def build_response_html_app_info(response, language): Stringifying the application information, for output in a html file, with the response_id and applicant name contact info as paragraphs (

) """ - + stringified_app_info = f"

Response ID: {response.id}

Full name: {response.user.firstname} {response.user.lastname}

" return "Application Responses" + stringified_app_info @@ -96,7 +120,7 @@ def build_response_html_app_info(response, language): def build_response_html_answers(answers, language, application_form): """ - Stringifying the dictionary answers, for output in a html file, with sections as headers(

), + Stringifying the dictionary answers, for output in a html file, with sections as headers(

), questions as second headings (

) and answers as paragraphs (

) """ @@ -107,21 +131,29 @@ def build_response_html_answers(answers, language, application_form): continue section_translation = section.get_translation(language) if section_translation is None: - LOGGER.error('Missing {} translation for section {}.'.format(language, section.id)) - section_translation = section.get_translation('en') - stringified_answers += '

' + section_translation.name + '

' + LOGGER.error( + "Missing {} translation for section {}.".format(language, section.id) + ) + section_translation = section.get_translation("en") + stringified_answers += "

" + section_translation.name + "

" for question in section.questions: question_translation = question.get_translation(language) if question_translation is None: - LOGGER.error('Missing {} translation for question {}.'.format(language, question.id)) - question_translation = question.get_translation('en') + LOGGER.error( + "Missing {} translation for question {}.".format( + language, question.id + ) + ) + question_translation = question.get_translation("en") answer = _find_answer(question, answers) if answer: - answer_value = _get_answer_value(answer, answer.question, question_translation) - stringified_answers += f"

{question_translation.headline}

{answer_value}

" - + answer_value = _get_answer_value( + answer, answer.question, question_translation + ) + stringified_answers += ( + f"

{question_translation.headline}

{answer_value}

" + ) return stringified_answers - diff --git a/api/app/utils/testing.py b/api/app/utils/testing.py index 02c445076..435e45d05 100644 --- a/api/app/utils/testing.py +++ b/api/app/utils/testing.py @@ -14,17 +14,31 @@ from sqlalchemy.exc import ProgrammingError from app import LOGGER, app, db -from app.applicationModel.models import (ApplicationForm, Question, QuestionTranslation, Section, - SectionTranslation) +from app.applicationModel.models import ( + ApplicationForm, + Question, + QuestionTranslation, + Section, + SectionTranslation, +) +from app.email_template.models import EmailTemplate from app.events.models import Event, EventType from app.invitedGuest.models import InvitedGuest from app.organisation.models import Organisation from app.registration.models import Offer, RegistrationForm from app.responses.models import Answer, Response, ResponseReviewer, ResponseTag -from app.users.models import AppUser, Country, UserCategory -from app.email_template.models import EmailTemplate -from app.reviews.models import ReviewConfiguration, ReviewForm, ReviewSection, ReviewSectionTranslation, ReviewResponse, ReviewQuestion, ReviewQuestionTranslation, ReviewScore +from app.reviews.models import ( + ReviewConfiguration, + ReviewForm, + ReviewQuestion, + ReviewQuestionTranslation, + ReviewResponse, + ReviewScore, + ReviewSection, + ReviewSectionTranslation, +) from app.tags.models import Tag, TagTranslation +from app.users.models import AppUser, Country, UserCategory @event.listens_for(Engine, "connect") @@ -33,7 +47,9 @@ def set_sqlite_pragma(dbapi_connection, connection_record): cursor.execute("PRAGMA foreign_keys=ON") cursor.close() -titles = ('Mr', "Ms", 'Mrs', 'Dr', 'Prof', 'Rev', 'Mx') + +titles = ("Mr", "Ms", "Mrs", "Dr", "Prof", "Rev", "Mx") + def strip_accents(text): """ @@ -48,12 +64,12 @@ def strip_accents(text): :rtype: String. """ text = six.ensure_text(text) - text = unicodedata.normalize('NFD', text) - text = text.encode('ascii', 'ignore') + text = unicodedata.normalize("NFD", text) + text = text.encode("ascii", "ignore") return str(text) -class ApiTestCase(unittest.TestCase): +class ApiTestCase(unittest.TestCase): def __init__(self, *args, **kwargs): super(ApiTestCase, self).__init__(*args, **kwargs) self.test_users = [] @@ -70,12 +86,39 @@ def _get_names(self): names = file_with_names.readlines() else: # why yes, these are names of African Hollywood actors (according to Wikipedia) - names = ["Mehcad Brooks", "Malcolm Barrett", "Nick Cannon", "Lamorne Morris", "Neil Brown Jr.", - "William Jackson Harper", "Marques Houston", "Jennifer Hudson", "Alicia Keys", "Meghan Markle", - "Beyonce Knowles", "Jesse Williams", "Lance Gross", "Hosea Chanchez", "Daveed Diggs", - "Damon Wayans Jr.", "Columbus Short", "Terrence Jenkins", "Ron Funches", "Jussie Smollett", - "Donald Glover", "Brian Tyree Henry", "Gabourey Sidibe", "Trai Byers", "Robert Ri'chard", - "Arjay Smith", "Tessa Thompson", "J.Lee", "Lauren London", "DeVaughn Nixon", "Rob Brown", ] + names = [ + "Mehcad Brooks", + "Malcolm Barrett", + "Nick Cannon", + "Lamorne Morris", + "Neil Brown Jr.", + "William Jackson Harper", + "Marques Houston", + "Jennifer Hudson", + "Alicia Keys", + "Meghan Markle", + "Beyonce Knowles", + "Jesse Williams", + "Lance Gross", + "Hosea Chanchez", + "Daveed Diggs", + "Damon Wayans Jr.", + "Columbus Short", + "Terrence Jenkins", + "Ron Funches", + "Jussie Smollett", + "Donald Glover", + "Brian Tyree Henry", + "Gabourey Sidibe", + "Trai Byers", + "Robert Ri'chard", + "Arjay Smith", + "Tessa Thompson", + "J.Lee", + "Lauren London", + "DeVaughn Nixon", + "Rob Brown", + ] for _name in names: split_name = _name.strip().split(" ") self.firstnames.append(split_name[0]) @@ -83,22 +126,20 @@ def _get_names(self): self.lastnames.append(lastname) return self.firstnames, self.lastnames - def add_user(self, - email='user@user.com', - firstname='User', - lastname='Lastname', - user_title='Mrs', - password='abc', - organisation_id=1, - is_admin=False, - post_create_fn=lambda x: None): - user = AppUser(email, - firstname, - lastname, - user_title, - password, - organisation_id, - is_admin) + def add_user( + self, + email="user@user.com", + firstname="User", + lastname="Lastname", + user_title="Mrs", + password="abc", + organisation_id=1, + is_admin=False, + post_create_fn=lambda x: None, + ): + user = AppUser( + email, firstname, lastname, user_title, password, organisation_id, is_admin + ) user.verify() post_create_fn(user) @@ -108,11 +149,14 @@ def add_user(self, self.test_users.append(user) return user - def add_n_users(self, n, - password='abcd', - organisation_id=1, - is_admin=False, - post_create_fn=lambda x: None): + def add_n_users( + self, + n, + password="abcd", + organisation_id=1, + is_admin=False, + post_create_fn=lambda x: None, + ): firstnames, lastnames = self._get_names() users = [] @@ -121,101 +165,163 @@ def add_n_users(self, n, title = random.choice(titles) firstname = random.choice(firstnames) lastname = random.choice(lastnames) - email = "{firstname}.{lastname}{num}@bestemail.com".format(firstname=firstname, - lastname=lastname if lastname != "" else "x", - num=len(self.test_users)) + email = "{firstname}.{lastname}{num}@bestemail.com".format( + firstname=firstname, + lastname=lastname if lastname != "" else "x", + num=len(self.test_users), + ) email = strip_accents(email) try: - user = self.add_user(email, firstname, lastname, title, password, organisation_id, is_admin, post_create_fn) + user = self.add_user( + email, + firstname, + lastname, + title, + password, + organisation_id, + is_admin, + post_create_fn, + ) users.append(user) except ProgrammingError as err: - LOGGER.debug("info not added for user: {} {} {} {}".format(email, firstname, lastname, title)) + LOGGER.debug( + "info not added for user: {} {} {} {}".format( + email, firstname, lastname, title + ) + ) db.session.rollback() return users def setUp(self): - app.config['TESTING'] = True - app.config['DEBUG'] = True - app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://' + app.config["TESTING"] = True + app.config["DEBUG"] = True + app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite://" self.app = app.test_client() db.reflect() db.drop_all() db.create_all() - LOGGER.setLevel('ERROR') + LOGGER.setLevel("ERROR") # Add dummy metadata - self.user_category = UserCategory('Postdoc') + self.user_category = UserCategory("Postdoc") db.session.add(self.user_category) - self.country = Country('South Africa') + self.country = Country("South Africa") db.session.add(self.country) # Add a dummy organisation - dummy_org = self.add_organisation(domain='org') + dummy_org = self.add_organisation(domain="org") db.session.flush() self.dummy_org_id = dummy_org.id - def add_organisation(self, name='My Org', system_name='Baobab', small_logo='org.png', - large_logo='org_big.png', icon_logo='org_icon.png', domain='com', url='www.org.com', - email_from='contact@org.com', system_url='baobab.deeplearningindaba.com', - privacy_policy='PrivacyPolicy.pdf', languages=[{"code": "en", "description": "English"}]): - org = Organisation(name, system_name, small_logo, large_logo, icon_logo, domain, url, email_from, system_url, privacy_policy, languages) + def add_organisation( + self, + name="My Org", + system_name="Baobab", + small_logo="org.png", + large_logo="org_big.png", + icon_logo="org_icon.png", + domain="com", + url="www.org.com", + email_from="contact@org.com", + system_url="baobab.deeplearningindaba.com", + privacy_policy="PrivacyPolicy.pdf", + languages=[{"code": "en", "description": "English"}], + ): + org = Organisation( + name, + system_name, + small_logo, + large_logo, + icon_logo, + domain, + url, + email_from, + system_url, + privacy_policy, + languages, + ) db.session.add(org) db.session.commit() return org def add_country(self): - country = Country('South Africa') + country = Country("South Africa") db.session.add(country) db.session.commit() return country - + def add_category(self): - category = UserCategory('Student') + category = UserCategory("Student") db.session.add(category) db.session.commit() return category - def add_event(self, - name ={'en': 'Test Event'}, - description = {'en': 'Event Description'}, - start_date = datetime.now() + timedelta(days=30), - end_date = datetime.now() + timedelta(days=60), - key = 'INDABA2025', - organisation_id = 1, - email_from = 'abx@indaba.deeplearning', - url = 'indaba.deeplearning', - application_open = datetime.now(), - application_close = datetime.now() + timedelta(days=10), - review_open = datetime.now() , - review_close = datetime.now() + timedelta(days=15), - selection_open = datetime.now(), - selection_close = datetime.now() + timedelta(days=15), - offer_open = datetime.now(), - offer_close = datetime.now(), - registration_open = datetime.now(), - registration_close = datetime.now() + timedelta(days=15), - event_type = EventType.EVENT, - travel_grant = False): - - event = Event(name, description, start_date, end_date, key, organisation_id, email_from, url, - application_open, application_close, review_open, review_close, selection_open, - selection_close, offer_open, offer_close, registration_open, registration_close, event_type, - travel_grant) + def add_event( + self, + name={"en": "Test Event"}, + description={"en": "Event Description"}, + start_date=datetime.now() + timedelta(days=30), + end_date=datetime.now() + timedelta(days=60), + key="INDABA2025", + organisation_id=1, + email_from="abx@indaba.deeplearning", + url="indaba.deeplearning", + application_open=datetime.now(), + application_close=datetime.now() + timedelta(days=10), + review_open=datetime.now(), + review_close=datetime.now() + timedelta(days=15), + selection_open=datetime.now(), + selection_close=datetime.now() + timedelta(days=15), + offer_open=datetime.now(), + offer_close=datetime.now(), + registration_open=datetime.now(), + registration_close=datetime.now() + timedelta(days=15), + event_type=EventType.EVENT, + travel_grant=False, + ): + + event = Event( + name, + description, + start_date, + end_date, + key, + organisation_id, + email_from, + url, + application_open, + application_close, + review_open, + review_close, + selection_open, + selection_close, + offer_open, + offer_close, + registration_open, + registration_close, + event_type, + travel_grant, + ) db.session.add(event) db.session.commit() return event - def add_review_config(self, review_form_id=1, num_reviews_required=1, num_optional_reviews=1): + def add_review_config( + self, review_form_id=1, num_reviews_required=1, num_optional_reviews=1 + ): review_config = ReviewConfiguration( - review_form_id=review_form_id, - num_reviews_required=num_reviews_required, - num_optional_reviews=num_optional_reviews) + review_form_id=review_form_id, + num_reviews_required=num_reviews_required, + num_optional_reviews=num_optional_reviews, + ) db.session.add(review_config) db.session.commit() return review_config - def add_review_form(self, application_form_id=1, deadline=None, stage=1, active=True): + def add_review_form( + self, application_form_id=1, deadline=None, stage=1, active=True + ): deadline = deadline or datetime.now() review_form = ReviewForm(application_form_id, deadline, stage, active) db.session.add(review_form) @@ -229,30 +335,48 @@ def add_review_section(self, review_form_id, order=1): return review_section - def add_review_section_translation(self, review_section_id, language, headline='Review Section', description='Review Section Description'): - translation = ReviewSectionTranslation(review_section_id, language, headline, description) + def add_review_section_translation( + self, + review_section_id, + language, + headline="Review Section", + description="Review Section Description", + ): + translation = ReviewSectionTranslation( + review_section_id, language, headline, description + ) db.session.add(translation) db.session.commit() return translation - def add_review_question(self, review_section_id, weight=0, type='short-text', question_id=None, order=1): - review_question = ReviewQuestion(review_section_id, question_id, type=type, is_required=True, order=order, weight=weight) + def add_review_question( + self, review_section_id, weight=0, type="short-text", question_id=None, order=1 + ): + review_question = ReviewQuestion( + review_section_id, + question_id, + type=type, + is_required=True, + order=order, + weight=weight, + ) db.session.add(review_question) db.session.commit() return review_question - + def add_review_question_translation( self, review_question_id, language, - description='Review question description', - headline='Review question headline', + description="Review question description", + headline="Review question headline", placeholder=None, options=None, validation_regex=None, - validation_text=None): + validation_text=None, + ): review_question_translation = ReviewQuestionTranslation( review_question_id, language, @@ -261,7 +385,7 @@ def add_review_question_translation( placeholder=placeholder, options=options, validation_regex=validation_regex, - validation_text=validation_text + validation_text=validation_text, ) db.session.add(review_question_translation) @@ -269,20 +393,26 @@ def add_review_question_translation( return review_question_translation - def add_email_template(self, template_key, template='This is an email', language='en', subject='Subject', event_id=None): - email_template = EmailTemplate(template_key, event_id, subject, template, language) + def add_email_template( + self, + template_key, + template="This is an email", + language="en", + subject="Subject", + event_id=None, + ): + email_template = EmailTemplate( + template_key, event_id, subject, template, language + ) db.session.add(email_template) db.session.commit() return email_template - def get_auth_header_for(self, email, password='abc'): - body = { - 'email': email, - 'password': password - } - response = self.app.post('api/v1/authenticate', data=body) + def get_auth_header_for(self, email, password="abc"): + body = {"email": email, "password": password} + response = self.app.post("api/v1/authenticate", data=body) data = json.loads(response.data) - header = {'Authorization': data['token']} + header = {"Authorization": data["token"]} return header def add_to_db(self, obj): @@ -294,11 +424,8 @@ def tearDown(self): db.reflect() db.drop_all() - def create_application_form(self, - event_id = 1, - is_open = True, - nominations = False): - + def create_application_form(self, event_id=1, is_open=True, nominations=False): + application_form = ApplicationForm(event_id, is_open, nominations) db.session.add(application_form) db.session.commit() @@ -309,21 +436,24 @@ def create_registration_form(self, event_id=1): db.session.add(registration_form) db.session.commit() return registration_form - + def add_section(self, application_form_id, order=1): section = Section(application_form_id, order) db.session.add(section) db.session.commit() return section - + def add_section_translation( self, section_id, language, - name='Section Name', - description='Section Description', - show_for_values=None): - section_translation = SectionTranslation(section_id, language, name, description, show_for_values) + name="Section Name", + description="Section Description", + show_for_values=None, + ): + section_translation = SectionTranslation( + section_id, language, name, description, show_for_values + ) db.session.add(section_translation) db.session.commit() return section_translation @@ -333,23 +463,28 @@ def add_question( application_form_id, section_id, order=1, - question_type='short-text', - key=None): - question = Question(application_form_id, section_id, order, question_type, key=key) + question_type="short-text", + key=None, + ): + question = Question( + application_form_id, section_id, order, question_type, key=key + ) db.session.add(question) db.session.commit() return question - def add_question_translation(self, + def add_question_translation( + self, question_id, language, - headline='Question Headline', + headline="Question Headline", description=None, placeholder=None, validation_regex=None, validation_text=None, options=None, - show_for_values=None): + show_for_values=None, + ): question_translation = QuestionTranslation( question_id, language, @@ -359,12 +494,20 @@ def add_question_translation(self, validation_regex, validation_text, options, - show_for_values) + show_for_values, + ) db.session.add(question_translation) db.session.commit() return question_translation - - def add_response(self, application_form_id, user_id, is_submitted=False, is_withdrawn=False, language='en'): + + def add_response( + self, + application_form_id, + user_id, + is_submitted=False, + is_withdrawn=False, + language="en", + ): response = Response(application_form_id, user_id, language) if is_submitted: response.submit() @@ -374,7 +517,7 @@ def add_response(self, application_form_id, user_id, is_submitted=False, is_with db.session.add(response) db.session.commit() return response - + def add_response_reviewer(self, response_id, reviewer_user_id): rr = ResponseReviewer(response_id, reviewer_user_id) db.session.add(rr) @@ -387,7 +530,14 @@ def add_answer(self, response_id, question_id, answer_value): db.session.commit() return answer - def add_review_response(self, reviewer_user_id, response_id, review_form_id=1, language='en', is_submitted=False): + def add_review_response( + self, + reviewer_user_id, + response_id, + review_form_id=1, + language="en", + is_submitted=False, + ): rr = ReviewResponse(review_form_id, reviewer_user_id, response_id, language) if is_submitted: rr.submit() @@ -403,38 +553,53 @@ def add_review_score(self, review_response_id, review_question_id, value): db.session.commit() return rs - def add_offer(self, user_id, event_id=1, offer_date=None, expiry_date=None, payment_required=False, travel_award=False, accommodation_award=False, candidate_response=None): + def add_offer( + self, + user_id, + event_id=1, + offer_date=None, + expiry_date=None, + payment_required=False, + travel_award=False, + accommodation_award=False, + candidate_response=None, + ): offer_date = offer_date or datetime.now() expiry_date = expiry_date or datetime.now() + timedelta(10) offer = Offer( - user_id=user_id, - event_id=event_id, - offer_date=offer_date, + user_id=user_id, + event_id=event_id, + offer_date=offer_date, expiry_date=expiry_date, payment_required=payment_required, travel_award=travel_award, accommodation_award=accommodation_award, - candidate_response=candidate_response) + candidate_response=candidate_response, + ) db.session.add(offer) db.session.commit() return offer - def add_invited_guest(self, user_id, event_id=1, role='Guest'): - print(('Adding invited guest for user: {}, event: {}, role: {}'.format(user_id, event_id, role))) + def add_invited_guest(self, user_id, event_id=1, role="Guest"): + print( + ( + "Adding invited guest for user: {}, event: {}, role: {}".format( + user_id, event_id, role + ) + ) + ) guest = InvitedGuest(event_id, user_id, role) db.session.add(guest) db.session.commit() return guest - def add_tag(self, event_id=1, names={'en': 'Tag 1 en', 'fr': 'Tag 1 fr'}): + def add_tag(self, event_id=1, names={"en": "Tag 1 en", "fr": "Tag 1 fr"}): tag = Tag(event_id) db.session.add(tag) db.session.commit() - translations = [ - TagTranslation(tag.id, k, name) for k, name in names.items() - ] + translations = [TagTranslation(tag.id, k, name) for k, name in names.items()] db.session.add_all(translations) db.session.commit() return tag diff --git a/api/app/utils/tests.py b/api/app/utils/tests.py index eb431dd24..7b4612f00 100644 --- a/api/app/utils/tests.py +++ b/api/app/utils/tests.py @@ -1,21 +1,18 @@ # -*- coding: latin-1 -*- import json -from app.applicationModel.repository import ApplicationFormRepository as application_form_repository - -from app.responses.repository import ResponseRepository as response_repository - from functools import partial -from app import LOGGER, app, db - - +from mock import patch +from app import LOGGER, app, db +from app.applicationModel.repository import ( + ApplicationFormRepository as application_form_repository, +) +from app.responses.repository import ResponseRepository as response_repository from app.utils.emailer import email_user from app.utils.strings import build_response_html_answers, build_response_html_app_info from app.utils.testing import ApiTestCase -from mock import patch - class EmailerTest(ApiTestCase): @@ -25,155 +22,218 @@ def seed_static_data(self): def set_language(u, language): u.user_primaryLanguage = language - self.english_user = self.add_user(email='english@person.com', post_create_fn=partial(set_language, language='en')) - self.french_user = self.add_user(email='french@person.com', post_create_fn=partial(set_language, language='fr')) - self.zulu_user = self.add_user(email='zulu@person.com', post_create_fn=partial(set_language, language='zu')) + self.english_user = self.add_user( + email="english@person.com", + post_create_fn=partial(set_language, language="en"), + ) + self.french_user = self.add_user( + email="french@person.com", + post_create_fn=partial(set_language, language="fr"), + ) + self.zulu_user = self.add_user( + email="zulu@person.com", post_create_fn=partial(set_language, language="zu") + ) self.event = self.add_event( - name={'en': 'English Event Name', 'fr': 'Nom de lévénement en français'}, - description={'en': 'English Description', 'fr': 'Description en français'} + name={"en": "English Event Name", "fr": "Nom de lévénement en français"}, + description={"en": "English Description", "fr": "Description en français"}, + ) + self.add_email_template( + "template1", + "English template no event {param}", + subject="English subject no event", + ) + self.add_email_template( + "template1", + "Modèle français sans événement {param}", + subject="Sujet français sans événement", + language="fr", + ) + self.add_email_template( + "template1", + "English template {event_name} {param}", + subject="English subject {event_name}", + event_id=1, + ) + self.add_email_template( + "template1", + "Modèle français {event_name} {param}", + subject="Sujet français {event_name}", + language="fr", + event_id=1, ) - self.add_email_template('template1', 'English template no event {param}', subject='English subject no event') - self.add_email_template('template1', 'Modèle français sans événement {param}', subject='Sujet français sans événement', language='fr') - self.add_email_template('template1', 'English template {event_name} {param}', subject='English subject {event_name}', event_id=1) - self.add_email_template('template1', 'Modèle français {event_name} {param}', subject='Sujet français {event_name}', language='fr', event_id=1) - @patch('app.utils.emailer.send_mail') + @patch("app.utils.emailer.send_mail") def test_email_no_event_english(self, send_mail_fn): """Check email to an English user with no event.""" self.seed_static_data() - - email_user('template1', template_parameters={'param': 'Blah'}, user=self.english_user) + + email_user( + "template1", template_parameters={"param": "Blah"}, user=self.english_user + ) send_mail_fn.assert_called_with( - recipient=self.english_user.email, - subject='English subject no event', - body_text='English template no event Blah', - file_name='', - file_path='') + recipient=self.english_user.email, + subject="English subject no event", + body_text="English template no event Blah", + file_name="", + file_path="", + ) - @patch('app.utils.emailer.send_mail') + @patch("app.utils.emailer.send_mail") def test_email_no_event_french(self, send_mail_fn): """Check email to an French user with no event.""" self.seed_static_data() - - email_user('template1', template_parameters={'param': 'bleu'}, user=self.french_user) + + email_user( + "template1", template_parameters={"param": "bleu"}, user=self.french_user + ) send_mail_fn.assert_called_with( - recipient=self.french_user.email, - subject='Sujet français sans événement', - body_text='Modèle français sans événement bleu', - file_name='', - file_path='') + recipient=self.french_user.email, + subject="Sujet français sans événement", + body_text="Modèle français sans événement bleu", + file_name="", + file_path="", + ) - @patch('app.utils.emailer.send_mail') + @patch("app.utils.emailer.send_mail") def test_email_english_default(self, send_mail_fn): """Check email to use with unsupported language defaults to English.""" self.seed_static_data() - - email_user('template1', template_parameters={'param': 'Blah'}, user=self.zulu_user) + + email_user( + "template1", template_parameters={"param": "Blah"}, user=self.zulu_user + ) send_mail_fn.assert_called_with( - recipient=self.zulu_user.email, - subject='English subject no event', - body_text='English template no event Blah', - file_name='', - file_path='') + recipient=self.zulu_user.email, + subject="English subject no event", + body_text="English template no event Blah", + file_name="", + file_path="", + ) - @patch('app.utils.emailer.send_mail') + @patch("app.utils.emailer.send_mail") def test_files(self, send_mail_fn): """Check that file parameters are correctly passed to send_mail function.""" self.seed_static_data() - - email_user('template1', template_parameters={'param': 'Blah'}, user=self.english_user, - file_name='myfile.pdf', file_path='/long/way/home') + + email_user( + "template1", + template_parameters={"param": "Blah"}, + user=self.english_user, + file_name="myfile.pdf", + file_path="/long/way/home", + ) send_mail_fn.assert_called_with( - recipient=self.english_user.email, - subject='English subject no event', - body_text='English template no event Blah', - file_name='myfile.pdf', - file_path='/long/way/home') - - - @patch('app.utils.emailer.send_mail') + recipient=self.english_user.email, + subject="English subject no event", + body_text="English template no event Blah", + file_name="myfile.pdf", + file_path="/long/way/home", + ) + + @patch("app.utils.emailer.send_mail") def test_email_event_english(self, send_mail_fn): """Check email to an English user with an event.""" self.seed_static_data() - - email_user('template1', template_parameters={'param': 'Blah'}, user=self.english_user, event=self.event) + + email_user( + "template1", + template_parameters={"param": "Blah"}, + user=self.english_user, + event=self.event, + ) send_mail_fn.assert_called_with( - recipient=self.english_user.email, - subject='English subject English Event Name', - body_text='English template English Event Name Blah', - file_name='', - file_path='') + recipient=self.english_user.email, + subject="English subject English Event Name", + body_text="English template English Event Name Blah", + file_name="", + file_path="", + ) - @patch('app.utils.emailer.send_mail') + @patch("app.utils.emailer.send_mail") def test_email_event_french(self, send_mail_fn): """Check email to an French user with an event.""" self.seed_static_data() - - email_user('template1', template_parameters={'param': 'bleu'}, user=self.french_user, event=self.event) + + email_user( + "template1", + template_parameters={"param": "bleu"}, + user=self.french_user, + event=self.event, + ) send_mail_fn.assert_called_with( - recipient=self.french_user.email, - subject='Sujet français Nom de lévénement en français', - body_text='Modèle français Nom de lévénement en français bleu', - file_name='', - file_path='') + recipient=self.french_user.email, + subject="Sujet français Nom de lévénement en français", + body_text="Modèle français Nom de lévénement en français bleu", + file_name="", + file_path="", + ) + class BuildResponseHTMLTest(ApiTestCase): """ - Test HTML builder functionality for the application information as well as + Test HTML builder functionality for the application information as well as the question-answer mapping functionalities """ def _seed_static_data(self): - self.event1 = self.add_event(key='event1') - self.event1admin = self.add_user('event1admin@mail.com', is_admin=True) - self.user1 = self.add_user('user1@mail.com', user_title='Ms', firstname='Danai', lastname='Gurira') + self.event1 = self.add_event(key="event1") + self.event1admin = self.add_user("event1admin@mail.com", is_admin=True) + self.user1 = self.add_user( + "user1@mail.com", user_title="Ms", firstname="Danai", lastname="Gurira" + ) application_form = self.create_application_form(self.event1.id) self.application_form_id = application_form.id # Section 1, one question section1 = self.add_section(application_form.id) - self.add_section_translation(section1.id, 'en', name='Section1') + self.add_section_translation(section1.id, "en", name="Section1") question1 = self.add_question(application_form.id, section1.id) - self.add_question_translation(question1.id, 'en', headline='Question 1, S1') + self.add_question_translation(question1.id, "en", headline="Question 1, S1") # Section 2, 1 question section2 = self.add_section(application_form.id) - self.add_section_translation(section2.id, 'en', name='Section2') + self.add_section_translation(section2.id, "en", name="Section2") question2_1 = self.add_question(application_form.id, section2.id) - self.add_question_translation(question2_1.id, 'en', headline='Question 1, S2') - + self.add_question_translation(question2_1.id, "en", headline="Question 1, S2") - self.response = self.add_response(application_form.id, self.user1.id, is_submitted=True) + self.response = self.add_response( + application_form.id, self.user1.id, is_submitted=True + ) self.response_submitted = self.response.submitted_timestamp self.response_started = self.response.started_timestamp self.response_id = self.response.id - self.add_answer(self.response_id, question1.id, 'Section 1 Answer 1') + self.add_answer(self.response_id, question1.id, "Section 1 Answer 1") - self.add_answer(self.response_id, question2_1.id, 'Section 2 Answer 1') + self.add_answer(self.response_id, question2_1.id, "Section 2 Answer 1") # Second response, same application form with missing answers - self.response2 = self.add_response(application_form.id, self.user1.id, is_submitted=True) + self.response2 = self.add_response( + application_form.id, self.user1.id, is_submitted=True + ) self.response_submitted = self.response2.submitted_timestamp self.response_started = self.response2.started_timestamp self.response_id2 = self.response2.id - def test_build_response_html_answers(self): self._seed_static_data() - application_form = application_form_repository.get_by_id(self.application_form_id) + application_form = application_form_repository.get_by_id( + self.application_form_id + ) answers = response_repository.get_by_id(self.response_id).answers - html_answer_string = build_response_html_answers(answers, 'en', application_form) - + html_answer_string = build_response_html_answers( + answers, "en", application_form + ) + self.assertIsNotNone(html_answer_string) def test_build_response_html_answers_with_answers_missing(self): @@ -183,20 +243,21 @@ def test_build_response_html_answers_with_answers_missing(self): self._seed_static_data() - application_form = application_form_repository.get_by_id(self.application_form_id) + application_form = application_form_repository.get_by_id( + self.application_form_id + ) answers = response_repository.get_by_id(self.response_id2).answers - html_string = build_response_html_answers(answers, 'en', application_form) - + html_string = build_response_html_answers(answers, "en", application_form) + self.assertIsNotNone(html_string) def test_build_response_html_app_info(self): - + self._seed_static_data() response = response_repository.get_by_id(self.response_id) - - html_info_string = build_response_html_app_info(response, 'en') - - self.assertIsNotNone(html_info_string) + html_info_string = build_response_html_app_info(response, "en") + + self.assertIsNotNone(html_info_string) diff --git a/api/app/utils/zipping.py b/api/app/utils/zipping.py index bdbe14203..576720153 100644 --- a/api/app/utils/zipping.py +++ b/api/app/utils/zipping.py @@ -10,4 +10,4 @@ def zip_in_memory(files): for file_name, data in files: zip_file.writestr(file_name, data.getvalue()) - return zip_buffer \ No newline at end of file + return zip_buffer diff --git a/api/config.py b/api/config.py index 0443871a3..ca6e3a6b7 100644 --- a/api/config.py +++ b/api/config.py @@ -1,39 +1,40 @@ import os - BASE_DIR = os.path.abspath(os.path.dirname(__file__)) DATABASE_CONNECT_OPTIONS = {} -DEBUG = os.getenv('DEBUG', False) +DEBUG = os.getenv("DEBUG", False) -PORT = os.getenv('PORT', 5000) +PORT = os.getenv("PORT", 5000) -SQLALCHEMY_DATABASE_URI = os.getenv('DATABASE_URL', 'postgres://docker:docker@db/docker') +SQLALCHEMY_DATABASE_URI = os.getenv( + "DATABASE_URL", "postgres://docker:docker@db/docker" +) -SECRET_KEY = os.getenv('SECRET_KEY', None) +SECRET_KEY = os.getenv("SECRET_KEY", None) assert SECRET_KEY THREADS_PER_PAGE = 2 SQLALCHEMY_TRACK_MODIFICATIONS = False -SMTP_USERNAME = os.getenv('SMTP_USERNAME', None) -SMTP_PASSWORD = os.getenv('SMTP_PASSWORD', None) -SMTP_SENDER_NAME = os.getenv('SMTP_SENDER_NAME', None) -SMTP_SENDER_EMAIL = os.getenv('SMTP_SENDER_EMAIL', None) -SMTP_HOST = os.getenv('SMTP_HOST', None) -SMTP_PORT = os.getenv('SMTP_PORT', None) +SMTP_USERNAME = os.getenv("SMTP_USERNAME", None) +SMTP_PASSWORD = os.getenv("SMTP_PASSWORD", None) +SMTP_SENDER_NAME = os.getenv("SMTP_SENDER_NAME", None) +SMTP_SENDER_EMAIL = os.getenv("SMTP_SENDER_EMAIL", None) +SMTP_HOST = os.getenv("SMTP_HOST", None) +SMTP_PORT = os.getenv("SMTP_PORT", None) GCP_CREDENTIALS_DICT = { - 'type': 'service_account', - 'client_id': os.getenv('GCP_CLIENT_ID', None), - 'client_email': os.getenv('GCP_CLIENT_EMAIL', None), - 'private_key_id': os.getenv('GCP_PRIVATE_KEY_ID', None), - 'private_key': os.getenv('GCP_PRIVATE_KEY', None), - 'token_uri': 'https://oauth2.googleapis.com/token' + "type": "service_account", + "client_id": os.getenv("GCP_CLIENT_ID", None), + "client_email": os.getenv("GCP_CLIENT_EMAIL", None), + "private_key_id": os.getenv("GCP_PRIVATE_KEY_ID", None), + "private_key": os.getenv("GCP_PRIVATE_KEY", None), + "token_uri": "https://oauth2.googleapis.com/token", } -GCP_PROJECT_NAME = os.getenv('GCP_PROJECT_NAME', None) -GCP_BUCKET_NAME = os.getenv('GCP_BUCKET_NAME', None) -FILE_SIZE_LIMIT = int(os.getenv('FILE_SIZE_LIMIT', None)) +GCP_PROJECT_NAME = os.getenv("GCP_PROJECT_NAME", None) +GCP_BUCKET_NAME = os.getenv("GCP_BUCKET_NAME", None) +FILE_SIZE_LIMIT = int(os.getenv("FILE_SIZE_LIMIT", None)) -BOABAB_HOST = os.getenv('BOABAB_HOST', None) +BOABAB_HOST = os.getenv("BOABAB_HOST", None) diff --git a/api/gunicorn.config.py b/api/gunicorn.config.py index 9214c3e7c..29c0ce8fc 100644 --- a/api/gunicorn.config.py +++ b/api/gunicorn.config.py @@ -1,5 +1,5 @@ """Gunicorn configuration.""" -forwarded_allow_ips = '*' -secure_scheme_headers = {'X-Forwarded-Proto': 'https'} +forwarded_allow_ips = "*" +secure_scheme_headers = {"X-Forwarded-Proto": "https"} workers = 4 diff --git a/api/main.py b/api/main.py index d55711b41..e80b6bee5 100644 --- a/api/main.py +++ b/api/main.py @@ -1,4 +1,5 @@ from app import app + application = app # make uwsgi happy if __name__ == "__main__": - app.run() \ No newline at end of file + app.run() diff --git a/api/migrations/env.py b/api/migrations/env.py index 48fe1522d..ac516cefc 100644 --- a/api/migrations/env.py +++ b/api/migrations/env.py @@ -1,7 +1,7 @@ +from logging.config import fileConfig from alembic import context from sqlalchemy import engine_from_config, pool -from logging.config import fileConfig # this is the Alembic Config object, which provides # access to the values within the .ini file in use. @@ -16,15 +16,19 @@ # from myapp import mymodel # target_metadata = mymodel.Base.metadata from flask import current_app -config.set_main_option('sqlalchemy.url', current_app.config.get('SQLALCHEMY_DATABASE_URI')) -print((current_app.config.get('SQLALCHEMY_DATABASE_URI'))) -target_metadata = current_app.extensions['migrate'].db.metadata + +config.set_main_option( + "sqlalchemy.url", current_app.config.get("SQLALCHEMY_DATABASE_URI") +) +print((current_app.config.get("SQLALCHEMY_DATABASE_URI"))) +target_metadata = current_app.extensions["migrate"].db.metadata # other values from the config, defined by the needs of env.py, # can be acquired: # my_important_option = config.get_main_option("my_important_option") # ... etc. + def run_migrations_offline(): """Run migrations in 'offline' mode. @@ -43,6 +47,7 @@ def run_migrations_offline(): with context.begin_transaction(): context.run_migrations() + def run_migrations_online(): """Run migrations in 'online' mode. @@ -51,15 +56,13 @@ def run_migrations_online(): """ engine = engine_from_config( - config.get_section(config.config_ini_section), - prefix='sqlalchemy.', - poolclass=pool.NullPool) + config.get_section(config.config_ini_section), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) connection = engine.connect() - context.configure( - connection=connection, - target_metadata=target_metadata - ) + context.configure(connection=connection, target_metadata=target_metadata) try: with context.begin_transaction(): @@ -67,8 +70,8 @@ def run_migrations_online(): finally: connection.close() + if context.is_offline_mode(): run_migrations_offline() else: run_migrations_online() - diff --git a/api/migrations/versions/0023aeda53b0_.py b/api/migrations/versions/0023aeda53b0_.py index 682db6176..5a6c8dc45 100644 --- a/api/migrations/versions/0023aeda53b0_.py +++ b/api/migrations/versions/0023aeda53b0_.py @@ -7,21 +7,31 @@ """ # revision identifiers, used by Alembic. -revision = '0023aeda53b0' -down_revision = 'd7f3adff2c9f' +revision = "0023aeda53b0" +down_revision = "d7f3adff2c9f" -from alembic import op import sqlalchemy as sa +from alembic import op from sqlalchemy.dialects import postgresql + def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('application_form', 'deadline') + op.drop_column("application_form", "deadline") # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.add_column('application_form', sa.Column('deadline', postgresql.TIMESTAMP(), autoincrement=False, nullable=False, server_default='2016-06-22 19:10:25-07')) - op.alter_column('application_form', 'deadline', server_default=None) + op.add_column( + "application_form", + sa.Column( + "deadline", + postgresql.TIMESTAMP(), + autoincrement=False, + nullable=False, + server_default="2016-06-22 19:10:25-07", + ), + ) + op.alter_column("application_form", "deadline", server_default=None) # ### end Alembic commands ### diff --git a/api/migrations/versions/016571f41a20_add_organisation.py b/api/migrations/versions/016571f41a20_add_organisation.py index df1b6a74b..7726a8013 100644 --- a/api/migrations/versions/016571f41a20_add_organisation.py +++ b/api/migrations/versions/016571f41a20_add_organisation.py @@ -7,51 +7,53 @@ """ # revision identifiers, used by Alembic. -revision = '016571f41a20' -down_revision = '2873f4f08a4b' +revision = "016571f41a20" +down_revision = "2873f4f08a4b" -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): - organisation = op.create_table('organisation', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('name', sa.String(length=50), nullable=False), - sa.Column('small_logo', sa.String(length=100), nullable=False), - sa.Column('large_logo', sa.String(length=100), nullable=False), - sa.Column('domain', sa.String(length=100), nullable=False), - - sa.PrimaryKeyConstraint('id') + organisation = op.create_table( + "organisation", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("name", sa.String(length=50), nullable=False), + sa.Column("small_logo", sa.String(length=100), nullable=False), + sa.Column("large_logo", sa.String(length=100), nullable=False), + sa.Column("domain", sa.String(length=100), nullable=False), + sa.PrimaryKeyConstraint("id"), ) - op.bulk_insert(organisation, - [ - { - 'id': 1, - 'name': 'Deep Learning Indaba', - 'small_logo': 'logo-32x32-white.png', - 'large_logo': 'indaba-logo-dark.png', - 'domain': 'deeplearningindaba' - }, - { - 'id': 2, - 'name': 'Eastern European Machine Learning Summer School', - 'small_logo': 'logo_eeml_white.png', - 'large_logo': 'logo_eeml_black.png', - 'domain': 'eeml' - }, - { - 'id': 3, - 'name': 'Deep Learning Indaba', - 'small_logo': 'logo-32x32-white.png', - 'large_logo': 'indaba-logo-dark.png', - 'domain': 'localhost' - } - ]) + op.bulk_insert( + organisation, + [ + { + "id": 1, + "name": "Deep Learning Indaba", + "small_logo": "logo-32x32-white.png", + "large_logo": "indaba-logo-dark.png", + "domain": "deeplearningindaba", + }, + { + "id": 2, + "name": "Eastern European Machine Learning Summer School", + "small_logo": "logo_eeml_white.png", + "large_logo": "logo_eeml_black.png", + "domain": "eeml", + }, + { + "id": 3, + "name": "Deep Learning Indaba", + "small_logo": "logo-32x32-white.png", + "large_logo": "indaba-logo-dark.png", + "domain": "localhost", + }, + ], + ) def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('organisation') + op.drop_table("organisation") # ### end Alembic commands ### diff --git a/api/migrations/versions/02242641e122_add_local_event.py b/api/migrations/versions/02242641e122_add_local_event.py index 0166f0643..4a3f282c0 100644 --- a/api/migrations/versions/02242641e122_add_local_event.py +++ b/api/migrations/versions/02242641e122_add_local_event.py @@ -6,29 +6,31 @@ """ # revision identifiers, used by Alembic. -revision = '02242641e122' -down_revision = '85d13062a5e7' +revision = "02242641e122" +down_revision = "85d13062a5e7" + +import datetime +from enum import Enum -from alembic import op import sqlalchemy as sa +from alembic import op from sqlalchemy import orm from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy import orm + from app import db -import datetime -from enum import Enum Base = declarative_base() + class EventType(Enum): - EVENT = 'event' - AWARD = 'award' + EVENT = "event" + AWARD = "award" class Event(Base): __tablename__ = "event" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(50), nullable=False) @@ -52,27 +54,28 @@ class Event(Base): registration_close = db.Column(db.DateTime(), nullable=False) event_type = db.Column(db.Enum(EventType), nullable=False) - def __init__(self, - name, - description, - start_date, - end_date, - key, - organisation_id, - email_from, - url, - application_open, - application_close, - review_open, - review_close, - selection_open, - selection_close, - offer_open, - offer_close, - registration_open, - registration_close, - event_type - ): + def __init__( + self, + name, + description, + start_date, + end_date, + key, + organisation_id, + email_from, + url, + application_open, + application_close, + review_open, + review_close, + selection_open, + selection_close, + offer_open, + offer_close, + registration_open, + registration_close, + event_type, + ): self.name = name self.description = description @@ -94,15 +97,16 @@ def __init__(self, self.registration_close = registration_close self.event_type = event_type + class ApplicationForm(Base): - __tablename__ = 'application_form' - __table_args__ = {'extend_existing': True} + __tablename__ = "application_form" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - event_id = db.Column(db.Integer(), db.ForeignKey('event.id'), nullable=False) + event_id = db.Column(db.Integer(), db.ForeignKey("event.id"), nullable=False) is_open = db.Column(db.Boolean(), nullable=False) - event = db.relationship('Event', foreign_keys=[event_id]) + event = db.relationship("Event", foreign_keys=[event_id]) nominations = db.Column(db.Boolean(), nullable=False) def __init__(self, event_id, is_open, nominations): @@ -110,12 +114,15 @@ def __init__(self, event_id, is_open, nominations): self.is_open = is_open self.nominations = nominations + class ReviewForm(Base): - __tablename__ = 'review_form' - __table_args__ = {'extend_existing': True} + __tablename__ = "review_form" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) is_open = db.Column(db.Boolean(), nullable=False) deadline = db.Column(db.DateTime(), nullable=False) @@ -124,24 +131,41 @@ def __init__(self, application_form_id, deadline): self.is_open = True self.deadline = deadline + class RegistrationForm(Base): - + __tablename__ = "registration_form" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - event_id = db.Column(db.Integer(), db.ForeignKey( - "event.id"), nullable=False) + event_id = db.Column(db.Integer(), db.ForeignKey("event.id"), nullable=False) def __init__(self, event_id): self.event_id = event_id + def upgrade(): - local_event = Event('Test Event', 'Amazing Machine Learning Summer School 2020, 6-11 July 2021, Cape Town, South Africa', - datetime.date(2021, 7, 6), datetime.date(2021, 7, 11), 'test2021', 3, 'contact@testevent.com', - 'http://www.deeplearningindaba.com', datetime.date(2020, 2, 2), datetime.date(2021, 3, 20), datetime.date(2020, 2, 2), - datetime.date(2021, 4, 20), datetime.date(2020, 2, 2), datetime.date(2021, 5, 20), datetime.date(2020, 2, 2), - datetime.date(2021, 6, 20), datetime.date(2020, 2, 2), datetime.date(2021, 6, 30), EventType.EVENT) + local_event = Event( + "Test Event", + "Amazing Machine Learning Summer School 2020, 6-11 July 2021, Cape Town, South Africa", + datetime.date(2021, 7, 6), + datetime.date(2021, 7, 11), + "test2021", + 3, + "contact@testevent.com", + "http://www.deeplearningindaba.com", + datetime.date(2020, 2, 2), + datetime.date(2021, 3, 20), + datetime.date(2020, 2, 2), + datetime.date(2021, 4, 20), + datetime.date(2020, 2, 2), + datetime.date(2021, 5, 20), + datetime.date(2020, 2, 2), + datetime.date(2021, 6, 20), + datetime.date(2020, 2, 2), + datetime.date(2021, 6, 30), + EventType.EVENT, + ) Base.metadata.bind = op.get_bind() session = orm.Session(bind=Base.metadata.bind) @@ -162,7 +186,9 @@ def upgrade(): SELECT {}, section.name, section.description, section."order" FROM section WHERE application_form_id = 1 - """.format(application_form_id) + """.format( + application_form_id + ) op.get_bind().execute(query) # Copy application form questions from Indaba 2019 @@ -183,19 +209,29 @@ def upgrade(): FROM question q INNER JOIN section s on q.section_id = s.id WHERE q.application_form_id = 1 - """.format(application_form_id=application_form_id) + """.format( + application_form_id=application_form_id + ) op.get_bind().execute(query) # Make file types optional because file uploads don't work locally (yet) - op.get_bind().execute("""UPDATE question SET is_required=False WHERE application_form_id={} AND type='file'""".format(application_form_id)) - - op.get_bind().execute("""SELECT setval('review_form_id_seq', (SELECT max(id) FROM review_form));""") + op.get_bind().execute( + """UPDATE question SET is_required=False WHERE application_form_id={} AND type='file'""".format( + application_form_id + ) + ) + + op.get_bind().execute( + """SELECT setval('review_form_id_seq', (SELECT max(id) FROM review_form));""" + ) review_form = ReviewForm(application_form_id, datetime.date(2021, 6, 30)) session.add(review_form) session.commit() review_form_id = review_form.id - op.get_bind().execute("""SELECT setval('review_question_id_seq', (SELECT max(id) FROM review_question));""") + op.get_bind().execute( + """SELECT setval('review_question_id_seq', (SELECT max(id) FROM review_question));""" + ) query = """ INSERT INTO review_question(review_form_id, question_id, description, headline, type, placeholder, options, is_required, "order", validation_regex, validation_text, weight) SELECT @@ -214,26 +250,36 @@ def upgrade(): FROM review_question q LEFT JOIN question qq ON q.question_id = qq.id WHERE q.review_form_id = 1 - """.format(review_form_id=review_form_id, application_form_id=application_form_id) + """.format( + review_form_id=review_form_id, application_form_id=application_form_id + ) op.get_bind().execute(query) - op.get_bind().execute("""SELECT setval('registration_form_id_seq', (SELECT max(id) FROM registration_form));""") + op.get_bind().execute( + """SELECT setval('registration_form_id_seq', (SELECT max(id) FROM registration_form));""" + ) registration_form = RegistrationForm(event_id) session.add(registration_form) session.commit() registration_form_id = registration_form.id - op.get_bind().execute("""SELECT setval('registration_section_id_seq', (SELECT max(id) FROM registration_section));""") + op.get_bind().execute( + """SELECT setval('registration_section_id_seq', (SELECT max(id) FROM registration_section));""" + ) query = """ INSERT INTO registration_section(registration_form_id, name, description, "order", show_for_travel_award, show_for_accommodation_award, show_for_payment_required) SELECT {}, name, description, "order", show_for_travel_award, show_for_accommodation_award, show_for_payment_required FROM registration_section WHERE registration_form_id = 1 - """.format(registration_form_id) + """.format( + registration_form_id + ) op.get_bind().execute(query) - op.get_bind().execute("""SELECT setval('registration_question_id_seq', (SELECT max(id) FROM registration_question));""") + op.get_bind().execute( + """SELECT setval('registration_question_id_seq', (SELECT max(id) FROM registration_question));""" + ) query = """ INSERT INTO registration_question(registration_form_id, section_id, type, description, headline, placeholder, validation_regex, validation_text, "order", options, is_required, depends_on_question_id, hide_for_dependent_value, required_value) @@ -260,10 +306,15 @@ def upgrade(): FROM registration_question q INNER JOIN registration_section s on q.section_id = s.id WHERE q.registration_form_id = 1 - """.format(registration_form_id=registration_form_id) + """.format( + registration_form_id=registration_form_id + ) op.get_bind().execute(query) - op.get_bind().execute("""UPDATE organisation set system_url='http://localhost:8080' where id=3""") + op.get_bind().execute( + """UPDATE organisation set system_url='http://localhost:8080' where id=3""" + ) + def downgrade(): Base.metadata.bind = op.get_bind() @@ -349,4 +400,6 @@ def downgrade(): """ op.get_bind().execute(query) - op.get_bind().execute("""UPDATE organisation set system_url='http://localhost' where id=3""") + op.get_bind().execute( + """UPDATE organisation set system_url='http://localhost' where id=3""" + ) diff --git a/api/migrations/versions/039addb92a03_ai4d_call_launch.py b/api/migrations/versions/039addb92a03_ai4d_call_launch.py index 1425daa32..f328f8ebb 100644 --- a/api/migrations/versions/039addb92a03_ai4d_call_launch.py +++ b/api/migrations/versions/039addb92a03_ai4d_call_launch.py @@ -9,26 +9,27 @@ """ # revision identifiers, used by Alembic. -revision = '039addb92a03' -down_revision = '8339a70d7345' +revision = "039addb92a03" +down_revision = "8339a70d7345" + +import datetime +from enum import Enum -from alembic import op import sqlalchemy as sa -from sqlalchemy import orm +from alembic import op +from sqlalchemy import orm, select from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy import orm -from sqlalchemy import select from sqlalchemy.orm import column_property + from app import db -import datetime -from enum import Enum Base = declarative_base() + class Organisation(Base): __tablename__ = "organisation" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(50), nullable=False) @@ -40,7 +41,17 @@ class Organisation(Base): email_from = db.Column(db.String(100), nullable=True) system_url = db.Column(db.String(100), nullable=False) - def __init__(self, name, system_name, small_logo, large_logo, domain, url, email_from, system_url): + def __init__( + self, + name, + system_name, + small_logo, + large_logo, + domain, + url, + email_from, + system_url, + ): self.name = name self.small_logo = small_logo self.large_logo = large_logo @@ -50,9 +61,10 @@ def __init__(self, name, system_name, small_logo, large_logo, domain, url, email self.email_from = email_from self.system_url = system_url + class Country(Base): __tablename__ = "country" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(100), nullable=False) @@ -60,22 +72,25 @@ class Country(Base): def __init__(self, name): self.name = name + class EventType(Enum): - EVENT = 'event' - AWARD = 'award' - CALL = 'call' + EVENT = "event" + AWARD = "award" + CALL = "call" class Event(Base): __tablename__ = "event" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) start_date = db.Column(db.DateTime(), nullable=False) end_date = db.Column(db.DateTime(), nullable=False) key = db.Column(db.String(255), nullable=False, unique=True) - organisation_id = db.Column(db.Integer(), db.ForeignKey('organisation.id'), nullable=False) + organisation_id = db.Column( + db.Integer(), db.ForeignKey("organisation.id"), nullable=False + ) email_from = db.Column(db.String(255), nullable=False) url = db.Column(db.String(255), nullable=False) application_open = db.Column(db.DateTime(), nullable=False) @@ -92,7 +107,7 @@ class Event(Base): travel_grant = db.Column(db.Boolean(), nullable=False) miniconf_url = db.Column(db.String(100), nullable=True) - event_translations = db.relationship('EventTranslation', lazy='dynamic') + event_translations = db.relationship("EventTranslation", lazy="dynamic") def __init__( self, @@ -116,7 +131,7 @@ def __init__( registration_close, event_type, travel_grant, - miniconf_url=None + miniconf_url=None, ): self.start_date = start_date self.end_date = end_date @@ -148,10 +163,11 @@ def add_event_translations(self, names, descriptions): event_translation = EventTranslation(name, description, language) self.event_translations.append(event_translation) + class EventTranslation(Base): __tablename__ = "event_translation" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) event_id = db.Column(db.Integer(), db.ForeignKey("event.id"), nullable=False) @@ -159,19 +175,20 @@ class EventTranslation(Base): description = db.Column(db.String(255), nullable=False) language = db.Column(db.String(2)) - event = db.relationship('Event', foreign_keys=[event_id]) + event = db.relationship("Event", foreign_keys=[event_id]) def __init__(self, name, description, language): self.name = name self.description = description self.language = language + class ApplicationForm(Base): - __tablename__ = 'application_form' - __table_args__ = {'extend_existing': True} + __tablename__ = "application_form" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - event_id = db.Column(db.Integer(), db.ForeignKey('event.id'), nullable=False) + event_id = db.Column(db.Integer(), db.ForeignKey("event.id"), nullable=False) is_open = db.Column(db.Boolean(), nullable=False) nominations = db.Column(db.Boolean(), nullable=False) @@ -180,22 +197,29 @@ def __init__(self, event_id, is_open, nominations): self.is_open = is_open self.nominations = nominations + class Question(Base): - __tablename__ = 'question' - __table_args__ = {'extend_existing': True} + __tablename__ = "question" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) - section_id = db.Column(db.Integer(), db.ForeignKey('section.id'), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) + section_id = db.Column(db.Integer(), db.ForeignKey("section.id"), nullable=False) type = db.Column(db.String(), nullable=False) order = db.Column(db.Integer(), nullable=False) is_required = db.Column(db.Boolean(), nullable=False) - depends_on_question_id = db.Column(db.Integer(), db.ForeignKey('question.id'), nullable=True) + depends_on_question_id = db.Column( + db.Integer(), db.ForeignKey("question.id"), nullable=True + ) key = db.Column(db.String(255), nullable=True) - question_translations = db.relationship('QuestionTranslation', lazy='dynamic') + question_translations = db.relationship("QuestionTranslation", lazy="dynamic") - def __init__(self, application_form_id, section_id, order, questionType, is_required=True): + def __init__( + self, application_form_id, section_id, order, questionType, is_required=True + ): self.application_form_id = application_form_id self.section_id = section_id self.order = order @@ -203,45 +227,55 @@ def __init__(self, application_form_id, section_id, order, questionType, is_requ self.is_required = is_required def get_translation(self, language): - question_translation = self.question_translations.filter_by(language=language).first() + question_translation = self.question_translations.filter_by( + language=language + ).first() return question_translation class Section(Base): - __tablename__ = 'section' - __table_args__ = {'extend_existing': True} + __tablename__ = "section" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) order = db.Column(db.Integer(), nullable=False) - depends_on_question_id = db.Column(db.Integer(), db.ForeignKey('question.id', use_alter=True), nullable=True) + depends_on_question_id = db.Column( + db.Integer(), db.ForeignKey("question.id", use_alter=True), nullable=True + ) key = db.Column(db.String(255), nullable=True) - section_translations = db.relationship('SectionTranslation', lazy='dynamic') + section_translations = db.relationship("SectionTranslation", lazy="dynamic") - def __init__(self, application_form_id, order, depends_on_question_id=None, key=None): + def __init__( + self, application_form_id, order, depends_on_question_id=None, key=None + ): self.application_form_id = application_form_id self.order = order self.depends_on_question_id = depends_on_question_id self.key = key def get_translation(self, language): - section_translation = self.section_translations.filter_by(language=language).first() + section_translation = self.section_translations.filter_by( + language=language + ).first() return section_translation class SectionTranslation(Base): - __tablename__ = 'section_translation' - __table_args__ = {'extend_existing': True} + __tablename__ = "section_translation" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - section_id = db.Column(db.Integer(), db.ForeignKey('section.id'), nullable=False) + section_id = db.Column(db.Integer(), db.ForeignKey("section.id"), nullable=False) language = db.Column(db.String(2), nullable=False) name = db.Column(db.String(255), nullable=False) description = db.Column(db.String(), nullable=False) show_for_values = db.Column(db.JSON(), nullable=True) - section = db.relationship('Section', foreign_keys=[section_id]) + section = db.relationship("Section", foreign_keys=[section_id]) def __init__(self, section_id, language, name, description, show_for_values=None): self.section_id = section_id @@ -252,11 +286,11 @@ def __init__(self, section_id, language, name, description, show_for_values=None class QuestionTranslation(Base): - __tablename__ = 'question_translation' - __table_args__ = {'extend_existing': True} + __tablename__ = "question_translation" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - question_id = db.Column(db.Integer(), db.ForeignKey('question.id'), nullable=False) + question_id = db.Column(db.Integer(), db.ForeignKey("question.id"), nullable=False) language = db.Column(db.String(2), nullable=False) headline = db.Column(db.String(), nullable=False) description = db.Column(db.String(), nullable=True) @@ -266,7 +300,7 @@ class QuestionTranslation(Base): options = db.Column(db.JSON(), nullable=True) show_for_values = db.Column(db.JSON(), nullable=True) - question = db.relationship('Question', foreign_keys=[question_id]) + question = db.relationship("Question", foreign_keys=[question_id]) def __init__( self, @@ -278,7 +312,7 @@ def __init__( validation_regex=None, validation_text=None, options=None, - show_for_values=None + show_for_values=None, ): self.question_id = question_id self.language = language @@ -295,17 +329,24 @@ def upgrade(): Base.metadata.bind = op.get_bind() session = orm.Session(bind=Base.metadata.bind) - event = session.query(Event).filter_by(key='prc').first() + event = session.query(Event).filter_by(key="prc").first() form = session.query(ApplicationForm).filter_by(event_id=event.id).first() - en = (session.query(SectionTranslation) - .filter_by(language='en', name='Call for Proposals for Policy Research Centres') - .first()) + en = ( + session.query(SectionTranslation) + .filter_by(language="en", name="Call for Proposals for Policy Research Centres") + .first() + ) + + fr = ( + session.query(SectionTranslation) + .filter_by( + language="fr", + name="Appel à Propositions pour les Centres de Recherche Politique", + ) + .first() + ) - fr = (session.query(SectionTranslation) - .filter_by(language='fr', name='Appel à Propositions pour les Centres de Recherche Politique') - .first()) - en_desc = """The International Development Research Centre (IDRC) and the Swedish International Development Agency (Sida) invite proposals from independent policy research organizations from across the African continent that are committed to using research to inform and influence national-level artificial intelligence (AI) policies and research ecosystems. This funding opportunity will provide funding to two (2) AI policy research organizations representing distinct linguistic regions (anglophone and francophone). The goal of this initiative is to enable African think tanks to inform and shape policies and strategies on Artificial Intelligence to support the adoption of responsible AI on the continent, and to ensure African experts have a voice in global fora. diff --git a/api/migrations/versions/04188674173e_.py b/api/migrations/versions/04188674173e_.py index 453a8fb8e..1d944528a 100644 --- a/api/migrations/versions/04188674173e_.py +++ b/api/migrations/versions/04188674173e_.py @@ -7,30 +7,38 @@ """ # revision identifiers, used by Alembic. -revision = '04188674173e' -down_revision = '2717ef90a874' +revision = "04188674173e" +down_revision = "2717ef90a874" -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.add_column('question', sa.Column('depends_on_question_id', sa.Integer(), nullable=True)) - op.add_column('question', sa.Column('show_for_values', sa.JSON(), nullable=True)) - op.create_foreign_key(None, 'question', 'question', ['depends_on_question_id'], ['id']) - op.add_column('section', sa.Column('depends_on_question_id', sa.Integer(), nullable=True)) - op.add_column('section', sa.Column('show_for_values', sa.JSON(), nullable=True)) - op.create_foreign_key(None, 'section', 'question', ['depends_on_question_id'], ['id']) + op.add_column( + "question", sa.Column("depends_on_question_id", sa.Integer(), nullable=True) + ) + op.add_column("question", sa.Column("show_for_values", sa.JSON(), nullable=True)) + op.create_foreign_key( + None, "question", "question", ["depends_on_question_id"], ["id"] + ) + op.add_column( + "section", sa.Column("depends_on_question_id", sa.Integer(), nullable=True) + ) + op.add_column("section", sa.Column("show_for_values", sa.JSON(), nullable=True)) + op.create_foreign_key( + None, "section", "question", ["depends_on_question_id"], ["id"] + ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_constraint(None, 'section', type_='foreignkey') - op.drop_column('section', 'show_for_values') - op.drop_column('section', 'depends_on_question_id') - op.drop_constraint(None, 'question', type_='foreignkey') - op.drop_column('question', 'show_for_values') - op.drop_column('question', 'depends_on_question_id') + op.drop_constraint(None, "section", type_="foreignkey") + op.drop_column("section", "show_for_values") + op.drop_column("section", "depends_on_question_id") + op.drop_constraint(None, "question", type_="foreignkey") + op.drop_column("question", "show_for_values") + op.drop_column("question", "depends_on_question_id") # ### end Alembic commands ### diff --git a/api/migrations/versions/07d536b58953_review_form_stages.py b/api/migrations/versions/07d536b58953_review_form_stages.py index 1c7302eaa..7986616fa 100644 --- a/api/migrations/versions/07d536b58953_review_form_stages.py +++ b/api/migrations/versions/07d536b58953_review_form_stages.py @@ -7,28 +7,28 @@ """ # revision identifiers, used by Alembic. -revision = '07d536b58953' -down_revision = '2dfc0d119bc2' +revision = "07d536b58953" +down_revision = "2dfc0d119bc2" -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.add_column('review_form', sa.Column('active', sa.Boolean(), nullable=True)) - op.add_column('review_form', sa.Column('stage', sa.Integer(), nullable=True)) + op.add_column("review_form", sa.Column("active", sa.Boolean(), nullable=True)) + op.add_column("review_form", sa.Column("stage", sa.Integer(), nullable=True)) op.execute("""UPDATE review_form SET stage = 1, active = True;""") - op.alter_column('review_form', 'active', nullable=False) - op.alter_column('review_form', 'stage', nullable=False) + op.alter_column("review_form", "active", nullable=False) + op.alter_column("review_form", "stage", nullable=False) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('review_form', 'stage') - op.drop_column('review_form', 'active') + op.drop_column("review_form", "stage") + op.drop_column("review_form", "active") # ### end Alembic commands ### diff --git a/api/migrations/versions/09b2cc2105be_.py b/api/migrations/versions/09b2cc2105be_.py index 8cf71d26c..bbf1b00f6 100644 --- a/api/migrations/versions/09b2cc2105be_.py +++ b/api/migrations/versions/09b2cc2105be_.py @@ -7,37 +7,43 @@ """ # revision identifiers, used by Alembic. -revision = '09b2cc2105be' -down_revision = 'a5876b93baee' +revision = "09b2cc2105be" +down_revision = "a5876b93baee" -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.create_table('offer', - sa.Column('id',sa.Integer(),primary_key=True), - sa.Column('user_id',sa.Integer(),nullable=False), - sa.Column('event_id',sa.Integer(),nullable=False), - sa.Column('offer_date',sa.DateTime(),nullable=False), - sa.Column('expiry_date',sa.DateTime(),nullable=False), - sa.Column('payement_required',sa.String(50),nullable=False), - sa.Column('travel_award',sa.String(50),nullable=False), - sa.Column('accomodation_award',sa.String(50),nullable=False), - sa.Column('accepted',sa.String(50),nullable=False), - sa.Column('rejected',sa.String(50),nullable=False), - sa.Column('rejected_reason',sa.String(50),nullable=False), - sa.Column('updated_at',sa.DateTime(),nullable=False), - sa.ForeignKeyConstraint(['event_id'], ['event.id'], ), - sa.ForeignKeyConstraint(['user_id'], ['app_user.id'], ), - sa.PrimaryKeyConstraint('id') + op.create_table( + "offer", + sa.Column("id", sa.Integer(), primary_key=True), + sa.Column("user_id", sa.Integer(), nullable=False), + sa.Column("event_id", sa.Integer(), nullable=False), + sa.Column("offer_date", sa.DateTime(), nullable=False), + sa.Column("expiry_date", sa.DateTime(), nullable=False), + sa.Column("payement_required", sa.String(50), nullable=False), + sa.Column("travel_award", sa.String(50), nullable=False), + sa.Column("accomodation_award", sa.String(50), nullable=False), + sa.Column("accepted", sa.String(50), nullable=False), + sa.Column("rejected", sa.String(50), nullable=False), + sa.Column("rejected_reason", sa.String(50), nullable=False), + sa.Column("updated_at", sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint( + ["event_id"], + ["event.id"], + ), + sa.ForeignKeyConstraint( + ["user_id"], + ["app_user.id"], + ), + sa.PrimaryKeyConstraint("id"), ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('offer') + op.drop_table("offer") # ### end Alembic commands ### - diff --git a/api/migrations/versions/0ab2d6bca67d_.py b/api/migrations/versions/0ab2d6bca67d_.py index 231c0033e..e460d661e 100644 --- a/api/migrations/versions/0ab2d6bca67d_.py +++ b/api/migrations/versions/0ab2d6bca67d_.py @@ -1,5 +1,6 @@ import sqlalchemy as sa from alembic import op + """empty message Revision ID: 0ab2d6bca67d @@ -9,21 +10,24 @@ """ # revision identifiers, used by Alembic. -revision = '0ab2d6bca67d' -down_revision = 'efc34996e565' +revision = "0ab2d6bca67d" +down_revision = "efc34996e565" def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.add_column('app_user', sa.Column( - 'user_dateOfBirth', sa.DateTime(), nullable=True)) - op.add_column('app_user', sa.Column('user_primaryLanguage', - sa.String(length=255), nullable=True)) + op.add_column( + "app_user", sa.Column("user_dateOfBirth", sa.DateTime(), nullable=True) + ) + op.add_column( + "app_user", + sa.Column("user_primaryLanguage", sa.String(length=255), nullable=True), + ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('app_user', 'user_primaryLanguage') - op.drop_column('app_user', 'user_dateOfBirth') + op.drop_column("app_user", "user_primaryLanguage") + op.drop_column("app_user", "user_dateOfBirth") # ### end Alembic commands ### diff --git a/api/migrations/versions/0e13fd6f0d2a_.py b/api/migrations/versions/0e13fd6f0d2a_.py index c3912810a..6ab1ae6fe 100644 --- a/api/migrations/versions/0e13fd6f0d2a_.py +++ b/api/migrations/versions/0e13fd6f0d2a_.py @@ -7,28 +7,32 @@ """ # revision identifiers, used by Alembic. -revision = '0e13fd6f0d2a' -down_revision = '016571f41a20' +revision = "0e13fd6f0d2a" +down_revision = "016571f41a20" -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.create_table('email_template', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('key', sa.String(length=50), nullable=False), - sa.Column('event_id', sa.Integer(), nullable=False), - sa.Column('template', sa.String(), nullable=False), - sa.ForeignKeyConstraint(['event_id'], ['event.id'], ), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('key', 'event_id', name='uq_email_template_key_event_id') + op.create_table( + "email_template", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("key", sa.String(length=50), nullable=False), + sa.Column("event_id", sa.Integer(), nullable=False), + sa.Column("template", sa.String(), nullable=False), + sa.ForeignKeyConstraint( + ["event_id"], + ["event.id"], + ), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("key", "event_id", name="uq_email_template_key_event_id"), ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('email_template') + op.drop_table("email_template") # ### end Alembic commands ### diff --git a/api/migrations/versions/10b4b888c16b_ai4d_first_call_updates.py b/api/migrations/versions/10b4b888c16b_ai4d_first_call_updates.py index 4ebbcb47f..693ac4dc7 100644 --- a/api/migrations/versions/10b4b888c16b_ai4d_first_call_updates.py +++ b/api/migrations/versions/10b4b888c16b_ai4d_first_call_updates.py @@ -9,28 +9,27 @@ """ # revision identifiers, used by Alembic. -revision = '10b4b888c16b' -down_revision = '2241922a3305' +revision = "10b4b888c16b" +down_revision = "2241922a3305" + +import datetime +from enum import Enum -from alembic import op import sqlalchemy as sa -from sqlalchemy import orm +from alembic import op +from sqlalchemy import orm, select from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy import orm -from sqlalchemy import select from sqlalchemy.orm import column_property + from app import db -import datetime -from enum import Enum Base = declarative_base() - class EmailTemplate(Base): - __tablename__ = 'email_template' - __table_args__ = {'extend_existing': True} + __tablename__ = "email_template" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) key = db.Column(db.String(50), nullable=False) @@ -50,7 +49,7 @@ def __init__(self, key, event_id, subject, template, language): class Organisation(Base): __tablename__ = "organisation" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(50), nullable=False) @@ -62,7 +61,17 @@ class Organisation(Base): email_from = db.Column(db.String(100), nullable=True) system_url = db.Column(db.String(100), nullable=False) - def __init__(self, name, system_name, small_logo, large_logo, domain, url, email_from, system_url): + def __init__( + self, + name, + system_name, + small_logo, + large_logo, + domain, + url, + email_from, + system_url, + ): self.name = name self.small_logo = small_logo self.large_logo = large_logo @@ -72,9 +81,10 @@ def __init__(self, name, system_name, small_logo, large_logo, domain, url, email self.email_from = email_from self.system_url = system_url + class Country(Base): __tablename__ = "country" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(100), nullable=False) @@ -82,22 +92,25 @@ class Country(Base): def __init__(self, name): self.name = name + class EventType(Enum): - EVENT = 'event' - AWARD = 'award' - CALL = 'call' + EVENT = "event" + AWARD = "award" + CALL = "call" class Event(Base): __tablename__ = "event" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) start_date = db.Column(db.DateTime(), nullable=False) end_date = db.Column(db.DateTime(), nullable=False) key = db.Column(db.String(255), nullable=False, unique=True) - organisation_id = db.Column(db.Integer(), db.ForeignKey('organisation.id'), nullable=False) + organisation_id = db.Column( + db.Integer(), db.ForeignKey("organisation.id"), nullable=False + ) email_from = db.Column(db.String(255), nullable=False) url = db.Column(db.String(255), nullable=False) application_open = db.Column(db.DateTime(), nullable=False) @@ -114,7 +127,7 @@ class Event(Base): travel_grant = db.Column(db.Boolean(), nullable=False) miniconf_url = db.Column(db.String(100), nullable=True) - event_translations = db.relationship('EventTranslation', lazy='dynamic') + event_translations = db.relationship("EventTranslation", lazy="dynamic") def __init__( self, @@ -138,7 +151,7 @@ def __init__( registration_close, event_type, travel_grant, - miniconf_url=None + miniconf_url=None, ): self.start_date = start_date self.end_date = end_date @@ -170,10 +183,11 @@ def add_event_translations(self, names, descriptions): event_translation = EventTranslation(name, description, language) self.event_translations.append(event_translation) + class EventTranslation(Base): __tablename__ = "event_translation" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) event_id = db.Column(db.Integer(), db.ForeignKey("event.id"), nullable=False) @@ -181,19 +195,20 @@ class EventTranslation(Base): description = db.Column(db.String(255), nullable=False) language = db.Column(db.String(2)) - event = db.relationship('Event', foreign_keys=[event_id]) + event = db.relationship("Event", foreign_keys=[event_id]) def __init__(self, name, description, language): self.name = name self.description = description self.language = language + class ApplicationForm(Base): - __tablename__ = 'application_form' - __table_args__ = {'extend_existing': True} + __tablename__ = "application_form" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - event_id = db.Column(db.Integer(), db.ForeignKey('event.id'), nullable=False) + event_id = db.Column(db.Integer(), db.ForeignKey("event.id"), nullable=False) is_open = db.Column(db.Boolean(), nullable=False) nominations = db.Column(db.Boolean(), nullable=False) @@ -202,22 +217,29 @@ def __init__(self, event_id, is_open, nominations): self.is_open = is_open self.nominations = nominations + class Question(Base): - __tablename__ = 'question' - __table_args__ = {'extend_existing': True} + __tablename__ = "question" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) - section_id = db.Column(db.Integer(), db.ForeignKey('section.id'), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) + section_id = db.Column(db.Integer(), db.ForeignKey("section.id"), nullable=False) type = db.Column(db.String(), nullable=False) order = db.Column(db.Integer(), nullable=False) is_required = db.Column(db.Boolean(), nullable=False) - depends_on_question_id = db.Column(db.Integer(), db.ForeignKey('question.id'), nullable=True) + depends_on_question_id = db.Column( + db.Integer(), db.ForeignKey("question.id"), nullable=True + ) key = db.Column(db.String(255), nullable=True) - question_translations = db.relationship('QuestionTranslation', lazy='dynamic') + question_translations = db.relationship("QuestionTranslation", lazy="dynamic") - def __init__(self, application_form_id, section_id, order, questionType, is_required=True): + def __init__( + self, application_form_id, section_id, order, questionType, is_required=True + ): self.application_form_id = application_form_id self.section_id = section_id self.order = order @@ -225,39 +247,49 @@ def __init__(self, application_form_id, section_id, order, questionType, is_requ self.is_required = is_required def get_translation(self, language): - question_translation = self.question_translations.filter_by(language=language).first() + question_translation = self.question_translations.filter_by( + language=language + ).first() return question_translation class Section(Base): - __tablename__ = 'section' - __table_args__ = {'extend_existing': True} + __tablename__ = "section" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) order = db.Column(db.Integer(), nullable=False) - depends_on_question_id = db.Column(db.Integer(), db.ForeignKey('question.id', use_alter=True), nullable=True) + depends_on_question_id = db.Column( + db.Integer(), db.ForeignKey("question.id", use_alter=True), nullable=True + ) key = db.Column(db.String(255), nullable=True) - section_translations = db.relationship('SectionTranslation', lazy='dynamic') + section_translations = db.relationship("SectionTranslation", lazy="dynamic") - def __init__(self, application_form_id, order, depends_on_question_id=None, key=None): + def __init__( + self, application_form_id, order, depends_on_question_id=None, key=None + ): self.application_form_id = application_form_id self.order = order self.depends_on_question_id = depends_on_question_id self.key = key def get_translation(self, language): - section_translation = self.section_translations.filter_by(language=language).first() + section_translation = self.section_translations.filter_by( + language=language + ).first() return section_translation class SectionTranslation(Base): - __tablename__ = 'section_translation' - __table_args__ = {'extend_existing': True} + __tablename__ = "section_translation" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - section_id = db.Column(db.Integer(), db.ForeignKey('section.id'), nullable=False) + section_id = db.Column(db.Integer(), db.ForeignKey("section.id"), nullable=False) language = db.Column(db.String(2), nullable=False) name = db.Column(db.String(255), nullable=False) description = db.Column(db.String(), nullable=False) @@ -272,11 +304,11 @@ def __init__(self, section_id, language, name, description, show_for_values=None class QuestionTranslation(Base): - __tablename__ = 'question_translation' - __table_args__ = {'extend_existing': True} + __tablename__ = "question_translation" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - question_id = db.Column(db.Integer(), db.ForeignKey('question.id'), nullable=False) + question_id = db.Column(db.Integer(), db.ForeignKey("question.id"), nullable=False) language = db.Column(db.String(2), nullable=False) headline = db.Column(db.String(), nullable=False) description = db.Column(db.String(), nullable=True) @@ -286,7 +318,7 @@ class QuestionTranslation(Base): options = db.Column(db.JSON(), nullable=True) show_for_values = db.Column(db.JSON(), nullable=True) - question = db.relationship('Question', foreign_keys=[question_id]) + question = db.relationship("Question", foreign_keys=[question_id]) def __init__( self, @@ -298,7 +330,7 @@ def __init__( validation_regex=None, validation_text=None, options=None, - show_for_values=None + show_for_values=None, ): self.question_id = question_id self.language = language @@ -314,10 +346,12 @@ def __init__( class Response(Base): __tablename__ = "response" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(),db.ForeignKey("application_form.id"), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) user_id = db.Column(db.Integer(), db.ForeignKey("app_user.id"), nullable=False) is_submitted = db.Column(db.Boolean(), nullable=False) submitted_timestamp = db.Column(db.DateTime(), nullable=True) @@ -326,7 +360,7 @@ class Response(Base): started_timestamp = db.Column(db.DateTime(), nullable=True) language = db.Column(db.String(2), nullable=False) - answers = db.relationship('Answer', order_by='Answer.order') + answers = db.relationship("Answer", order_by="Answer.order") def __init__(self, application_form_id, user_id, language): self.application_form_id = application_form_id @@ -341,138 +375,147 @@ def __init__(self, application_form_id, user_id, language): class Answer(Base): __tablename__ = "answer" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) response_id = db.Column(db.Integer(), db.ForeignKey("response.id"), nullable=False) question_id = db.Column(db.Integer(), db.ForeignKey("question.id"), nullable=False) value = db.Column(db.String(), nullable=False) - order = column_property(select([Question.order]).where(Question.id==question_id).correlate_except(Question)) + order = column_property( + select([Question.order]) + .where(Question.id == question_id) + .correlate_except(Question) + ) def __init__(self, response_id, question_id, value): self.response_id = response_id self.question_id = question_id self.value = value + en_countries = [ - 'Algeria', - 'Angola', - 'Benin', - 'Botswana', - 'Burkina Faso', - 'Burundi', - 'Cameroon', - 'Cape Verde', - 'Central African Republic', - 'Chad', - 'Comoros', - 'Democratic Republic of the Congo', - 'Republic of the Congo', - 'Côte d’Ivoire', - 'Djibouti', - 'Egypt', - 'Equatorial Guinea', - 'Eritrea', - 'Eswatini', - 'Ethiopia', - 'Gabon', - 'The Gambia', - 'Ghana', - 'Guinea', - 'Guinea-Bissau', - 'Kenya', - 'Lesotho', - 'Liberia', - 'Libya', - 'Madagascar', - 'Malawi', - 'Mali', - 'Mauritania', - 'Mauritius', - 'Morocco', - 'Mozambique', - 'Namibia', - 'Niger', - 'Nigeria', - 'Rwanda', - 'Sao Tome and Principe', - 'Senegal', - 'Seychelles', - 'Sierra Leone', - 'Somalia', - 'South Africa', - 'South Sudan', - 'Sudan', - 'Tanzania', - 'Togo', - 'Tunisia', - 'Uganda', - 'Zambia', - 'Zimbabwe' + "Algeria", + "Angola", + "Benin", + "Botswana", + "Burkina Faso", + "Burundi", + "Cameroon", + "Cape Verde", + "Central African Republic", + "Chad", + "Comoros", + "Democratic Republic of the Congo", + "Republic of the Congo", + "Côte d’Ivoire", + "Djibouti", + "Egypt", + "Equatorial Guinea", + "Eritrea", + "Eswatini", + "Ethiopia", + "Gabon", + "The Gambia", + "Ghana", + "Guinea", + "Guinea-Bissau", + "Kenya", + "Lesotho", + "Liberia", + "Libya", + "Madagascar", + "Malawi", + "Mali", + "Mauritania", + "Mauritius", + "Morocco", + "Mozambique", + "Namibia", + "Niger", + "Nigeria", + "Rwanda", + "Sao Tome and Principe", + "Senegal", + "Seychelles", + "Sierra Leone", + "Somalia", + "South Africa", + "South Sudan", + "Sudan", + "Tanzania", + "Togo", + "Tunisia", + "Uganda", + "Zambia", + "Zimbabwe", ] fr_countries = [ - 'Algérie', - 'Angola', - 'Bénin', - 'Botswana', - 'Burkina Faso', - 'Burundi', - 'Cameroun', - 'Cap-Vert', - 'République centrafricaine', - 'Tchad', - 'Comores', - 'République démocratique du Congo', - 'République du Congo', - 'Côte d’Ivoire', - 'Djibouti', - 'Égypte', - 'Guinée équatoriale', - 'Érythrée', - 'Eswatini', - 'Éthiopie', - 'Gabon', - 'Gambie', - 'Ghana', - 'Guinée', - 'Guinée-Bissau', - 'Kenya', - 'Lesotho', - 'Libéria', - 'Libye', - 'Madagascar', - 'Malawi', - 'Mali', - 'Maroc', - 'Mauritanie', - 'île Maurice', - 'Mozambique', - 'Namibie', - 'Niger', - 'Nigeria', - 'Rwanda', - 'Sao Tomé-et-Principe', - 'Sénégal', - 'Seychelles', - 'Sierra Leone', - 'Somalie', - 'Afrique du Sud', - 'Soudan du Sud', - 'Soudan', - 'Tanzanie', - 'Togo', - 'Tunisie', - 'Ouganda', - 'Zambie', - 'Zimbabwe' + "Algérie", + "Angola", + "Bénin", + "Botswana", + "Burkina Faso", + "Burundi", + "Cameroun", + "Cap-Vert", + "République centrafricaine", + "Tchad", + "Comores", + "République démocratique du Congo", + "République du Congo", + "Côte d’Ivoire", + "Djibouti", + "Égypte", + "Guinée équatoriale", + "Érythrée", + "Eswatini", + "Éthiopie", + "Gabon", + "Gambie", + "Ghana", + "Guinée", + "Guinée-Bissau", + "Kenya", + "Lesotho", + "Libéria", + "Libye", + "Madagascar", + "Malawi", + "Mali", + "Maroc", + "Mauritanie", + "île Maurice", + "Mozambique", + "Namibie", + "Niger", + "Nigeria", + "Rwanda", + "Sao Tomé-et-Principe", + "Sénégal", + "Seychelles", + "Sierra Leone", + "Somalie", + "Afrique du Sud", + "Soudan du Sud", + "Soudan", + "Tanzanie", + "Togo", + "Tunisie", + "Ouganda", + "Zambie", + "Zimbabwe", ] def upgrade(): Base.metadata.bind = op.get_bind() session = orm.Session(bind=Base.metadata.bind) - password_reset_template = session.query(EmailTemplate).filter_by(key='password-reset', language='en').first() + password_reset_template = ( + session.query(EmailTemplate) + .filter_by(key="password-reset", language="en") + .first() + ) password_reset_template.template = """Dear {title} {firstname} {lastname}, You recently requested a password reset on {system_name}, please use the following link to reset you password: @@ -491,9 +534,15 @@ def upgrade(): Cordialement, Les organisateurs de {event_name}""" - session.add(EmailTemplate('withdrawal', None, 'Retrait de la demande de {event_name}', template, 'en')) - - withdrawal_template = session.query(EmailTemplate).filter_by(key='withdrawal', language='en').first() + session.add( + EmailTemplate( + "withdrawal", None, "Retrait de la demande de {event_name}", template, "en" + ) + ) + + withdrawal_template = ( + session.query(EmailTemplate).filter_by(key="withdrawal", language="en").first() + ) withdrawal_template.template = """Dear {title} {firstname} {lastname}, This email serves to confirm that you have withdrawn your application to {event_name}. @@ -505,9 +554,9 @@ def upgrade(): session.commit() - event = session.query(Event).filter_by(key='prc').first() + event = session.query(Event).filter_by(key="prc").first() form = session.query(ApplicationForm).filter_by(event_id=event.id).first() - + # Delete responses responses = session.query(Response).filter_by(application_form_id=form.id).all() for response in responses: @@ -530,24 +579,40 @@ def upgrade(): session.query(Section).filter_by(application_form_id=form.id).delete() session.commit() - def add_section(names, descriptions, order, depends_on_question_id=None, key=None, show_for_values=None): + def add_section( + names, + descriptions, + order, + depends_on_question_id=None, + key=None, + show_for_values=None, + ): section = Section(form.id, order) session.add(section) session.commit() translations = [] for language in names: - translations.append(SectionTranslation( - section.id, - language, - names[language], - '' if descriptions is None else descriptions[language], - show_for_values=None if show_for_values is None else show_for_values[language])) + translations.append( + SectionTranslation( + section.id, + language, + names[language], + "" if descriptions is None else descriptions[language], + show_for_values=None + if show_for_values is None + else show_for_values[language], + ) + ) session.add_all(translations) session.commit() return section - def add_question(section, order, questionType, headlines, + def add_question( + section, + order, + questionType, + headlines, descriptions=None, placeholders=None, validation_regexs=None, @@ -555,8 +620,11 @@ def add_question(section, order, questionType, headlines, options=None, show_for_values=None, is_required=True, - depends_on_question_id=None): - question = Question(form.id, section.id, order, questionType, is_required=is_required) + depends_on_question_id=None, + ): + question = Question( + form.id, section.id, order, questionType, is_required=is_required + ) if depends_on_question_id is not None: question.depends_on_question_id = depends_on_question_id @@ -566,14 +634,29 @@ def add_question(section, order, questionType, headlines, translations = [] for language in headlines: - translations.append(QuestionTranslation(question.id, language, - headline=headlines[language], - description=None if descriptions is None else descriptions[language], - placeholder=None if placeholders is None else placeholders[language], - validation_regex=None if validation_regexs is None else validation_regexs[language], - validation_text=None if validation_texts is None else validation_texts[language], - options=None if options is None else options[language], - show_for_values=None if show_for_values is None else show_for_values[language])) + translations.append( + QuestionTranslation( + question.id, + language, + headline=headlines[language], + description=None + if descriptions is None + else descriptions[language], + placeholder=None + if placeholders is None + else placeholders[language], + validation_regex=None + if validation_regexs is None + else validation_regexs[language], + validation_text=None + if validation_texts is None + else validation_texts[language], + options=None if options is None else options[language], + show_for_values=None + if show_for_values is None + else show_for_values[language], + ) + ) session.add_all(translations) session.commit() return question @@ -598,565 +681,703 @@ def add_question(section, order, questionType, headlines, Pour toute question, veuillez envoyer un e-mail à calls@ai4d.ai""" # (Description Page) - add_section({ - 'en': 'Call for Proposals for Policy Research Centres', - 'fr': 'Appel à Propositions pour les Centres de Recherche Politique' - }, - {'en': en_description, 'fr': fr_description}, - 1) + add_section( + { + "en": "Call for Proposals for Policy Research Centres", + "fr": "Appel à Propositions pour les Centres de Recherche Politique", + }, + {"en": en_description, "fr": fr_description}, + 1, + ) # Organisation and lead contact details - org_section = add_section({ - 'en': 'Organization and lead applicant contact details', - 'fr': "Coordonnées de l'organisation et de la personne-ressource pour cette demande" - }, None, 2) - - add_question(org_section, 1, 'information', { - 'en': 'Organization Information', - 'fr': 'Renseignements sur l’organisation' - }, is_required=False) - - add_question(org_section, 2, 'short-text', { - 'en': 'Name of Organisation', - 'fr': 'Nom de l’organisation' - }, is_required=True) - - add_question(org_section, 3, 'short-text', { - 'en': 'Director/CEO', - 'fr': 'Directeur ou premier dirigeant' - }, is_required=True) - - add_question(org_section, 4, 'short-text', { - 'en': 'Mailing address', - 'fr': 'Adresse postale' - }, is_required=True) - - add_question(org_section, 5, 'short-text', { - 'en': 'City', - 'fr': 'Ville' - }, is_required=True) + org_section = add_section( + { + "en": "Organization and lead applicant contact details", + "fr": "Coordonnées de l'organisation et de la personne-ressource pour cette demande", + }, + None, + 2, + ) + + add_question( + org_section, + 1, + "information", + {"en": "Organization Information", "fr": "Renseignements sur l’organisation"}, + is_required=False, + ) + + add_question( + org_section, + 2, + "short-text", + {"en": "Name of Organisation", "fr": "Nom de l’organisation"}, + is_required=True, + ) - en_options = [ - {'label': c, 'value': c} for c in en_countries - ] - fr_options = [ - {'label': f, 'value': e} for e, f in zip(en_countries, fr_countries) - ] + add_question( + org_section, + 3, + "short-text", + {"en": "Director/CEO", "fr": "Directeur ou premier dirigeant"}, + is_required=True, + ) - add_question(org_section, 6, 'multi-choice', { - 'en': 'Country', - 'fr': 'Pays' - }, - descriptions={ - 'en': 'Eligible countries in sub-Saharan and North Africa', - 'fr': 'Pays admissibles de l’Afrique subsaharienne et de l’Afrique du Nord' - }, - placeholders={ - 'en': 'Select a Country', - 'fr': 'Choisissez un Pays' - }, - options={ - 'en': en_options, - 'fr': fr_options - }, - is_required=True) - - add_question(org_section, 7, 'short-text', { - 'en': 'Telephone', - 'fr': 'Numéro de téléphone' - }, is_required=True) - - add_question(org_section, 8, 'short-text', { - 'en': 'Mobile (Optional)', - 'fr': 'Numéro de téléphone mobile (facultatif)' - }, is_required=False) - - add_question(org_section, 9, 'short-text', { - 'en': 'Email Address', - 'fr': 'Adresse de courriel' - }, is_required=True) - - add_question(org_section, 10, 'short-text', { - 'en': 'Website', - 'fr': 'Site Web' - }, is_required=False) - - add_question(org_section, 11, 'short-text', { - 'en': 'Social media handles', - 'fr': 'Pseudonymes sur les médias sociaux' - }, is_required=False) - - add_question(org_section, 12, 'information', { - 'en': 'Name and contact details of principal contact for the application', - 'fr': 'Nom et coordonnées de la personne-ressource pour cette demande' - }, is_required=False) - - add_question(org_section, 13, 'short-text', { - 'en': 'Name of principal contact', - 'fr': 'Nom du contact principal' - }, is_required=True) - - add_question(org_section, 14, 'short-text', { - 'en': 'Email Address of principal contact', - 'fr': 'Adresse de courriel du contact principal' - }, is_required=True) - - add_question(org_section, 15, 'short-text', { - 'en': 'Telephone/Mobile of principal contact', - 'fr': 'Numéro de téléphone ou de téléphone mobile du contact principal' - }, is_required=True) + add_question( + org_section, + 4, + "short-text", + {"en": "Mailing address", "fr": "Adresse postale"}, + is_required=True, + ) + + add_question( + org_section, 5, "short-text", {"en": "City", "fr": "Ville"}, is_required=True + ) + + en_options = [{"label": c, "value": c} for c in en_countries] + fr_options = [{"label": f, "value": e} for e, f in zip(en_countries, fr_countries)] + + add_question( + org_section, + 6, + "multi-choice", + {"en": "Country", "fr": "Pays"}, + descriptions={ + "en": "Eligible countries in sub-Saharan and North Africa", + "fr": "Pays admissibles de l’Afrique subsaharienne et de l’Afrique du Nord", + }, + placeholders={"en": "Select a Country", "fr": "Choisissez un Pays"}, + options={"en": en_options, "fr": fr_options}, + is_required=True, + ) + + add_question( + org_section, + 7, + "short-text", + {"en": "Telephone", "fr": "Numéro de téléphone"}, + is_required=True, + ) + + add_question( + org_section, + 8, + "short-text", + {"en": "Mobile (Optional)", "fr": "Numéro de téléphone mobile (facultatif)"}, + is_required=False, + ) + + add_question( + org_section, + 9, + "short-text", + {"en": "Email Address", "fr": "Adresse de courriel"}, + is_required=True, + ) + + add_question( + org_section, + 10, + "short-text", + {"en": "Website", "fr": "Site Web"}, + is_required=False, + ) + + add_question( + org_section, + 11, + "short-text", + {"en": "Social media handles", "fr": "Pseudonymes sur les médias sociaux"}, + is_required=False, + ) + + add_question( + org_section, + 12, + "information", + { + "en": "Name and contact details of principal contact for the application", + "fr": "Nom et coordonnées de la personne-ressource pour cette demande", + }, + is_required=False, + ) + + add_question( + org_section, + 13, + "short-text", + {"en": "Name of principal contact", "fr": "Nom du contact principal"}, + is_required=True, + ) + + add_question( + org_section, + 14, + "short-text", + { + "en": "Email Address of principal contact", + "fr": "Adresse de courriel du contact principal", + }, + is_required=True, + ) + + add_question( + org_section, + 15, + "short-text", + { + "en": "Telephone/Mobile of principal contact", + "fr": "Numéro de téléphone ou de téléphone mobile du contact principal", + }, + is_required=True, + ) # Section 1 - section1 = add_section({ - 'en': 'Section I', - 'fr': 'Section I' - }, None, 3) - - add_question(section1, 1, 'information', { - 'en': '1. Organizational capacity and experience', - 'fr': 'Expérience et capacités de l’organisation' - }, is_required=False) - - add_question(section1, 2, 'short-text', { - 'en': 'How many people in total work in your organization?', - 'fr': 'Combien d’employés compte votre organisation en tout?' - }, descriptions={ - 'en': 'Total number of employees', - 'fr': 'Nombre total d’employés' - }, - is_required=True) - - add_question(section1, 3, 'short-text', { - 'en': 'Of these, how many are full time policy researchers?', - 'fr': 'Combien d’entre eux occupent un poste de chercheur en matière de politiques à temps plein?' - }, is_required=True) - - add_question(section1, 4, 'multi-file', { - 'en': "Please provide the names and CVs of your organization's principal researchers (up to five)", - 'fr': 'Veuillez fournir les noms et les CV des principaux chercheurs de votre organisation (maximum de cinq)' - }, descriptions={ - 'en': 'Upload the CVs of relevant researchers in the organization here.', - 'fr': 'Téléchargez ici les CV des chercheurs concernés de votre organisation' - }, is_required=True, options={ - 'en': {'num_uploads': 5}, - 'fr': {'num_uploads': 5}, - }, placeholders={ - 'en': "Enter person's name", - 'fr': "Entrez le nom de la personne", - }) - - add_question(section1, 5, 'long-text', { - 'en': '2. Mission for Responsible AI', - 'fr': '2. Mission pour une IA responsable' - }, descriptions={ - 'en': """Responsible AI is an approach to artificial intelligence that is inclusive, sustainable, ethical and which supports human rights. Please describe the mission of your organization as it relates to the principles of responsible AI. + section1 = add_section({"en": "Section I", "fr": "Section I"}, None, 3) + + add_question( + section1, + 1, + "information", + { + "en": "1. Organizational capacity and experience", + "fr": "Expérience et capacités de l’organisation", + }, + is_required=False, + ) + + add_question( + section1, + 2, + "short-text", + { + "en": "How many people in total work in your organization?", + "fr": "Combien d’employés compte votre organisation en tout?", + }, + descriptions={ + "en": "Total number of employees", + "fr": "Nombre total d’employés", + }, + is_required=True, + ) + + add_question( + section1, + 3, + "short-text", + { + "en": "Of these, how many are full time policy researchers?", + "fr": "Combien d’entre eux occupent un poste de chercheur en matière de politiques à temps plein?", + }, + is_required=True, + ) + + add_question( + section1, + 4, + "multi-file", + { + "en": "Please provide the names and CVs of your organization's principal researchers (up to five)", + "fr": "Veuillez fournir les noms et les CV des principaux chercheurs de votre organisation (maximum de cinq)", + }, + descriptions={ + "en": "Upload the CVs of relevant researchers in the organization here.", + "fr": "Téléchargez ici les CV des chercheurs concernés de votre organisation", + }, + is_required=True, + options={ + "en": {"num_uploads": 5}, + "fr": {"num_uploads": 5}, + }, + placeholders={ + "en": "Enter person's name", + "fr": "Entrez le nom de la personne", + }, + ) + + add_question( + section1, + 5, + "long-text", + { + "en": "2. Mission for Responsible AI", + "fr": "2. Mission pour une IA responsable", + }, + descriptions={ + "en": """Responsible AI is an approach to artificial intelligence that is inclusive, sustainable, ethical and which supports human rights. Please describe the mission of your organization as it relates to the principles of responsible AI. Maximum 250 words""", - 'fr': """L’intelligence artificielle (IA) responsable est une approche de l’IA à la fois inclusive, durable, éthique et fondée sur les droits de la personne. Veuillez décrire la mission de votre organisation par rapport aux principes de l’IA responsable. + "fr": """L’intelligence artificielle (IA) responsable est une approche de l’IA à la fois inclusive, durable, éthique et fondée sur les droits de la personne. Veuillez décrire la mission de votre organisation par rapport aux principes de l’IA responsable. -Maximum 250 mots""" - }, placeholders={ - 'en': 'Enter up to 250 words', - 'fr': "Entrez jusqu'à 250 mots" - }, validation_regexs={ - 'en': r'^\s*(\S+(\s+|$)){0,250}$', - 'fr': r'^\s*(\S+(\s+|$)){0,250}$', - }, validation_texts={ - 'en': 'Maximum 250 words', - 'fr': 'Maximum 250 mots' - }, is_required=True) +Maximum 250 mots""", + }, + placeholders={"en": "Enter up to 250 words", "fr": "Entrez jusqu'à 250 mots"}, + validation_regexs={ + "en": r"^\s*(\S+(\s+|$)){0,250}$", + "fr": r"^\s*(\S+(\s+|$)){0,250}$", + }, + validation_texts={"en": "Maximum 250 words", "fr": "Maximum 250 mots"}, + is_required=True, + ) en_options = [ - 'Enabling beneficial AI research and development', - 'Economic impacts, labor shifts, inequality, and technological unemployment', - 'Accountability, transparency, and explainability', - 'Surveillance, privacy, and civil liberties', - 'Fairness, ethics, and human rights', - 'Diversity and gender equality', - 'Data capacity, analytics and governance', - 'Political manipulation and computational propaganda', - 'Human dignity, autonomy, and psychological impact', - 'AI safety' - ] - - fr_options = [ - 'Recherche-développement utile en lien avec l’IA', - 'Répercussions économiques, mouvements de main-d’oeuvre, inégalité et chômage technologique', - 'Responsabilité, transparence et explicabilité', - 'Surveillance, vie privée et libertés civiles', - 'Équité, éthique et droits de l’homme', - 'Diversité et égalité des genres', - 'Capacités en matière de données, d’analyses et de gouvernance', - 'Manipulation politique et propagande virtuelle', - 'Dignité humaine, autonomie et répercussions psychologiques', - 'Sécurité de l’IA' + "Enabling beneficial AI research and development", + "Economic impacts, labor shifts, inequality, and technological unemployment", + "Accountability, transparency, and explainability", + "Surveillance, privacy, and civil liberties", + "Fairness, ethics, and human rights", + "Diversity and gender equality", + "Data capacity, analytics and governance", + "Political manipulation and computational propaganda", + "Human dignity, autonomy, and psychological impact", + "AI safety", ] fr_options = [ - {'label': f, 'value': e} for e, f in zip(en_options, fr_options) - ] - en_options = [ - {'label': o, 'value': o} for o in en_options + "Recherche-développement utile en lien avec l’IA", + "Répercussions économiques, mouvements de main-d’oeuvre, inégalité et chômage technologique", + "Responsabilité, transparence et explicabilité", + "Surveillance, vie privée et libertés civiles", + "Équité, éthique et droits de l’homme", + "Diversité et égalité des genres", + "Capacités en matière de données, d’analyses et de gouvernance", + "Manipulation politique et propagande virtuelle", + "Dignité humaine, autonomie et répercussions psychologiques", + "Sécurité de l’IA", ] - add_question(section1, 6, 'multi-checkbox', { - 'en': '3. Demonstrated expertise', - 'fr': '3. Une expertise reconnue' - }, descriptions={ - 'en': 'Please indicate the subject areas that your organization has experience in developing policy research and frameworks for, as they relate to artificial intelligence or any advanced technologies. Select all that apply.', - 'fr': 'Veuillez préciser les domaines dans lesquels votre organisation a acquis de l’expérience en matière de recherches et de cadres stratégiques liés à l’IA ou à d’autres technologies de pointe. Cochez toutes les réponses qui s’appliquent.' - }, options={ - 'en': en_options, - 'fr': fr_options - }, is_required=True) - - add_question(section1, 7, 'long-text', { - 'en': 'Examples', - 'fr': 'Exemples' - }, descriptions={ - 'en': 'Please provide three to five examples here that demonstrate your research and policy analysis conducted in these areas. You may provide links, or upload documents and/or PDFs.', - 'fr': 'Veuillez fournir trois à cinq exemples qui témoignent des recherches et des analyses des politiques que vous avez effectuées dans ces domaines. Vous pouvez fournir des liens ou télécharger des documents ou des documents PDF.' - }, is_required=True) - - add_question(section1, 8, 'multi-file', { - 'en': 'Examples: Supporting documents', - 'fr': 'Exemples: Documents justificatifs' - }, is_required=False) + fr_options = [{"label": f, "value": e} for e, f in zip(en_options, fr_options)] + en_options = [{"label": o, "value": o} for o in en_options] + + add_question( + section1, + 6, + "multi-checkbox", + {"en": "3. Demonstrated expertise", "fr": "3. Une expertise reconnue"}, + descriptions={ + "en": "Please indicate the subject areas that your organization has experience in developing policy research and frameworks for, as they relate to artificial intelligence or any advanced technologies. Select all that apply.", + "fr": "Veuillez préciser les domaines dans lesquels votre organisation a acquis de l’expérience en matière de recherches et de cadres stratégiques liés à l’IA ou à d’autres technologies de pointe. Cochez toutes les réponses qui s’appliquent.", + }, + options={"en": en_options, "fr": fr_options}, + is_required=True, + ) + + add_question( + section1, + 7, + "long-text", + {"en": "Examples", "fr": "Exemples"}, + descriptions={ + "en": "Please provide three to five examples here that demonstrate your research and policy analysis conducted in these areas. You may provide links, or upload documents and/or PDFs.", + "fr": "Veuillez fournir trois à cinq exemples qui témoignent des recherches et des analyses des politiques que vous avez effectuées dans ces domaines. Vous pouvez fournir des liens ou télécharger des documents ou des documents PDF.", + }, + is_required=True, + ) + + add_question( + section1, + 8, + "multi-file", + { + "en": "Examples: Supporting documents", + "fr": "Exemples: Documents justificatifs", + }, + is_required=False, + ) # Section 2 - section2 = add_section({ - 'en': 'Section II: Policy Research', - 'fr': 'Section II: Recherche sur les politiques' - }, None, 4) - - add_question(section2, 1, 'long-text', { - 'en': '1. Research background, problem statement and justification', - 'fr': '1. Contexte, énoncé du problème et justification' - }, descriptions={ - 'en': """Please briefly outline the top three priorities of your research agenda and what the key research questions will be as an AI Policy Center. Explain why you are selecting these priorities. Provide a background and review of the local context which describes these priorities within your country/region and how the focus of this project clearly links with the local needs of the country and region. Please also indicate whether others are addressing this problem and why these proposed priorities are justified, or whether the project derives some of its importance from involving research which is largely neglected. + section2 = add_section( + { + "en": "Section II: Policy Research", + "fr": "Section II: Recherche sur les politiques", + }, + None, + 4, + ) + + add_question( + section2, + 1, + "long-text", + { + "en": "1. Research background, problem statement and justification", + "fr": "1. Contexte, énoncé du problème et justification", + }, + descriptions={ + "en": """Please briefly outline the top three priorities of your research agenda and what the key research questions will be as an AI Policy Center. Explain why you are selecting these priorities. Provide a background and review of the local context which describes these priorities within your country/region and how the focus of this project clearly links with the local needs of the country and region. Please also indicate whether others are addressing this problem and why these proposed priorities are justified, or whether the project derives some of its importance from involving research which is largely neglected. Maximum 1000 words""", - 'fr': """Veuillez décrire brièvement les trois principales priorités de votre programme de recherche et les principales questions de recherche que vous aborderez en tant que centre des politiques sur l’IA, puis expliquer pourquoi vous avez sélectionné ces priorités. Donnez une vue d’ensemble et effectuez un examen du contexte local décrivant ces priorités dans le pays ou la région et expliquez le lien établi entre l’axe thématique du projet et les besoins particuliers du pays ou de la région. Vous devez également préciser s’il existe d’autres parties concernées qui se penchent sur le problème énoncé, les raisons justifiant les priorités proposées, et si l’importance du projet tient au fait qu’il porte sur un sujet de recherche grandement négligé. - -Maximum 1000 mots""" - }, placeholders={ - 'en': 'Enter up to 1000 words', - 'fr': "Entrez jusqu'à 1000 mots" - }, validation_regexs={ - 'en': r'^\s*(\S+(\s+|$)){0,1000}$', - 'fr': r'^\s*(\S+(\s+|$)){0,1000}$', - }, validation_texts={ - 'en': 'Maximum 1000 words', - 'fr': 'Maximum 1000 mots' - }, is_required=True) - - add_question(section2, 2, 'long-text', { - 'en': '2. General and specific objectives', - 'fr': '2. Objectifs généraux et précis.' - }, descriptions={ - 'en': """The general objective should state the development goal being pursued by the research. The specific objectives should indicate the specific types of knowledge to be produced, the audiences to be reached, and forms of capacity to be reinforced. These are the objectives against which the success of the project will be judged. Use only active verbs (no passive). Typical objectives focus on 1) deepening understanding of a topic through research; 2) influencing policy and practice; and 3) building capacity. + "fr": """Veuillez décrire brièvement les trois principales priorités de votre programme de recherche et les principales questions de recherche que vous aborderez en tant que centre des politiques sur l’IA, puis expliquer pourquoi vous avez sélectionné ces priorités. Donnez une vue d’ensemble et effectuez un examen du contexte local décrivant ces priorités dans le pays ou la région et expliquez le lien établi entre l’axe thématique du projet et les besoins particuliers du pays ou de la région. Vous devez également préciser s’il existe d’autres parties concernées qui se penchent sur le problème énoncé, les raisons justifiant les priorités proposées, et si l’importance du projet tient au fait qu’il porte sur un sujet de recherche grandement négligé. + +Maximum 1000 mots""", + }, + placeholders={"en": "Enter up to 1000 words", "fr": "Entrez jusqu'à 1000 mots"}, + validation_regexs={ + "en": r"^\s*(\S+(\s+|$)){0,1000}$", + "fr": r"^\s*(\S+(\s+|$)){0,1000}$", + }, + validation_texts={"en": "Maximum 1000 words", "fr": "Maximum 1000 mots"}, + is_required=True, + ) + + add_question( + section2, + 2, + "long-text", + { + "en": "2. General and specific objectives", + "fr": "2. Objectifs généraux et précis.", + }, + descriptions={ + "en": """The general objective should state the development goal being pursued by the research. The specific objectives should indicate the specific types of knowledge to be produced, the audiences to be reached, and forms of capacity to be reinforced. These are the objectives against which the success of the project will be judged. Use only active verbs (no passive). Typical objectives focus on 1) deepening understanding of a topic through research; 2) influencing policy and practice; and 3) building capacity. Maximum 250 words""", - 'fr': """L’objectif général correspond à l’objectif de développement que l’on souhaite atteindre au moyen de la recherche. Les objectifs particuliers doivent signaler les types précis de connaissances recherchées, les destinataires et les capacités à renforcer. C’est en fonction de ces objectifs que sera jaugée la réussite du projet. Utilisez seulement des verbes d’action (évitez les verbes d’état). En règle générale, les objectifs permettent : 1) de comprendre un sujet en profondeur grâce à la recherche; 2) d’influencer les politiques et les pratiques; et 3) de favoriser le renforcement des capacités. + "fr": """L’objectif général correspond à l’objectif de développement que l’on souhaite atteindre au moyen de la recherche. Les objectifs particuliers doivent signaler les types précis de connaissances recherchées, les destinataires et les capacités à renforcer. C’est en fonction de ces objectifs que sera jaugée la réussite du projet. Utilisez seulement des verbes d’action (évitez les verbes d’état). En règle générale, les objectifs permettent : 1) de comprendre un sujet en profondeur grâce à la recherche; 2) d’influencer les politiques et les pratiques; et 3) de favoriser le renforcement des capacités. -Maximum 250 mots""" - }, placeholders={ - 'en': 'Enter up to 250 words', - 'fr': "Entrez jusqu'à 250 mots" - }, validation_regexs={ - 'en': r'^\s*(\S+(\s+|$)){0,250}$', - 'fr': r'^\s*(\S+(\s+|$)){0,250}$', - }, validation_texts={ - 'en': 'Maximum 250 words', - 'fr': 'Maximum 250 mots' - }, is_required=True) - - add_question(section2, 3, 'information', { - 'en': '3. Methodology and approach', - 'fr': '3. Méthodologie et approche' - }, is_required=False) - - add_question(section2, 4, 'long-text', { - 'en': 'Approach', - 'fr': 'Approche' - }, descriptions={ - 'en': """Briefly describe the overall approach/logic/pathway to achieve the above objectives. +Maximum 250 mots""", + }, + placeholders={"en": "Enter up to 250 words", "fr": "Entrez jusqu'à 250 mots"}, + validation_regexs={ + "en": r"^\s*(\S+(\s+|$)){0,250}$", + "fr": r"^\s*(\S+(\s+|$)){0,250}$", + }, + validation_texts={"en": "Maximum 250 words", "fr": "Maximum 250 mots"}, + is_required=True, + ) + + add_question( + section2, + 3, + "information", + {"en": "3. Methodology and approach", "fr": "3. Méthodologie et approche"}, + is_required=False, + ) + + add_question( + section2, + 4, + "long-text", + {"en": "Approach", "fr": "Approche"}, + descriptions={ + "en": """Briefly describe the overall approach/logic/pathway to achieve the above objectives. Maximum 350 words""", - 'fr': """Décrivez brièvement l’approche, la logique ou le cheminement permettant d’atteindre les objectifs mentionnés précédemment. + "fr": """Décrivez brièvement l’approche, la logique ou le cheminement permettant d’atteindre les objectifs mentionnés précédemment. -Maximum 350 mots""" - }, placeholders={ - 'en': 'Enter up to 350 words', - 'fr': "Entrez jusqu'à 350 mots" - }, validation_regexs={ - 'en': r'^\s*(\S+(\s+|$)){0,350}$', - 'fr': r'^\s*(\S+(\s+|$)){0,350}$', - }, validation_texts={ - 'en': 'Maximum 350 words', - 'fr': 'Maximum 350 mots' - }, is_required=True) - - add_question(section2, 5, 'long-text', { - 'en': 'Policy research', - 'fr': 'Recherche sur les politiques' - }, descriptions={ - 'en': """How will you answer the proposed research questions in the most rigorous way possible? This includes conceptual and theoretical frameworks, user participation, data collection and analysis. +Maximum 350 mots""", + }, + placeholders={"en": "Enter up to 350 words", "fr": "Entrez jusqu'à 350 mots"}, + validation_regexs={ + "en": r"^\s*(\S+(\s+|$)){0,350}$", + "fr": r"^\s*(\S+(\s+|$)){0,350}$", + }, + validation_texts={"en": "Maximum 350 words", "fr": "Maximum 350 mots"}, + is_required=True, + ) + + add_question( + section2, + 5, + "long-text", + {"en": "Policy research", "fr": "Recherche sur les politiques"}, + descriptions={ + "en": """How will you answer the proposed research questions in the most rigorous way possible? This includes conceptual and theoretical frameworks, user participation, data collection and analysis. Maximum 750 words""", - 'fr': """Comment comptez-vous répondre aux questions de recherche proposées de la façon la plus rigoureuse possible ? Veuillez notamment fournir des précisions à l’égard des cadres conceptuels et théoriques, de la participation des utilisateurs, et de la collecte et de l’analyse des données. + "fr": """Comment comptez-vous répondre aux questions de recherche proposées de la façon la plus rigoureuse possible ? Veuillez notamment fournir des précisions à l’égard des cadres conceptuels et théoriques, de la participation des utilisateurs, et de la collecte et de l’analyse des données. -Maximum 750 mots""" - }, placeholders={ - 'en': 'Enter up to 750 words', - 'fr': "Entrez jusqu'à 750 mots" - }, validation_regexs={ - 'en': r'^\s*(\S+(\s+|$)){0,750}$', - 'fr': r'^\s*(\S+(\s+|$)){0,750}$', - }, validation_texts={ - 'en': 'Maximum 750 words', - 'fr': 'Maximum 750 mots' - }, is_required=True) - - add_question(section2, 6, 'long-text', { - 'en': 'Gender and inclusion', - 'fr': 'Sexospécificité et inclusion' - }, descriptions={ - 'en': """How has your organization addressed gender-equality and other kinds of inclusion and diversity dimensions [socioeconomic, political, cultural, ethnic] in its policy research? +Maximum 750 mots""", + }, + placeholders={"en": "Enter up to 750 words", "fr": "Entrez jusqu'à 750 mots"}, + validation_regexs={ + "en": r"^\s*(\S+(\s+|$)){0,750}$", + "fr": r"^\s*(\S+(\s+|$)){0,750}$", + }, + validation_texts={"en": "Maximum 750 words", "fr": "Maximum 750 mots"}, + is_required=True, + ) + + add_question( + section2, + 6, + "long-text", + {"en": "Gender and inclusion", "fr": "Sexospécificité et inclusion"}, + descriptions={ + "en": """How has your organization addressed gender-equality and other kinds of inclusion and diversity dimensions [socioeconomic, political, cultural, ethnic] in its policy research? How do you anticipate this will improve with this AI4D support? Please give a brief explanation of your strategy with gender, inclusion and diversity related issues as it relates to AI/advanced technologies and offer examples of the research work. Please provide examples [PDF or links] to material that demonstrates your strategy and research on gender, inclusion and diversity. Maximum 750 words""", - 'fr': """Comment votre organisation a-t-elle tenu compte de l’égalité des genres et d’autres dimensions (p. ex. socio-économiques, politiques, culturelles, ethniques) liées à l’inclusion et à la diversité dans sa recherche sur les politiques? + "fr": """Comment votre organisation a-t-elle tenu compte de l’égalité des genres et d’autres dimensions (p. ex. socio-économiques, politiques, culturelles, ethniques) liées à l’inclusion et à la diversité dans sa recherche sur les politiques? Dans quelle mesure pensez-vous que ces éléments s’amélioreront grâce au soutien de l’initiative Intelligence artificielle pour le développement (IAPD) ? Veuillez expliquer brièvement votre stratégie en ce qui a trait aux questions liées à la sexospécificité, à l’inclusion et à la diversité, ainsi qu’à l’IA et aux technologies de pointe, et fournir des exemples de travaux de recherche. Veuillez fournir des exemples (c.-à-d. des documents en format PDF, ou des liens menant vers des fichiers) qui démontrent votre stratégie et vos recherches sur la sexospécificité, l’inclusion et la diversité. Maximum 750 mots -""" - }, placeholders={ - 'en': 'Enter up to 750 words', - 'fr': "Entrez jusqu'à 750 mots" - }, validation_regexs={ - 'en': r'^\s*(\S+(\s+|$)){0,750}$', - 'fr': r'^\s*(\S+(\s+|$)){0,750}$', - }, validation_texts={ - 'en': 'Maximum 750 words', - 'fr': 'Maximum 750 mots' - }, is_required=True) - - add_question(section2, 7, 'multi-file', { - 'en': 'Gender and inclusion: Supporting documents', - 'fr': 'Sexospécificité et inclusion: Documents justificatifs' - }, descriptions={ - 'en': 'Attach any supplemental documentation that will help clarify your application (e.g., Theory of Change, supporting graphics)', - 'fr': "Veuillez joindre toute documentation supplémentaire qui aidera à clarifier votre demande (p. ex. théorie du changement, graphiques à l’appui)." - }, is_required=False) - - add_question(section2, 8, 'long-text', { - 'en': 'Ethical considerations', - 'fr': 'Considérations d’ordre éthique' - }, descriptions={ - 'en': """All projects that include human subjects must ensure that their privacy, dignity, and integrity are protected. An independent ethical review committee must approve the protocols. Please describe the key ethical considerations for the proposed research and the research ethics protocols your organization uses. +""", + }, + placeholders={"en": "Enter up to 750 words", "fr": "Entrez jusqu'à 750 mots"}, + validation_regexs={ + "en": r"^\s*(\S+(\s+|$)){0,750}$", + "fr": r"^\s*(\S+(\s+|$)){0,750}$", + }, + validation_texts={"en": "Maximum 750 words", "fr": "Maximum 750 mots"}, + is_required=True, + ) + + add_question( + section2, + 7, + "multi-file", + { + "en": "Gender and inclusion: Supporting documents", + "fr": "Sexospécificité et inclusion: Documents justificatifs", + }, + descriptions={ + "en": "Attach any supplemental documentation that will help clarify your application (e.g., Theory of Change, supporting graphics)", + "fr": "Veuillez joindre toute documentation supplémentaire qui aidera à clarifier votre demande (p. ex. théorie du changement, graphiques à l’appui).", + }, + is_required=False, + ) + + add_question( + section2, + 8, + "long-text", + {"en": "Ethical considerations", "fr": "Considérations d’ordre éthique"}, + descriptions={ + "en": """All projects that include human subjects must ensure that their privacy, dignity, and integrity are protected. An independent ethical review committee must approve the protocols. Please describe the key ethical considerations for the proposed research and the research ethics protocols your organization uses. Maximum 350 words """, - 'fr': """Dans le cadre de tout projet de recherche portant sur des sujets humains, il faut garantir la protection de la vie privée, de la dignité et de l’intégrité de la personne. Un comité d’éthique indépendant doit autoriser les protocoles. Veuillez décrire les principales considérations d’ordre éthique pour la recherche proposée, ainsi que les protocoles d’éthique de la recherche utilisés par votre organisation. + "fr": """Dans le cadre de tout projet de recherche portant sur des sujets humains, il faut garantir la protection de la vie privée, de la dignité et de l’intégrité de la personne. Un comité d’éthique indépendant doit autoriser les protocoles. Veuillez décrire les principales considérations d’ordre éthique pour la recherche proposée, ainsi que les protocoles d’éthique de la recherche utilisés par votre organisation. -Maximum 350 mots""" - }, placeholders={ - 'en': 'Enter up to 350 words', - 'fr': "Entrez jusqu'à 350 mots" - }, validation_regexs={ - 'en': r'^\s*(\S+(\s+|$)){0,350}$', - 'fr': r'^\s*(\S+(\s+|$)){0,350}$', - }, validation_texts={ - 'en': 'Maximum 350 words', - 'fr': 'Maximum 350 mots' - }, is_required=True) - - add_question(section2, 9, 'long-text', { - 'en': '4. Capacity development', - 'fr': '4. Développement des capacités' - }, descriptions={ - 'en': """In terms of developing capacity, how would your organization use this grant to advance the capacity of your research team in any of the above subject areas? What would you need to do to build capacity to improve your work and outcomes to advance responsible artificial intelligence in your country/region? +Maximum 350 mots""", + }, + placeholders={"en": "Enter up to 350 words", "fr": "Entrez jusqu'à 350 mots"}, + validation_regexs={ + "en": r"^\s*(\S+(\s+|$)){0,350}$", + "fr": r"^\s*(\S+(\s+|$)){0,350}$", + }, + validation_texts={"en": "Maximum 350 words", "fr": "Maximum 350 mots"}, + is_required=True, + ) + + add_question( + section2, + 9, + "long-text", + {"en": "4. Capacity development", "fr": "4. Développement des capacités"}, + descriptions={ + "en": """In terms of developing capacity, how would your organization use this grant to advance the capacity of your research team in any of the above subject areas? What would you need to do to build capacity to improve your work and outcomes to advance responsible artificial intelligence in your country/region? Maximum 350 words""", - 'fr': """En matière de développement des capacités, comment votre organisation utiliserait-elle cette subvention pour renforcer les capacités de votre équipe de recherche dans l’un des domaines susmentionnés ? Que devriez- vous faire pour renforcer vos capacités, améliorer la qualité de votre travail, obtenir de meilleurs résultats, et faire progresser l’IA responsable dans votre pays ou votre région? + "fr": """En matière de développement des capacités, comment votre organisation utiliserait-elle cette subvention pour renforcer les capacités de votre équipe de recherche dans l’un des domaines susmentionnés ? Que devriez- vous faire pour renforcer vos capacités, améliorer la qualité de votre travail, obtenir de meilleurs résultats, et faire progresser l’IA responsable dans votre pays ou votre région? -Maximum 350 mots""" - }, placeholders={ - 'en': 'Enter up to 350 words', - 'fr': "Entrez jusqu'à 350 mots" - }, validation_regexs={ - 'en': r'^\s*(\S+(\s+|$)){0,350}$', - 'fr': r'^\s*(\S+(\s+|$)){0,350}$', - }, validation_texts={ - 'en': 'Maximum 350 words', - 'fr': 'Maximum 350 mots' - }, is_required=True) - - add_question(section2, 10, 'long-text', { - 'en': '5. Communicating for influence', - 'fr': '5. Communication en vue d’exercer une influence' - }, descriptions={ - 'en': """Please provide a description of your intended research communications strategy to influence policy in the context of this AI4D Africa funding. +Maximum 350 mots""", + }, + placeholders={"en": "Enter up to 350 words", "fr": "Entrez jusqu'à 350 mots"}, + validation_regexs={ + "en": r"^\s*(\S+(\s+|$)){0,350}$", + "fr": r"^\s*(\S+(\s+|$)){0,350}$", + }, + validation_texts={"en": "Maximum 350 words", "fr": "Maximum 350 mots"}, + is_required=True, + ) + + add_question( + section2, + 10, + "long-text", + { + "en": "5. Communicating for influence", + "fr": "5. Communication en vue d’exercer une influence", + }, + descriptions={ + "en": """Please provide a description of your intended research communications strategy to influence policy in the context of this AI4D Africa funding. Maximum 350 words""", - 'fr': """Veuillez fournir une description de la stratégie de communication que vous comptez adopter dans le cadre de votre recherche pour exercer une influence sur les politiques, grâce à ce financement de l’initiative IAPD Afrique. + "fr": """Veuillez fournir une description de la stratégie de communication que vous comptez adopter dans le cadre de votre recherche pour exercer une influence sur les politiques, grâce à ce financement de l’initiative IAPD Afrique. -Maximum 350 mots""" - }, placeholders={ - 'en': 'Enter up to 350 words', - 'fr': "Entrez jusqu'à 350 mots" - }, validation_regexs={ - 'en': r'^\s*(\S+(\s+|$)){0,350}$', - 'fr': r'^\s*(\S+(\s+|$)){0,350}$', - }, validation_texts={ - 'en': 'Maximum 350 words', - 'fr': 'Maximum 350 mots' - }, is_required=True) - - add_question(section2, 11, 'long-text', { - 'en': '6. Results', - 'fr': '6. Résultats' - }, descriptions={ - 'en': """Please describe what success will look like at the end of the three years. What markers will you use to gauge your success in AI4D policy and research? What are your expected research outputs and outcomes? +Maximum 350 mots""", + }, + placeholders={"en": "Enter up to 350 words", "fr": "Entrez jusqu'à 350 mots"}, + validation_regexs={ + "en": r"^\s*(\S+(\s+|$)){0,350}$", + "fr": r"^\s*(\S+(\s+|$)){0,350}$", + }, + validation_texts={"en": "Maximum 350 words", "fr": "Maximum 350 mots"}, + is_required=True, + ) + + add_question( + section2, + 11, + "long-text", + {"en": "6. Results", "fr": "6. Résultats"}, + descriptions={ + "en": """Please describe what success will look like at the end of the three years. What markers will you use to gauge your success in AI4D policy and research? What are your expected research outputs and outcomes? Maximum 500 words""", - 'fr': """Veuillez décrire ce à quoi ressemblera la réussite à la fin de la période de trois années. Quels marqueurs utiliserez-vous pour évaluer votre réussite pour les politiques et les recherches liées à l’initiative IAPD Afrique? Quels sont les résultats escomptés de votre recherche? + "fr": """Veuillez décrire ce à quoi ressemblera la réussite à la fin de la période de trois années. Quels marqueurs utiliserez-vous pour évaluer votre réussite pour les politiques et les recherches liées à l’initiative IAPD Afrique? Quels sont les résultats escomptés de votre recherche? -Maximum 500 mots""" - }, placeholders={ - 'en': 'Enter up to 500 words', - 'fr': "Entrez jusqu'à 500 mots" - }, validation_regexs={ - 'en': r'^\s*(\S+(\s+|$)){0,500}$', - 'fr': r'^\s*(\S+(\s+|$)){0,500}$', - }, validation_texts={ - 'en': 'Maximum 500 words', - 'fr': 'Maximum 500 mots' - }, is_required=True) - - add_question(section2, 12, 'long-text', { - 'en': '7. Project schedule', - 'fr': '7. Calendrier du projet' - }, descriptions={ - 'en': """Please provide a draft schedule for how you envision the activities will be organized according to the specific objectives of the project. The project schedule should include a list of key outputs and milestones (key events or deliverables). These need to be related to the items included in the project budget (Section IV). +Maximum 500 mots""", + }, + placeholders={"en": "Enter up to 500 words", "fr": "Entrez jusqu'à 500 mots"}, + validation_regexs={ + "en": r"^\s*(\S+(\s+|$)){0,500}$", + "fr": r"^\s*(\S+(\s+|$)){0,500}$", + }, + validation_texts={"en": "Maximum 500 words", "fr": "Maximum 500 mots"}, + is_required=True, + ) + + add_question( + section2, + 12, + "long-text", + {"en": "7. Project schedule", "fr": "7. Calendrier du projet"}, + descriptions={ + "en": """Please provide a draft schedule for how you envision the activities will be organized according to the specific objectives of the project. The project schedule should include a list of key outputs and milestones (key events or deliverables). These need to be related to the items included in the project budget (Section IV). Maximum 350 words""", - 'fr': """Veuillez fournir un calendrier provisoire pour expliquer la façon dont vous comptez organiser les activités, compte tenu des objectifs spécifiques du projet. Le calendrier du projet doit comprendre une liste des principaux extrants et jalons (événements ou produits livrables clés). Ils doivent être liés aux éléments qui figurent dans le budget du projet (section IV). + "fr": """Veuillez fournir un calendrier provisoire pour expliquer la façon dont vous comptez organiser les activités, compte tenu des objectifs spécifiques du projet. Le calendrier du projet doit comprendre une liste des principaux extrants et jalons (événements ou produits livrables clés). Ils doivent être liés aux éléments qui figurent dans le budget du projet (section IV). -Maximum 350 mots""" - }, placeholders={ - 'en': 'Enter up to 350 words', - 'fr': "Entrez jusqu'à 350 mots" - }, validation_regexs={ - 'en': r'^\s*(\S+(\s+|$)){0,350}$', - 'fr': r'^\s*(\S+(\s+|$)){0,350}$', - }, validation_texts={ - 'en': 'Maximum 350 words', - 'fr': 'Maximum 350 mots' - }, is_required=True) - - add_question(section2, 13, 'multi-file', { - 'en': '8. Supplemental documentation (optional).', - 'fr': '8. Documents supplémentaires (facultatifs).' - }, descriptions={ - 'en': 'Attach any supplemental documentation that will help clarify your application (e.g., Theory of Change, supporting graphics)', - 'fr': "Veuillez joindre toute documentation supplémentaire qui aidera à clarifier votre demande (p. ex. théorie du changement, graphiques à l’appui)." - }, is_required=False) +Maximum 350 mots""", + }, + placeholders={"en": "Enter up to 350 words", "fr": "Entrez jusqu'à 350 mots"}, + validation_regexs={ + "en": r"^\s*(\S+(\s+|$)){0,350}$", + "fr": r"^\s*(\S+(\s+|$)){0,350}$", + }, + validation_texts={"en": "Maximum 350 words", "fr": "Maximum 350 mots"}, + is_required=True, + ) + + add_question( + section2, + 13, + "multi-file", + { + "en": "8. Supplemental documentation (optional).", + "fr": "8. Documents supplémentaires (facultatifs).", + }, + descriptions={ + "en": "Attach any supplemental documentation that will help clarify your application (e.g., Theory of Change, supporting graphics)", + "fr": "Veuillez joindre toute documentation supplémentaire qui aidera à clarifier votre demande (p. ex. théorie du changement, graphiques à l’appui).", + }, + is_required=False, + ) # Section 3 - section3 = add_section({ - 'en': 'Section III: Policy engagement experience', - 'fr': 'Section III: Expérience en matière d’interaction avec la sphère des politiques' - }, None, 5) - - add_question(section3, 1, 'long-text', { - 'en': '1. Policy engagement', - 'fr': '1. Expérience en matière d’interaction avec la sphère des politiques' - }, descriptions={ - 'en': """Please describe the kinds of engagement your organization has had with national governing institutions (e.g. government departments, bodies and agencies, commissions, parliaments, regulators, and other public-sector institutions) on any of the above topics (from Section 1, question 3)? This can include a reflection on what you have done well with communications, and what you have done not so well. + section3 = add_section( + { + "en": "Section III: Policy engagement experience", + "fr": "Section III: Expérience en matière d’interaction avec la sphère des politiques", + }, + None, + 5, + ) + + add_question( + section3, + 1, + "long-text", + { + "en": "1. Policy engagement", + "fr": "1. Expérience en matière d’interaction avec la sphère des politiques", + }, + descriptions={ + "en": """Please describe the kinds of engagement your organization has had with national governing institutions (e.g. government departments, bodies and agencies, commissions, parliaments, regulators, and other public-sector institutions) on any of the above topics (from Section 1, question 3)? This can include a reflection on what you have done well with communications, and what you have done not so well. Please give up to three examples of this engagement, such as requests for input, sitting on policy steering committees and meetings with policy makers. Maximum 350 words""", - 'fr': """Veuillez décrire les types d’interactions que votre organisation a eus avec des institutions gouvernementales nationales (p. ex. ministères et organismes gouvernementaux, commissions, parlements, organismes de réglementation et autres institutions du secteur public) en ce qui concerne les sujets susmentionnés (à la section 1, question 3). Vous pouvez notamment faire part de vos réflexions sur ce que vous avez bien fait en matière de communication, ainsi que sur les éléments que vous devez améliorer. + "fr": """Veuillez décrire les types d’interactions que votre organisation a eus avec des institutions gouvernementales nationales (p. ex. ministères et organismes gouvernementaux, commissions, parlements, organismes de réglementation et autres institutions du secteur public) en ce qui concerne les sujets susmentionnés (à la section 1, question 3). Vous pouvez notamment faire part de vos réflexions sur ce que vous avez bien fait en matière de communication, ainsi que sur les éléments que vous devez améliorer. Veuillez fournir un maximum de trois exemples de ces interactions (p. ex. demandes de consultation, participation à des comités directeurs, réunions avec des décideurs politiques). Maximum 350 mots - """ - }, placeholders={ - 'en': 'Enter up to 350 words', - 'fr': "Entrez jusqu'à 350 mots" - }, validation_regexs={ - 'en': r'^\s*(\S+(\s+|$)){0,350}$', - 'fr': r'^\s*(\S+(\s+|$)){0,350}$', - }, validation_texts={ - 'en': 'Maximum 350 words', - 'fr': 'Maximum 350 mots' - }, is_required=True) - - add_question(section3, 2, 'long-text', { - 'en': '2. Informing debates and policies', - 'fr': '2. Informer les débats et les politiques' - }, descriptions={ - 'en': """Describe and give examples of how your research has informed public debate or policy processes nationally and/or regionally. + """, + }, + placeholders={"en": "Enter up to 350 words", "fr": "Entrez jusqu'à 350 mots"}, + validation_regexs={ + "en": r"^\s*(\S+(\s+|$)){0,350}$", + "fr": r"^\s*(\S+(\s+|$)){0,350}$", + }, + validation_texts={"en": "Maximum 350 words", "fr": "Maximum 350 mots"}, + is_required=True, + ) + + add_question( + section3, + 2, + "long-text", + { + "en": "2. Informing debates and policies", + "fr": "2. Informer les débats et les politiques", + }, + descriptions={ + "en": """Describe and give examples of how your research has informed public debate or policy processes nationally and/or regionally. Maximum 350 words""", - 'fr': """Décrivez votre recherche et donnez des exemples de la façon dont elle a exercé une influence sur le débat public ou les processus politiques à l’échelle nationale ou régionale. - -Maximum 350 mots""" - }, placeholders={ - 'en': 'Enter up to 350 words', - 'fr': "Entrez jusqu'à 350 mots" - }, validation_regexs={ - 'en': r'^\s*(\S+(\s+|$)){0,350}$', - 'fr': r'^\s*(\S+(\s+|$)){0,350}$', - }, validation_texts={ - 'en': 'Maximum 350 words', - 'fr': 'Maximum 350 mots' - }, is_required=True) - - q3 = add_question(section3, 3, 'multi-choice', { - 'en': '3. Regional Engagement', - 'fr': '3. Engagement régional' - }, descriptions={ - 'en': """Does your organization also work regionally and/or globally on technology policy related issues?""", - 'fr': """Votre organisation travaille-t-elle aussi sur des questions liées aux politiques technologiques à l’échelle régionale ou mondiale?""" - }, options={ - 'en': [{'label': 'Yes', 'value': 'Yes'}, {'label': 'No', 'value': 'No'}], - 'fr': [{'label': 'Oui', 'value': 'Yes'}, {'label': 'Non', 'value': 'No'}] - }, is_required=True) - - add_question(section3, 4, 'long-text', { - 'en': 'Regional Engagement', - 'fr': 'Engagement régional' - }, descriptions={ - 'en': """Please describe this engagement. List any regional and global networks/association memberships if appropriate + "fr": """Décrivez votre recherche et donnez des exemples de la façon dont elle a exercé une influence sur le débat public ou les processus politiques à l’échelle nationale ou régionale. + +Maximum 350 mots""", + }, + placeholders={"en": "Enter up to 350 words", "fr": "Entrez jusqu'à 350 mots"}, + validation_regexs={ + "en": r"^\s*(\S+(\s+|$)){0,350}$", + "fr": r"^\s*(\S+(\s+|$)){0,350}$", + }, + validation_texts={"en": "Maximum 350 words", "fr": "Maximum 350 mots"}, + is_required=True, + ) + + q3 = add_question( + section3, + 3, + "multi-choice", + {"en": "3. Regional Engagement", "fr": "3. Engagement régional"}, + descriptions={ + "en": """Does your organization also work regionally and/or globally on technology policy related issues?""", + "fr": """Votre organisation travaille-t-elle aussi sur des questions liées aux politiques technologiques à l’échelle régionale ou mondiale?""", + }, + options={ + "en": [{"label": "Yes", "value": "Yes"}, {"label": "No", "value": "No"}], + "fr": [{"label": "Oui", "value": "Yes"}, {"label": "Non", "value": "No"}], + }, + is_required=True, + ) + + add_question( + section3, + 4, + "long-text", + {"en": "Regional Engagement", "fr": "Engagement régional"}, + descriptions={ + "en": """Please describe this engagement. List any regional and global networks/association memberships if appropriate Maximum 250 words""", - 'fr': """Veuillez décrire la nature de ces travaux. Dressez une liste des associations ou des réseaux auxquels vous appartenez à l’échelle régionale et mondiale, le cas échéant + "fr": """Veuillez décrire la nature de ces travaux. Dressez une liste des associations ou des réseaux auxquels vous appartenez à l’échelle régionale et mondiale, le cas échéant -Maximum 250 mots""" - }, placeholders={ - 'en': 'Enter up to 250 words', - 'fr': "Entrez jusqu'à 250 mots" - }, validation_regexs={ - 'en': r'^\s*(\S+(\s+|$)){0,250}$', - 'fr': r'^\s*(\S+(\s+|$)){0,250}$', - }, validation_texts={ - 'en': 'Maximum 250 words', - 'fr': 'Maximum 250 mots' - }, is_required=True, depends_on_question_id=q3.id, show_for_values={ - 'en': 'Yes', - 'fr': 'Yes' - }) +Maximum 250 mots""", + }, + placeholders={"en": "Enter up to 250 words", "fr": "Entrez jusqu'à 250 mots"}, + validation_regexs={ + "en": r"^\s*(\S+(\s+|$)){0,250}$", + "fr": r"^\s*(\S+(\s+|$)){0,250}$", + }, + validation_texts={"en": "Maximum 250 words", "fr": "Maximum 250 mots"}, + is_required=True, + depends_on_question_id=q3.id, + show_for_values={"en": "Yes", "fr": "Yes"}, + ) # Section 4 en_desc = """Please submit a full three year budget with notes using the Excel sheet available on the IDRC public website: [https://www.idrc.ca/en/resources/guides-and-forms](https://www.idrc.ca/en/resources/guides-and-forms) @@ -1173,27 +1394,37 @@ def add_question(section, order, questionType, headlines, Insérez ce budget en tant que pièce jointe à votre demande. Dans le cas contraire, elle sera rejetée. """ - section4 = add_section({ - 'en': 'Section IV: Budget', - 'fr': 'Section IV: Budget' - }, { - 'en': en_desc, - 'fr': fr_desc - }, 6) - - add_question(section4, 1, 'file', { - 'en': 'Budget', - 'fr': 'Budget' - }, is_required=True, options={ - 'en': {"accept": ".csv, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"}, - 'fr': {"accept": ".csv, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"}, - }) + section4 = add_section( + {"en": "Section IV: Budget", "fr": "Section IV: Budget"}, + {"en": en_desc, "fr": fr_desc}, + 6, + ) + + add_question( + section4, + 1, + "file", + {"en": "Budget", "fr": "Budget"}, + is_required=True, + options={ + "en": { + "accept": ".csv, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel" + }, + "fr": { + "accept": ".csv, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel" + }, + }, + ) def downgrade(): Base.metadata.bind = op.get_bind() session = orm.Session(bind=Base.metadata.bind) - password_reset_template = session.query(EmailTemplate).filter_by(key='password-reset', language='en').first() + password_reset_template = ( + session.query(EmailTemplate) + .filter_by(key="password-reset", language="en") + .first() + ) password_reset_template.template = """Dear {title} {firstname} {lastname}, You recently requested a password reset on {system}, please use the following link to reset you password: @@ -1204,9 +1435,11 @@ def downgrade(): Kind Regards, {organisation} """ - session.query(EmailTemplate).filter_by(key='withdrawal', language='fr').delete() + session.query(EmailTemplate).filter_by(key="withdrawal", language="fr").delete() - withdrawal_template = session.query(EmailTemplate).filter_by(key='withdrawal', language='en').first() + withdrawal_template = ( + session.query(EmailTemplate).filter_by(key="withdrawal", language="en").first() + ) withdrawal_template.template = """Dear {title} {firstname} {lastname}, This email serves to confirm that you have withdrawn your application to attend {event_name}. @@ -1215,5 +1448,5 @@ def downgrade(): Kind Regards, The {event_name} Team """ - + session.commit() diff --git a/api/migrations/versions/111c4f9eab84_add_ai4d_prc_keys.py b/api/migrations/versions/111c4f9eab84_add_ai4d_prc_keys.py index 95a3de474..9d2166363 100644 --- a/api/migrations/versions/111c4f9eab84_add_ai4d_prc_keys.py +++ b/api/migrations/versions/111c4f9eab84_add_ai4d_prc_keys.py @@ -7,29 +7,27 @@ """ # revision identifiers, used by Alembic. -revision = '111c4f9eab84' -down_revision = '34a5a1fce672' +revision = "111c4f9eab84" +down_revision = "34a5a1fce672" -from alembic import op -import sqlalchemy as sa +import datetime +from enum import Enum -from alembic import op import sqlalchemy as sa -from sqlalchemy import orm +from alembic import op +from sqlalchemy import orm, select from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy import orm -from sqlalchemy import select from sqlalchemy.orm import column_property + from app import db -import datetime -from enum import Enum Base = declarative_base() + class Organisation(Base): __tablename__ = "organisation" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(50), nullable=False) @@ -41,7 +39,17 @@ class Organisation(Base): email_from = db.Column(db.String(100), nullable=True) system_url = db.Column(db.String(100), nullable=False) - def __init__(self, name, system_name, small_logo, large_logo, domain, url, email_from, system_url): + def __init__( + self, + name, + system_name, + small_logo, + large_logo, + domain, + url, + email_from, + system_url, + ): self.name = name self.small_logo = small_logo self.large_logo = large_logo @@ -51,9 +59,10 @@ def __init__(self, name, system_name, small_logo, large_logo, domain, url, email self.email_from = email_from self.system_url = system_url + class Country(Base): __tablename__ = "country" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(100), nullable=False) @@ -63,21 +72,23 @@ def __init__(self, name): class EventType(Enum): - EVENT = 'event' - AWARD = 'award' - CALL = 'call' + EVENT = "event" + AWARD = "award" + CALL = "call" class Event(Base): __tablename__ = "event" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) start_date = db.Column(db.DateTime(), nullable=False) end_date = db.Column(db.DateTime(), nullable=False) key = db.Column(db.String(255), nullable=False, unique=True) - organisation_id = db.Column(db.Integer(), db.ForeignKey('organisation.id'), nullable=False) + organisation_id = db.Column( + db.Integer(), db.ForeignKey("organisation.id"), nullable=False + ) email_from = db.Column(db.String(255), nullable=False) url = db.Column(db.String(255), nullable=False) application_open = db.Column(db.DateTime(), nullable=False) @@ -94,7 +105,7 @@ class Event(Base): travel_grant = db.Column(db.Boolean(), nullable=False) miniconf_url = db.Column(db.String(100), nullable=True) - event_translations = db.relationship('EventTranslation', lazy='dynamic') + event_translations = db.relationship("EventTranslation", lazy="dynamic") def __init__( self, @@ -118,7 +129,7 @@ def __init__( registration_close, event_type, travel_grant, - miniconf_url=None + miniconf_url=None, ): self.start_date = start_date self.end_date = end_date @@ -150,10 +161,11 @@ def add_event_translations(self, names, descriptions): event_translation = EventTranslation(name, description, language) self.event_translations.append(event_translation) + class EventTranslation(Base): __tablename__ = "event_translation" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) event_id = db.Column(db.Integer(), db.ForeignKey("event.id"), nullable=False) @@ -161,19 +173,20 @@ class EventTranslation(Base): description = db.Column(db.String(255), nullable=False) language = db.Column(db.String(2)) - event = db.relationship('Event', foreign_keys=[event_id]) + event = db.relationship("Event", foreign_keys=[event_id]) def __init__(self, name, description, language): self.name = name self.description = description self.language = language + class ApplicationForm(Base): - __tablename__ = 'application_form' - __table_args__ = {'extend_existing': True} + __tablename__ = "application_form" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - event_id = db.Column(db.Integer(), db.ForeignKey('event.id'), nullable=False) + event_id = db.Column(db.Integer(), db.ForeignKey("event.id"), nullable=False) is_open = db.Column(db.Boolean(), nullable=False) nominations = db.Column(db.Boolean(), nullable=False) @@ -182,38 +195,52 @@ def __init__(self, event_id, is_open, nominations): self.is_open = is_open self.nominations = nominations + class Section(Base): - __tablename__ = 'section' - __table_args__ = {'extend_existing': True} + __tablename__ = "section" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) order = db.Column(db.Integer(), nullable=False) - depends_on_question_id = db.Column(db.Integer(), db.ForeignKey('question.id', use_alter=True), nullable=True) + depends_on_question_id = db.Column( + db.Integer(), db.ForeignKey("question.id", use_alter=True), nullable=True + ) key = db.Column(db.String(255), nullable=True) - def __init__(self, application_form_id, order, depends_on_question_id=None, key=None): + def __init__( + self, application_form_id, order, depends_on_question_id=None, key=None + ): self.application_form_id = application_form_id self.order = order self.depends_on_question_id = depends_on_question_id self.key = key + class Question(Base): - __tablename__ = 'question' - __table_args__ = {'extend_existing': True} + __tablename__ = "question" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) - section_id = db.Column(db.Integer(), db.ForeignKey('section.id'), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) + section_id = db.Column(db.Integer(), db.ForeignKey("section.id"), nullable=False) type = db.Column(db.String(), nullable=False) order = db.Column(db.Integer(), nullable=False) is_required = db.Column(db.Boolean(), nullable=False) - depends_on_question_id = db.Column(db.Integer(), db.ForeignKey('question.id'), nullable=True) + depends_on_question_id = db.Column( + db.Integer(), db.ForeignKey("question.id"), nullable=True + ) key = db.Column(db.String(255), nullable=True) - question_translations = db.relationship('QuestionTranslation', lazy='dynamic') + question_translations = db.relationship("QuestionTranslation", lazy="dynamic") - def __init__(self, application_form_id, section_id, order, questionType, is_required=True): + def __init__( + self, application_form_id, section_id, order, questionType, is_required=True + ): self.application_form_id = application_form_id self.section_id = section_id self.order = order @@ -221,15 +248,18 @@ def __init__(self, application_form_id, section_id, order, questionType, is_requ self.is_required = is_required def get_translation(self, language): - question_translation = self.question_translations.filter_by(language=language).first() + question_translation = self.question_translations.filter_by( + language=language + ).first() return question_translation + class QuestionTranslation(Base): - __tablename__ = 'question_translation' - __table_args__ = {'extend_existing': True} + __tablename__ = "question_translation" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - question_id = db.Column(db.Integer(), db.ForeignKey('question.id'), nullable=False) + question_id = db.Column(db.Integer(), db.ForeignKey("question.id"), nullable=False) language = db.Column(db.String(2), nullable=False) headline = db.Column(db.String(), nullable=False) description = db.Column(db.String(), nullable=True) @@ -239,7 +269,7 @@ class QuestionTranslation(Base): options = db.Column(db.JSON(), nullable=True) show_for_values = db.Column(db.JSON(), nullable=True) - question = db.relationship('Question', foreign_keys=[question_id]) + question = db.relationship("Question", foreign_keys=[question_id]) def __init__( self, @@ -251,7 +281,7 @@ def __init__( validation_regex=None, validation_text=None, options=None, - show_for_values=None + show_for_values=None, ): self.question_id = question_id self.language = language @@ -263,30 +293,39 @@ def __init__( self.options = options self.show_for_values = show_for_values + def get_question(form_id, en_headline): - en = (db.session.query(QuestionTranslation, Question) - .filter_by(headline=en_headline, language='en') - .join(Question, QuestionTranslation.question_id == Question.id) - .filter_by(application_form_id=form_id) - .first()) + en = ( + db.session.query(QuestionTranslation, Question) + .filter_by(headline=en_headline, language="en") + .join(Question, QuestionTranslation.question_id == Question.id) + .filter_by(application_form_id=form_id) + .first() + ) return en.Question + def upgrade(): - print('Starting 111c4f9eab84') - event = db.session.query(Event).filter_by(key='prc').first() - application_form = db.session.query(ApplicationForm).filter_by(event_id=event.id).first() - organisation_question = get_question(application_form.id, 'Name of organization') - organisation_question.key = 'review-identifier' - country_question = get_question(application_form.id, 'Country') - country_question.key = 'review-identifier' + print("Starting 111c4f9eab84") + event = db.session.query(Event).filter_by(key="prc").first() + application_form = ( + db.session.query(ApplicationForm).filter_by(event_id=event.id).first() + ) + organisation_question = get_question(application_form.id, "Name of organization") + organisation_question.key = "review-identifier" + country_question = get_question(application_form.id, "Country") + country_question.key = "review-identifier" db.session.commit() - print('Finished 111c4f9eab84') + print("Finished 111c4f9eab84") + def downgrade(): - event = db.session.query(Event).filter_by(key='prc').first() - application_form = db.session.query(ApplicationForm).filter_by(event_id=event.id).first() - organisation_question = get_question(application_form.id, 'Name of organization') + event = db.session.query(Event).filter_by(key="prc").first() + application_form = ( + db.session.query(ApplicationForm).filter_by(event_id=event.id).first() + ) + organisation_question = get_question(application_form.id, "Name of organization") organisation_question.key = None - country_question = get_question(application_form.id, 'Country') + country_question = get_question(application_form.id, "Country") country_question.key = None db.session.commit() diff --git a/api/migrations/versions/122dd6a5c035_.py b/api/migrations/versions/122dd6a5c035_.py index 6821e6431..062adc00e 100644 --- a/api/migrations/versions/122dd6a5c035_.py +++ b/api/migrations/versions/122dd6a5c035_.py @@ -7,24 +7,25 @@ """ # revision identifiers, used by Alembic. -revision = '122dd6a5c035' -down_revision = '09b2cc2105be' +revision = "122dd6a5c035" +down_revision = "09b2cc2105be" -from alembic import op import sqlalchemy as sa +from alembic import op from sqlalchemy.dialects import postgresql + def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.alter_column('offer', 'accepted', nullable=True) - op.alter_column('offer', 'rejected', nullable=True) - op.alter_column('offer', 'rejected_reason', nullable=True) + op.alter_column("offer", "accepted", nullable=True) + op.alter_column("offer", "rejected", nullable=True) + op.alter_column("offer", "rejected_reason", nullable=True) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.alter_column('offer', 'accepted', nullable=False) - op.alter_column('offer', 'rejected', nullable=False) - op.alter_column('offer', 'rejected_reason', nullable=False) + op.alter_column("offer", "accepted", nullable=False) + op.alter_column("offer", "rejected", nullable=False) + op.alter_column("offer", "rejected_reason", nullable=False) # ### end Alembic commands ### diff --git a/api/migrations/versions/1349a2c924f4_.py b/api/migrations/versions/1349a2c924f4_.py index 1c1490f09..d96cc37b7 100644 --- a/api/migrations/versions/1349a2c924f4_.py +++ b/api/migrations/versions/1349a2c924f4_.py @@ -7,279 +7,289 @@ """ # revision identifiers, used by Alembic. -revision = '1349a2c924f4' -down_revision = '8a1468838847' +revision = "1349a2c924f4" +down_revision = "8a1468838847" -from alembic import op import sqlalchemy as sa -from sqlalchemy.sql import table, column +from alembic import op +from sqlalchemy.sql import column, table def upgrade(): # ### commands auto generated by Alembic - please adjust! ### review_form_table = table( - 'review_form', - column('id', sa.Integer()), - column('application_form_id', sa.Integer()), - column('is_open', sa.Boolean()), - column('deadline', sa.DateTime()) + "review_form", + column("id", sa.Integer()), + column("application_form_id", sa.Integer()), + column("is_open", sa.Boolean()), + column("deadline", sa.DateTime()), ) op.bulk_insert( review_form_table, [ { - 'id': 1, - 'application_form_id': 1, - 'is_open': True, - 'deadline': '10 May 2019' + "id": 1, + "application_form_id": 1, + "is_open": True, + "deadline": "10 May 2019", } - ] + ], ) review_question_table = table( - 'review_question', - sa.Column('id', sa.Integer()), - sa.Column('review_form_id', sa.Integer()), - sa.Column('question_id', sa.Integer()), - sa.Column('description', sa.String()), - sa.Column('headline', sa.String()), - sa.Column('type', sa.String()), - sa.Column('placeholder', sa.String()), - sa.Column('options', sa.JSON()), - sa.Column('is_required', sa.Boolean()), - sa.Column('order', sa.Integer()), - sa.Column('validation_regex', sa.String()), - sa.Column('validation_text', sa.String()), - sa.Column('weight', sa.Float()) + "review_question", + sa.Column("id", sa.Integer()), + sa.Column("review_form_id", sa.Integer()), + sa.Column("question_id", sa.Integer()), + sa.Column("description", sa.String()), + sa.Column("headline", sa.String()), + sa.Column("type", sa.String()), + sa.Column("placeholder", sa.String()), + sa.Column("options", sa.JSON()), + sa.Column("is_required", sa.Boolean()), + sa.Column("order", sa.Integer()), + sa.Column("validation_regex", sa.String()), + sa.Column("validation_text", sa.String()), + sa.Column("weight", sa.Float()), ) op.bulk_insert( review_question_table, [ { - 'id': 1, - 'review_form_id': 1, - 'question_id': None, - 'headline': 'Encourage field of study?', - 'description': 'Is the applicant from a field of study we want to encourage? ie outside of CS/Maths/Stats?', - 'type': 'checkbox', - 'options': None, - 'is_required': False, - 'order': 1, - 'weight': 0 + "id": 1, + "review_form_id": 1, + "question_id": None, + "headline": "Encourage field of study?", + "description": "Is the applicant from a field of study we want to encourage? ie outside of CS/Maths/Stats?", + "type": "checkbox", + "options": None, + "is_required": False, + "order": 1, + "weight": 0, }, { - 'id': 2, - 'review_form_id': 1, - 'question_id': 1, - 'headline': None, - 'description': None, - 'type': 'multi-choice', - 'options': [ - {'label': 'Poor', 'value': 0}, - {'label': 'Below Average', 'value': 1}, - {'label': 'Average', 'value': 2}, - {'label': 'Above Average', 'value': 3}, - {'label': 'Excellent', 'value': 4} + "id": 2, + "review_form_id": 1, + "question_id": 1, + "headline": None, + "description": None, + "type": "multi-choice", + "options": [ + {"label": "Poor", "value": 0}, + {"label": "Below Average", "value": 1}, + {"label": "Average", "value": 2}, + {"label": "Above Average", "value": 3}, + {"label": "Excellent", "value": 4}, ], - 'is_required': True, - 'order': 2, - 'weight': 0 + "is_required": True, + "order": 2, + "weight": 0, }, { - 'id': 3, - 'review_form_id': 1, - 'question_id': 2, - 'headline': None, - 'description': None, - 'type': 'multi-choice', - 'options': [ - {'label': 'Poor', 'value': 0}, - {'label': 'Below Average', 'value': 1}, - {'label': 'Average', 'value': 2}, - {'label': 'Above Average', 'value': 3}, - {'label': 'Excellent', 'value': 4} + "id": 3, + "review_form_id": 1, + "question_id": 2, + "headline": None, + "description": None, + "type": "multi-choice", + "options": [ + {"label": "Poor", "value": 0}, + {"label": "Below Average", "value": 1}, + {"label": "Average", "value": 2}, + {"label": "Above Average", "value": 3}, + {"label": "Excellent", "value": 4}, ], - 'is_required': True, - 'order': 3, - 'weight': 0 + "is_required": True, + "order": 3, + "weight": 0, }, { - 'id': 4, - 'review_form_id': 1, - 'question_id': 3, - 'headline': None, - 'description': None, - 'type': 'multi-choice', - 'options': [ - {'label': 'Poor', 'value': 0}, - {'label': 'Below Average', 'value': 1}, - {'label': 'Average', 'value': 2}, - {'label': 'Above Average', 'value': 3}, - {'label': 'Excellent', 'value': 4} + "id": 4, + "review_form_id": 1, + "question_id": 3, + "headline": None, + "description": None, + "type": "multi-choice", + "options": [ + {"label": "Poor", "value": 0}, + {"label": "Below Average", "value": 1}, + {"label": "Average", "value": 2}, + {"label": "Above Average", "value": 3}, + {"label": "Excellent", "value": 4}, ], - 'is_required': True, - 'order': 4, - 'weight': 0 + "is_required": True, + "order": 4, + "weight": 0, }, { - 'id': 5, - 'review_form_id': 1, - 'question_id': 4, - 'headline': None, - 'description': 'Based on this answer, do you believe the applicant has been a tutor for a relevant course?', - 'type': 'multi-choice', - 'options': [ - {'label': 'Poor', 'value': 0}, - {'label': 'Below Average', 'value': 1}, - {'label': 'Average', 'value': 2}, - {'label': 'Above Average', 'value': 3}, - {'label': 'Excellent', 'value': 4} + "id": 5, + "review_form_id": 1, + "question_id": 4, + "headline": None, + "description": "Based on this answer, do you believe the applicant has been a tutor for a relevant course?", + "type": "multi-choice", + "options": [ + {"label": "Poor", "value": 0}, + {"label": "Below Average", "value": 1}, + {"label": "Average", "value": 2}, + {"label": "Above Average", "value": 3}, + {"label": "Excellent", "value": 4}, ], - 'is_required': True, - 'order': 5, - 'weight': 0 + "is_required": True, + "order": 5, + "weight": 0, }, { - 'id': 6, - 'review_form_id': 1, - 'question_id': 5, - 'headline': None, - 'description': None, - 'type': 'multi-choice', - 'options': [ - {'label': 'Poor', 'value': 0}, - {'label': 'Below Average', 'value': 1}, - {'label': 'Average', 'value': 2}, - {'label': 'Above Average', 'value': 3}, - {'label': 'Excellent', 'value': 4} + "id": 6, + "review_form_id": 1, + "question_id": 5, + "headline": None, + "description": None, + "type": "multi-choice", + "options": [ + {"label": "Poor", "value": 0}, + {"label": "Below Average", "value": 1}, + {"label": "Average", "value": 2}, + {"label": "Above Average", "value": 3}, + {"label": "Excellent", "value": 4}, ], - 'is_required': True, - 'order': 6, - 'weight': 0 + "is_required": True, + "order": 6, + "weight": 0, }, { - 'id': 7, - 'review_form_id': 1, - 'question_id': 15, - 'headline': None, - 'description': None, - 'type': 'information', - 'options': None, - 'is_required': False, - 'order': 7, - 'weight': 0 + "id": 7, + "review_form_id": 1, + "question_id": 15, + "headline": None, + "description": None, + "type": "information", + "options": None, + "is_required": False, + "order": 7, + "weight": 0, }, { - 'id': 8, - 'review_form_id': 1, - 'question_id': 16, - 'headline': None, - 'description': None, - 'type': 'multi-choice', - 'options': [ - {'label': 'Poor', 'value': 0}, - {'label': 'Below Average', 'value': 1}, - {'label': 'Average', 'value': 2}, - {'label': 'Above Average', 'value': 3}, - {'label': 'Excellent', 'value': 4} + "id": 8, + "review_form_id": 1, + "question_id": 16, + "headline": None, + "description": None, + "type": "multi-choice", + "options": [ + {"label": "Poor", "value": 0}, + {"label": "Below Average", "value": 1}, + {"label": "Average", "value": 2}, + {"label": "Above Average", "value": 3}, + {"label": "Excellent", "value": 4}, ], - 'is_required': False, - 'order': 8, - 'weight': 0 + "is_required": False, + "order": 8, + "weight": 0, }, { - 'id': 9, - 'review_form_id': 1, - 'question_id': 8, - 'headline': None, - 'description': None, - 'type': 'information', - 'options': None, - 'is_required': False, - 'order': 9, - 'weight': 0 + "id": 9, + "review_form_id": 1, + "question_id": 8, + "headline": None, + "description": None, + "type": "information", + "options": None, + "is_required": False, + "order": 9, + "weight": 0, }, { - 'id': 10, - 'review_form_id': 1, - 'question_id': 11, - 'headline': None, - 'description': None, - 'type': 'information', - 'options': None, - 'is_required': False, - 'order': 10, - 'weight': 0 + "id": 10, + "review_form_id": 1, + "question_id": 11, + "headline": None, + "description": None, + "type": "information", + "options": None, + "is_required": False, + "order": 10, + "weight": 0, }, { - 'id': 11, - 'review_form_id': 1, - 'question_id': 9, - 'headline': None, - 'description': None, - 'type': 'multi-choice', - 'options': [ - {'label': 'Poor', 'value': 0}, - {'label': 'Below Average', 'value': 1}, - {'label': 'Average', 'value': 2}, - {'label': 'Above Average', 'value': 3}, - {'label': 'Excellent', 'value': 4} + "id": 11, + "review_form_id": 1, + "question_id": 9, + "headline": None, + "description": None, + "type": "multi-choice", + "options": [ + {"label": "Poor", "value": 0}, + {"label": "Below Average", "value": 1}, + {"label": "Average", "value": 2}, + {"label": "Above Average", "value": 3}, + {"label": "Excellent", "value": 4}, ], - 'is_required': False, - 'order': 11, - 'weight': 0 + "is_required": False, + "order": 11, + "weight": 0, }, { - 'id': 12, - 'review_form_id': 1, - 'question_id': 17, - 'headline': None, - 'description': None, - 'type': 'information', - 'options': None, - 'is_required': False, - 'order': 12, - 'weight': 0 + "id": 12, + "review_form_id": 1, + "question_id": 17, + "headline": None, + "description": None, + "type": "information", + "options": None, + "is_required": False, + "order": 12, + "weight": 0, }, { - 'id': 13, - 'review_form_id': 1, - 'question_id': 18, - 'headline': None, - 'description': None, - 'type': 'information', - 'options': None, - 'is_required': False, - 'order': 13, - 'weight': 0 + "id": 13, + "review_form_id": 1, + "question_id": 18, + "headline": None, + "description": None, + "type": "information", + "options": None, + "is_required": False, + "order": 13, + "weight": 0, }, { - 'id': 14, - 'review_form_id': 1, - 'question_id': None, - 'headline': 'Final Verdict', - 'description': 'Do you think this applicant should attend the Indaba?', - 'type': 'multi-choice', - 'options': [ - {'label': 'No', 'value': 0}, - {'label': 'Maybe', 'value': 1}, - {'label': 'Yes', 'value': 2} + "id": 14, + "review_form_id": 1, + "question_id": None, + "headline": "Final Verdict", + "description": "Do you think this applicant should attend the Indaba?", + "type": "multi-choice", + "options": [ + {"label": "No", "value": 0}, + {"label": "Maybe", "value": 1}, + {"label": "Yes", "value": 2}, ], - 'is_required': True, - 'order': 14, - 'weight': 0 - } - ] + "is_required": True, + "order": 14, + "weight": 0, + }, + ], ) - op.get_bind().execute("""SELECT setval('review_question_id_seq', (SELECT max(id) FROM review_question));""") - op.get_bind().execute("""SELECT setval('review_form_id_seq', (SELECT max(id) FROM review_form));""") + op.get_bind().execute( + """SELECT setval('review_question_id_seq', (SELECT max(id) FROM review_question));""" + ) + op.get_bind().execute( + """SELECT setval('review_form_id_seq', (SELECT max(id) FROM review_form));""" + ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.get_bind().execute('DELETE FROM review_question WHERE id <= 14; DELETE FROM review_form WHERE id = 1;') - op.get_bind().execute("""SELECT setval('review_question_id_seq', (SELECT max(id) FROM review_question));""") - op.get_bind().execute("""SELECT setval('review_form_id_seq', (SELECT max(id) FROM review_form));""") + op.get_bind().execute( + "DELETE FROM review_question WHERE id <= 14; DELETE FROM review_form WHERE id = 1;" + ) + op.get_bind().execute( + """SELECT setval('review_question_id_seq', (SELECT max(id) FROM review_question));""" + ) + op.get_bind().execute( + """SELECT setval('review_form_id_seq', (SELECT max(id) FROM review_form));""" + ) # ### end Alembic commands ### diff --git a/api/migrations/versions/14d53fa0c9c8_.py b/api/migrations/versions/14d53fa0c9c8_.py index daca48938..6346ae044 100644 --- a/api/migrations/versions/14d53fa0c9c8_.py +++ b/api/migrations/versions/14d53fa0c9c8_.py @@ -7,24 +7,26 @@ """ # revision identifiers, used by Alembic. -revision = '14d53fa0c9c8' -down_revision = ('405d106c8292', 'f4bc3108c330') +revision = "14d53fa0c9c8" +down_revision = ("405d106c8292", "f4bc3108c330") + +import datetime +from enum import Enum -from alembic import op import sqlalchemy as sa +from alembic import op from sqlalchemy import orm from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy import orm + from app import db -import datetime -from enum import Enum Base = declarative_base() + class Organisation(Base): __tablename__ = "organisation" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(50), nullable=False) @@ -37,7 +39,18 @@ class Organisation(Base): system_url = db.Column(db.String(100), nullable=False) privacy_policy = db.Column(db.String(100), nullable=False) - def __init__(self, name, system_name, small_logo, large_logo, domain, url, email_from, system_url, privacy_policy): + def __init__( + self, + name, + system_name, + small_logo, + large_logo, + domain, + url, + email_from, + system_url, + privacy_policy, + ): self.name = name self.small_logo = small_logo self.large_logo = large_logo @@ -48,10 +61,11 @@ def __init__(self, name, system_name, small_logo, large_logo, domain, url, email self.system_url = system_url self.privacy_policy = privacy_policy + class Country(Base): __tablename__ = "country" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(100), nullable=False) @@ -59,14 +73,16 @@ class Country(Base): def __init__(self, name): self.name = name + class EventType(Enum): - EVENT = 'event' - AWARD = 'award' + EVENT = "event" + AWARD = "award" + class Event(Base): __tablename__ = "event" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(50), nullable=False) @@ -74,8 +90,9 @@ class Event(Base): start_date = db.Column(db.DateTime(), nullable=False) end_date = db.Column(db.DateTime(), nullable=False) key = db.Column(db.String(255), nullable=False, unique=True) - organisation_id = db.Column(db.Integer(), db.ForeignKey( - 'organisation.id'), nullable=False) + organisation_id = db.Column( + db.Integer(), db.ForeignKey("organisation.id"), nullable=False + ) email_from = db.Column(db.String(255), nullable=False) url = db.Column(db.String(255), nullable=False) @@ -91,27 +108,28 @@ class Event(Base): registration_close = db.Column(db.DateTime(), nullable=False) event_type = db.Column(db.Enum(EventType), nullable=False) - def __init__(self, - name, - description, - start_date, - end_date, - key, - organisation_id, - email_from, - url, - application_open, - application_close, - review_open, - review_close, - selection_open, - selection_close, - offer_open, - offer_close, - registration_open, - registration_close, - event_type - ): + def __init__( + self, + name, + description, + start_date, + end_date, + key, + organisation_id, + email_from, + url, + application_open, + application_close, + review_open, + review_close, + selection_open, + selection_close, + offer_open, + offer_close, + registration_open, + registration_close, + event_type, + ): self.name = name self.description = description @@ -134,15 +152,16 @@ def __init__(self, self.event_roles = [] self.event_type = event_type + class ApplicationForm(Base): - __tablename__ = 'application_form' - __table_args__ = {'extend_existing': True} + __tablename__ = "application_form" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - event_id = db.Column(db.Integer(), db.ForeignKey('event.id'), nullable=False) + event_id = db.Column(db.Integer(), db.ForeignKey("event.id"), nullable=False) is_open = db.Column(db.Boolean(), nullable=False) - event = db.relationship('Event', foreign_keys=[event_id]) + event = db.relationship("Event", foreign_keys=[event_id]) nominations = db.Column(db.Boolean(), nullable=False) def __init__(self, event_id, is_open, nominations): @@ -150,13 +169,16 @@ def __init__(self, event_id, is_open, nominations): self.is_open = is_open self.nominations = nominations + class Question(Base): - __tablename__ = 'question' - __table_args__ = {'extend_existing': True} + __tablename__ = "question" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) - section_id = db.Column(db.Integer(), db.ForeignKey('section.id'), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) + section_id = db.Column(db.Integer(), db.ForeignKey("section.id"), nullable=False) type = db.Column(db.String(), nullable=False) description = db.Column(db.String(), nullable=True) headline = db.Column(db.String(), nullable=False) @@ -166,10 +188,25 @@ class Question(Base): order = db.Column(db.Integer(), nullable=False) options = db.Column(db.JSON(), nullable=True) is_required = db.Column(db.Boolean(), nullable=False) - depends_on_question_id = db.Column(db.Integer(), db.ForeignKey('question.id'), nullable=True) + depends_on_question_id = db.Column( + db.Integer(), db.ForeignKey("question.id"), nullable=True + ) show_for_values = db.Column(db.JSON(), nullable=True) - def __init__(self, application_form_id, section_id, headline, placeholder, order, questionType, validation_regex, validation_text=None, is_required = True, description = None, options = None): + def __init__( + self, + application_form_id, + section_id, + headline, + placeholder, + order, + questionType, + validation_regex, + validation_text=None, + is_required=True, + description=None, + options=None, + ): self.application_form_id = application_form_id self.section_id = section_id self.headline = headline @@ -184,15 +221,19 @@ def __init__(self, application_form_id, section_id, headline, placeholder, order class Section(Base): - __tablename__ = 'section' - __table_args__ = {'extend_existing': True} + __tablename__ = "section" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) name = db.Column(db.String(255), nullable=False) description = db.Column(db.String(255), nullable=False) order = db.Column(db.Integer(), nullable=False) - depends_on_question_id = db.Column(db.Integer(), db.ForeignKey('question.id', use_alter=True), nullable=True) + depends_on_question_id = db.Column( + db.Integer(), db.ForeignKey("question.id", use_alter=True), nullable=True + ) show_for_values = db.Column(db.JSON(), nullable=True) def __init__(self, application_form_id, name, description, order): @@ -201,14 +242,12 @@ def __init__(self, application_form_id, name, description, order): self.description = description self.order = order + def get_country_list(session): countries = session.query(Country).all() country_list = [] for country in countries: - country_list.append({ - 'label': country.name, - 'value': country.name - }) + country_list.append({"label": country.name, "value": country.name}) return country_list @@ -217,14 +256,27 @@ def upgrade(): Base.metadata.bind = op.get_bind() session = orm.Session(bind=Base.metadata.bind) - kambuledoctoral2020 = Event('Thamsanqa Kambule Doctoral Dissertation Award 2020', - 'Thamsanqa Kambule Doctoral Dissertation Award 2020', - datetime.date(2020, 8, 23),datetime.date(2020, 8, 28), 'kambuledoctoral2020', - 1,'baobab@deeplearningindaba.com','http://www.deeplearningindaba.com', - datetime.date(2020,3,1), datetime.date(2020,4,17),datetime.date(2020,4,25), - datetime.date(2020,5,15),datetime.date(2020,1,1),datetime.date(2020,1,1), - datetime.date(2020,1,1), datetime.date(2020,1,1),datetime.date(2020,1,1), - datetime.date(2020,1,1), EventType.AWARD) + kambuledoctoral2020 = Event( + "Thamsanqa Kambule Doctoral Dissertation Award 2020", + "Thamsanqa Kambule Doctoral Dissertation Award 2020", + datetime.date(2020, 8, 23), + datetime.date(2020, 8, 28), + "kambuledoctoral2020", + 1, + "baobab@deeplearningindaba.com", + "http://www.deeplearningindaba.com", + datetime.date(2020, 3, 1), + datetime.date(2020, 4, 17), + datetime.date(2020, 4, 25), + datetime.date(2020, 5, 15), + datetime.date(2020, 1, 1), + datetime.date(2020, 1, 1), + datetime.date(2020, 1, 1), + datetime.date(2020, 1, 1), + datetime.date(2020, 1, 1), + datetime.date(2020, 1, 1), + EventType.AWARD, + ) session.add(kambuledoctoral2020) session.commit() @@ -237,7 +289,10 @@ def upgrade(): app_form_id = application_form.id - main_section = Section(app_form_id,'Thamsanqa Kambule Doctoral Dissertation Award 2020',""" + main_section = Section( + app_form_id, + "Thamsanqa Kambule Doctoral Dissertation Award 2020", + """ This is the official application form for the Thamsanqa Kambule Doctoral Dissertation Award 2020, an award to encourage and recognise excellence in research and writing by Doctoral candidates at African universities, in any area of computational and statistical science. This award will be made at the Deep Learning Indaba at Tunis, Tunisia, in August 2020. This application will require: @@ -248,7 +303,9 @@ def upgrade(): For eligibility criteria for the Kambule Doctoral Award, please see www.deeplearningindaba.com/awards For any queries, please email awards@deeplearningindaba.com. - """,1) + """, + 1, + ) session.add(main_section) session.commit() @@ -256,249 +313,283 @@ def upgrade(): q1_nomination_capacity = Question( application_form_id=app_form_id, section_id=main_section.id, - headline='Nomination Capacity', - placeholder='', + headline="Nomination Capacity", + placeholder="", order=1, - questionType='multi-choice', + questionType="multi-choice", validation_regex=1, is_required=True, options=[ - {'label':'Self-nomination', 'value':'self'}, - {'label':'Nomination on behalf of a candidate','value':'other'} - ] + {"label": "Self-nomination", "value": "self"}, + {"label": "Nomination on behalf of a candidate", "value": "other"}, + ], ) session.add(q1_nomination_capacity) session.commit() - nominator_information = Section(app_form_id, 'Nomination Information', """ + nominator_information = Section( + app_form_id, + "Nomination Information", + """ Details of the person nominating the doctoral candidate. - """,2) + """, + 2, + ) nominator_information.depends_on_question_id = q1_nomination_capacity.id - nominator_information.show_for_values = ['other'] + nominator_information.show_for_values = ["other"] session.add(nominator_information) session.commit() nomination_q1 = Question( application_form_id=app_form_id, section_id=nominator_information.id, - headline='University', - placeholder='University', + headline="University", + placeholder="University", order=1, - questionType='short-text', + questionType="short-text", validation_regex=1, is_required=True, - description='The university that you (the nominator) are based at.' + description="The university that you (the nominator) are based at.", ) nomination_q2 = Question( application_form_id=app_form_id, section_id=nominator_information.id, - headline='Department', - placeholder='Department', + headline="Department", + placeholder="Department", order=2, - questionType='short-text', + questionType="short-text", validation_regex=1, is_required=True, - description='The department that you (the nominator) are based at.' + description="The department that you (the nominator) are based at.", ) nomination_q3 = Question( application_form_id=app_form_id, section_id=nominator_information.id, - headline='Describe your relationship to the doctoral candidate', - placeholder='', + headline="Describe your relationship to the doctoral candidate", + placeholder="", order=3, - questionType='long-text', + questionType="long-text", validation_regex=1, - is_required=True + is_required=True, ) - session.add_all([nomination_q1,nomination_q2,nomination_q3]) + session.add_all([nomination_q1, nomination_q2, nomination_q3]) session.commit() - candidate_information = Section(app_form_id, 'Doctoral Candidate Information', 'Details of the nominated doctoral candidate to be considered for the award.',3) + candidate_information = Section( + app_form_id, + "Doctoral Candidate Information", + "Details of the nominated doctoral candidate to be considered for the award.", + 3, + ) session.add(candidate_information) session.commit() - candidate_q1 = Question( application_form_id=app_form_id, section_id=candidate_information.id, - headline='Title', - placeholder='Title', + headline="Title", + placeholder="Title", order=1, - questionType='short-text', + questionType="short-text", validation_regex=1, - is_required=True + is_required=True, ) - candidate_q1.depends_on_question_id=q1_nomination_capacity.id - candidate_q1.show_for_values=['other'] + candidate_q1.depends_on_question_id = q1_nomination_capacity.id + candidate_q1.show_for_values = ["other"] candidate_q2 = Question( application_form_id=app_form_id, section_id=candidate_information.id, - headline='Firstname', - placeholder='Firstname', + headline="Firstname", + placeholder="Firstname", order=2, - questionType='short-text', + questionType="short-text", validation_regex=1, - is_required=True + is_required=True, ) - candidate_q2.depends_on_question_id=q1_nomination_capacity.id - candidate_q2.show_for_values=['other'] - + candidate_q2.depends_on_question_id = q1_nomination_capacity.id + candidate_q2.show_for_values = ["other"] + candidate_q3 = Question( application_form_id=app_form_id, section_id=candidate_information.id, - headline='Lastname', - placeholder='Lastname', + headline="Lastname", + placeholder="Lastname", order=3, - questionType='short-text', + questionType="short-text", validation_regex=1, - is_required=True + is_required=True, ) - candidate_q3.depends_on_question_id=q1_nomination_capacity.id - candidate_q3.show_for_values=['other'] - + candidate_q3.depends_on_question_id = q1_nomination_capacity.id + candidate_q3.show_for_values = ["other"] + candidate_q4 = Question( application_form_id=app_form_id, section_id=candidate_information.id, - headline='Email Address', - placeholder='Email Address', + headline="Email Address", + placeholder="Email Address", order=4, - questionType='short-text', + questionType="short-text", validation_regex=1, - is_required=True + is_required=True, ) - candidate_q4.depends_on_question_id=q1_nomination_capacity.id - candidate_q4.show_for_values=['other'] + candidate_q4.depends_on_question_id = q1_nomination_capacity.id + candidate_q4.show_for_values = ["other"] candidate_q5 = Question( application_form_id=app_form_id, section_id=candidate_information.id, - headline='University', - placeholder='University', + headline="University", + placeholder="University", order=5, - questionType='short-text', + questionType="short-text", validation_regex=1, - is_required=True + is_required=True, ) candidate_q6 = Question( application_form_id=app_form_id, section_id=candidate_information.id, - headline='Country of Residence', - placeholder='Choose an option', + headline="Country of Residence", + placeholder="Choose an option", order=6, - questionType='multi-choice', + questionType="multi-choice", validation_regex=1, is_required=True, - options=get_country_list(session) + options=get_country_list(session), ) candidate_q7 = Question( application_form_id=app_form_id, section_id=candidate_information.id, - headline='Nationality', - placeholder='Choose an option', + headline="Nationality", + placeholder="Choose an option", order=7, - questionType='multi-choice', + questionType="multi-choice", validation_regex=1, is_required=True, - options=get_country_list(session) + options=get_country_list(session), + ) + session.add_all( + [ + candidate_q1, + candidate_q2, + candidate_q3, + candidate_q4, + candidate_q5, + candidate_q6, + candidate_q7, + ] ) - session.add_all([candidate_q1, candidate_q2,candidate_q3,candidate_q4,candidate_q5,candidate_q6,candidate_q7]) session.commit() - dissertation_information = Section(app_form_id, 'Doctoral dissertation information', 'Details of the Doctoral dissertation of the nominated candidate.', 4) + dissertation_information = Section( + app_form_id, + "Doctoral dissertation information", + "Details of the Doctoral dissertation of the nominated candidate.", + 4, + ) session.add(dissertation_information) session.commit() dissertation_q1 = Question( application_form_id=app_form_id, section_id=dissertation_information.id, - headline='Title', - placeholder='Title', + headline="Title", + placeholder="Title", order=1, - questionType='short-text', + questionType="short-text", validation_regex=1, - is_required=True + is_required=True, ) dissertation_q2 = Question( application_form_id=app_form_id, section_id=dissertation_information.id, - headline='Abstract', - placeholder='Enter up to 800 words', + headline="Abstract", + placeholder="Enter up to 800 words", order=2, - questionType='long-text', - validation_regex='^\s*(\S+(\s+|$)){0,800}$', + questionType="long-text", + validation_regex="^\s*(\S+(\s+|$)){0,800}$", is_required=True, - description='Abstract of dissertation (<= 800 words)' + description="Abstract of dissertation (<= 800 words)", ) dissertation_q3 = Question( application_form_id=app_form_id, section_id=dissertation_information.id, headline="What are the dissertation's primary contributions to its field of research?", - placeholder='Enter 400-500 words', + placeholder="Enter 400-500 words", order=3, - questionType='long-text', - validation_regex='^\s*(\S+(\s+|$)){400,500}$', + questionType="long-text", + validation_regex="^\s*(\S+(\s+|$)){400,500}$", is_required=True, - description='Succinctly describe the novel contributions of this work (400-500 words)' + description="Succinctly describe the novel contributions of this work (400-500 words)", ) dissertation_q4 = Question( application_form_id=app_form_id, section_id=dissertation_information.id, - headline='Name of Doctoral academic supervisor', - placeholder='Name of Supervisor', + headline="Name of Doctoral academic supervisor", + placeholder="Name of Supervisor", order=4, - questionType='short-text', + questionType="short-text", validation_regex=1, - is_required=True + is_required=True, ) dissertation_q5 = Question( application_form_id=app_form_id, section_id=dissertation_information.id, - headline='E-mail address of Doctoral academic supervisor', - placeholder='Supervisor Email Address', + headline="E-mail address of Doctoral academic supervisor", + placeholder="Supervisor Email Address", order=5, - questionType='short-text', + questionType="short-text", validation_regex=1, - is_required=True + is_required=True, ) dissertation_q6 = Question( application_form_id=app_form_id, section_id=dissertation_information.id, - headline='Date of submission/defence of dissertation', - placeholder='Date of submission', + headline="Date of submission/defence of dissertation", + placeholder="Date of submission", order=6, - questionType='date', + questionType="date", validation_regex=1, - is_required=True + is_required=True, ) dissertation_q7 = Question( application_form_id=app_form_id, section_id=dissertation_information.id, - headline='Name(s) of examiner/s', - placeholder='Name(s) of examiner/s', + headline="Name(s) of examiner/s", + placeholder="Name(s) of examiner/s", order=7, - questionType='short-text', + questionType="short-text", validation_regex=1, - is_required=False + is_required=False, ) - session.add_all([dissertation_q1,dissertation_q2,dissertation_q3,dissertation_q4,dissertation_q5,dissertation_q6,dissertation_q7]) + session.add_all( + [ + dissertation_q1, + dissertation_q2, + dissertation_q3, + dissertation_q4, + dissertation_q5, + dissertation_q6, + dissertation_q7, + ] + ) session.commit() - supporting_information = Section(app_form_id, 'Supporting Documentation','',5) + supporting_information = Section(app_form_id, "Supporting Documentation", "", 5) session.add(supporting_information) session.commit() supporting_q1 = Question( application_form_id=app_form_id, section_id=supporting_information.id, - headline='Dissertation', - placeholder='Dissertation', + headline="Dissertation", + placeholder="Dissertation", order=1, - questionType='file', + questionType="file", validation_regex=1, is_required=True, - description='We require the electronic submission of the dissertation. We recommended that dissertations be written in English (the Awards Committee may require an English translation for full consideration of theses written in other languages).' + description="We require the electronic submission of the dissertation. We recommended that dissertations be written in English (the Awards Committee may require an English translation for full consideration of theses written in other languages).", ) supporting_q2 = Question( application_form_id=app_form_id, @@ -506,39 +597,39 @@ def upgrade(): headline="Examiners' reports", placeholder="Examiners' reports", order=2, - questionType='file', + questionType="file", validation_regex=1, is_required=True, - description="We require the examiners' report to be submitted. If this is not available, please contact awards@deeplearningindaba.com." + description="We require the examiners' report to be submitted. If this is not available, please contact awards@deeplearningindaba.com.", ) supporting_q3 = Question( application_form_id=app_form_id, section_id=supporting_information.id, - headline='Supporting Letter', - placeholder='', + headline="Supporting Letter", + placeholder="", order=3, - questionType='reference', + questionType="reference", validation_regex='{"min_num_referrals": 1, "max_num_referrals": 1}', is_required=True, description=""" A supporting letter that describes the main theoretical, methodological, and/or applied contributions of the thesis should be submitted by an academic who is in a position to comment on the merits of the work and the candidate, e.g., Doctoral supervisor, thesis examiner, academic mentor, collaborators, etc. The letter should be written by someone other than the person who is nominating the candidate. Supporting letters should be 600 words at most, written in English, and submitted electronically in PDF format by the closing date, using Baobab - """ + """, ) supporting_q4 = Question( application_form_id=app_form_id, section_id=supporting_information.id, - headline='Additional Comments', - placeholder='', + headline="Additional Comments", + placeholder="", order=4, - questionType='long-text', + questionType="long-text", validation_regex=1, is_required=False, - description='Use this space to provide any additional details which you feel are relevant to this nomination and have not been captured by this form.' + description="Use this space to provide any additional details which you feel are relevant to this nomination and have not been captured by this form.", ) - session.add_all([supporting_q1,supporting_q2,supporting_q3,supporting_q4]) + session.add_all([supporting_q1, supporting_q2, supporting_q3, supporting_q4]) session.commit() @@ -547,12 +638,12 @@ def downgrade(): Base.metadata.bind = op.get_bind() session = orm.Session(bind=Base.metadata.bind) - event = session.query(Event).filter_by(key='kambuledoctoral2020').first() + event = session.query(Event).filter_by(key="kambuledoctoral2020").first() app_form = session.query(ApplicationForm).filter_by(event_id=event.id).first() session.query(Question).filter_by(application_form_id=app_form.id).delete() session.query(Section).filter_by(application_form_id=app_form.id).delete() session.query(ApplicationForm).filter_by(event_id=event.id).delete() - session.query(Event).filter_by(key='kambuledoctoral2020').delete() + session.query(Event).filter_by(key="kambuledoctoral2020").delete() - session.commit() \ No newline at end of file + session.commit() diff --git a/api/migrations/versions/1993beca93d7_.py b/api/migrations/versions/1993beca93d7_.py index 518b837d1..56d3d0a43 100644 --- a/api/migrations/versions/1993beca93d7_.py +++ b/api/migrations/versions/1993beca93d7_.py @@ -7,27 +7,31 @@ """ # revision identifiers, used by Alembic. -revision = '1993beca93d7' -down_revision = 'c3b4ca7ceef9' +revision = "1993beca93d7" +down_revision = "c3b4ca7ceef9" -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.create_table('reference', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('reference_request_id', sa.Integer(), nullable=False), - sa.Column('uploaded_document', sa.String(length=128), nullable=True), - sa.Column('timestamp', sa.DateTime(), nullable=False), - sa.ForeignKeyConstraint(['reference_request_id'], ['reference_request.id'], ), - sa.PrimaryKeyConstraint('id') + op.create_table( + "reference", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("reference_request_id", sa.Integer(), nullable=False), + sa.Column("uploaded_document", sa.String(length=128), nullable=True), + sa.Column("timestamp", sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint( + ["reference_request_id"], + ["reference_request.id"], + ), + sa.PrimaryKeyConstraint("id"), ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('reference') + op.drop_table("reference") # ### end Alembic commands ### diff --git a/api/migrations/versions/1afd8dfc8d05_add_neuro_imbizo_org.py b/api/migrations/versions/1afd8dfc8d05_add_neuro_imbizo_org.py index afadab5f6..6a2df6dcd 100644 --- a/api/migrations/versions/1afd8dfc8d05_add_neuro_imbizo_org.py +++ b/api/migrations/versions/1afd8dfc8d05_add_neuro_imbizo_org.py @@ -7,23 +7,25 @@ """ # revision identifiers, used by Alembic. -revision = '1afd8dfc8d05' -down_revision = '07d536b58953' +revision = "1afd8dfc8d05" +down_revision = "07d536b58953" + +import datetime -from alembic import op import sqlalchemy as sa +from alembic import op from sqlalchemy import orm from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy import orm + from app import db -import datetime Base = declarative_base() + class Organisation(Base): __tablename__ = "organisation" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(50), nullable=False) @@ -38,7 +40,20 @@ class Organisation(Base): privacy_policy = db.Column(db.String(100), nullable=False) languages = db.Column(db.JSON(), nullable=False) - def __init__(self, name, system_name, small_logo, large_logo, icon_logo, domain, url, email_from, system_url, privacy_policy, languages): + def __init__( + self, + name, + system_name, + small_logo, + large_logo, + icon_logo, + domain, + url, + email_from, + system_url, + privacy_policy, + languages, + ): self.name = name self.small_logo = small_logo self.large_logo = large_logo @@ -57,17 +72,18 @@ def upgrade(): session = orm.Session(bind=Base.metadata.bind) imbizo = Organisation( - name='IBRO-Simons Computational Neuroscience Imbizo', - system_name='Baobab', - small_logo='imbizo_logo_small.png', - large_logo='imbizo_logo_large.png', - icon_logo='imbizo_favicon.png', - domain='imbizo', - url='http://imbizo.africa', - email_from='baobab@imbizo.africa', - system_url='https://baobab.imbizo.africa', - privacy_policy='PrivacyPolicy_Imbizo.pdf', - languages=[{"code": "en", "description": "English"}]) + name="IBRO-Simons Computational Neuroscience Imbizo", + system_name="Baobab", + small_logo="imbizo_logo_small.png", + large_logo="imbizo_logo_large.png", + icon_logo="imbizo_favicon.png", + domain="imbizo", + url="http://imbizo.africa", + email_from="baobab@imbizo.africa", + system_url="https://baobab.imbizo.africa", + privacy_policy="PrivacyPolicy_Imbizo.pdf", + languages=[{"code": "en", "description": "English"}], + ) session.add(imbizo) session.commit() @@ -77,5 +93,7 @@ def downgrade(): Base.metadata.bind = op.get_bind() session = orm.Session(bind=Base.metadata.bind) - session.query(Organisation).filter_by(name='IBRO-Simons Computational Neuroscience Imbizo').delete() + session.query(Organisation).filter_by( + name="IBRO-Simons Computational Neuroscience Imbizo" + ).delete() session.commit() diff --git a/api/migrations/versions/1c3fda18bad6_.py b/api/migrations/versions/1c3fda18bad6_.py index a573b0416..53936ab8c 100644 --- a/api/migrations/versions/1c3fda18bad6_.py +++ b/api/migrations/versions/1c3fda18bad6_.py @@ -7,17 +7,18 @@ """ # revision identifiers, used by Alembic. -revision = '1c3fda18bad6' -down_revision = 'c3c5ef958ca1' +revision = "1c3fda18bad6" +down_revision = "c3c5ef958ca1" -from alembic import op +import datetime + import sqlalchemy as sa +from alembic import op from sqlalchemy import orm from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy import orm + from app import db -import datetime Base = declarative_base() @@ -25,7 +26,7 @@ class Organisation(Base): __tablename__ = "organisation" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(50), nullable=False) @@ -36,7 +37,18 @@ class Organisation(Base): url = db.Column(db.String(100), nullable=False) email_from = db.Column(db.String(100), nullable=True) system_url = db.Column(db.String(100), nullable=False) - def __init__(self, name, system_name, small_logo, large_logo, domain, url, email_from, system_url): + + def __init__( + self, + name, + system_name, + small_logo, + large_logo, + domain, + url, + email_from, + system_url, + ): self.name = name self.small_logo = small_logo self.large_logo = large_logo @@ -45,19 +57,23 @@ def __init__(self, name, system_name, small_logo, large_logo, domain, url, email self.url = url self.email_from = email_from self.system_url = system_url + + class Country(Base): __tablename__ = "country" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(100), nullable=False) def __init__(self, name): self.name = name + + class Event(Base): __tablename__ = "event" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(50), nullable=False) @@ -65,8 +81,9 @@ class Event(Base): start_date = db.Column(db.DateTime(), nullable=False) end_date = db.Column(db.DateTime(), nullable=False) key = db.Column(db.String(255), nullable=False, unique=True) - organisation_id = db.Column(db.Integer(), db.ForeignKey( - 'organisation.id'), nullable=False) + organisation_id = db.Column( + db.Integer(), db.ForeignKey("organisation.id"), nullable=False + ) email_from = db.Column(db.String(255), nullable=False) url = db.Column(db.String(255), nullable=False) @@ -80,29 +97,30 @@ class Event(Base): offer_close = db.Column(db.DateTime(), nullable=False) registration_open = db.Column(db.DateTime(), nullable=False) registration_close = db.Column(db.DateTime(), nullable=False) - event_type = db.Column(db.String(),nullable = False) - - def __init__(self, - name, - description, - start_date, - end_date, - key, - organisation_id, - email_from, - url, - application_open, - application_close, - review_open, - review_close, - selection_open, - selection_close, - offer_open, - offer_close, - registration_open, - registration_close, - event_type, - ): + event_type = db.Column(db.String(), nullable=False) + + def __init__( + self, + name, + description, + start_date, + end_date, + key, + organisation_id, + email_from, + url, + application_open, + application_close, + review_open, + review_close, + selection_open, + selection_close, + offer_open, + offer_close, + registration_open, + registration_close, + event_type, + ): self.name = name self.description = description self.start_date = start_date @@ -122,31 +140,37 @@ def __init__(self, self.registration_open = registration_open self.registration_close = registration_close self.event_type = event_type + + class ApplicationForm(Base): - __tablename__ = 'application_form' - __table_args__ = {'extend_existing': True} + __tablename__ = "application_form" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - event_id = db.Column(db.Integer(), db.ForeignKey('event.id'), nullable=False) + event_id = db.Column(db.Integer(), db.ForeignKey("event.id"), nullable=False) nominations = db.Column(db.Boolean(), nullable=False) is_open = db.Column(db.Boolean(), nullable=False) - def __init__(self, event_id, nominations, is_open): self.event_id = event_id self.nominations = nominations self.is_open = is_open + class Section(Base): - __tablename__ = 'section' - __table_args__ = {'extend_existing': True} + __tablename__ = "section" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) name = db.Column(db.String(255), nullable=False) description = db.Column(db.String(255), nullable=False) order = db.Column(db.Integer(), nullable=False) - depends_on_question_id = db.Column(db.Integer(), db.ForeignKey('question.id', use_alter=True), nullable=True) + depends_on_question_id = db.Column( + db.Integer(), db.ForeignKey("question.id", use_alter=True), nullable=True + ) show_for_values = db.Column(db.JSON(), nullable=True) def __init__(self, application_form_id, name, description, order): @@ -157,12 +181,14 @@ def __init__(self, application_form_id, name, description, order): class Question(Base): - __tablename__ = 'question' - __table_args__ = {'extend_existing': True} + __tablename__ = "question" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) - section_id = db.Column(db.Integer(), db.ForeignKey('section.id'), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) + section_id = db.Column(db.Integer(), db.ForeignKey("section.id"), nullable=False) type = db.Column(db.String(), nullable=False) description = db.Column(db.String(), nullable=True) headline = db.Column(db.String(), nullable=False) @@ -172,10 +198,25 @@ class Question(Base): order = db.Column(db.Integer(), nullable=False) options = db.Column(db.JSON(), nullable=True) is_required = db.Column(db.Boolean(), nullable=False) - depends_on_question_id = db.Column(db.Integer(), db.ForeignKey('question.id'), nullable=True) + depends_on_question_id = db.Column( + db.Integer(), db.ForeignKey("question.id"), nullable=True + ) show_for_values = db.Column(db.JSON(), nullable=True) - - def __init__(self, application_form_id, section_id, questionType, headline, placeholder, order, validation_regex, validation_text=None, description = None, is_required = True, options = None): + + def __init__( + self, + application_form_id, + section_id, + questionType, + headline, + placeholder, + order, + validation_regex, + validation_text=None, + description=None, + is_required=True, + options=None, + ): self.application_form_id = application_form_id self.section_id = section_id self.headline = headline @@ -188,230 +229,625 @@ def __init__(self, application_form_id, section_id, questionType, headline, plac self.validation_regex = validation_regex self.validation_text = validation_text + def upgrade(): Base.metadata.bind = op.get_bind() session = orm.Session(bind=Base.metadata.bind) # Reset auto-increment ids - op.get_bind().execute("""SELECT setval('event_id_seq', (SELECT max(id) FROM event));""") - op.get_bind().execute("""SELECT setval('application_form_id_seq', (SELECT max(id) FROM application_form));""") - op.get_bind().execute("""SELECT setval('section_id_seq', (SELECT max(id) FROM section));""") - op.get_bind().execute("""SELECT setval('question_id_seq', (SELECT max(id) FROM question));""") - - #Add event - DeepLearningIndaba2020 = Event('Deep Learning Indaba 2020','The Deep Learning Indaba 2020, Tunis, Tunisia', - datetime.date(2020, 8, 23),datetime.date(2020, 8, 28), 'indaba2020',1,'baobab@deeplearningindaba.com', - 'http://www.deeplearningindaba.com/',datetime.date(2020, 3, 1),datetime.date(2020, 4, 17), - datetime.date(2020, 4, 25), datetime.date(2020, 5, 15), datetime.date(2020, 5, 15), - datetime.date(2020, 6, 1),datetime.date(2020, 6, 1),datetime.date(2020, 7, 31), - datetime.date(2020, 6, 1),datetime.date(2020, 7, 31),'EVENT') + op.get_bind().execute( + """SELECT setval('event_id_seq', (SELECT max(id) FROM event));""" + ) + op.get_bind().execute( + """SELECT setval('application_form_id_seq', (SELECT max(id) FROM application_form));""" + ) + op.get_bind().execute( + """SELECT setval('section_id_seq', (SELECT max(id) FROM section));""" + ) + op.get_bind().execute( + """SELECT setval('question_id_seq', (SELECT max(id) FROM question));""" + ) + + # Add event + DeepLearningIndaba2020 = Event( + "Deep Learning Indaba 2020", + "The Deep Learning Indaba 2020, Tunis, Tunisia", + datetime.date(2020, 8, 23), + datetime.date(2020, 8, 28), + "indaba2020", + 1, + "baobab@deeplearningindaba.com", + "http://www.deeplearningindaba.com/", + datetime.date(2020, 3, 1), + datetime.date(2020, 4, 17), + datetime.date(2020, 4, 25), + datetime.date(2020, 5, 15), + datetime.date(2020, 5, 15), + datetime.date(2020, 6, 1), + datetime.date(2020, 6, 1), + datetime.date(2020, 7, 31), + datetime.date(2020, 6, 1), + datetime.date(2020, 7, 31), + "EVENT", + ) session.add(DeepLearningIndaba2020) session.commit() - app_form = ApplicationForm(DeepLearningIndaba2020.id,False,True) + app_form = ApplicationForm(DeepLearningIndaba2020.id, False, True) session.add(app_form) session.commit() - - #Add Section - main_section = Section(app_form.id, 'Deep Learning Indaba 2020 Application Form', 'This is the official application form to apply for participation in the Deep Learning Indaba to be held 23-28 August 2020 in Tunis, Tunisia.', 1) + + # Add Section + main_section = Section( + app_form.id, + "Deep Learning Indaba 2020 Application Form", + "This is the official application form to apply for participation in the Deep Learning Indaba to be held 23-28 August 2020 in Tunis, Tunisia.", + 1, + ) session.add(main_section) session.commit() - main_q1 = Question(app_form.id, main_section.id, 'multi-choice', 'Application Category', 'Category',1, None, description = 'Please select the option that best describes you', - options = [ - {'label': 'An undergraduate student', 'value': 'undergrad'}, - {'label': 'A masters student', 'value': 'masters'}, - {'label': 'A PhD student', 'value': 'phd'}, - {'label': 'A Post-doc', 'value': 'postdoc'}, - {'label': 'Academic Faculty', 'value': 'faculty'}, - {'label': 'Industry Professional', 'value': 'industry'}, - {'label': 'Student at a coding academy', 'value': 'student-coding-academy'}, - {'label': 'Unemployed', 'value': 'unemployed'} - ]) + main_q1 = Question( + app_form.id, + main_section.id, + "multi-choice", + "Application Category", + "Category", + 1, + None, + description="Please select the option that best describes you", + options=[ + {"label": "An undergraduate student", "value": "undergrad"}, + {"label": "A masters student", "value": "masters"}, + {"label": "A PhD student", "value": "phd"}, + {"label": "A Post-doc", "value": "postdoc"}, + {"label": "Academic Faculty", "value": "faculty"}, + {"label": "Industry Professional", "value": "industry"}, + {"label": "Student at a coding academy", "value": "student-coding-academy"}, + {"label": "Unemployed", "value": "unemployed"}, + ], + ) session.add(main_q1) - demographics = Section(app_form.id, 'Demographics', '', 2) + demographics = Section(app_form.id, "Demographics", "", 2) session.add(demographics) session.commit() - - demographics_q1 = Question(app_form.id, demographics.id, 'multi-choice', 'Country of nationality', 'Country of nationality', 1, None, options = get_country_list(session)) - demographics_q2 = Question(app_form.id, demographics.id, 'multi-choice', 'Country of residence', 'Country of residence', 2, None, options=get_country_list(session)) - demographics_q3 = Question(app_form.id, demographics.id, 'multi-choice', 'Gender', 'Gender', 3, None, - options = [ - {'label': 'Male', 'value': 'male'}, - {'label': 'Female', 'value': 'female'}, - {'label': 'Transgender', 'value': 'transgender'}, - {'label': 'Gender variant/non-conforming', 'value': 'gender-variant/non-conforming'}, - {'label': 'Prefer not to say', 'value': 'prefer-not-to-say'} - ]) - demographics_q4 = Question(app_form.id, demographics.id, 'multi-choice', 'Disabilities', 'Disabilities', 4, None, description = 'We collect this information to ensure that we provide suitable facilities at our venue.', - options = [ - {"label": "No disabilities", "value": "none"}, - {"label": "Sight disability", "value": "sight"}, - {"label": "Hearing disability", "value": "hearing"}, - {"label": "Communication disability", "value": "communication"}, - {"label": "Physical disability(e.g. difficulty in walking)", "value": "physical"}, - {"label": "Mental disability(e.g. difficulty in remembering or concentrating)", "value": "mental"}, - {"label": "Difficulty in self-care", "value": "self-care"}, - {"label": "Other", "value": "other"} - ]) - demographics_q5 = Question(app_form.id, demographics.id, 'short-text', 'Affiliation', 'Affiliation', 5, None, description = 'The university / institution / company you are based at') - demographics_q6 = Question(app_form.id, demographics.id, 'short-text', 'Department', 'Department', 6, None, description = 'The department or field of study you fall under at your affiliation') - - session.add_all([demographics_q1, demographics_q2, demographics_q3, demographics_q4, demographics_q5, demographics_q6]) - - about_you = Section(app_form.id, 'Tell Us a Bit About You', """ + demographics_q1 = Question( + app_form.id, + demographics.id, + "multi-choice", + "Country of nationality", + "Country of nationality", + 1, + None, + options=get_country_list(session), + ) + demographics_q2 = Question( + app_form.id, + demographics.id, + "multi-choice", + "Country of residence", + "Country of residence", + 2, + None, + options=get_country_list(session), + ) + demographics_q3 = Question( + app_form.id, + demographics.id, + "multi-choice", + "Gender", + "Gender", + 3, + None, + options=[ + {"label": "Male", "value": "male"}, + {"label": "Female", "value": "female"}, + {"label": "Transgender", "value": "transgender"}, + { + "label": "Gender variant/non-conforming", + "value": "gender-variant/non-conforming", + }, + {"label": "Prefer not to say", "value": "prefer-not-to-say"}, + ], + ) + demographics_q4 = Question( + app_form.id, + demographics.id, + "multi-choice", + "Disabilities", + "Disabilities", + 4, + None, + description="We collect this information to ensure that we provide suitable facilities at our venue.", + options=[ + {"label": "No disabilities", "value": "none"}, + {"label": "Sight disability", "value": "sight"}, + {"label": "Hearing disability", "value": "hearing"}, + {"label": "Communication disability", "value": "communication"}, + { + "label": "Physical disability(e.g. difficulty in walking)", + "value": "physical", + }, + { + "label": "Mental disability(e.g. difficulty in remembering or concentrating)", + "value": "mental", + }, + {"label": "Difficulty in self-care", "value": "self-care"}, + {"label": "Other", "value": "other"}, + ], + ) + demographics_q5 = Question( + app_form.id, + demographics.id, + "short-text", + "Affiliation", + "Affiliation", + 5, + None, + description="The university / institution / company you are based at", + ) + demographics_q6 = Question( + app_form.id, + demographics.id, + "short-text", + "Department", + "Department", + 6, + None, + description="The department or field of study you fall under at your affiliation", + ) + + session.add_all( + [ + demographics_q1, + demographics_q2, + demographics_q3, + demographics_q4, + demographics_q5, + demographics_q6, + ] + ) + + about_you = Section( + app_form.id, + "Tell Us a Bit About You", + """ Please use this section to tell us a bit more about yourself and your intentions as a future Deep Learning Indaba ambassador - Take your time to fill in this section as it has the highest impact on whether or not you will be offered a place at the Indaba!""", 3) + Take your time to fill in this section as it has the highest impact on whether or not you will be offered a place at the Indaba!""", + 3, + ) session.add(about_you) session.commit() - about_you_q1 = Question(app_form.id, about_you.id, 'long-text', 'Why is attending the Deep Learning Indaba 2020 important to you?', 'Enter 100 to 200 words', 1, validation_regex = '^\s*(\S+(\s+|$)){100,200}$', validation_text = 'You must enter 100 to 200 words', description = 'Enter 100 to 200 words') - - about_you_q2 = Question(app_form.id, about_you.id, 'long-text', 'How will you share what you have learned after the Indaba?', 'Enter 50 to 150 words', 2, validation_regex = '^\s*(\S+(\s+|$)){50,150}$', validation_text = 'You must enter 50 to 150 words', description = 'Enter 50 to 150 words') + about_you_q1 = Question( + app_form.id, + about_you.id, + "long-text", + "Why is attending the Deep Learning Indaba 2020 important to you?", + "Enter 100 to 200 words", + 1, + validation_regex="^\s*(\S+(\s+|$)){100,200}$", + validation_text="You must enter 100 to 200 words", + description="Enter 100 to 200 words", + ) + + about_you_q2 = Question( + app_form.id, + about_you.id, + "long-text", + "How will you share what you have learned after the Indaba?", + "Enter 50 to 150 words", + 2, + validation_regex="^\s*(\S+(\s+|$)){50,150}$", + validation_text="You must enter 50 to 150 words", + description="Enter 50 to 150 words", + ) about_you_q2.depends_on_question_id = main_q1.id - about_you_q2.show_for_values = ['undergraduate', 'masters', 'phd', 'post-doc', 'student at a coding academy', 'unemployed'] - - about_you_q3 = Question(app_form.id, about_you.id, 'long-text', 'How will you use your experience at the Deep Learning Indaba to impact your teaching, research, supervision, and/or institution?', 'Enter 50 to 150 words', 3, validation_regex = '^\s*(\S+(\s+|$)){50,150}$', validation_text = 'You must enter 50 to 150 words', description = 'Enter 50 to 150 words') + about_you_q2.show_for_values = [ + "undergraduate", + "masters", + "phd", + "post-doc", + "student at a coding academy", + "unemployed", + ] + + about_you_q3 = Question( + app_form.id, + about_you.id, + "long-text", + "How will you use your experience at the Deep Learning Indaba to impact your teaching, research, supervision, and/or institution?", + "Enter 50 to 150 words", + 3, + validation_regex="^\s*(\S+(\s+|$)){50,150}$", + validation_text="You must enter 50 to 150 words", + description="Enter 50 to 150 words", + ) about_you_q3.depends_on_question_id = main_q1.id - about_you_q3.show_for_values = ['academic-faculty'] - - about_you_q4 = Question(app_form.id, about_you.id, 'long-text', 'Share with us a favourite machine learning resource you use: a paper, blog post, algorithm, result, or finding. Tell us why.', 'Enter up to 80 words', 4, validation_regex = "^\s*(\S+(\s+|$)){0,80}$", validation_text = 'You must enter up to 80 words', description = 'Enter up to 80 words, remember to include *why*') + about_you_q3.show_for_values = ["academic-faculty"] + + about_you_q4 = Question( + app_form.id, + about_you.id, + "long-text", + "Share with us a favourite machine learning resource you use: a paper, blog post, algorithm, result, or finding. Tell us why.", + "Enter up to 80 words", + 4, + validation_regex="^\s*(\S+(\s+|$)){0,80}$", + validation_text="You must enter up to 80 words", + description="Enter up to 80 words, remember to include *why*", + ) about_you_q4.depends_on_question_id = main_q1.id - about_you_q4.show_for_values = ['undergraduate', 'masters', 'phd', 'post-doc', 'student at a coding academy', 'unemployed', 'academic-faculty'] - - about_you_q5 = Question(app_form.id, about_you.id, 'long-text', 'Are you or have you been a tutor for any relevant course, or part of any machine learning or data science society or meetup? If yes, give details.', 'Enter up to 80 words', 5, validation_regex = '^\s*(\S+(\s+|$)){0,80}$', validation_text = 'You must enter up to 80 words', description = 'Enter up to 80 words') + about_you_q4.show_for_values = [ + "undergraduate", + "masters", + "phd", + "post-doc", + "student at a coding academy", + "unemployed", + "academic-faculty", + ] + + about_you_q5 = Question( + app_form.id, + about_you.id, + "long-text", + "Are you or have you been a tutor for any relevant course, or part of any machine learning or data science society or meetup? If yes, give details.", + "Enter up to 80 words", + 5, + validation_regex="^\s*(\S+(\s+|$)){0,80}$", + validation_text="You must enter up to 80 words", + description="Enter up to 80 words", + ) about_you_q5.depends_on_question_id = main_q1.id - about_you_q5.show_for_values = ['undergraduate', 'masters', 'phd', 'post-doc', 'student at a coding academy', 'unemployed'] - - about_you_q6 = Question(app_form.id, about_you.id, 'long-text', 'Have you taught any Machine Learning courses at your institution or supervised any postgraduate students on Machine Learning projects?', 'Enter up to 80 words', 6, validation_regex = '^\s*(\S+(\s+|$)){0,80}$', validation_text = 'You must enter up to 80 words', description = 'Enter up to 80 words') + about_you_q5.show_for_values = [ + "undergraduate", + "masters", + "phd", + "post-doc", + "student at a coding academy", + "unemployed", + ] + + about_you_q6 = Question( + app_form.id, + about_you.id, + "long-text", + "Have you taught any Machine Learning courses at your institution or supervised any postgraduate students on Machine Learning projects?", + "Enter up to 80 words", + 6, + validation_regex="^\s*(\S+(\s+|$)){0,80}$", + validation_text="You must enter up to 80 words", + description="Enter up to 80 words", + ) about_you_q6.depends_on_question_id = main_q1.id - about_you_q6.show_for_values = ['academic-faculty'] - - about_you_q7 = Question(app_form.id, about_you.id, 'multi-choice', 'Are you currently actively involved in Machine Learning Research?', 'Choose an option', 7, None, description = 'Choosing no will not count against you', - options = [ - {'label': 'Yes', 'value': 'yes'}, - {'label': 'No', 'value': 'no'} - ]) - about_you_q8 = Question(app_form.id, about_you.id, 'long-text', 'Add a short abstract describing your current research', 'Enter 150 to 250 words', 8, validation_regex = '^\s*(\S+(\s+|$)){150,250}$', validation_text = 'You must enter 150 to 250 words', description = 'This can be completed research or research in progress. Remember to include a description of your methodology and any key results you have so far.') + about_you_q6.show_for_values = ["academic-faculty"] + + about_you_q7 = Question( + app_form.id, + about_you.id, + "multi-choice", + "Are you currently actively involved in Machine Learning Research?", + "Choose an option", + 7, + None, + description="Choosing no will not count against you", + options=[{"label": "Yes", "value": "yes"}, {"label": "No", "value": "no"}], + ) + about_you_q8 = Question( + app_form.id, + about_you.id, + "long-text", + "Add a short abstract describing your current research", + "Enter 150 to 250 words", + 8, + validation_regex="^\s*(\S+(\s+|$)){150,250}$", + validation_text="You must enter 150 to 250 words", + description="This can be completed research or research in progress. Remember to include a description of your methodology and any key results you have so far.", + ) about_you_q8.depends_on_question_id = about_you_q7.id - about_you_q8.show_for_values = ['yes'] - - about_you_q9 = Question(app_form.id, about_you.id, 'multi-choice', 'Would you be interested in submitting an extended abstract or paper to the Indaba Symposium if you are selected to attend the Indaba?', 'Choose an option', 9, None, description = "We won't hold you to this, it's just to gauge interest.") + about_you_q8.show_for_values = ["yes"] + + about_you_q9 = Question( + app_form.id, + about_you.id, + "multi-choice", + "Would you be interested in submitting an extended abstract or paper to the Indaba Symposium if you are selected to attend the Indaba?", + "Choose an option", + 9, + None, + description="We won't hold you to this, it's just to gauge interest.", + ) about_you_q9.depends_on_question_id = about_you_q7.id - about_you_q9.show_for_values = ['yes'] - - about_you_q10 = Question(app_form.id, about_you.id, 'long-text', 'Have you worked on a project that uses machine learning? Give a short description.', 'Enter upto 150 words', 10, validation_regex = '^\s*(\S+(\s+|$)){0,150}$', validation_text = 'You must enter up to 150 words') + about_you_q9.show_for_values = ["yes"] + + about_you_q10 = Question( + app_form.id, + about_you.id, + "long-text", + "Have you worked on a project that uses machine learning? Give a short description.", + "Enter upto 150 words", + 10, + validation_regex="^\s*(\S+(\s+|$)){0,150}$", + validation_text="You must enter up to 150 words", + ) about_you_q10.depends_on_question_id = about_you_q7.id - about_you_q10.show_for_values = ['no'] - - about_you_q11 = Question(app_form.id, about_you.id, 'file', 'Upload CV', None, 11, None, is_required = True) - about_you_q11.depends_on_question_id = main_q1.id - about_you_q11.show_for_values = ['undergraduate', 'masters', 'phd', 'post-doc', 'student at a coding academy', 'unemployed'] + about_you_q10.show_for_values = ["no"] - about_you_q12 = Question(app_form.id, about_you.id, 'multi-choice', 'May we add your CV and email address to a database for sharing with our sponsors?', 'Choose an option', 12, None, - options = [ - {'label': 'Yes', 'value': 'yes'}, - {'label': 'No', 'value': 'no'} - ]) + about_you_q11 = Question( + app_form.id, about_you.id, "file", "Upload CV", None, 11, None, is_required=True + ) + about_you_q11.depends_on_question_id = main_q1.id + about_you_q11.show_for_values = [ + "undergraduate", + "masters", + "phd", + "post-doc", + "student at a coding academy", + "unemployed", + ] + + about_you_q12 = Question( + app_form.id, + about_you.id, + "multi-choice", + "May we add your CV and email address to a database for sharing with our sponsors?", + "Choose an option", + 12, + None, + options=[{"label": "Yes", "value": "yes"}, {"label": "No", "value": "no"}], + ) about_you_q12.depends_on_question_id = main_q1.id - about_you_q12.show_for_values = ['undergraduate', 'masters', 'phd', 'post-doc', 'student at a coding academy', 'unemployed'] - - session.add_all([about_you_q1, about_you_q2, about_you_q3, about_you_q4, about_you_q5, about_you_q6, about_you_q7, about_you_q8, about_you_q9, about_you_q10, about_you_q11, about_you_q12]) - - travel_support = Section(app_form.id, 'Travel Support', """ We may be able to sponsor the cost of travel and accommodation for some attendees. These travel awards are limited and highly competitive, + about_you_q12.show_for_values = [ + "undergraduate", + "masters", + "phd", + "post-doc", + "student at a coding academy", + "unemployed", + ] + + session.add_all( + [ + about_you_q1, + about_you_q2, + about_you_q3, + about_you_q4, + about_you_q5, + about_you_q6, + about_you_q7, + about_you_q8, + about_you_q9, + about_you_q10, + about_you_q11, + about_you_q12, + ] + ) + + travel_support = Section( + app_form.id, + "Travel Support", + """ We may be able to sponsor the cost of travel and accommodation for some attendees. These travel awards are limited and highly competitive, but are assessed independently of Indaba attendance: applying for a travel award neither enhances nor undermines your chances of being accepted. To help as many people attend as possible, before applying for travel support, please check if your supervisor, department or university is able to support you in any way. - """, 4) + """, + 4, + ) travel_support.depends_on_question_id = main_q1.id - travel_support.show_for_values = ['undergraduate', 'masters', 'phd', 'post-doc', 'student at a coding academy', 'unemployed', 'academic-faculty'] + travel_support.show_for_values = [ + "undergraduate", + "masters", + "phd", + "post-doc", + "student at a coding academy", + "unemployed", + "academic-faculty", + ] session.add(travel_support) session.commit() - travel_support_q1 = Question(app_form.id, travel_support.id, 'multi-choice', 'Would you like to be considered for a travel grant?', 'Choose an option', 1, None, description = 'Travel awards will be used to cover the costs of return flights to the host city and/or accommodation in shared dorm rooms close to the venue.', - options = [ - {'label': 'Travel', 'value': 'travel'}, - {'label': 'Accommodation', 'value': 'accommodation'}, - {'label': 'Travel and Accommodation', 'value': 'travel-and-accommodation'}, - {'label': 'None', 'value': 'none'} - ]) - travel_support_q2 = Question(app_form.id, travel_support.id, 'short-text', 'Please state your intended airport of departure.', 'Airport of Departure', 2, None, description = 'Please note that we will only provide flights in the form of a return ticket to and from a single airport on the continent of Africa.') + travel_support_q1 = Question( + app_form.id, + travel_support.id, + "multi-choice", + "Would you like to be considered for a travel grant?", + "Choose an option", + 1, + None, + description="Travel awards will be used to cover the costs of return flights to the host city and/or accommodation in shared dorm rooms close to the venue.", + options=[ + {"label": "Travel", "value": "travel"}, + {"label": "Accommodation", "value": "accommodation"}, + {"label": "Travel and Accommodation", "value": "travel-and-accommodation"}, + {"label": "None", "value": "none"}, + ], + ) + travel_support_q2 = Question( + app_form.id, + travel_support.id, + "short-text", + "Please state your intended airport of departure.", + "Airport of Departure", + 2, + None, + description="Please note that we will only provide flights in the form of a return ticket to and from a single airport on the continent of Africa.", + ) travel_support_q2.depends_on_question_id = travel_support_q1.id - travel_support_q2.show_for_values =['travel', 'travel-accommodation'] - - travel_support_q3 = Question(app_form.id, travel_support.id, 'multi-choice', 'If you do not receive a travel award from us, will you still be able to attend?', 'Choose an option', 3, None, - options = [ - {'label': 'Yes', 'value': 'yes'}, - {'label': 'No', 'value': 'no'} - ]) + travel_support_q2.show_for_values = ["travel", "travel-accommodation"] + + travel_support_q3 = Question( + app_form.id, + travel_support.id, + "multi-choice", + "If you do not receive a travel award from us, will you still be able to attend?", + "Choose an option", + 3, + None, + options=[{"label": "Yes", "value": "yes"}, {"label": "No", "value": "no"}], + ) travel_support_q3.depends_on_question_id = travel_support_q1.id - travel_support_q3.show_for_values =['travel', 'accommodation', 'travel-accommodation'] - - travel_support_q4 = Question(app_form.id, travel_support.id, 'long-text', 'Would you like to be considered for a registration fee waiver? If so, please provide a motivation', 'Enter up to 80 words', 4, None, description = 'Enter up to 80 words', is_required = False) + travel_support_q3.show_for_values = [ + "travel", + "accommodation", + "travel-accommodation", + ] + + travel_support_q4 = Question( + app_form.id, + travel_support.id, + "long-text", + "Would you like to be considered for a registration fee waiver? If so, please provide a motivation", + "Enter up to 80 words", + 4, + None, + description="Enter up to 80 words", + is_required=False, + ) travel_support_q4.depends_on_question_id = main_q1.id - travel_support_q4.show_for_values = ['academic-faculty'] - - session.add_all([travel_support_q1, travel_support_q2, travel_support_q3, travel_support_q4]) - - attendance = Section(app_form.id, 'Previous Attendance', 'Help us quantify our progress by giving us some info on your previous Indaba experiences.', 5) + travel_support_q4.show_for_values = ["academic-faculty"] + + session.add_all( + [travel_support_q1, travel_support_q2, travel_support_q3, travel_support_q4] + ) + + attendance = Section( + app_form.id, + "Previous Attendance", + "Help us quantify our progress by giving us some info on your previous Indaba experiences.", + 5, + ) session.add(attendance) session.commit() - attendance_q1 = Question(app_form.id, attendance.id, 'multi-checkbox', 'Did you attend a previous edition of the Indaba?', None, 1, None, is_required = False, - options = [ - {'label': 'Indaba 2017', 'value': 'indaba-2017'}, - {'label': 'Indaba 2018', 'value': 'indaba-2018'}, - {'label': 'Indaba 2019', 'value': 'indaba-2019'} - ]) - attendance_q2 = Question(app_form.id, attendance.id, 'long-text', 'Tell us how your previous attendance has helped you grow or how you have used what you have learned.', 'Enter up to 150 words', 2, validation_regex ='^\s*(\S+(\s+|$)){0,150}$', validation_text = 'You must enter up to 150 words', description = 'Enter up to 150 words') + attendance_q1 = Question( + app_form.id, + attendance.id, + "multi-checkbox", + "Did you attend a previous edition of the Indaba?", + None, + 1, + None, + is_required=False, + options=[ + {"label": "Indaba 2017", "value": "indaba-2017"}, + {"label": "Indaba 2018", "value": "indaba-2018"}, + {"label": "Indaba 2019", "value": "indaba-2019"}, + ], + ) + attendance_q2 = Question( + app_form.id, + attendance.id, + "long-text", + "Tell us how your previous attendance has helped you grow or how you have used what you have learned.", + "Enter up to 150 words", + 2, + validation_regex="^\s*(\S+(\s+|$)){0,150}$", + validation_text="You must enter up to 150 words", + description="Enter up to 150 words", + ) attendance_q2.depends_on_question_id = attendance_q1.id - attendance_q2.show_for_values = ['indaba-2017', 'indaba-2018', 'indaba-2019', 'indaba-2017-indaba-2018', 'indaba-2017-indaba-2019', 'indaba-2018-indaba-2019', 'indaba-2017-indaba-2018-indaba-2019'] + attendance_q2.show_for_values = [ + "indaba-2017", + "indaba-2018", + "indaba-2019", + "indaba-2017-indaba-2018", + "indaba-2017-indaba-2019", + "indaba-2018-indaba-2019", + "indaba-2017-indaba-2018-indaba-2019", + ] session.add_all([attendance_q1, attendance_q2]) - additional_info = Section(app_form.id, 'Additional Information', 'Anything else you may want us to know', 6) + additional_info = Section( + app_form.id, + "Additional Information", + "Anything else you may want us to know", + 6, + ) session.add(additional_info) session.commit() - info_q1 = Question(app_form.id, additional_info.id, 'long-text', 'Any additional comments or remarks for the selection committee', 'Enter up to 80 words', 1, validation_regex = '^\s*(\S+(\s+|$)){0,80}$', validation_text = 'You must enter up to 80 words', description = 'Enter up to 80 words', is_required = False) - info_q2 = Question(app_form.id, additional_info.id, 'long-text', 'Anything else you think relevant, for example links to personal webpage, papers, GitHub/code repositories, community and outreach activities.', 'Enter up to 80 words', 2, validation_regex = '^\s*(\S+(\s+|$)){0,80}$', validation_text = 'You must enter up to 80 words', description = 'Enter up to 80 words', is_required = False) - info_q3 = Question(app_form.id, additional_info.id, 'multi-choice', 'If you are selected to attend the Indaba, would you also like to attend the AI Hack Tunisia.', 'Choose an option', 3, None, description = 'The AI Hack Tunisia will take place in the week of the 31st of August. Accepted Indaba attendees will automatically qualify for a place at the event.', - options = [ - {'label': 'Yes', 'value': 'yes'}, - {'label': 'No', 'value': 'no'}, - {'label': 'Maybe', 'value': 'maybe'} - - ]) + info_q1 = Question( + app_form.id, + additional_info.id, + "long-text", + "Any additional comments or remarks for the selection committee", + "Enter up to 80 words", + 1, + validation_regex="^\s*(\S+(\s+|$)){0,80}$", + validation_text="You must enter up to 80 words", + description="Enter up to 80 words", + is_required=False, + ) + info_q2 = Question( + app_form.id, + additional_info.id, + "long-text", + "Anything else you think relevant, for example links to personal webpage, papers, GitHub/code repositories, community and outreach activities.", + "Enter up to 80 words", + 2, + validation_regex="^\s*(\S+(\s+|$)){0,80}$", + validation_text="You must enter up to 80 words", + description="Enter up to 80 words", + is_required=False, + ) + info_q3 = Question( + app_form.id, + additional_info.id, + "multi-choice", + "If you are selected to attend the Indaba, would you also like to attend the AI Hack Tunisia.", + "Choose an option", + 3, + None, + description="The AI Hack Tunisia will take place in the week of the 31st of August. Accepted Indaba attendees will automatically qualify for a place at the event.", + options=[ + {"label": "Yes", "value": "yes"}, + {"label": "No", "value": "no"}, + {"label": "Maybe", "value": "maybe"}, + ], + ) session.add_all([info_q1, info_q2, info_q3]) + def get_country_list(session): countries = session.query(Country).all() country_list = [] for country in countries: - country_list.append({ - 'label': country.name, - 'value': country.name - }) + country_list.append({"label": country.name, "value": country.name}) return country_list + def downgrade(): Base.metadata.bind = op.get_bind() session = orm.Session(bind=Base.metadata.bind) - event = session.query(Event).filter_by(key='indaba2020').first() + event = session.query(Event).filter_by(key="indaba2020").first() app_form = session.query(ApplicationForm).filter_by(event_id=event.id).first() - op.get_bind().execute("""UPDATE Section SET depends_on_question_id=NULL WHERE application_form_id={}""".format(app_form.id)) + op.get_bind().execute( + """UPDATE Section SET depends_on_question_id=NULL WHERE application_form_id={}""".format( + app_form.id + ) + ) session.query(Question).filter_by(application_form_id=app_form.id).delete() session.query(Section).filter_by(application_form_id=app_form.id).delete() session.query(ApplicationForm).filter_by(event_id=event.id).delete() - session.query(Event).filter_by(key='indaba2020').delete() + session.query(Event).filter_by(key="indaba2020").delete() session.commit() - op.get_bind().execute("""SELECT setval('event_id_seq', (SELECT max(id) FROM event));""") - op.get_bind().execute("""SELECT setval('application_form_id_seq', (SELECT max(id) FROM application_form));""") - op.get_bind().execute("""SELECT setval('section_id_seq', (SELECT max(id) FROM section));""") - op.get_bind().execute("""SELECT setval('question_id_seq', (SELECT max(id) FROM question));""") + op.get_bind().execute( + """SELECT setval('event_id_seq', (SELECT max(id) FROM event));""" + ) + op.get_bind().execute( + """SELECT setval('application_form_id_seq', (SELECT max(id) FROM application_form));""" + ) + op.get_bind().execute( + """SELECT setval('section_id_seq', (SELECT max(id) FROM section));""" + ) + op.get_bind().execute( + """SELECT setval('question_id_seq', (SELECT max(id) FROM question));""" + ) diff --git a/api/migrations/versions/1e05a293f402_eeml_registration_form.py b/api/migrations/versions/1e05a293f402_eeml_registration_form.py index 516dd4216..665b1648e 100644 --- a/api/migrations/versions/1e05a293f402_eeml_registration_form.py +++ b/api/migrations/versions/1e05a293f402_eeml_registration_form.py @@ -7,35 +7,31 @@ """ # revision identifiers, used by Alembic. -revision = '1e05a293f402' -down_revision = '91a7bc19a5ec' +revision = "1e05a293f402" +down_revision = "91a7bc19a5ec" -from alembic import op -import sqlalchemy as sa +import datetime +from enum import Enum -from alembic import op import sqlalchemy as sa +from alembic import op from sqlalchemy import orm from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy import orm + from app import db -import datetime -from enum import Enum Base = declarative_base() - - class EventType(Enum): - EVENT = 'event' - AWARD = 'award' + EVENT = "event" + AWARD = "award" class Event(Base): __tablename__ = "event" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(50), nullable=False) @@ -43,8 +39,9 @@ class Event(Base): start_date = db.Column(db.DateTime(), nullable=False) end_date = db.Column(db.DateTime(), nullable=False) key = db.Column(db.String(255), nullable=False, unique=True) - organisation_id = db.Column(db.Integer(), db.ForeignKey( - 'organisation.id'), nullable=False) + organisation_id = db.Column( + db.Integer(), db.ForeignKey("organisation.id"), nullable=False + ) email_from = db.Column(db.String(255), nullable=False) url = db.Column(db.String(255), nullable=False) travel_grant = db.Column(db.Boolean(), nullable=False) @@ -61,28 +58,29 @@ class Event(Base): registration_close = db.Column(db.DateTime(), nullable=False) event_type = db.Column(db.Enum(EventType), nullable=False) - def __init__(self, - name, - description, - start_date, - end_date, - key, - organisation_id, - email_from, - url, - application_open, - application_close, - review_open, - review_close, - selection_open, - selection_close, - offer_open, - offer_close, - registration_open, - registration_close, - event_type, - travel_grant - ): + def __init__( + self, + name, + description, + start_date, + end_date, + key, + organisation_id, + email_from, + url, + application_open, + application_close, + review_open, + review_close, + selection_open, + selection_close, + offer_open, + offer_close, + registration_open, + registration_close, + event_type, + travel_grant, + ): self.name = name self.description = description @@ -105,14 +103,14 @@ def __init__(self, self.event_type = event_type self.travel_grant = travel_grant + class RegistrationForm(Base): __tablename__ = "registration_form" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - event_id = db.Column(db.Integer(), db.ForeignKey( - "event.id"), nullable=False) + event_id = db.Column(db.Integer(), db.ForeignKey("event.id"), nullable=False) def __init__(self, event_id): self.event_id = event_id @@ -121,11 +119,12 @@ def __init__(self, event_id): class RegistrationSection(Base): __tablename__ = "registration_section" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - registration_form_id = db.Column(db.Integer(), db.ForeignKey( - "registration_form.id"), nullable=False) + registration_form_id = db.Column( + db.Integer(), db.ForeignKey("registration_form.id"), nullable=False + ) name = db.Column(db.String(), nullable=False) description = db.Column(db.String(), nullable=False) order = db.Column(db.Integer(), nullable=False) @@ -133,7 +132,16 @@ class RegistrationSection(Base): show_for_accommodation_award = db.Column(db.Boolean(), nullable=True) show_for_payment_required = db.Column(db.Boolean(), nullable=True) - def __init__(self, registration_form_id, name, description, order, show_for_travel_award, show_for_accommodation_award, show_for_payment_required): + def __init__( + self, + registration_form_id, + name, + description, + order, + show_for_travel_award, + show_for_accommodation_award, + show_for_payment_required, + ): self.registration_form_id = registration_form_id self.name = name self.description = description @@ -142,16 +150,19 @@ def __init__(self, registration_form_id, name, description, order, show_for_trav self.show_for_accommodation_award = show_for_accommodation_award self.show_for_travel_award = show_for_travel_award + class RegistrationQuestion(Base): __tablename__ = "registration_question" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - registration_form_id = db.Column(db.Integer(), db.ForeignKey( - "registration_form.id"), nullable=False) - section_id = db.Column(db.Integer(), db.ForeignKey( - "registration_section.id"), nullable=False) + registration_form_id = db.Column( + db.Integer(), db.ForeignKey("registration_form.id"), nullable=False + ) + section_id = db.Column( + db.Integer(), db.ForeignKey("registration_section.id"), nullable=False + ) type = db.Column(db.String(), nullable=False) description = db.Column(db.String(), nullable=False) headline = db.Column(db.String(), nullable=False) @@ -162,11 +173,25 @@ class RegistrationQuestion(Base): options = db.Column(db.JSON(), nullable=True) is_required = db.Column(db.Boolean(), nullable=False) required_value = db.Column(db.String(), nullable=True) - depends_on_question_id = db.Column(db.Integer(), db.ForeignKey( - "registration_question.id"), nullable=True) + depends_on_question_id = db.Column( + db.Integer(), db.ForeignKey("registration_question.id"), nullable=True + ) hide_for_dependent_value = db.Column(db.String(), nullable=True) - def __init__(self, registration_form_id, section_id, headline, placeholder, order, type, validation_regex, validation_text=None, is_required=True, description='', options=None): + def __init__( + self, + registration_form_id, + section_id, + headline, + placeholder, + order, + type, + validation_regex, + validation_text=None, + is_required=True, + description="", + options=None, + ): self.registration_form_id = registration_form_id self.section_id = section_id self.headline = headline @@ -179,134 +204,322 @@ def __init__(self, registration_form_id, section_id, headline, placeholder, orde self.validation_regex = validation_regex self.validation_text = validation_text + def upgrade(): Base.metadata.bind = op.get_bind() session = orm.Session(bind=Base.metadata.bind) - event = session.query(Event).filter_by(key='eeml2020').first() + event = session.query(Event).filter_by(key="eeml2020").first() form = RegistrationForm(event.id) session.add(form) session.commit() - mentorship = RegistrationSection(form.id, 'Mentorship', '', 1, None, None, None) + mentorship = RegistrationSection(form.id, "Mentorship", "", 1, None, None, None) session.add(mentorship) session.commit() - - yes_no = [ - {'value': 'yes', 'label': 'Yes'}, - {'value': 'no', 'label': 'No'} - ] - mentorship_q1 = RegistrationQuestion(form.id, mentorship.id, 'Would you be interested in a mentorship session?', 'Choose an option', 1, 'multi-choice', None, None, True, 'We have a limited number of spots for 1:1 mentorship sessions (15 minutes by teleconference) with speakers from the school or other ML experts. Would you be interested in such a session?', options=yes_no) + + yes_no = [{"value": "yes", "label": "Yes"}, {"value": "no", "label": "No"}] + mentorship_q1 = RegistrationQuestion( + form.id, + mentorship.id, + "Would you be interested in a mentorship session?", + "Choose an option", + 1, + "multi-choice", + None, + None, + True, + "We have a limited number of spots for 1:1 mentorship sessions (15 minutes by teleconference) with speakers from the school or other ML experts. Would you be interested in such a session?", + options=yes_no, + ) session.add(mentorship_q1) session.commit() - mentorship_q2 = RegistrationQuestion(form.id, mentorship.id, 'On what topic would you like to ask questions?', 'Choose an option', 2, 'choice-with-other', None, None, True, options=[ - {'value': 'career-advice', 'label': 'Career advice'}, - {'value': 'reinforcement-learning', 'label': 'Reinforcement learning'}, - {'value': 'unsupervised-learning', 'label': 'Unsupervised learning'}, - {'value': 'computer-vision', 'label': 'Computer Vision'}, - {'value': 'nlp', 'label': 'NLP'}, - {'value': 'theory-of-deep-learning', 'label': 'Theory of Deep Learning'}, - ]) + mentorship_q2 = RegistrationQuestion( + form.id, + mentorship.id, + "On what topic would you like to ask questions?", + "Choose an option", + 2, + "choice-with-other", + None, + None, + True, + options=[ + {"value": "career-advice", "label": "Career advice"}, + {"value": "reinforcement-learning", "label": "Reinforcement learning"}, + {"value": "unsupervised-learning", "label": "Unsupervised learning"}, + {"value": "computer-vision", "label": "Computer Vision"}, + {"value": "nlp", "label": "NLP"}, + {"value": "theory-of-deep-learning", "label": "Theory of Deep Learning"}, + ], + ) mentorship_q2.depends_on_question_id = mentorship_q1.id - mentorship_q2.hide_for_dependent_value = 'no' - - mentorship_q3 = RegistrationQuestion(form.id, mentorship.id, 'Any other information we should have when doing the matching?', '', 3, 'long-text', None, is_required=False, description='e.g. expert from academia or industry, preferred gender/race of the expert for you to be more comfortable in the discussion etc.') + mentorship_q2.hide_for_dependent_value = "no" + + mentorship_q3 = RegistrationQuestion( + form.id, + mentorship.id, + "Any other information we should have when doing the matching?", + "", + 3, + "long-text", + None, + is_required=False, + description="e.g. expert from academia or industry, preferred gender/race of the expert for you to be more comfortable in the discussion etc.", + ) mentorship_q3.depends_on_question_id = mentorship_q1.id - mentorship_q3.hide_for_dependent_value = 'no' - - mentorship_q4 = RegistrationQuestion(form.id, mentorship.id, 'Mentorship discussions are scheduled normally during lunch time (13:10-15:00 CEST time). If this time does not work for you because of the time zone, please indicate another time (in CEST), and we will do our best to accommodate that.', 'Choose an option', 4, 'choice-with-other', None, None, is_required=True, options=[ - {'value': 'yes', 'label': 'Yes, 13:10-15:00 CEST time works for me'}, - ]) + mentorship_q3.hide_for_dependent_value = "no" + + mentorship_q4 = RegistrationQuestion( + form.id, + mentorship.id, + "Mentorship discussions are scheduled normally during lunch time (13:10-15:00 CEST time). If this time does not work for you because of the time zone, please indicate another time (in CEST), and we will do our best to accommodate that.", + "Choose an option", + 4, + "choice-with-other", + None, + None, + is_required=True, + options=[ + {"value": "yes", "label": "Yes, 13:10-15:00 CEST time works for me"}, + ], + ) mentorship_q4.depends_on_question_id = mentorship_q1.id - mentorship_q4.hide_for_dependent_value = 'no' + mentorship_q4.hide_for_dependent_value = "no" session.add_all([mentorship_q2, mentorship_q3, mentorship_q4]) session.commit() - background = RegistrationSection(form.id, 'Your Background', '', 2, None, None, None) + background = RegistrationSection( + form.id, "Your Background", "", 2, None, None, None + ) session.add(background) session.commit() - background_q1 = RegistrationQuestion(form.id, background.id, 'What topic are you most familiar with?', '', 1, 'multi-checkbox-with-other', None, options=[ - {'value': 'reinforcementLearning', 'label': 'Reinforcement learning'}, - {'value': 'unsupervisedAndGenerative', 'label': 'Unsupervised learning & Generative Models'}, - {'value': 'computerVision', 'label': 'Computer Vision'}, - {'value': 'rnnNlp', 'label': 'RNNs & NLP'}, - {'value': 'theoryDeepLearning', 'label': 'Theory of Deep Learning'}, - {'value': 'robotics', 'label': 'Robotics'}, - ]) - background_q2 = RegistrationQuestion(form.id, background.id, 'Please indicate your level of familiarity with Python.', '', 2, 'multi-choice', None, options=[ - {'value': '1', 'label': '1 - never used or used very rarely'}, - {'value': '2', 'label': '2'}, - {'value': '3', 'label': '3'}, - {'value': '4', 'label': '4'}, - {'value': '5', 'label': '5 - very familiar'}, - ]) - background_q3 = RegistrationQuestion(form.id, background.id, 'Which Deep Learning library are you most familiar with?', '', 3, 'multi-checkbox-with-other', None, options=[ - {'value': 'pytorch', 'label': 'Pytorch'}, - {'value': 'tensorflow1', 'label': 'Tensorflow 1'}, - {'value': 'tensorflow2', 'label': 'Tensorflow 2'}, - {'value': 'jax', 'label': 'Jax'}, - {'value': 'none', 'label': """I haven't used a DL library before"""}, - ]) - background_q4 = RegistrationQuestion(form.id, background.id, 'The online school will rely on a number of platforms. Please confirm that you will be able to install and/or use the following:', '', 4, 'multi-checkbox', None, options=[ - {'value': 'gmail', 'label': 'Gmail account (for practical sessions)'}, - {'value': 'chrome', 'label': 'Google Chrome (for practical sessions)'}, - {'value': 'slack', 'label': 'Slack'}, - {'value': 'youtube', 'label': 'Youtube (to watch, upload content)'}, - {'value': 'none', 'label': """I can't access any of the above"""}, - ]) + background_q1 = RegistrationQuestion( + form.id, + background.id, + "What topic are you most familiar with?", + "", + 1, + "multi-checkbox-with-other", + None, + options=[ + {"value": "reinforcementLearning", "label": "Reinforcement learning"}, + { + "value": "unsupervisedAndGenerative", + "label": "Unsupervised learning & Generative Models", + }, + {"value": "computerVision", "label": "Computer Vision"}, + {"value": "rnnNlp", "label": "RNNs & NLP"}, + {"value": "theoryDeepLearning", "label": "Theory of Deep Learning"}, + {"value": "robotics", "label": "Robotics"}, + ], + ) + background_q2 = RegistrationQuestion( + form.id, + background.id, + "Please indicate your level of familiarity with Python.", + "", + 2, + "multi-choice", + None, + options=[ + {"value": "1", "label": "1 - never used or used very rarely"}, + {"value": "2", "label": "2"}, + {"value": "3", "label": "3"}, + {"value": "4", "label": "4"}, + {"value": "5", "label": "5 - very familiar"}, + ], + ) + background_q3 = RegistrationQuestion( + form.id, + background.id, + "Which Deep Learning library are you most familiar with?", + "", + 3, + "multi-checkbox-with-other", + None, + options=[ + {"value": "pytorch", "label": "Pytorch"}, + {"value": "tensorflow1", "label": "Tensorflow 1"}, + {"value": "tensorflow2", "label": "Tensorflow 2"}, + {"value": "jax", "label": "Jax"}, + {"value": "none", "label": """I haven't used a DL library before"""}, + ], + ) + background_q4 = RegistrationQuestion( + form.id, + background.id, + "The online school will rely on a number of platforms. Please confirm that you will be able to install and/or use the following:", + "", + 4, + "multi-checkbox", + None, + options=[ + {"value": "gmail", "label": "Gmail account (for practical sessions)"}, + {"value": "chrome", "label": "Google Chrome (for practical sessions)"}, + {"value": "slack", "label": "Slack"}, + {"value": "youtube", "label": "Youtube (to watch, upload content)"}, + {"value": "none", "label": """I can't access any of the above"""}, + ], + ) session.add_all([background_q1, background_q2, background_q3, background_q4]) session.commit() - - poster = RegistrationSection(form.id, 'Poster', 'Only if you were selected to present a poster! If you were, please provide the following (Note: You can make changes to the project / title / authors submitted initially in your application.):', 3, None, None, None) + + poster = RegistrationSection( + form.id, + "Poster", + "Only if you were selected to present a poster! If you were, please provide the following (Note: You can make changes to the project / title / authors submitted initially in your application.):", + 3, + None, + None, + None, + ) session.add(poster) session.commit() - poster_q1 = RegistrationQuestion(form.id, poster.id, 'Title', 'Title', 1, 'short-text', None, is_required=False) - poster_q2 = RegistrationQuestion(form.id, poster.id, 'Authors', 'Authors', 2, 'short-text', None, is_required=False) - poster_q3 = RegistrationQuestion(form.id, poster.id, 'Affiliations of authors (in the same order as authors)', 'Affiliation(s)', 3, 'short-text', None, is_required=False) - poster_q4 = RegistrationQuestion(form.id, poster.id, 'Short abstract', 'Enter up to 100 words', 4, 'long-text', r'^\W*(\w+(\W+|$)){0,150}$', validation_text='Enter less than 100 words', is_required=False, description='Max 100 words') - poster_q5 = RegistrationQuestion(form.id, poster.id, 'Teaser image', '', 5, 'file', None, is_required=False) - poster_q6 = RegistrationQuestion(form.id, poster.id, 'Topic', 'Choose an option', 6, 'choice-with-other', None, is_required=False, options=[ - {'value': 'computeVision', 'label': 'Computer Vision'}, - {'value': 'robotics', 'label': 'Robotics'}, - {'value': 'reinforcementLearning', 'label': 'Reinforcement learning'}, - {'value': 'mlMedicalData', 'label': 'ML for medical data'}, - {'value': 'neuroscience', 'label': 'Neuroscience'}, - {'value': 'unsupervisedLearning', 'label': 'Unsupervised learning'}, - {'value': 'nlp', 'label': 'NLP'}, - {'value': 'deepLearning', 'label': 'Deep Learning'}, - {'value': 'optimization', 'label': 'Optimization'}, - {'value': 'theory', 'label': 'Theory'}, - {'value': 'applications', 'label': 'Applications'}, - ]) - poster_q7 = RegistrationQuestion(form.id, poster.id, 'Youtube link to video presentation', 'Link', 7, 'short-text', None, is_required=False) - poster_q8 = RegistrationQuestion(form.id, poster.id, 'Please check the following', '', 8, 'multi-checkbox', None, is_required=False, options=[ - {'value': 'less3Minutes', 'label': 'My video is no longer than 3 minutes.'}, - {'value': 'wontAlter', 'label': 'I will not alter the video after submission.'} - ]) - session.add_all([ - poster_q1, poster_q2, poster_q3, poster_q4, poster_q5, poster_q6, poster_q7, poster_q8 - ]) + poster_q1 = RegistrationQuestion( + form.id, poster.id, "Title", "Title", 1, "short-text", None, is_required=False + ) + poster_q2 = RegistrationQuestion( + form.id, + poster.id, + "Authors", + "Authors", + 2, + "short-text", + None, + is_required=False, + ) + poster_q3 = RegistrationQuestion( + form.id, + poster.id, + "Affiliations of authors (in the same order as authors)", + "Affiliation(s)", + 3, + "short-text", + None, + is_required=False, + ) + poster_q4 = RegistrationQuestion( + form.id, + poster.id, + "Short abstract", + "Enter up to 100 words", + 4, + "long-text", + r"^\W*(\w+(\W+|$)){0,150}$", + validation_text="Enter less than 100 words", + is_required=False, + description="Max 100 words", + ) + poster_q5 = RegistrationQuestion( + form.id, poster.id, "Teaser image", "", 5, "file", None, is_required=False + ) + poster_q6 = RegistrationQuestion( + form.id, + poster.id, + "Topic", + "Choose an option", + 6, + "choice-with-other", + None, + is_required=False, + options=[ + {"value": "computeVision", "label": "Computer Vision"}, + {"value": "robotics", "label": "Robotics"}, + {"value": "reinforcementLearning", "label": "Reinforcement learning"}, + {"value": "mlMedicalData", "label": "ML for medical data"}, + {"value": "neuroscience", "label": "Neuroscience"}, + {"value": "unsupervisedLearning", "label": "Unsupervised learning"}, + {"value": "nlp", "label": "NLP"}, + {"value": "deepLearning", "label": "Deep Learning"}, + {"value": "optimization", "label": "Optimization"}, + {"value": "theory", "label": "Theory"}, + {"value": "applications", "label": "Applications"}, + ], + ) + poster_q7 = RegistrationQuestion( + form.id, + poster.id, + "Youtube link to video presentation", + "Link", + 7, + "short-text", + None, + is_required=False, + ) + poster_q8 = RegistrationQuestion( + form.id, + poster.id, + "Please check the following", + "", + 8, + "multi-checkbox", + None, + is_required=False, + options=[ + {"value": "less3Minutes", "label": "My video is no longer than 3 minutes."}, + { + "value": "wontAlter", + "label": "I will not alter the video after submission.", + }, + ], + ) + session.add_all( + [ + poster_q1, + poster_q2, + poster_q3, + poster_q4, + poster_q5, + poster_q6, + poster_q7, + poster_q8, + ] + ) session.commit() - - other = RegistrationSection(form.id, 'Other', '', 4, None, None, None) + + other = RegistrationSection(form.id, "Other", "", 4, None, None, None) session.add(other) session.commit() - other_q1 = RegistrationQuestion(form.id, other.id, 'Would you be interested in sharing your CV with EEML2020 sponsors and partners for recruiting purposes?', '', 1, 'multi-choice', None, description='Please check the website https://www.eeml.eu for a complete list of sponsors and partners', options=[ - {'value': 'yes', 'label': 'Yes'}, - {'value': 'no', 'label': 'No or Does not apply (e.g. you represent a sponsor or you did not submit your CV)'} - ]) - other_q2 = RegistrationQuestion(form.id, other.id, 'For the weekend days during the school period (July 4-5) we plan to organise relaxing/socialising activities. Please indicate below if you would be interested to participate in such sessions and which sessions or suggest ideas for different sessions.', '', 2, 'multi-checkbox-with-other', None, is_required=True, options=[ - {'value': 'notInterested', 'label': 'Not interested'}, - {'value': 'chess', 'label': 'Chess'}, - {'value': 'go', 'label': 'Go'}, - {'value': 'videoGames', 'label': 'Video games'}, - {'value': 'yoga', 'label': 'Yoga'}, - ]) + other_q1 = RegistrationQuestion( + form.id, + other.id, + "Would you be interested in sharing your CV with EEML2020 sponsors and partners for recruiting purposes?", + "", + 1, + "multi-choice", + None, + description="Please check the website https://www.eeml.eu for a complete list of sponsors and partners", + options=[ + {"value": "yes", "label": "Yes"}, + { + "value": "no", + "label": "No or Does not apply (e.g. you represent a sponsor or you did not submit your CV)", + }, + ], + ) + other_q2 = RegistrationQuestion( + form.id, + other.id, + "For the weekend days during the school period (July 4-5) we plan to organise relaxing/socialising activities. Please indicate below if you would be interested to participate in such sessions and which sessions or suggest ideas for different sessions.", + "", + 2, + "multi-checkbox-with-other", + None, + is_required=True, + options=[ + {"value": "notInterested", "label": "Not interested"}, + {"value": "chess", "label": "Chess"}, + {"value": "go", "label": "Go"}, + {"value": "videoGames", "label": "Video games"}, + {"value": "yoga", "label": "Yoga"}, + ], + ) session.add_all([other_q1, other_q2]) session.commit() @@ -314,10 +527,18 @@ def upgrade(): def downgrade(): Base.metadata.bind = op.get_bind() session = orm.Session(bind=Base.metadata.bind) - event = session.query(Event).filter_by(key='eeml2020').first() + event = session.query(Event).filter_by(key="eeml2020").first() form = session.query(RegistrationForm).filter_by(event_id=event.id).first() if form: - op.execute("""DELETE FROM registration_question WHERE registration_form_id={}""".format(form.id)) - op.execute("""DELETE FROM registration_section WHERE registration_form_id={}""".format(form.id)) + op.execute( + """DELETE FROM registration_question WHERE registration_form_id={}""".format( + form.id + ) + ) + op.execute( + """DELETE FROM registration_section WHERE registration_form_id={}""".format( + form.id + ) + ) op.execute("""DELETE FROM registration_form WHERE id={}""".format(form.id)) session.commit() diff --git a/api/migrations/versions/20a94c6b2952_update_eeml_form.py b/api/migrations/versions/20a94c6b2952_update_eeml_form.py index 0979b77cd..d01809064 100644 --- a/api/migrations/versions/20a94c6b2952_update_eeml_form.py +++ b/api/migrations/versions/20a94c6b2952_update_eeml_form.py @@ -7,18 +7,26 @@ """ # revision identifiers, used by Alembic. -revision = '20a94c6b2952' -down_revision = 'c3b4ca7ceef9' +revision = "20a94c6b2952" +down_revision = "c3b4ca7ceef9" -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): - op.execute(r"""UPDATE question SET placeholder='Enter 150 to 500 words' WHERE headline='Research Interests' and application_form_id=2""") - op.execute(r"""UPDATE question SET placeholder='Enter up to 150 words', description='Max 150 words' WHERE headline='Motivation' and application_form_id=2""") + op.execute( + r"""UPDATE question SET placeholder='Enter 150 to 500 words' WHERE headline='Research Interests' and application_form_id=2""" + ) + op.execute( + r"""UPDATE question SET placeholder='Enter up to 150 words', description='Max 150 words' WHERE headline='Motivation' and application_form_id=2""" + ) def downgrade(): - op.execute(r"""UPDATE question SET placeholder='Enter 500 - 2000 characters' WHERE headline='Research Interests' and application_form_id=2""") - op.execute(r"""UPDATE question SET placeholder='Enter up to 500 characters', description='Max 500 characters' WHERE headline='Motivation' and application_form_id=2""") + op.execute( + r"""UPDATE question SET placeholder='Enter 500 - 2000 characters' WHERE headline='Research Interests' and application_form_id=2""" + ) + op.execute( + r"""UPDATE question SET placeholder='Enter up to 500 characters', description='Max 500 characters' WHERE headline='Motivation' and application_form_id=2""" + ) diff --git a/api/migrations/versions/2241922a3305_redo_french_email_templates.py b/api/migrations/versions/2241922a3305_redo_french_email_templates.py index 2aa35d840..7a71b7551 100644 --- a/api/migrations/versions/2241922a3305_redo_french_email_templates.py +++ b/api/migrations/versions/2241922a3305_redo_french_email_templates.py @@ -9,25 +9,25 @@ """ # revision identifiers, used by Alembic. -revision = '2241922a3305' -down_revision = '43597f3ea193' +revision = "2241922a3305" +down_revision = "43597f3ea193" + +import datetime -from alembic import op import sqlalchemy as sa +from alembic import op from sqlalchemy import orm from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy import orm + from app import db -import datetime Base = declarative_base() - class EmailTemplate(Base): - __tablename__ = 'email_template' - __table_args__ = {'extend_existing': True} + __tablename__ = "email_template" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) key = db.Column(db.String(50), nullable=False) @@ -45,12 +45,31 @@ def __init__(self, key, event_id, subject, template, language): def upgrade(): - added_keys = ['outcome-rejected', 'outcome-waitlist', 'attendance-confirmation', 'guest-registration-confirmation', - 'invitation-letter', 'guest-invitation', 'guest-invitation-with-registration', 'new-guest-no-registration', - 'new-guest-registration', 'reference-request-self-nomination', 'reference-request', 'offer', - 'registration-pending-confirmation', 'registration-with-confirmation', 'registration-confirmed', - 'reviews-assigned', 'verify-email', 'password-reset'] - op.execute("""DELETE FROM email_template WHERE language='fr' and key in ({})""".format(', '.join(["'" + k + "'" for k in added_keys]))) + added_keys = [ + "outcome-rejected", + "outcome-waitlist", + "attendance-confirmation", + "guest-registration-confirmation", + "invitation-letter", + "guest-invitation", + "guest-invitation-with-registration", + "new-guest-no-registration", + "new-guest-registration", + "reference-request-self-nomination", + "reference-request", + "offer", + "registration-pending-confirmation", + "registration-with-confirmation", + "registration-confirmed", + "reviews-assigned", + "verify-email", + "password-reset", + ] + op.execute( + """DELETE FROM email_template WHERE language='fr' and key in ({})""".format( + ", ".join(["'" + k + "'" for k in added_keys]) + ) + ) Base.metadata.bind = op.get_bind() session = orm.Session(bind=Base.metadata.bind) @@ -62,10 +81,17 @@ def upgrade(): Cordialement, Les organisateurs de {event_name}""" - session.add(EmailTemplate('outcome-rejected', None, 'Mise à jour de l’état de la demande pour {event_name}', template, 'fr')) - -######### - + session.add( + EmailTemplate( + "outcome-rejected", + None, + "Mise à jour de l’état de la demande pour {event_name}", + template, + "fr", + ) + ) + + ######### template = """Madame / Monsieur {lastname}, @@ -74,10 +100,17 @@ def upgrade(): Cordialement, Les organisateurs de {event_name}""" - session.add(EmailTemplate('outcome-waitlist', None, 'Mise à jour de l’état de la demande pour {event_name}', template, 'fr')) - -######### - + session.add( + EmailTemplate( + "outcome-waitlist", + None, + "Mise à jour de l’état de la demande pour {event_name}", + template, + "fr", + ) + ) + + ######### template = """Madame / Monsieur {lastname}, @@ -89,9 +122,13 @@ def upgrade(): Les organisateurs de {event_name} """ - session.add(EmailTemplate('attendance-confirmation', None, 'Bienvenue à {event_name}!', template, 'fr')) + session.add( + EmailTemplate( + "attendance-confirmation", None, "Bienvenue à {event_name}!", template, "fr" + ) + ) -######### + ######### template = """Madame / Monsieur {lastname}, @@ -104,9 +141,17 @@ def upgrade(): l’équipe {event_name} """ - session.add(EmailTemplate('guest-registration-confirmation', None, 'Inscription à {event_name}', template, 'fr')) + session.add( + EmailTemplate( + "guest-registration-confirmation", + None, + "Inscription à {event_name}", + template, + "fr", + ) + ) -######### + ######### template = """Madame / Monsieur {lastname}, @@ -116,10 +161,17 @@ def upgrade(): l’équipe {event_name} """ - session.add(EmailTemplate('invitation-letter', None, "Lettre d'invitation à {event_name}", template, 'fr')) - -######### + session.add( + EmailTemplate( + "invitation-letter", + None, + "Lettre d'invitation à {event_name}", + template, + "fr", + ) + ) + ######### template = """Madame / Monsieur {lastname}, @@ -129,10 +181,13 @@ def upgrade(): Cordialement, Les organisateurs de {event_name}""" - session.add(EmailTemplate('guest-invitation', None, 'Votre invitation à {event_name}', template, 'fr')) - -######### + session.add( + EmailTemplate( + "guest-invitation", None, "Votre invitation à {event_name}", template, "fr" + ) + ) + ######### template = """Madame / Monsieur {lastname}, @@ -146,10 +201,17 @@ def upgrade(): Cordialement, Les organisateurs de {event_name}""" - session.add(EmailTemplate('guest-invitation-with-registration', None, 'Votre invitation à {event_name}', template, 'fr')) - -######### - + session.add( + EmailTemplate( + "guest-invitation-with-registration", + None, + "Votre invitation à {event_name}", + template, + "fr", + ) + ) + + ######### template = """Madame / Monsieur {lastname}, @@ -167,10 +229,17 @@ def upgrade(): Cordialement, Les organisateurs de {event_name} """ - session.add(EmailTemplate('new-guest-no-registration', None, 'Votre invitation à {event_name}', template, 'fr')) - -######### - + session.add( + EmailTemplate( + "new-guest-no-registration", + None, + "Votre invitation à {event_name}", + template, + "fr", + ) + ) + + ######### template = """Madame / Monsieur {lastname}, @@ -189,10 +258,17 @@ def upgrade(): Cordialement, Les organisateurs de {event_name} """ - session.add(EmailTemplate('new-guest-registration', None, 'Votre invitation à {event_name}', template, 'fr')) - -######### - + session.add( + EmailTemplate( + "new-guest-registration", + None, + "Votre invitation à {event_name}", + template, + "fr", + ) + ) + + ######### template = """Madame / Monsieur {lastname}, @@ -211,10 +287,17 @@ def upgrade(): L’équipe de {event_name} """ - session.add(EmailTemplate('reference-request-self-nomination', None, 'DEMANDE DE RÉFÉRENCE – {event_name}', template, 'fr')) - -######### - + session.add( + EmailTemplate( + "reference-request-self-nomination", + None, + "DEMANDE DE RÉFÉRENCE – {event_name}", + template, + "fr", + ) + ) + + ######### template = """Madame / Monsieur {lastname}, @@ -232,10 +315,17 @@ def upgrade(): Cordialement, L’équipe de {event_name} """ - session.add(EmailTemplate('reference-request', None, 'DEMANDE DE RÉFÉRENCE – {event_name}', template, 'fr')) - -######### - + session.add( + EmailTemplate( + "reference-request", + None, + "DEMANDE DE RÉFÉRENCE – {event_name}", + template, + "fr", + ) + ) + + ######### template = """Madame / Monsieur {lastname}, @@ -252,10 +342,17 @@ def upgrade(): Les organisateurs de {event_name} """ - session.add(EmailTemplate('offer', None, 'Mise à jour de l’état de la demande pour {event_name}', template, 'fr')) - -######### - + session.add( + EmailTemplate( + "offer", + None, + "Mise à jour de l’état de la demande pour {event_name}", + template, + "fr", + ) + ) + + ######### template = """Madame / Monsieur {lastname}, @@ -269,11 +366,17 @@ def upgrade(): Cordialement, Les organisateurs de {event_name} """ - session.add(EmailTemplate('registration-pending-confirmation', None, 'Inscription à {event_name}', template, 'fr')) - -######### - - + session.add( + EmailTemplate( + "registration-pending-confirmation", + None, + "Inscription à {event_name}", + template, + "fr", + ) + ) + + ######### template = """Madame / Monsieur {lastname}, @@ -287,10 +390,17 @@ def upgrade(): Cordialement, Les organisateurs de {event_name} """ - session.add(EmailTemplate('registration-with-confirmation', None, 'Inscription à {event_name}', template, 'fr')) - -######### - + session.add( + EmailTemplate( + "registration-with-confirmation", + None, + "Inscription à {event_name}", + template, + "fr", + ) + ) + + ######### template = """Madame / Monsieur {lastname}, @@ -301,10 +411,17 @@ def upgrade(): Cordialement, Les organisateurs de {event_name} """ - session.add(EmailTemplate('registration-confirmed', None, "La confirmation d'inscription à {event_name}", template, 'fr')) - -######### - + session.add( + EmailTemplate( + "registration-confirmed", + None, + "La confirmation d'inscription à {event_name}", + template, + "fr", + ) + ) + + ######### template = """Madame / Monsieur {lastname}, @@ -317,10 +434,22 @@ def upgrade(): Cordialement, Les organisateurs de {event_name} """ - session.add(EmailTemplate('reviews-assigned', None, 'Des tâches de révision vous ont été attribuées dans {system_name}', template, 'fr')) - + session.add( + EmailTemplate( + "reviews-assigned", + None, + "Des tâches de révision vous ont été attribuées dans {system_name}", + template, + "fr", + ) + ) + # Update the English template - en_template = session.query(EmailTemplate).filter_by(key='reviews-assigned', language='en').first() + en_template = ( + session.query(EmailTemplate) + .filter_by(key="reviews-assigned", language="en") + .first() + ) en_template.template = """Dear {title} {firstname} {lastname}, You have been assigned {num_reviews} reviews on {system_name}. Please visit {baobab_host}/{event_key}/review to begin. @@ -332,8 +461,7 @@ def upgrade(): The {event_name} Organisers """ -######### - + ######### template = """Madame / Monsieur {lastname}, @@ -345,10 +473,13 @@ def upgrade(): Cordialement, {organisation} """ - session.add(EmailTemplate('verify-email', None, 'Courriel {system} Vérification', template, 'fr')) - -######### + session.add( + EmailTemplate( + "verify-email", None, "Courriel {system} Vérification", template, "fr" + ) + ) + ######### template = """Madame / Monsieur {lastname}, @@ -360,9 +491,17 @@ def upgrade(): Cordialement, {organisation} """ - session.add(EmailTemplate('password-reset', None, 'Réinitialisation du mot de passe pour {system_name}', template, 'fr')) - -######### + session.add( + EmailTemplate( + "password-reset", + None, + "Réinitialisation du mot de passe pour {system_name}", + template, + "fr", + ) + ) + + ######### template = """Madame / Monsieur {lastname}, @@ -374,9 +513,21 @@ def upgrade(): L’équipe de {event_name} """ - session.add(EmailTemplate('confirmation-response', None, 'Votre demande pour {event_name}', template, 'fr')) - - en_template = session.query(EmailTemplate).filter_by(key='confirmation-response', language='en').first() + session.add( + EmailTemplate( + "confirmation-response", + None, + "Votre demande pour {event_name}", + template, + "fr", + ) + ) + + en_template = ( + session.query(EmailTemplate) + .filter_by(key="confirmation-response", language="en") + .first() + ) en_template.template = """Dear {title} {firstname} {lastname}, Thank you for applying to {event_description}. Your application is being reviewed by our committee and we will get back to you as soon as possible. Included below is a copy of your responses. @@ -388,6 +539,11 @@ def upgrade(): session.commit() + def downgrade(): - added_keys = ['confirmation-response'] - op.execute("""DELETE FROM email_template WHERE language='fr' and key in ({})""".format(', '.join(["'" + k + "'" for k in added_keys]))) + added_keys = ["confirmation-response"] + op.execute( + """DELETE FROM email_template WHERE language='fr' and key in ({})""".format( + ", ".join(["'" + k + "'" for k in added_keys]) + ) + ) diff --git a/api/migrations/versions/2279e1fa2e49_eeml_review_update2.py b/api/migrations/versions/2279e1fa2e49_eeml_review_update2.py index 1857b319c..ccf47ba10 100644 --- a/api/migrations/versions/2279e1fa2e49_eeml_review_update2.py +++ b/api/migrations/versions/2279e1fa2e49_eeml_review_update2.py @@ -7,28 +7,30 @@ """ # revision identifiers, used by Alembic. -revision = '2279e1fa2e49' -down_revision = 'eb6f96db82e8' +revision = "2279e1fa2e49" +down_revision = "eb6f96db82e8" + +import datetime +from enum import Enum -from alembic import op import sqlalchemy as sa +from alembic import op from sqlalchemy import orm from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy import orm + from app import db -import datetime -from enum import Enum Base = declarative_base() + class EventType(Enum): - EVENT = 'event' - AWARD = 'award' + EVENT = "event" + AWARD = "award" class Event(Base): - __tablename__ = 'event' - __table_args__ = {'extend_existing': True} + __tablename__ = "event" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(50), nullable=False) @@ -36,8 +38,9 @@ class Event(Base): start_date = db.Column(db.DateTime(), nullable=False) end_date = db.Column(db.DateTime(), nullable=False) key = db.Column(db.String(255), nullable=False, unique=True) - organisation_id = db.Column(db.Integer(), db.ForeignKey( - 'organisation.id'), nullable=False) + organisation_id = db.Column( + db.Integer(), db.ForeignKey("organisation.id"), nullable=False + ) email_from = db.Column(db.String(255), nullable=False) url = db.Column(db.String(255), nullable=False) @@ -55,21 +58,23 @@ class Event(Base): class ApplicationForm(Base): - __tablename__ = 'application_form' - __table_args__ = {'extend_existing': True} + __tablename__ = "application_form" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - event_id = db.Column(db.Integer(), db.ForeignKey('event.id'), nullable=False) + event_id = db.Column(db.Integer(), db.ForeignKey("event.id"), nullable=False) is_open = db.Column(db.Boolean(), nullable=False) class Question(Base): - __tablename__ = 'question' - __table_args__ = {'extend_existing': True} + __tablename__ = "question" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) - section_id = db.Column(db.Integer(), db.ForeignKey('section.id'), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) + section_id = db.Column(db.Integer(), db.ForeignKey("section.id"), nullable=False) type = db.Column(db.String(), nullable=False) description = db.Column(db.String(), nullable=True) headline = db.Column(db.String(), nullable=False) @@ -79,17 +84,21 @@ class Question(Base): order = db.Column(db.Integer(), nullable=False) options = db.Column(db.JSON(), nullable=True) is_required = db.Column(db.Boolean(), nullable=False) - depends_on_question_id = db.Column(db.Integer(), db.ForeignKey('question.id'), nullable=True) + depends_on_question_id = db.Column( + db.Integer(), db.ForeignKey("question.id"), nullable=True + ) show_for_values = db.Column(db.JSON(), nullable=True) key = db.Column(db.String(255), nullable=True) class ReviewForm(Base): - __tablename__ = 'review_form' - __table_args__ = {'extend_existing': True} + __tablename__ = "review_form" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) is_open = db.Column(db.Boolean(), nullable=False) deadline = db.Column(db.DateTime(), nullable=False) @@ -103,12 +112,14 @@ def close(self): class ReviewQuestion(Base): - __tablename__ = 'review_question' - __table_args__ = {'extend_existing': True} + __tablename__ = "review_question" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer, primary_key=True) - review_form_id = db.Column(db.Integer(), db.ForeignKey('review_form.id'), nullable=False) - question_id = db.Column(db.Integer(), db.ForeignKey('question.id'), nullable=True) + review_form_id = db.Column( + db.Integer(), db.ForeignKey("review_form.id"), nullable=False + ) + question_id = db.Column(db.Integer(), db.ForeignKey("question.id"), nullable=True) description = db.Column(db.String(), nullable=True) headline = db.Column(db.String(), nullable=True) type = db.Column(db.String(), nullable=False) @@ -120,19 +131,21 @@ class ReviewQuestion(Base): validation_text = db.Column(db.String(), nullable=True) weight = db.Column(db.Float(), nullable=False) - def __init__(self, - review_form_id, - type, - is_required, - order, - question_id=None, - description=None, - headline=None, - placeholder=None, - options=None, - validation_regex=None, - validation_text=None, - weight=0): + def __init__( + self, + review_form_id, + type, + is_required, + order, + question_id=None, + description=None, + headline=None, + placeholder=None, + options=None, + validation_regex=None, + validation_text=None, + weight=0, + ): self.review_form_id = review_form_id self.question_id = question_id self.description = description @@ -148,17 +161,28 @@ def __init__(self, class ReviewConfiguration(Base): - __tablename__ = 'review_configuration' - __table_args__ = {'extend_existing': True} + __tablename__ = "review_configuration" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - review_form_id = db.Column(db.Integer(), db.ForeignKey('review_form.id'), nullable=False) + review_form_id = db.Column( + db.Integer(), db.ForeignKey("review_form.id"), nullable=False + ) num_reviews_required = db.Column(db.Integer(), nullable=False) num_optional_reviews = db.Column(db.Integer(), nullable=False) - drop_optional_question_id = db.Column(db.Integer(), db.ForeignKey('review_question.id'), nullable=True) + drop_optional_question_id = db.Column( + db.Integer(), db.ForeignKey("review_question.id"), nullable=True + ) drop_optional_agreement_values = db.Column(db.String(), nullable=True) - def __init__(self, review_form_id, num_reviews_required, num_optional_reviews=0, drop_optional_question_id=None, drop_optional_agreement_values=None): + def __init__( + self, + review_form_id, + num_reviews_required, + num_optional_reviews=0, + drop_optional_question_id=None, + drop_optional_agreement_values=None, + ): self.review_form_id = review_form_id self.num_reviews_required = num_reviews_required self.num_optional_reviews = num_optional_reviews @@ -169,95 +193,175 @@ def __init__(self, review_form_id, num_reviews_required, num_optional_reviews=0, def find_question(session, headline): return session.query(Question).filter_by(headline=headline).first() + def update_review_question_order(session, headline, new_order): review_question = session.query(ReviewQuestion).filter_by(headline=headline).first() if review_question is None: - review_question = session.query(ReviewQuestion).join(Question, ReviewQuestion.question_id == Question.id).filter(Question.headline == headline).first() - + review_question = ( + session.query(ReviewQuestion) + .join(Question, ReviewQuestion.question_id == Question.id) + .filter(Question.headline == headline) + .first() + ) + review_question.order = new_order - + def delete_review_question(session, form_id, headline): - session.query(ReviewQuestion).filter_by(review_form_id=form_id, headline=headline).delete() + session.query(ReviewQuestion).filter_by( + review_form_id=form_id, headline=headline + ).delete() def upgrade(): Base.metadata.bind = op.get_bind() session = orm.Session(bind=Base.metadata.bind) - event = session.query(Event).filter_by(key='eeml2020').first() - application_form = session.query(ApplicationForm).filter_by(event_id=event.id).first() - form = session.query(ReviewForm).filter_by(application_form_id=application_form.id).first() - - divider1 = ReviewQuestion(form.id, 'section-divider', False, 1, headline='Candidate Application') + event = session.query(Event).filter_by(key="eeml2020").first() + application_form = ( + session.query(ApplicationForm).filter_by(event_id=event.id).first() + ) + form = ( + session.query(ReviewForm) + .filter_by(application_form_id=application_form.id) + .first() + ) + + divider1 = ReviewQuestion( + form.id, "section-divider", False, 1, headline="Candidate Application" + ) session.add(divider1) - category_question = find_question(session, 'I Am:') - category_info = ReviewQuestion(form.id, 'information', False, 2, question_id=category_question.id, headline='Applicant Category') + category_question = find_question(session, "I Am:") + category_info = ReviewQuestion( + form.id, + "information", + False, + 2, + question_id=category_question.id, + headline="Applicant Category", + ) session.add(category_info) - update_review_question_order(session, 'Affiliation', 3) - - country_question = find_question(session, 'Country') - country_info = ReviewQuestion(form.id, 'information', False, 4, question_id=country_question.id, headline='Country') + update_review_question_order(session, "Affiliation", 3) + + country_question = find_question(session, "Country") + country_info = ReviewQuestion( + form.id, + "information", + False, + 4, + question_id=country_question.id, + headline="Country", + ) session.add(country_info) - research_group_question = find_question(session, 'Research Group / Laboratory') - research_group_info = ReviewQuestion(form.id, 'information', False, 5, question_id=research_group_question.id, headline='Research Group / Laboratory') + research_group_question = find_question(session, "Research Group / Laboratory") + research_group_info = ReviewQuestion( + form.id, + "information", + False, + 5, + question_id=research_group_question.id, + headline="Research Group / Laboratory", + ) session.add(research_group_info) - supervisor_question = find_question(session, 'Supervisor') - supervisor_info = ReviewQuestion(form.id, 'information', False, 6, question_id=supervisor_question.id, headline='Supervisor') + supervisor_question = find_question(session, "Supervisor") + supervisor_info = ReviewQuestion( + form.id, + "information", + False, + 6, + question_id=supervisor_question.id, + headline="Supervisor", + ) session.add(supervisor_info) - previous_attendance_question = find_question(session, 'Previous attendance') - previous_attendance_info = ReviewQuestion(form.id, 'information', False, 7, question_id=previous_attendance_question.id, headline='Previous attendance') + previous_attendance_question = find_question(session, "Previous attendance") + previous_attendance_info = ReviewQuestion( + form.id, + "information", + False, + 7, + question_id=previous_attendance_question.id, + headline="Previous attendance", + ) session.add(previous_attendance_info) - update_review_question_order(session, 'Research Interests', 8) - update_review_question_order(session, 'Extended abstract type', 9) - update_review_question_order(session, 'Extended abstract', 10) - update_review_question_order(session, 'CV', 11) + update_review_question_order(session, "Research Interests", 8) + update_review_question_order(session, "Extended abstract type", 9) + update_review_question_order(session, "Extended abstract", 10) + update_review_question_order(session, "CV", 11) - session.query(ReviewQuestion).filter_by(headline='Financial Support Motivation').delete() + session.query(ReviewQuestion).filter_by( + headline="Financial Support Motivation" + ).delete() - divider2 = ReviewQuestion(form.id, 'section-divider', False, 12, headline='Your Review') + divider2 = ReviewQuestion( + form.id, "section-divider", False, 12, headline="Your Review" + ) session.add(divider2) - project = ReviewQuestion(form.id, 'long-text', True, 13, description='Please describe in 2-3 sentences what the project is about', headline='Project') + project = ReviewQuestion( + form.id, + "long-text", + True, + 13, + description="Please describe in 2-3 sentences what the project is about", + headline="Project", + ) session.add(project) - update_review_question_order(session, 'Would this person benefit from attending the school?', 14) - update_review_question_order(session, 'Would the school benefit from this person''s participation?', 15) - update_review_question_order(session, 'Overall assessment', 16) - + update_review_question_order( + session, "Would this person benefit from attending the school?", 14 + ) + update_review_question_order( + session, "Would the school benefit from this person" "s participation?", 15 + ) + update_review_question_order(session, "Overall assessment", 16) + session.commit() session.flush() + def downgrade(): Base.metadata.bind = op.get_bind() session = orm.Session(bind=Base.metadata.bind) - event = session.query(Event).filter_by(key='eeml2020').first() - application_form = session.query(ApplicationForm).filter_by(event_id=event.id).first() - form = session.query(ReviewForm).filter_by(application_form_id=application_form.id).first() + event = session.query(Event).filter_by(key="eeml2020").first() + application_form = ( + session.query(ApplicationForm).filter_by(event_id=event.id).first() + ) + form = ( + session.query(ReviewForm) + .filter_by(application_form_id=application_form.id) + .first() + ) to_delete = [ - 'Applicant Category', - 'Candidate Application', - 'Research Group / Laboratory', - 'Country', - 'Research Group / Laboratory', - 'Supervisor', - 'Previous attendance', - 'Your Review', - 'Project' + "Applicant Category", + "Candidate Application", + "Research Group / Laboratory", + "Country", + "Research Group / Laboratory", + "Supervisor", + "Previous attendance", + "Your Review", + "Project", ] for headline in to_delete: delete_review_question(session, form.id, headline) - financial_support_question = find_question(session, 'Motivation') - financial_support_info = ReviewQuestion(form.id, 'information', False, 6, question_id=financial_support_question.id, headline='Financial Support Motivation') + financial_support_question = find_question(session, "Motivation") + financial_support_info = ReviewQuestion( + form.id, + "information", + False, + 6, + question_id=financial_support_question.id, + headline="Financial Support Motivation", + ) session.add(financial_support_info) - + session.commit() - session.flush() \ No newline at end of file + session.flush() diff --git a/api/migrations/versions/245d12695c69_.py b/api/migrations/versions/245d12695c69_.py index 2505b147f..18cbc0e6e 100755 --- a/api/migrations/versions/245d12695c69_.py +++ b/api/migrations/versions/245d12695c69_.py @@ -7,15 +7,16 @@ """ # revision identifiers, used by Alembic. -revision = '245d12695c69' -down_revision = '14d53fa0c9c8' +revision = "245d12695c69" +down_revision = "14d53fa0c9c8" -from alembic import op import sqlalchemy as sa +from alembic import op + def upgrade(): pass - + def downgrade(): pass diff --git a/api/migrations/versions/2717ef90a874_add_review_configuration_table.py b/api/migrations/versions/2717ef90a874_add_review_configuration_table.py index 72665a4e3..41f6912df 100644 --- a/api/migrations/versions/2717ef90a874_add_review_configuration_table.py +++ b/api/migrations/versions/2717ef90a874_add_review_configuration_table.py @@ -7,30 +7,37 @@ """ # revision identifiers, used by Alembic. -revision = '2717ef90a874' -down_revision = '02242641e122' +revision = "2717ef90a874" +down_revision = "02242641e122" -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.create_table('review_configuration', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('review_form_id', sa.Integer(), nullable=False), - sa.Column('num_reviews_required', sa.Integer(), nullable=False), - sa.Column('num_optional_reviews', sa.Integer(), nullable=False), - sa.Column('drop_optional_question_id', sa.Integer(), nullable=True), - sa.Column('drop_optional_agreement_values', sa.String(), nullable=True), - sa.ForeignKeyConstraint(['drop_optional_question_id'], ['review_question.id'], ), - sa.ForeignKeyConstraint(['review_form_id'], ['review_form.id'], ), - sa.PrimaryKeyConstraint('id') + op.create_table( + "review_configuration", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("review_form_id", sa.Integer(), nullable=False), + sa.Column("num_reviews_required", sa.Integer(), nullable=False), + sa.Column("num_optional_reviews", sa.Integer(), nullable=False), + sa.Column("drop_optional_question_id", sa.Integer(), nullable=True), + sa.Column("drop_optional_agreement_values", sa.String(), nullable=True), + sa.ForeignKeyConstraint( + ["drop_optional_question_id"], + ["review_question.id"], + ), + sa.ForeignKeyConstraint( + ["review_form_id"], + ["review_form.id"], + ), + sa.PrimaryKeyConstraint("id"), ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('review_configuration') + op.drop_table("review_configuration") # ### end Alembic commands ### diff --git a/api/migrations/versions/2873f4f08a4b_.py b/api/migrations/versions/2873f4f08a4b_.py index 87dbd1875..f5e280583 100644 --- a/api/migrations/versions/2873f4f08a4b_.py +++ b/api/migrations/versions/2873f4f08a4b_.py @@ -7,30 +7,40 @@ """ # revision identifiers, used by Alembic. -revision = '2873f4f08a4b' -down_revision = 'ae94d067a229' +revision = "2873f4f08a4b" +down_revision = "ae94d067a229" -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.create_table('attendance', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('event_id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=False), - sa.Column('timestamp', sa.DateTime(), nullable=False), - sa.Column('updated_by_user_id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['event_id'], ['event.id'], ), - sa.ForeignKeyConstraint(['updated_by_user_id'], ['app_user.id'], ), - sa.ForeignKeyConstraint(['user_id'], ['app_user.id'], ), - sa.PrimaryKeyConstraint('id') + op.create_table( + "attendance", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("event_id", sa.Integer(), nullable=False), + sa.Column("user_id", sa.Integer(), nullable=False), + sa.Column("timestamp", sa.DateTime(), nullable=False), + sa.Column("updated_by_user_id", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint( + ["event_id"], + ["event.id"], + ), + sa.ForeignKeyConstraint( + ["updated_by_user_id"], + ["app_user.id"], + ), + sa.ForeignKeyConstraint( + ["user_id"], + ["app_user.id"], + ), + sa.PrimaryKeyConstraint("id"), ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('attendance') + op.drop_table("attendance") # ### end Alembic commands ### diff --git a/api/migrations/versions/297276d57414_.py b/api/migrations/versions/297276d57414_.py index 1d773c05c..71426bbe9 100644 --- a/api/migrations/versions/297276d57414_.py +++ b/api/migrations/versions/297276d57414_.py @@ -7,11 +7,11 @@ """ # revision identifiers, used by Alembic. -revision = '297276d57414' -down_revision = ('4b881c1c6dc2', '40ee6f91f6f3') +revision = "297276d57414" +down_revision = ("4b881c1c6dc2", "40ee6f91f6f3") -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): diff --git a/api/migrations/versions/29dd5b8bc663_.py b/api/migrations/versions/29dd5b8bc663_.py index f836e0731..1e023a16f 100644 --- a/api/migrations/versions/29dd5b8bc663_.py +++ b/api/migrations/versions/29dd5b8bc663_.py @@ -7,11 +7,11 @@ """ # revision identifiers, used by Alembic. -revision = '29dd5b8bc663' -down_revision = ('85ce7069fb96', 'a714c69d1a6c', 'e622c3deea5d') +revision = "29dd5b8bc663" +down_revision = ("85ce7069fb96", "a714c69d1a6c", "e622c3deea5d") -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): diff --git a/api/migrations/versions/2dfc0d119bc2_review_sections.py b/api/migrations/versions/2dfc0d119bc2_review_sections.py index 1174e2e8e..2a2c61e63 100644 --- a/api/migrations/versions/2dfc0d119bc2_review_sections.py +++ b/api/migrations/versions/2dfc0d119bc2_review_sections.py @@ -7,73 +7,110 @@ """ # revision identifiers, used by Alembic. -revision = '2dfc0d119bc2' -down_revision = '76c9226545bb' +revision = "2dfc0d119bc2" +down_revision = "76c9226545bb" -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.create_table('review_section', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('review_form_id', sa.Integer(), nullable=False), - sa.Column('order', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['review_form_id'], ['review_form.id'], ), - sa.PrimaryKeyConstraint('id') + op.create_table( + "review_section", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("review_form_id", sa.Integer(), nullable=False), + sa.Column("order", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint( + ["review_form_id"], + ["review_form.id"], + ), + sa.PrimaryKeyConstraint("id"), ) - op.create_table('review_section_translation', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('review_section_id', sa.Integer(), nullable=False), - sa.Column('language', sa.String(length=2), nullable=False), - sa.Column('headline', sa.String(), nullable=True), - sa.Column('description', sa.String(), nullable=True), - sa.ForeignKeyConstraint(['review_section_id'], ['review_section.id'], ), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('review_section_id', 'language', name='uq_review_section_id_language') + op.create_table( + "review_section_translation", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("review_section_id", sa.Integer(), nullable=False), + sa.Column("language", sa.String(length=2), nullable=False), + sa.Column("headline", sa.String(), nullable=True), + sa.Column("description", sa.String(), nullable=True), + sa.ForeignKeyConstraint( + ["review_section_id"], + ["review_section.id"], + ), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint( + "review_section_id", "language", name="uq_review_section_id_language" + ), ) - op.add_column('review_question', sa.Column('review_section_id', sa.Integer(), nullable=True)) - op.drop_constraint('review_question_review_form_id_fkey', 'review_question', type_='foreignkey') + op.add_column( + "review_question", sa.Column("review_section_id", sa.Integer(), nullable=True) + ) + op.drop_constraint( + "review_question_review_form_id_fkey", "review_question", type_="foreignkey" + ) # Create dummy section for all existing review forms, link existing questions to this new section - op.execute(""" + op.execute( + """ INSERT INTO review_section(review_form_id, "order") SELECT id, 1 FROM review_form; - """) - - op.execute(""" + """ + ) + + op.execute( + """ INSERT INTO review_section_translation(review_section_id, language, headline, description) SELECT id, 'en', NULL, NULL FROM review_section; - """) + """ + ) - op.execute(""" + op.execute( + """ UPDATE review_question SET review_section_id = review_section.id FROM review_section WHERE review_section.review_form_id = review_question.review_form_id; - """) - - op.alter_column('review_question', 'review_section_id', nullable=False) - op.create_foreign_key('review_question_review_section_id_fkey', 'review_question', 'review_section', ['review_section_id'], ['id']) - op.drop_column('review_question', 'review_form_id') + """ + ) + + op.alter_column("review_question", "review_section_id", nullable=False) + op.create_foreign_key( + "review_question_review_section_id_fkey", + "review_question", + "review_section", + ["review_section_id"], + ["id"], + ) + op.drop_column("review_question", "review_form_id") # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.add_column('review_question', sa.Column('review_form_id', sa.INTEGER(), autoincrement=False, nullable=True)) + op.add_column( + "review_question", + sa.Column("review_form_id", sa.INTEGER(), autoincrement=False, nullable=True), + ) # op.drop_constraint('review_question_review_section_id_fkey', 'review_question', type_='foreignkey') # Link review questions back to review_forms - op.execute(""" + op.execute( + """ UPDATE review_question SET review_form_id = review_section.review_form_id FROM review_section WHERE review_section.id = review_question.review_section_id; - """) + """ + ) - op.alter_column('review_question', 'review_form_id', nullable=False) - op.create_foreign_key('review_question_review_form_id_fkey', 'review_question', 'review_form', ['review_form_id'], ['id']) - op.drop_column('review_question', 'review_section_id') - op.drop_table('review_section_translation') - op.drop_table('review_section') + op.alter_column("review_question", "review_form_id", nullable=False) + op.create_foreign_key( + "review_question_review_form_id_fkey", + "review_question", + "review_form", + ["review_form_id"], + ["id"], + ) + op.drop_column("review_question", "review_section_id") + op.drop_table("review_section_translation") + op.drop_table("review_section") # ### end Alembic commands ### diff --git a/api/migrations/versions/2e81ebc8d90a_response_reviewer_active.py b/api/migrations/versions/2e81ebc8d90a_response_reviewer_active.py index 017e14dbb..43cac8512 100644 --- a/api/migrations/versions/2e81ebc8d90a_response_reviewer_active.py +++ b/api/migrations/versions/2e81ebc8d90a_response_reviewer_active.py @@ -7,23 +7,23 @@ """ # revision identifiers, used by Alembic. -revision = '2e81ebc8d90a' -down_revision = '2717ef90a874' +revision = "2e81ebc8d90a" +down_revision = "2717ef90a874" -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.add_column('response_reviewer', sa.Column('active', sa.Boolean(), nullable=True)) + op.add_column("response_reviewer", sa.Column("active", sa.Boolean(), nullable=True)) op.execute("""UPDATE response_reviewer SET active=True""") - op.alter_column('response_reviewer', 'active', nullable=False) - + op.alter_column("response_reviewer", "active", nullable=False) + # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('response_reviewer', 'active') + op.drop_column("response_reviewer", "active") # ### end Alembic commands ### diff --git a/api/migrations/versions/303140b3cefb_ai4d_scholarship_review1.py b/api/migrations/versions/303140b3cefb_ai4d_scholarship_review1.py index 5fcb53412..312839210 100644 --- a/api/migrations/versions/303140b3cefb_ai4d_scholarship_review1.py +++ b/api/migrations/versions/303140b3cefb_ai4d_scholarship_review1.py @@ -9,26 +9,27 @@ """ # revision identifiers, used by Alembic. -revision = '303140b3cefb' -down_revision = '6b64b8037b7b' +revision = "303140b3cefb" +down_revision = "6b64b8037b7b" + +import datetime +from enum import Enum -from alembic import op import sqlalchemy as sa -from sqlalchemy import orm +from alembic import op +from sqlalchemy import orm, select from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy import orm -from sqlalchemy import select from sqlalchemy.orm import column_property + from app import db -import datetime -from enum import Enum Base = declarative_base() + class Organisation(Base): __tablename__ = "organisation" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(50), nullable=False) @@ -40,7 +41,17 @@ class Organisation(Base): email_from = db.Column(db.String(100), nullable=True) system_url = db.Column(db.String(100), nullable=False) - def __init__(self, name, system_name, small_logo, large_logo, domain, url, email_from, system_url): + def __init__( + self, + name, + system_name, + small_logo, + large_logo, + domain, + url, + email_from, + system_url, + ): self.name = name self.small_logo = small_logo self.large_logo = large_logo @@ -50,9 +61,10 @@ def __init__(self, name, system_name, small_logo, large_logo, domain, url, email self.email_from = email_from self.system_url = system_url + class Country(Base): __tablename__ = "country" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(100), nullable=False) @@ -62,21 +74,23 @@ def __init__(self, name): class EventType(Enum): - EVENT = 'event' - AWARD = 'award' - CALL = 'call' + EVENT = "event" + AWARD = "award" + CALL = "call" class Event(Base): __tablename__ = "event" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) start_date = db.Column(db.DateTime(), nullable=False) end_date = db.Column(db.DateTime(), nullable=False) key = db.Column(db.String(255), nullable=False, unique=True) - organisation_id = db.Column(db.Integer(), db.ForeignKey('organisation.id'), nullable=False) + organisation_id = db.Column( + db.Integer(), db.ForeignKey("organisation.id"), nullable=False + ) email_from = db.Column(db.String(255), nullable=False) url = db.Column(db.String(255), nullable=False) application_open = db.Column(db.DateTime(), nullable=False) @@ -93,7 +107,7 @@ class Event(Base): travel_grant = db.Column(db.Boolean(), nullable=False) miniconf_url = db.Column(db.String(100), nullable=True) - event_translations = db.relationship('EventTranslation', lazy='dynamic') + event_translations = db.relationship("EventTranslation", lazy="dynamic") def __init__( self, @@ -117,7 +131,7 @@ def __init__( registration_close, event_type, travel_grant, - miniconf_url=None + miniconf_url=None, ): self.start_date = start_date self.end_date = end_date @@ -149,10 +163,11 @@ def add_event_translations(self, names, descriptions): event_translation = EventTranslation(name, description, language) self.event_translations.append(event_translation) + class EventTranslation(Base): __tablename__ = "event_translation" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) event_id = db.Column(db.Integer(), db.ForeignKey("event.id"), nullable=False) @@ -160,19 +175,20 @@ class EventTranslation(Base): description = db.Column(db.String(255), nullable=False) language = db.Column(db.String(2)) - event = db.relationship('Event', foreign_keys=[event_id]) + event = db.relationship("Event", foreign_keys=[event_id]) def __init__(self, name, description, language): self.name = name self.description = description self.language = language + class ApplicationForm(Base): - __tablename__ = 'application_form' - __table_args__ = {'extend_existing': True} + __tablename__ = "application_form" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - event_id = db.Column(db.Integer(), db.ForeignKey('event.id'), nullable=False) + event_id = db.Column(db.Integer(), db.ForeignKey("event.id"), nullable=False) is_open = db.Column(db.Boolean(), nullable=False) nominations = db.Column(db.Boolean(), nullable=False) @@ -181,22 +197,29 @@ def __init__(self, event_id, is_open, nominations): self.is_open = is_open self.nominations = nominations + class Question(Base): - __tablename__ = 'question' - __table_args__ = {'extend_existing': True} + __tablename__ = "question" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) - section_id = db.Column(db.Integer(), db.ForeignKey('section.id'), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) + section_id = db.Column(db.Integer(), db.ForeignKey("section.id"), nullable=False) type = db.Column(db.String(), nullable=False) order = db.Column(db.Integer(), nullable=False) is_required = db.Column(db.Boolean(), nullable=False) - depends_on_question_id = db.Column(db.Integer(), db.ForeignKey('question.id'), nullable=True) + depends_on_question_id = db.Column( + db.Integer(), db.ForeignKey("question.id"), nullable=True + ) key = db.Column(db.String(255), nullable=True) - question_translations = db.relationship('QuestionTranslation', lazy='dynamic') + question_translations = db.relationship("QuestionTranslation", lazy="dynamic") - def __init__(self, application_form_id, section_id, order, questionType, is_required=True): + def __init__( + self, application_form_id, section_id, order, questionType, is_required=True + ): self.application_form_id = application_form_id self.section_id = section_id self.order = order @@ -204,15 +227,18 @@ def __init__(self, application_form_id, section_id, order, questionType, is_requ self.is_required = is_required def get_translation(self, language): - question_translation = self.question_translations.filter_by(language=language).first() + question_translation = self.question_translations.filter_by( + language=language + ).first() return question_translation + class QuestionTranslation(Base): - __tablename__ = 'question_translation' - __table_args__ = {'extend_existing': True} + __tablename__ = "question_translation" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - question_id = db.Column(db.Integer(), db.ForeignKey('question.id'), nullable=False) + question_id = db.Column(db.Integer(), db.ForeignKey("question.id"), nullable=False) language = db.Column(db.String(2), nullable=False) headline = db.Column(db.String(), nullable=False) description = db.Column(db.String(), nullable=True) @@ -222,7 +248,7 @@ class QuestionTranslation(Base): options = db.Column(db.JSON(), nullable=True) show_for_values = db.Column(db.JSON(), nullable=True) - question = db.relationship('Question', foreign_keys=[question_id]) + question = db.relationship("Question", foreign_keys=[question_id]) def __init__( self, @@ -234,7 +260,7 @@ def __init__( validation_regex=None, validation_text=None, options=None, - show_for_values=None + show_for_values=None, ): self.question_id = question_id self.language = language @@ -246,43 +272,56 @@ def __init__( self.options = options self.show_for_values = show_for_values + class Section(Base): - __tablename__ = 'section' - __table_args__ = {'extend_existing': True} + __tablename__ = "section" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) order = db.Column(db.Integer(), nullable=False) - depends_on_question_id = db.Column(db.Integer(), db.ForeignKey('question.id', use_alter=True), nullable=True) + depends_on_question_id = db.Column( + db.Integer(), db.ForeignKey("question.id", use_alter=True), nullable=True + ) key = db.Column(db.String(255), nullable=True) - application_form = db.relationship('ApplicationForm', foreign_keys=[application_form_id]) - section_translations = db.relationship('SectionTranslation', lazy='dynamic') - questions = db.relationship('Question', primaryjoin=id==Question.section_id, order_by='Question.order') + application_form = db.relationship( + "ApplicationForm", foreign_keys=[application_form_id] + ) + section_translations = db.relationship("SectionTranslation", lazy="dynamic") + questions = db.relationship( + "Question", primaryjoin=id == Question.section_id, order_by="Question.order" + ) - def __init__(self, application_form_id, order, depends_on_question_id=None, key=None): + def __init__( + self, application_form_id, order, depends_on_question_id=None, key=None + ): self.application_form_id = application_form_id self.order = order self.depends_on_question_id = depends_on_question_id self.key = key def get_translation(self, language): - section_translation = self.section_translations.filter_by(language=language).first() + section_translation = self.section_translations.filter_by( + language=language + ).first() return section_translation class SectionTranslation(Base): - __tablename__ = 'section_translation' - __table_args__ = {'extend_existing': True} + __tablename__ = "section_translation" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - section_id = db.Column(db.Integer(), db.ForeignKey('section.id'), nullable=False) + section_id = db.Column(db.Integer(), db.ForeignKey("section.id"), nullable=False) language = db.Column(db.String(2), nullable=False) name = db.Column(db.String(255), nullable=False) description = db.Column(db.String(), nullable=False) show_for_values = db.Column(db.JSON(), nullable=True) - section = db.relationship('Section', foreign_keys=[section_id]) + section = db.relationship("Section", foreign_keys=[section_id]) def __init__(self, section_id, language, name, description, show_for_values=None): self.section_id = section_id @@ -291,17 +330,22 @@ def __init__(self, section_id, language, name, description, show_for_values=None self.description = description self.show_for_values = show_for_values + class ReviewForm(Base): - __tablename__ = 'review_form' - __table_args__ = {'extend_existing': True} + __tablename__ = "review_form" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) is_open = db.Column(db.Boolean(), nullable=False) deadline = db.Column(db.DateTime(), nullable=False) - application_form = db.relationship('ApplicationForm', foreign_keys=[application_form_id]) - review_questions = db.relationship('ReviewQuestion') + application_form = db.relationship( + "ApplicationForm", foreign_keys=[application_form_id] + ) + review_questions = db.relationship("ReviewQuestion") def __init__(self, application_form_id, deadline): self.application_form_id = application_form_id @@ -313,30 +357,26 @@ def close(self): class ReviewQuestion(Base): - __tablename__ = 'review_question' - __table_args__ = {'extend_existing': True} + __tablename__ = "review_question" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer, primary_key=True) - review_form_id = db.Column(db.Integer(), db.ForeignKey('review_form.id'), nullable=False) - question_id = db.Column(db.Integer(), db.ForeignKey('question.id'), nullable=True) - + review_form_id = db.Column( + db.Integer(), db.ForeignKey("review_form.id"), nullable=False + ) + question_id = db.Column(db.Integer(), db.ForeignKey("question.id"), nullable=True) + type = db.Column(db.String(), nullable=False) - + is_required = db.Column(db.Boolean(), nullable=False) order = db.Column(db.Integer(), nullable=False) weight = db.Column(db.Float(), nullable=False) - review_form = db.relationship('ReviewForm', foreign_keys=[review_form_id]) - question = db.relationship('Question', foreign_keys=[question_id]) - - translations = db.relationship('ReviewQuestionTranslation', lazy='dynamic') - - def __init__(self, - review_form_id, - question_id, - type, - is_required, - order, - weight): + review_form = db.relationship("ReviewForm", foreign_keys=[review_form_id]) + question = db.relationship("Question", foreign_keys=[question_id]) + + translations = db.relationship("ReviewQuestionTranslation", lazy="dynamic") + + def __init__(self, review_form_id, question_id, type, is_required, order, weight): self.review_form_id = review_form_id self.question_id = question_id self.type = type @@ -350,12 +390,20 @@ def get_translation(self, language): class ReviewQuestionTranslation(Base): - __tablename__ = 'review_question_translation' - __table_args__ = {'extend_existing': True} - __table_args__ = tuple([db.UniqueConstraint('review_question_id', 'language', name='uq_review_question_id_language')]) + __tablename__ = "review_question_translation" + __table_args__ = {"extend_existing": True} + __table_args__ = tuple( + [ + db.UniqueConstraint( + "review_question_id", "language", name="uq_review_question_id_language" + ) + ] + ) id = db.Column(db.Integer(), primary_key=True) - review_question_id = db.Column(db.Integer(), db.ForeignKey('review_question.id'), nullable=False) + review_question_id = db.Column( + db.Integer(), db.ForeignKey("review_question.id"), nullable=False + ) language = db.Column(db.String(2), nullable=False) description = db.Column(db.String(), nullable=True) @@ -365,15 +413,17 @@ class ReviewQuestionTranslation(Base): validation_regex = db.Column(db.String(), nullable=True) validation_text = db.Column(db.String(), nullable=True) - def __init__(self, - review_question_id, - language, - description=None, - headline=None, - placeholder=None, - options=None, - validation_regex=None, - validation_text=None): + def __init__( + self, + review_question_id, + language, + description=None, + headline=None, + placeholder=None, + options=None, + validation_regex=None, + validation_text=None, + ): self.review_question_id = review_question_id self.language = language self.description = description @@ -383,50 +433,59 @@ def __init__(self, self.validation_regex = validation_regex self.validation_text = validation_text + class ReviewConfiguration(Base): - __tablename__ = 'review_configuration' - __table_args__ = {'extend_existing': True} + __tablename__ = "review_configuration" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - review_form_id = db.Column(db.Integer(), db.ForeignKey('review_form.id'), nullable=False) + review_form_id = db.Column( + db.Integer(), db.ForeignKey("review_form.id"), nullable=False + ) num_reviews_required = db.Column(db.Integer(), nullable=False) num_optional_reviews = db.Column(db.Integer(), nullable=False) - drop_optional_question_id = db.Column(db.Integer(), db.ForeignKey('review_question.id'), nullable=True) + drop_optional_question_id = db.Column( + db.Integer(), db.ForeignKey("review_question.id"), nullable=True + ) drop_optional_agreement_values = db.Column(db.String(), nullable=True) + class ReviewScore(Base): - __tablename__ = 'review_score' - __table_args__ = {'extend_existing': True} + __tablename__ = "review_score" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - review_response_id = db.Column(db.Integer(), db.ForeignKey('review_response.id'), nullable=False) - review_question_id = db.Column(db.Integer(), db.ForeignKey('review_question.id'), nullable=False) + review_response_id = db.Column( + db.Integer(), db.ForeignKey("review_response.id"), nullable=False + ) + review_question_id = db.Column( + db.Integer(), db.ForeignKey("review_question.id"), nullable=False + ) value = db.Column(db.String(), nullable=False) - def __init__(self, - review_question_id, - value): + def __init__(self, review_question_id, value): self.review_question_id = review_question_id self.value = value + class ReviewResponse(Base): - __tablename__ = 'review_response' - __table_args__ = {'extend_existing': True} + __tablename__ = "review_response" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - review_form_id = db.Column(db.Integer(), db.ForeignKey('review_form.id'), nullable=False) - reviewer_user_id = db.Column(db.Integer(), db.ForeignKey('app_user.id'), nullable=False) - response_id = db.Column(db.Integer(), db.ForeignKey('response.id'), nullable=False) + review_form_id = db.Column( + db.Integer(), db.ForeignKey("review_form.id"), nullable=False + ) + reviewer_user_id = db.Column( + db.Integer(), db.ForeignKey("app_user.id"), nullable=False + ) + response_id = db.Column(db.Integer(), db.ForeignKey("response.id"), nullable=False) submitted_timestamp = db.Column(db.DateTime(), nullable=False) language = db.Column(db.String(2), nullable=False) is_submitted = db.Column(db.Boolean(), nullable=False) submitted_timestamp = db.Column(db.DateTime(), nullable=True) - def __init__(self, - review_form_id, - reviewer_user_id, - response_id, - language): + def __init__(self, review_form_id, reviewer_user_id, response_id, language): self.review_form_id = review_form_id self.reviewer_user_id = reviewer_user_id self.response_id = response_id @@ -440,31 +499,46 @@ def submit(self): current_order = 1 -def add_question(review_form_id, type, is_required, weight=0, question_id=None, - descriptions=None, headlines=None, placeholders=None, options=None, - validation_regexs=None, validation_texts=None): + +def add_question( + review_form_id, + type, + is_required, + weight=0, + question_id=None, + descriptions=None, + headlines=None, + placeholders=None, + options=None, + validation_regexs=None, + validation_texts=None, +): global current_order - question = ReviewQuestion(review_form_id, question_id, type, is_required, current_order, weight) + question = ReviewQuestion( + review_form_id, question_id, type, is_required, current_order, weight + ) db.session.add(question) db.session.commit() en = ReviewQuestionTranslation( - question.id, - 'en', - None if descriptions is None else descriptions['en'], - None if headlines is None else headlines['en'], - None if placeholders is None else placeholders['en'], - None if options is None else options['en'], - None if validation_regexs is None else validation_regexs['en'], - None if validation_texts is None else validation_texts['en']) + question.id, + "en", + None if descriptions is None else descriptions["en"], + None if headlines is None else headlines["en"], + None if placeholders is None else placeholders["en"], + None if options is None else options["en"], + None if validation_regexs is None else validation_regexs["en"], + None if validation_texts is None else validation_texts["en"], + ) fr = ReviewQuestionTranslation( - question.id, - 'fr', - None if descriptions is None else descriptions['fr'], - None if headlines is None else headlines['fr'], - None if placeholders is None else placeholders['fr'], - None if options is None else options['fr'], - None if validation_regexs is None else validation_regexs['fr'], - None if validation_texts is None else validation_texts['fr']) + question.id, + "fr", + None if descriptions is None else descriptions["fr"], + None if headlines is None else headlines["fr"], + None if placeholders is None else placeholders["fr"], + None if options is None else options["fr"], + None if validation_regexs is None else validation_regexs["fr"], + None if validation_texts is None else validation_texts["fr"], + ) db.session.add_all([en, fr]) db.session.commit() @@ -472,107 +546,177 @@ def add_question(review_form_id, type, is_required, weight=0, question_id=None, return question, en, fr + def get_question(application_form_id, en_headline): - en = (db.session.query(QuestionTranslation) - .filter_by(language='en', headline=en_headline) - .join(Question, QuestionTranslation.question_id == Question.id) - .filter_by(application_form_id=application_form_id) - .first()) - fr = db.session.query(QuestionTranslation).filter_by(language='fr', question_id=en.question_id).first() + en = ( + db.session.query(QuestionTranslation) + .filter_by(language="en", headline=en_headline) + .join(Question, QuestionTranslation.question_id == Question.id) + .filter_by(application_form_id=application_form_id) + .first() + ) + fr = ( + db.session.query(QuestionTranslation) + .filter_by(language="fr", question_id=en.question_id) + .first() + ) return en.question, en, fr + def add_evaluation_question(form_id, headlines, descriptions, min_score, max_score): - validation_regex = '^({})$'.format('|'.join([str(i) for i in range(max_score, min_score-1, -1)])) + validation_regex = "^({})$".format( + "|".join([str(i) for i in range(max_score, min_score - 1, -1)]) + ) validation_text = { - 'en': 'Enter a number between {} and {}'.format(min_score, max_score), - 'fr': 'Entrez un nombre entre {} et {}'.format(min_score, max_score) + "en": "Enter a number between {} and {}".format(min_score, max_score), + "fr": "Entrez un nombre entre {} et {}".format(min_score, max_score), } placeholders = { - 'en': 'Enter a score between {} and {}'.format(min_score, max_score), - 'fr': 'Saisissez un score compris entre {} et {}'.format(min_score, max_score) + "en": "Enter a score between {} and {}".format(min_score, max_score), + "fr": "Saisissez un score compris entre {} et {}".format(min_score, max_score), } - return add_question(form_id, 'short-text', True, 1, headlines=headlines, - descriptions=descriptions, validation_regexs={'en': validation_regex, 'fr': validation_regex}, - validation_texts=validation_text, placeholders=placeholders) + return add_question( + form_id, + "short-text", + True, + 1, + headlines=headlines, + descriptions=descriptions, + validation_regexs={"en": validation_regex, "fr": validation_regex}, + validation_texts=validation_text, + placeholders=placeholders, + ) -def add_comment_question(form_id, headlines, descriptions, is_required=False): - return add_question(form_id, 'long-text', is_required, weight=1, descriptions=descriptions, headlines=headlines) -def add_information_question(form_id, application_form_id, en_headline, type='information', heading_override=False, description_override=None): +def add_comment_question(form_id, headlines, descriptions, is_required=False): + return add_question( + form_id, + "long-text", + is_required, + weight=1, + descriptions=descriptions, + headlines=headlines, + ) + + +def add_information_question( + form_id, + application_form_id, + en_headline, + type="information", + heading_override=False, + description_override=None, +): question, en, fr = get_question(application_form_id, en_headline) - + headlines = { - 'en': None if heading_override else en.headline, - 'fr': None if heading_override else fr.headline + "en": None if heading_override else en.headline, + "fr": None if heading_override else fr.headline, } descriptions = { - 'en': description_override['en'] if description_override is not None else en.description, - 'fr': description_override['fr'] if description_override is not None else fr.description + "en": description_override["en"] + if description_override is not None + else en.description, + "fr": description_override["fr"] + if description_override is not None + else fr.description, } - if question.type == 'information': + if question.type == "information": return add_heading(form_id, headlines) - return add_question(form_id, type, False, question_id=question.id, headlines=headlines, descriptions=descriptions) + return add_question( + form_id, + type, + False, + question_id=question.id, + headlines=headlines, + descriptions=descriptions, + ) + def add_heading(form_id, headlines): - return add_question(form_id, 'heading', False, headlines=headlines) + return add_question(form_id, "heading", False, headlines=headlines) + def add_divider(form_id, headlines, descriptions, sub_heading=False): return add_question( - form_id, - 'sub-heading' if sub_heading else 'section-divider', - False, - headlines=headlines, - descriptions=descriptions) + form_id, + "sub-heading" if sub_heading else "section-divider", + False, + headlines=headlines, + descriptions=descriptions, + ) def upgrade(): - event = db.session.query(Event).filter_by(key='spm').first() - application_form = db.session.query(ApplicationForm).filter_by(event_id=event.id).first() + event = db.session.query(Event).filter_by(key="spm").first() + application_form = ( + db.session.query(ApplicationForm).filter_by(event_id=event.id).first() + ) form = ReviewForm(application_form.id, datetime.datetime(2020, 12, 31)) db.session.add(form) db.session.commit() review_config = ReviewConfiguration( - review_form_id=form.id, - num_reviews_required=3, - num_optional_reviews=0) + review_form_id=form.id, num_reviews_required=3, num_optional_reviews=0 + ) db.session.add(review_config) db.session.commit() # Set up application form responses - divider1 = add_divider(form.id, {'en': 'Application', 'fr': 'Application'}, descriptions=None) - - sections = db.session.query(Section).filter_by(application_form_id=application_form.id).all() + divider1 = add_divider( + form.id, {"en": "Application", "fr": "Application"}, descriptions=None + ) + + sections = ( + db.session.query(Section) + .filter_by(application_form_id=application_form.id) + .all() + ) for section in sections: - en = section.get_translation('en') - if en.name == 'Organisation and Lead Applicant Contact details' or en.name == 'AI4D Call for Proposals: Scholarships program manager': + en = section.get_translation("en") + if ( + en.name == "Organisation and Lead Applicant Contact details" + or en.name == "AI4D Call for Proposals: Scholarships program manager" + ): continue - add_divider(form.id, {'en': en.name, 'fr': section.get_translation('fr').name}, None, sub_heading=True) + add_divider( + form.id, + {"en": en.name, "fr": section.get_translation("fr").name}, + None, + sub_heading=True, + ) for question in section.questions: - translation = question.get_translation('en') - if question.type == 'multi-file': - q_type = 'multi-file' - elif question.type == 'file': - q_type = 'file' + translation = question.get_translation("en") + if question.type == "multi-file": + q_type = "multi-file" + elif question.type == "file": + q_type = "file" else: - q_type = 'information' - add_information_question(form.id, application_form.id, translation.headline, type=q_type) + q_type = "information" + add_information_question( + form.id, application_form.id, translation.headline, type=q_type + ) # Add review questions - add_divider(form.id, { - 'en': 'Review', - 'fr': 'Review' - }, None) - - add_divider(form.id, { - 'en': '1. Organizational capacity and program fit of the host institution', - 'fr': '1. Organizational capacity and program fit of the host institution', - }, None, sub_heading=True) - - add_evaluation_question(form.id, None, { - 'en': """1.1.a. To what extent does the organization and their proposal demonstrate strong fit with the AI4D programme outcomes and demonstrate sufficient organizational capacity to manage the call, such as: + add_divider(form.id, {"en": "Review", "fr": "Review"}, None) + + add_divider( + form.id, + { + "en": "1. Organizational capacity and program fit of the host institution", + "fr": "1. Organizational capacity and program fit of the host institution", + }, + None, + sub_heading=True, + ) + + add_evaluation_question( + form.id, + None, + { + "en": """1.1.a. To what extent does the organization and their proposal demonstrate strong fit with the AI4D programme outcomes and demonstrate sufficient organizational capacity to manage the call, such as: - A strong record of relevant experience in hosting scholarships in the African context; - Availability of, or ability to acquire, required resources and capacities; - A proposal for clear and appropriate coordination and governance of the partnership; @@ -586,16 +730,28 @@ def upgrade(): **10 - Satisfactory**: meets most criteria though needs further elaboration **15 - Good**: meets all criteria for capacity and program fit **20 - Very Good**: meets and exceeds criteria for capacity and program fit -""", 'fr': """""" - }, 0, 20) - - add_comment_question(form.id, None, { - 'en': """1.1.b: To what extent does the organization and their proposal demonstrate strong fit with the AI4D programme outcomes and demonstrate sufficient organizational capacity to manage the call?""", - 'fr': """""" - }, True) - - add_evaluation_question(form.id, None, { - 'en': """1.2.a To what extent does the organization and the proposed budget and approach demonstrate value for money (e.g. low management to scholarship cost ratios), and a history of good fiscal management? +""", + "fr": """""", + }, + 0, + 20, + ) + + add_comment_question( + form.id, + None, + { + "en": """1.1.b: To what extent does the organization and their proposal demonstrate strong fit with the AI4D programme outcomes and demonstrate sufficient organizational capacity to manage the call?""", + "fr": """""", + }, + True, + ) + + add_evaluation_question( + form.id, + None, + { + "en": """1.2.a To what extent does the organization and the proposed budget and approach demonstrate value for money (e.g. low management to scholarship cost ratios), and a history of good fiscal management? Reference: Section IV (Budget) @@ -606,18 +762,35 @@ def upgrade(): **20 - Very Good**: meets and exceeds criteria -""", 'fr': """""" - }, 0, 20) - - add_comment_question(form.id, None, { - 'en': """1.2.b: Qualitative assessment: To what extent does the organization and the proposed budget and approach demonstrate value for money (e.g. low management to scholarship cost ratios), and a history of good fiscal management? """, - 'fr': """""" - }, True) - - add_divider(form.id, {'en': '2. Scholarship Plan Proposal', 'fr': '2. Scholarship Plan Proposal'}, None, sub_heading=True) - - add_evaluation_question(form.id, None, { - 'en': """2.1 To what extent does the scholarship plan proposal demonstrate a feasible and coherent approach to supporting early career academics and winning PhD students, to meeting the basic call requirements for eligibility, structuring evaluation and selection of candidates, and ensuring gender, inclusion, equity and ethical considerations are included? +""", + "fr": """""", + }, + 0, + 20, + ) + + add_comment_question( + form.id, + None, + { + "en": """1.2.b: Qualitative assessment: To what extent does the organization and the proposed budget and approach demonstrate value for money (e.g. low management to scholarship cost ratios), and a history of good fiscal management? """, + "fr": """""", + }, + True, + ) + + add_divider( + form.id, + {"en": "2. Scholarship Plan Proposal", "fr": "2. Scholarship Plan Proposal"}, + None, + sub_heading=True, + ) + + add_evaluation_question( + form.id, + None, + { + "en": """2.1 To what extent does the scholarship plan proposal demonstrate a feasible and coherent approach to supporting early career academics and winning PhD students, to meeting the basic call requirements for eligibility, structuring evaluation and selection of candidates, and ensuring gender, inclusion, equity and ethical considerations are included? Reference: Section II (all) @@ -626,18 +799,38 @@ def upgrade(): **15 - Satisfactory**: Demonstrates satisfactory evidence and justification for approach and design, but certain improvements and clarifications are needed; meets the basic standards for meeting the call requirements; basic consideration of gender and inclusion principles, meets basic requirements **25 - Good**: Convincing and well cited justification for the proposed design; well-conceived approach with a likelihood for success; may go beyond the business-as-usual to support local contexts and needs; meets or slightly exceeds call requirements relating to evaluation of projects, engaging experts, and selection for gender, equity and inclusion as well as ethics. **35 - Very Good**: Very convincing approach with strong examples of evidence; very well conceived approach with a very high likelihood for success; innovative approaches will definitely go beyond the business-as-usual to support local contexts and needs; meets and exceeds call requirements relating to evaluation of projects, engaging experts in the selection process, and selection for gender, equity and inclusion as well as ethics. -""", 'fr': """""" - }, 0, 35) - - add_comment_question(form.id, None, { - 'en': """2.1.b To what extent does the scholarship plan proposal demonstrate a feasible and coherent approach to supporting early career academics and winning PhD students, to meeting the basic call requirements for eligibility, structuring evaluation and selection of candidates, and ensuring gender, inclusion, equity and ethical considerations are included?""", - 'fr': """""" - }, True) - - add_divider(form.id, {'en': '3. Complementary activities plan', 'fr': '3. Complementary activities plan'}, None, sub_heading=True) - - add_evaluation_question(form.id, None, { - 'en': """3.1 To what extent does the plan for complementary activities demonstrate that it meets the following criteria: +""", + "fr": """""", + }, + 0, + 35, + ) + + add_comment_question( + form.id, + None, + { + "en": """2.1.b To what extent does the scholarship plan proposal demonstrate a feasible and coherent approach to supporting early career academics and winning PhD students, to meeting the basic call requirements for eligibility, structuring evaluation and selection of candidates, and ensuring gender, inclusion, equity and ethical considerations are included?""", + "fr": """""", + }, + True, + ) + + add_divider( + form.id, + { + "en": "3. Complementary activities plan", + "fr": "3. Complementary activities plan", + }, + None, + sub_heading=True, + ) + + add_evaluation_question( + form.id, + None, + { + "en": """3.1 To what extent does the plan for complementary activities demonstrate that it meets the following criteria: - Support for PhD students to connect with their peers, form scientific communities, network with industry, and other relevant opportunities that enable their success; - That the program will create links between the individual early-career academics and their projects and teams to help add value across the program.; - Support for the success of women, ethnic and linguistic minorities in activity design; @@ -651,21 +844,35 @@ def upgrade(): **10 - Satisfactory**: meets most criteria though needs further elaboration **15 - Good**: meets all criteria **20 - Very Good**: meets and exceeds criteria -""", 'fr': """""" - }, 0, 20) - - add_comment_question(form.id, None, { - 'en': """3.1.b Please provide considerations to justify the score for question 3.1""", - 'fr': """""" - }, True) - - add_divider(form.id, { - 'en': """4. Marketing and communications approach""", - 'fr': """""" - }, None, sub_heading=True) - - add_evaluation_question(form.id, None, { - 'en': """4.1 To what extend does the proposal outline an effective marketing and communications strategy that will bring awareness to the AI4D Africa Scholars program? +""", + "fr": """""", + }, + 0, + 20, + ) + + add_comment_question( + form.id, + None, + { + "en": """3.1.b Please provide considerations to justify the score for question 3.1""", + "fr": """""", + }, + True, + ) + + add_divider( + form.id, + {"en": """4. Marketing and communications approach""", "fr": """"""}, + None, + sub_heading=True, + ) + + add_evaluation_question( + form.id, + None, + { + "en": """4.1 To what extend does the proposal outline an effective marketing and communications strategy that will bring awareness to the AI4D Africa Scholars program? Reference: Section 3.a. @@ -673,68 +880,106 @@ def upgrade(): **3 - Poor**: demonstrates some criteria **5 - Satisfactory**: meets most criteria though needs further elaboration **7 - Good**: meets all criteria -**10 - Very Good**: meets and exceeds criteria """, 'fr': """""" - }, 0, 10) - - add_comment_question(form.id, None, { - 'en': """4.1.b: To what extend does the proposal outline an effective marketing and communications strategy that will bring awareness to the AI4D Africa Scholars program?""", - 'fr': """""" - }, True) - - add_divider(form.id, {'en': """5. Final Overview""", 'fr': """5. Final Overview"""}, None, sub_heading=True) - - add_question(form.id, 'multi-choice', True, descriptions={ - 'en': '5.1 On the basis of your overall review, do you recommend this proposal for selection?', - 'fr': '5.1 On the basis of your overall review, do you recommend this proposal for selection?' - }, options={ - 'en': [ - {'value': 'recommend', 'label': 'I recommend this proposal'}, - {'value': 'reservations', 'label': 'I recommend with reservations'}, - {'value': 'not-recommend', 'label': 'I do not recommend'}, - ], - 'fr': [ - {'value': 'recommend', 'label': 'I recommend this proposal'}, - {'value': 'reservations', 'label': 'I recommend with reservations'}, - {'value': 'not-recommend', 'label': 'I do not recommend'}, - ] - }) - - add_comment_question(form.id, None, { - 'en': """5.2 Overall considerations about the proposal (if not covered by earlier responses):""", - 'fr': """""" - }, True) - - add_comment_question(form.id, None, { - 'en': """5.3 Feedback for applicant:""", - 'fr': """""" - }, True) - - en, q = (db.session.query(QuestionTranslation, Question) - .filter_by(headline='Name of Organisation', language='en') - .join(Question, QuestionTranslation.question_id == Question.id) - .filter_by(application_form_id=application_form.id).first()) - - q.key = 'review-identifier' - - en, q = (db.session.query(QuestionTranslation, Question) - .filter_by(headline='Country', language='en') - .join(Question, QuestionTranslation.question_id == Question.id) - .filter_by(application_form_id=application_form.id).first()) - - q.key = 'review-identifier' +**10 - Very Good**: meets and exceeds criteria """, + "fr": """""", + }, + 0, + 10, + ) + + add_comment_question( + form.id, + None, + { + "en": """4.1.b: To what extend does the proposal outline an effective marketing and communications strategy that will bring awareness to the AI4D Africa Scholars program?""", + "fr": """""", + }, + True, + ) + + add_divider( + form.id, + {"en": """5. Final Overview""", "fr": """5. Final Overview"""}, + None, + sub_heading=True, + ) + + add_question( + form.id, + "multi-choice", + True, + descriptions={ + "en": "5.1 On the basis of your overall review, do you recommend this proposal for selection?", + "fr": "5.1 On the basis of your overall review, do you recommend this proposal for selection?", + }, + options={ + "en": [ + {"value": "recommend", "label": "I recommend this proposal"}, + {"value": "reservations", "label": "I recommend with reservations"}, + {"value": "not-recommend", "label": "I do not recommend"}, + ], + "fr": [ + {"value": "recommend", "label": "I recommend this proposal"}, + {"value": "reservations", "label": "I recommend with reservations"}, + {"value": "not-recommend", "label": "I do not recommend"}, + ], + }, + ) + + add_comment_question( + form.id, + None, + { + "en": """5.2 Overall considerations about the proposal (if not covered by earlier responses):""", + "fr": """""", + }, + True, + ) + + add_comment_question( + form.id, None, {"en": """5.3 Feedback for applicant:""", "fr": """"""}, True + ) + + en, q = ( + db.session.query(QuestionTranslation, Question) + .filter_by(headline="Name of Organisation", language="en") + .join(Question, QuestionTranslation.question_id == Question.id) + .filter_by(application_form_id=application_form.id) + .first() + ) + + q.key = "review-identifier" + + en, q = ( + db.session.query(QuestionTranslation, Question) + .filter_by(headline="Country", language="en") + .join(Question, QuestionTranslation.question_id == Question.id) + .filter_by(application_form_id=application_form.id) + .first() + ) + + q.key = "review-identifier" db.session.commit() def downgrade(): - event = db.session.query(Event).filter_by(key='spm').first() - application_form = db.session.query(ApplicationForm).filter_by(event_id=event.id).first() - form = db.session.query(ReviewForm).filter_by(application_form_id=application_form.id).first() + event = db.session.query(Event).filter_by(key="spm").first() + application_form = ( + db.session.query(ApplicationForm).filter_by(event_id=event.id).first() + ) + form = ( + db.session.query(ReviewForm) + .filter_by(application_form_id=application_form.id) + .first() + ) questions = db.session.query(ReviewQuestion).filter_by(review_form_id=form.id).all() for q in questions: - db.session.query(ReviewQuestionTranslation).filter_by(review_question_id=q.id).delete() + db.session.query(ReviewQuestionTranslation).filter_by( + review_question_id=q.id + ).delete() db.session.query(ReviewScore).filter_by(review_question_id=q.id).delete() db.session.commit() @@ -742,6 +987,8 @@ def downgrade(): db.session.commit() db.session.query(ReviewConfiguration).filter_by(review_form_id=form.id).delete() - db.session.query(ReviewForm).filter_by(application_form_id=application_form.id).delete() + db.session.query(ReviewForm).filter_by( + application_form_id=application_form.id + ).delete() - db.session.commit() \ No newline at end of file + db.session.commit() diff --git a/api/migrations/versions/32c1ae6dd23a_.py b/api/migrations/versions/32c1ae6dd23a_.py index f12e50862..8d7ecb291 100644 --- a/api/migrations/versions/32c1ae6dd23a_.py +++ b/api/migrations/versions/32c1ae6dd23a_.py @@ -7,11 +7,11 @@ """ # revision identifiers, used by Alembic. -revision = '32c1ae6dd23a' -down_revision = ('0e13fd6f0d2a', '4d8b254d7e7e') +revision = "32c1ae6dd23a" +down_revision = ("0e13fd6f0d2a", "4d8b254d7e7e") -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): diff --git a/api/migrations/versions/33b563f7be6b_review_response_language.py b/api/migrations/versions/33b563f7be6b_review_response_language.py index aa0308e0a..da7ddac28 100644 --- a/api/migrations/versions/33b563f7be6b_review_response_language.py +++ b/api/migrations/versions/33b563f7be6b_review_response_language.py @@ -7,22 +7,24 @@ """ # revision identifiers, used by Alembic. -revision = '33b563f7be6b' -down_revision = '3454400e3738' +revision = "33b563f7be6b" +down_revision = "3454400e3738" -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.add_column('review_response', sa.Column('language', sa.String(length=2), nullable=True)) + op.add_column( + "review_response", sa.Column("language", sa.String(length=2), nullable=True) + ) op.execute("UPDATE review_response SET language = 'en'") - op.alter_column('review_response', 'language', nullable=False) + op.alter_column("review_response", "language", nullable=False) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('review_response', 'language') + op.drop_column("review_response", "language") # ### end Alembic commands ### diff --git a/api/migrations/versions/33e175de61ca_.py b/api/migrations/versions/33e175de61ca_.py index d90ffc95a..73d9c2b58 100644 --- a/api/migrations/versions/33e175de61ca_.py +++ b/api/migrations/versions/33e175de61ca_.py @@ -7,24 +7,24 @@ """ # revision identifiers, used by Alembic. -revision = '33e175de61ca' -down_revision = 'b7006cbdd923' +revision = "33e175de61ca" +down_revision = "b7006cbdd923" -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.alter_column('email_template', 'event_id', - existing_type=sa.INTEGER(), - nullable=True) + op.alter_column( + "email_template", "event_id", existing_type=sa.INTEGER(), nullable=True + ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.alter_column('email_template', 'event_id', - existing_type=sa.INTEGER(), - nullable=False) + op.alter_column( + "email_template", "event_id", existing_type=sa.INTEGER(), nullable=False + ) # ### end Alembic commands ### diff --git a/api/migrations/versions/3454400e3738_review_question_translation.py b/api/migrations/versions/3454400e3738_review_question_translation.py index 6c32222f8..f111f1007 100644 --- a/api/migrations/versions/3454400e3738_review_question_translation.py +++ b/api/migrations/versions/3454400e3738_review_question_translation.py @@ -7,32 +7,40 @@ """ # revision identifiers, used by Alembic. -revision = '3454400e3738' -down_revision = '039addb92a03' +revision = "3454400e3738" +down_revision = "039addb92a03" -from alembic import op import sqlalchemy as sa +from alembic import op from sqlalchemy.dialects import postgresql + def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.create_table('review_question_translation', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('review_question_id', sa.Integer(), nullable=False), - sa.Column('language', sa.String(length=2), nullable=False), - sa.Column('description', sa.String(), nullable=True), - sa.Column('headline', sa.String(), nullable=True), - sa.Column('placeholder', sa.String(), nullable=True), - sa.Column('options', sa.JSON(), nullable=True), - sa.Column('validation_regex', sa.String(), nullable=True), - sa.Column('validation_text', sa.String(), nullable=True), - sa.ForeignKeyConstraint(['review_question_id'], ['review_question.id'], ), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('review_question_id', 'language', name='uq_review_question_id_language') + op.create_table( + "review_question_translation", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("review_question_id", sa.Integer(), nullable=False), + sa.Column("language", sa.String(length=2), nullable=False), + sa.Column("description", sa.String(), nullable=True), + sa.Column("headline", sa.String(), nullable=True), + sa.Column("placeholder", sa.String(), nullable=True), + sa.Column("options", sa.JSON(), nullable=True), + sa.Column("validation_regex", sa.String(), nullable=True), + sa.Column("validation_text", sa.String(), nullable=True), + sa.ForeignKeyConstraint( + ["review_question_id"], + ["review_question.id"], + ), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint( + "review_question_id", "language", name="uq_review_question_id_language" + ), ) # Copy data from review_question to review_question_translation - op.execute(""" + op.execute( + """ INSERT INTO review_question_translation( review_question_id, language, @@ -52,27 +60,52 @@ def upgrade(): validation_regex, validation_text FROM review_question - """) + """ + ) - op.drop_column('review_question', 'description') - op.drop_column('review_question', 'headline') - op.drop_column('review_question', 'validation_text') - op.drop_column('review_question', 'validation_regex') - op.drop_column('review_question', 'placeholder') - op.drop_column('review_question', 'options') + op.drop_column("review_question", "description") + op.drop_column("review_question", "headline") + op.drop_column("review_question", "validation_text") + op.drop_column("review_question", "validation_regex") + op.drop_column("review_question", "placeholder") + op.drop_column("review_question", "options") # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.add_column('review_question', sa.Column('options', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=True)) - op.add_column('review_question', sa.Column('placeholder', sa.VARCHAR(), autoincrement=False, nullable=True)) - op.add_column('review_question', sa.Column('validation_regex', sa.VARCHAR(), autoincrement=False, nullable=True)) - op.add_column('review_question', sa.Column('validation_text', sa.VARCHAR(), autoincrement=False, nullable=True)) - op.add_column('review_question', sa.Column('headline', sa.VARCHAR(), autoincrement=False, nullable=True)) - op.add_column('review_question', sa.Column('description', sa.VARCHAR(), autoincrement=False, nullable=True)) + op.add_column( + "review_question", + sa.Column( + "options", + postgresql.JSON(astext_type=sa.Text()), + autoincrement=False, + nullable=True, + ), + ) + op.add_column( + "review_question", + sa.Column("placeholder", sa.VARCHAR(), autoincrement=False, nullable=True), + ) + op.add_column( + "review_question", + sa.Column("validation_regex", sa.VARCHAR(), autoincrement=False, nullable=True), + ) + op.add_column( + "review_question", + sa.Column("validation_text", sa.VARCHAR(), autoincrement=False, nullable=True), + ) + op.add_column( + "review_question", + sa.Column("headline", sa.VARCHAR(), autoincrement=False, nullable=True), + ) + op.add_column( + "review_question", + sa.Column("description", sa.VARCHAR(), autoincrement=False, nullable=True), + ) - op.execute(""" + op.execute( + """ UPDATE review_question SET options = (SELECT options from review_question_translation WHERE review_question.id = review_question_translation.review_question_id AND review_question_translation.language = 'en'), placeholder = (SELECT placeholder from review_question_translation WHERE review_question.id = review_question_translation.review_question_id AND review_question_translation.language = 'en'), @@ -80,7 +113,8 @@ def downgrade(): validation_text = (SELECT validation_text from review_question_translation WHERE review_question.id = review_question_translation.review_question_id AND review_question_translation.language = 'en'), headline = (SELECT headline from review_question_translation WHERE review_question.id = review_question_translation.review_question_id AND review_question_translation.language = 'en'), description = (SELECT description from review_question_translation WHERE review_question.id = review_question_translation.review_question_id AND review_question_translation.language = 'en') - """) + """ + ) - op.drop_table('review_question_translation') + op.drop_table("review_question_translation") # ### end Alembic commands ### diff --git a/api/migrations/versions/347a14922cff_ai4d_prc_review_form.py b/api/migrations/versions/347a14922cff_ai4d_prc_review_form.py index b122361df..6f80f1688 100644 --- a/api/migrations/versions/347a14922cff_ai4d_prc_review_form.py +++ b/api/migrations/versions/347a14922cff_ai4d_prc_review_form.py @@ -9,26 +9,27 @@ """ # revision identifiers, used by Alembic. -revision = '347a14922cff' -down_revision = '4b5d67699684' +revision = "347a14922cff" +down_revision = "4b5d67699684" + +import datetime +from enum import Enum -from alembic import op import sqlalchemy as sa -from sqlalchemy import orm +from alembic import op +from sqlalchemy import orm, select from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy import orm -from sqlalchemy import select from sqlalchemy.orm import column_property + from app import db -import datetime -from enum import Enum Base = declarative_base() + class Organisation(Base): __tablename__ = "organisation" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(50), nullable=False) @@ -40,7 +41,17 @@ class Organisation(Base): email_from = db.Column(db.String(100), nullable=True) system_url = db.Column(db.String(100), nullable=False) - def __init__(self, name, system_name, small_logo, large_logo, domain, url, email_from, system_url): + def __init__( + self, + name, + system_name, + small_logo, + large_logo, + domain, + url, + email_from, + system_url, + ): self.name = name self.small_logo = small_logo self.large_logo = large_logo @@ -50,9 +61,10 @@ def __init__(self, name, system_name, small_logo, large_logo, domain, url, email self.email_from = email_from self.system_url = system_url + class Country(Base): __tablename__ = "country" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(100), nullable=False) @@ -62,21 +74,23 @@ def __init__(self, name): class EventType(Enum): - EVENT = 'event' - AWARD = 'award' - CALL = 'call' + EVENT = "event" + AWARD = "award" + CALL = "call" class Event(Base): __tablename__ = "event" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) start_date = db.Column(db.DateTime(), nullable=False) end_date = db.Column(db.DateTime(), nullable=False) key = db.Column(db.String(255), nullable=False, unique=True) - organisation_id = db.Column(db.Integer(), db.ForeignKey('organisation.id'), nullable=False) + organisation_id = db.Column( + db.Integer(), db.ForeignKey("organisation.id"), nullable=False + ) email_from = db.Column(db.String(255), nullable=False) url = db.Column(db.String(255), nullable=False) application_open = db.Column(db.DateTime(), nullable=False) @@ -93,7 +107,7 @@ class Event(Base): travel_grant = db.Column(db.Boolean(), nullable=False) miniconf_url = db.Column(db.String(100), nullable=True) - event_translations = db.relationship('EventTranslation', lazy='dynamic') + event_translations = db.relationship("EventTranslation", lazy="dynamic") def __init__( self, @@ -117,7 +131,7 @@ def __init__( registration_close, event_type, travel_grant, - miniconf_url=None + miniconf_url=None, ): self.start_date = start_date self.end_date = end_date @@ -149,10 +163,11 @@ def add_event_translations(self, names, descriptions): event_translation = EventTranslation(name, description, language) self.event_translations.append(event_translation) + class EventTranslation(Base): __tablename__ = "event_translation" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) event_id = db.Column(db.Integer(), db.ForeignKey("event.id"), nullable=False) @@ -160,19 +175,20 @@ class EventTranslation(Base): description = db.Column(db.String(255), nullable=False) language = db.Column(db.String(2)) - event = db.relationship('Event', foreign_keys=[event_id]) + event = db.relationship("Event", foreign_keys=[event_id]) def __init__(self, name, description, language): self.name = name self.description = description self.language = language + class ApplicationForm(Base): - __tablename__ = 'application_form' - __table_args__ = {'extend_existing': True} + __tablename__ = "application_form" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - event_id = db.Column(db.Integer(), db.ForeignKey('event.id'), nullable=False) + event_id = db.Column(db.Integer(), db.ForeignKey("event.id"), nullable=False) is_open = db.Column(db.Boolean(), nullable=False) nominations = db.Column(db.Boolean(), nullable=False) @@ -181,22 +197,29 @@ def __init__(self, event_id, is_open, nominations): self.is_open = is_open self.nominations = nominations + class Question(Base): - __tablename__ = 'question' - __table_args__ = {'extend_existing': True} + __tablename__ = "question" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) - section_id = db.Column(db.Integer(), db.ForeignKey('section.id'), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) + section_id = db.Column(db.Integer(), db.ForeignKey("section.id"), nullable=False) type = db.Column(db.String(), nullable=False) order = db.Column(db.Integer(), nullable=False) is_required = db.Column(db.Boolean(), nullable=False) - depends_on_question_id = db.Column(db.Integer(), db.ForeignKey('question.id'), nullable=True) + depends_on_question_id = db.Column( + db.Integer(), db.ForeignKey("question.id"), nullable=True + ) key = db.Column(db.String(255), nullable=True) - question_translations = db.relationship('QuestionTranslation', lazy='dynamic') + question_translations = db.relationship("QuestionTranslation", lazy="dynamic") - def __init__(self, application_form_id, section_id, order, questionType, is_required=True): + def __init__( + self, application_form_id, section_id, order, questionType, is_required=True + ): self.application_form_id = application_form_id self.section_id = section_id self.order = order @@ -204,15 +227,18 @@ def __init__(self, application_form_id, section_id, order, questionType, is_requ self.is_required = is_required def get_translation(self, language): - question_translation = self.question_translations.filter_by(language=language).first() + question_translation = self.question_translations.filter_by( + language=language + ).first() return question_translation + class QuestionTranslation(Base): - __tablename__ = 'question_translation' - __table_args__ = {'extend_existing': True} + __tablename__ = "question_translation" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - question_id = db.Column(db.Integer(), db.ForeignKey('question.id'), nullable=False) + question_id = db.Column(db.Integer(), db.ForeignKey("question.id"), nullable=False) language = db.Column(db.String(2), nullable=False) headline = db.Column(db.String(), nullable=False) description = db.Column(db.String(), nullable=True) @@ -222,7 +248,7 @@ class QuestionTranslation(Base): options = db.Column(db.JSON(), nullable=True) show_for_values = db.Column(db.JSON(), nullable=True) - question = db.relationship('Question', foreign_keys=[question_id]) + question = db.relationship("Question", foreign_keys=[question_id]) def __init__( self, @@ -234,7 +260,7 @@ def __init__( validation_regex=None, validation_text=None, options=None, - show_for_values=None + show_for_values=None, ): self.question_id = question_id self.language = language @@ -246,17 +272,22 @@ def __init__( self.options = options self.show_for_values = show_for_values + class ReviewForm(Base): - __tablename__ = 'review_form' - __table_args__ = {'extend_existing': True} + __tablename__ = "review_form" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) is_open = db.Column(db.Boolean(), nullable=False) deadline = db.Column(db.DateTime(), nullable=False) - application_form = db.relationship('ApplicationForm', foreign_keys=[application_form_id]) - review_questions = db.relationship('ReviewQuestion') + application_form = db.relationship( + "ApplicationForm", foreign_keys=[application_form_id] + ) + review_questions = db.relationship("ReviewQuestion") def __init__(self, application_form_id, deadline): self.application_form_id = application_form_id @@ -268,30 +299,26 @@ def close(self): class ReviewQuestion(Base): - __tablename__ = 'review_question' - __table_args__ = {'extend_existing': True} + __tablename__ = "review_question" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer, primary_key=True) - review_form_id = db.Column(db.Integer(), db.ForeignKey('review_form.id'), nullable=False) - question_id = db.Column(db.Integer(), db.ForeignKey('question.id'), nullable=True) - + review_form_id = db.Column( + db.Integer(), db.ForeignKey("review_form.id"), nullable=False + ) + question_id = db.Column(db.Integer(), db.ForeignKey("question.id"), nullable=True) + type = db.Column(db.String(), nullable=False) - + is_required = db.Column(db.Boolean(), nullable=False) order = db.Column(db.Integer(), nullable=False) weight = db.Column(db.Float(), nullable=False) - review_form = db.relationship('ReviewForm', foreign_keys=[review_form_id]) - question = db.relationship('Question', foreign_keys=[question_id]) - - translations = db.relationship('ReviewQuestionTranslation', lazy='dynamic') - - def __init__(self, - review_form_id, - question_id, - type, - is_required, - order, - weight): + review_form = db.relationship("ReviewForm", foreign_keys=[review_form_id]) + question = db.relationship("Question", foreign_keys=[question_id]) + + translations = db.relationship("ReviewQuestionTranslation", lazy="dynamic") + + def __init__(self, review_form_id, question_id, type, is_required, order, weight): self.review_form_id = review_form_id self.question_id = question_id self.type = type @@ -305,12 +332,20 @@ def get_translation(self, language): class ReviewQuestionTranslation(Base): - __tablename__ = 'review_question_translation' - __table_args__ = {'extend_existing': True} - __table_args__ = tuple([db.UniqueConstraint('review_question_id', 'language', name='uq_review_question_id_language')]) + __tablename__ = "review_question_translation" + __table_args__ = {"extend_existing": True} + __table_args__ = tuple( + [ + db.UniqueConstraint( + "review_question_id", "language", name="uq_review_question_id_language" + ) + ] + ) id = db.Column(db.Integer(), primary_key=True) - review_question_id = db.Column(db.Integer(), db.ForeignKey('review_question.id'), nullable=False) + review_question_id = db.Column( + db.Integer(), db.ForeignKey("review_question.id"), nullable=False + ) language = db.Column(db.String(2), nullable=False) description = db.Column(db.String(), nullable=True) @@ -320,15 +355,17 @@ class ReviewQuestionTranslation(Base): validation_regex = db.Column(db.String(), nullable=True) validation_text = db.Column(db.String(), nullable=True) - def __init__(self, - review_question_id, - language, - description=None, - headline=None, - placeholder=None, - options=None, - validation_regex=None, - validation_text=None): + def __init__( + self, + review_question_id, + language, + description=None, + headline=None, + placeholder=None, + options=None, + validation_regex=None, + validation_text=None, + ): self.review_question_id = review_question_id self.language = language self.description = description @@ -338,121 +375,191 @@ def __init__(self, self.validation_regex = validation_regex self.validation_text = validation_text + class ReviewConfiguration(Base): - __tablename__ = 'review_configuration' - __table_args__ = {'extend_existing': True} + __tablename__ = "review_configuration" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - review_form_id = db.Column(db.Integer(), db.ForeignKey('review_form.id'), nullable=False) + review_form_id = db.Column( + db.Integer(), db.ForeignKey("review_form.id"), nullable=False + ) num_reviews_required = db.Column(db.Integer(), nullable=False) num_optional_reviews = db.Column(db.Integer(), nullable=False) - drop_optional_question_id = db.Column(db.Integer(), db.ForeignKey('review_question.id'), nullable=True) + drop_optional_question_id = db.Column( + db.Integer(), db.ForeignKey("review_question.id"), nullable=True + ) drop_optional_agreement_values = db.Column(db.String(), nullable=True) - review_form = db.relationship('ReviewForm', foreign_keys=[review_form_id]) - review_question = db.relationship('ReviewQuestion', foreign_keys=[drop_optional_question_id]) - -def add_question(review_form_id, type, is_required, order, weight=0, question_id=None, - descriptions=None, headlines=None, placeholders=None, options=None, - validation_regexs=None, validation_texts=None): - question = ReviewQuestion(review_form_id, question_id, type, is_required, order, weight) + review_form = db.relationship("ReviewForm", foreign_keys=[review_form_id]) + review_question = db.relationship( + "ReviewQuestion", foreign_keys=[drop_optional_question_id] + ) + + +def add_question( + review_form_id, + type, + is_required, + order, + weight=0, + question_id=None, + descriptions=None, + headlines=None, + placeholders=None, + options=None, + validation_regexs=None, + validation_texts=None, +): + question = ReviewQuestion( + review_form_id, question_id, type, is_required, order, weight + ) db.session.add(question) db.session.commit() en = ReviewQuestionTranslation( - question.id, - 'en', - None if descriptions is None else descriptions['en'], - None if headlines is None else headlines['en'], - None if placeholders is None else placeholders['en'], - None if options is None else options['en'], - None if validation_regexs is None else validation_regexs['en'], - None if validation_texts is None else validation_texts['en']) + question.id, + "en", + None if descriptions is None else descriptions["en"], + None if headlines is None else headlines["en"], + None if placeholders is None else placeholders["en"], + None if options is None else options["en"], + None if validation_regexs is None else validation_regexs["en"], + None if validation_texts is None else validation_texts["en"], + ) fr = ReviewQuestionTranslation( - question.id, - 'fr', - None if descriptions is None else descriptions['fr'], - None if headlines is None else headlines['fr'], - None if placeholders is None else placeholders['fr'], - None if options is None else options['fr'], - None if validation_regexs is None else validation_regexs['fr'], - None if validation_texts is None else validation_texts['fr']) + question.id, + "fr", + None if descriptions is None else descriptions["fr"], + None if headlines is None else headlines["fr"], + None if placeholders is None else placeholders["fr"], + None if options is None else options["fr"], + None if validation_regexs is None else validation_regexs["fr"], + None if validation_texts is None else validation_texts["fr"], + ) db.session.add_all([en, fr]) db.session.commit() return question, en, fr + def get_question(en_headline): - en = db.session.query(QuestionTranslation).filter_by(language='en', headline=en_headline).first() - fr = db.session.query(QuestionTranslation).filter_by(language='fr', question_id=en.question_id).first() + en = ( + db.session.query(QuestionTranslation) + .filter_by(language="en", headline=en_headline) + .first() + ) + fr = ( + db.session.query(QuestionTranslation) + .filter_by(language="fr", question_id=en.question_id) + .first() + ) return en.question, en, fr -def add_evaluation_question(form_id, order, headlines, descriptions, min_score, max_score): - validation_regex = '^({})$'.format('|'.join([str(i) for i in range(max_score, min_score-1, -1)])) + +def add_evaluation_question( + form_id, order, headlines, descriptions, min_score, max_score +): + validation_regex = "^({})$".format( + "|".join([str(i) for i in range(max_score, min_score - 1, -1)]) + ) validation_text = { - 'en': 'Enter a number between {} and {}'.format(min_score, max_score), - 'fr': 'Entrez un nombre entre {} et {}'.format(min_score, max_score) + "en": "Enter a number between {} and {}".format(min_score, max_score), + "fr": "Entrez un nombre entre {} et {}".format(min_score, max_score), } placeholders = { - 'en': 'Enter a score between {} and {}'.format(min_score, max_score), - 'fr': 'Saisissez un score compris entre {} et {}'.format(min_score, max_score) + "en": "Enter a score between {} and {}".format(min_score, max_score), + "fr": "Saisissez un score compris entre {} et {}".format(min_score, max_score), } - return add_question(form_id, 'short-text', True, order, 1, headlines=headlines, - descriptions=descriptions, validation_regexs={'en': validation_regex, 'fr': validation_regex}, - validation_texts=validation_text, placeholders=placeholders) + return add_question( + form_id, + "short-text", + True, + order, + 1, + headlines=headlines, + descriptions=descriptions, + validation_regexs={"en": validation_regex, "fr": validation_regex}, + validation_texts=validation_text, + placeholders=placeholders, + ) -def add_comment_question(form_id, order, headlines, descriptions, is_required=False): - return add_question(form_id, 'long-text', is_required, order, weight=1, descriptions=descriptions, headlines=headlines) -def add_information_question(form_id, order, en_headline, type='information'): +def add_comment_question(form_id, order, headlines, descriptions, is_required=False): + return add_question( + form_id, + "long-text", + is_required, + order, + weight=1, + descriptions=descriptions, + headlines=headlines, + ) + + +def add_information_question(form_id, order, en_headline, type="information"): question, en, fr = get_question(en_headline) - headlines = { - 'en': en.headline, - 'fr': fr.headline - } - return add_question(form_id, type, False, order, question_id=question.id, headlines=headlines) + headlines = {"en": en.headline, "fr": fr.headline} + return add_question( + form_id, type, False, order, question_id=question.id, headlines=headlines + ) + def add_divider(form_id, order, headlines, descriptions): - return add_question(form_id, 'section-divider', False, order, headlines=headlines, descriptions=descriptions) + return add_question( + form_id, + "section-divider", + False, + order, + headlines=headlines, + descriptions=descriptions, + ) + def upgrade(): - print('STARTING MIGRATION 347a14922cff') - event = db.session.query(Event).filter_by(key='prc').first() + print("STARTING MIGRATION 347a14922cff") + event = db.session.query(Event).filter_by(key="prc").first() event.review_open = datetime.date(2020, 11, 7) event.review_close = datetime.date(2020, 11, 30) db.session.commit() - application_form = db.session.query(ApplicationForm).filter_by(event_id=event.id).first() + application_form = ( + db.session.query(ApplicationForm).filter_by(event_id=event.id).first() + ) form = ReviewForm(application_form.id, datetime.date(2020, 11, 30)) db.session.add(form) db.session.commit() # Review configuration - config = ReviewConfiguration(review_form_id=form.id, num_reviews_required=5, num_optional_reviews=0) + config = ReviewConfiguration( + review_form_id=form.id, num_reviews_required=5, num_optional_reviews=0 + ) db.session.add(config) db.session.commit() - print('Added review configuration') + print("Added review configuration") # Set up questions + + # divider1 = add_question(form.id, 'section-divider', False, 1, headlines={ # 'en': 'Evaluation of proposals', # 'fr': u'Évaluation des propositions' # }, descriptions={ -# 'en': """Below are a series of weighted, multiple choice questions intended to evaluate the applicant’s proposal. The evaluation is divided up into the sections from the application: -# I. Organization capacity and experience; -# II. Policy research; -# III. Policy engagement experience; and -# IV. Budget, followed by section -# V. Overall Assessment. +# 'en': """Below are a series of weighted, multiple choice questions intended to evaluate the applicant’s proposal. The evaluation is divided up into the sections from the application: +# I. Organization capacity and experience; +# II. Policy research; +# III. Policy engagement experience; and +# IV. Budget, followed by section +# V. Overall Assessment. # At the end of each question and/or section, please reflect in your own words what you think of the answers in that section. Each evaluation question corresponds with the section and numbered question in the proposal. # """, -# 'fr': u"""Vous trouverez ci-dessous une série de questions à choix multiples pondérées destinées à évaluer la proposition du candidat. L’évaluation est divisée en plusieurs sections à partir de la candidature: -# I. Capacité et expérience de l’organisation; +# 'fr': u"""Vous trouverez ci-dessous une série de questions à choix multiples pondérées destinées à évaluer la proposition du candidat. L’évaluation est divisée en plusieurs sections à partir de la candidature: +# I. Capacité et expérience de l’organisation; # II. Recherche sur les politiques; -# III. Expérience en matière d’engagement politique; et -# IV. Budget, suivi par la section -# V. Évaluation globale. +# III. Expérience en matière d’engagement politique; et +# IV. Budget, suivi par la section +# V. Évaluation globale. # À la fin de chaque question et/ou section, veuillez exprimer dans vos propres mots ce que vous pensez des réponses de cette section. Chaque question d'évaluation correspond à la section et à la question numérotée dans la proposition. # """ # }) @@ -489,7 +596,7 @@ def upgrade(): # 'en': 'Evaluating Question I.1', # 'fr': u'Évaluation de la question I.1' # }, descriptions={ -# 'en': """Assessment of research team: After reviewing the CVs of the principal researchers, assess the strength of the research team with respect to achieving the goals and strategy of the proposed research agenda. +# 'en': """Assessment of research team: After reviewing the CVs of the principal researchers, assess the strength of the research team with respect to achieving the goals and strategy of the proposed research agenda. # \[Total possible points = 10\] # **Scale:** @@ -497,7 +604,7 @@ def upgrade(): # 4 - 7 : Research team is adequate but needs some broad capacity development # 8 - 10 : Research team is strong (although they might still benefit from limited capacity development) # """, -# 'fr': u"""Évaluation de l’équipe de recherche: Après avoir examiné les CV des principaux chercheurs, évaluez la force de l’équipe de recherche en ce qui concerne la réalisation des objectifs et de la stratégie du programme de recherche proposé. +# 'fr': u"""Évaluation de l’équipe de recherche: Après avoir examiné les CV des principaux chercheurs, évaluez la force de l’équipe de recherche en ce qui concerne la réalisation des objectifs et de la stratégie du programme de recherche proposé. # \[Total des points possibles = 10\] # **Scale:** @@ -521,7 +628,7 @@ def upgrade(): # 'en': 'Evaluating Question I.2', # 'fr': u'Évaluation de la question I.2.' # }, descriptions={ -# 'en': """To what extent does this mission statement of the organization relate to advancing the principles of responsible AI? +# 'en': """To what extent does this mission statement of the organization relate to advancing the principles of responsible AI? # \[Total possible points = 15\] # **Scale:** @@ -530,7 +637,7 @@ def upgrade(): # 11 - 15 : Superior demonstration of mission and the significance # """, # 'fr': u"""Dans quelle mesure cette déclaration de mission de l’organisation est-elle liée à la promotion des -# principes de l’IA responsable? +# principes de l’IA responsable? # \[Total des points possibles = 15\] # **Scale:** @@ -555,7 +662,7 @@ def upgrade(): # 'en': 'Evaluating Question I.3', # 'fr': u'Évaluation de la question I.3.' # }, descriptions={ -# 'en': """To what extent does the applicant demonstrate knowledge and expertise with at least three of the key research areas? +# 'en': """To what extent does the applicant demonstrate knowledge and expertise with at least three of the key research areas? # \[Total possible points = 15\] # **Scale:** @@ -563,7 +670,7 @@ def upgrade(): # 6 - 10 : Some demonstrated knowledge and expertise in at least two areas # 11 - 15 : Superior/significant demonstrated knowledge and expertise in three areas # """, -# 'fr': u"""Dans quelle mesure le candidat démontre-t-il des connaissances et une expertise dans au moins trois des principaux domaines de recherche? +# 'fr': u"""Dans quelle mesure le candidat démontre-t-il des connaissances et une expertise dans au moins trois des principaux domaines de recherche? # \[Total des points possibles = 15\] # **Scale:** @@ -577,7 +684,7 @@ def upgrade(): # 'en': 'Evaluating Question I.3', # 'fr': u'Évaluation de la question I.3.' # }, descriptions={ -# 'en': """Please review the examples provided by the applicant from the organization’s research portfolio. Based on what they’ve written and provided, to what extent does the organization demonstrate an ability to produce high quality policy research on these issues? +# 'en': """Please review the examples provided by the applicant from the organization’s research portfolio. Based on what they’ve written and provided, to what extent does the organization demonstrate an ability to produce high quality policy research on these issues? # \[Total possible points = 15\] # High quality research is relevant, shows integrity, legitimacy, importance (originality) and is well positioned for use. Please refer to the [Research Quality Plus](https://www.idrc.ca/en/research-in-action/research-quality-plus) assessment instrument if you have questions. @@ -586,7 +693,7 @@ def upgrade(): # 6 - 10 : Demonstrates variable research quality with some high-quality research # 11 - 15 : Significant experience producing high quality research # """, -# 'fr': u"""Veuillez examiner les exemples fournis par le candidat à partir du portefeuille de recherche de l’organisme. Sur la base de ce qu’ils ont écrit et fourni, dans quelle mesure l’organisme démontre-t-il sa capacité à produire des recherches politiques de haute qualité sur ces questions? +# 'fr': u"""Veuillez examiner les exemples fournis par le candidat à partir du portefeuille de recherche de l’organisme. Sur la base de ce qu’ils ont écrit et fourni, dans quelle mesure l’organisme démontre-t-il sa capacité à produire des recherches politiques de haute qualité sur ces questions? # \[Total des points possibles = 15\] # Une recherche de haute qualité est pertinente, fait preuve d’intégrité, de légitimité, d’importance (originalité) et est bien placée pour être utilisée. Veuillez-vous référer à l’instrument d’évaluation [Qualité de la recherche plus](https://idl-bnc-idrc.dspacedirect.org/bitstream/handle/10625/56600/IDL-56600.pdf?sequence=2&isAllowed=y) si vous avez des questions. @@ -597,7 +704,7 @@ def upgrade(): # 11 - 15 : Une expérience significative dans la production de recherches de haute qualité # """ # }, min_score=0, max_score=15) - + # add_comment_question(form.id, 15, headlines={ # 'en': 'Comments?', # 'fr': u'Des commentaires?' @@ -649,7 +756,7 @@ def upgrade(): # 'en': 'Evaluating Question II.1', # 'fr': u'Évaluation de la question II.1.' # }, descriptions={ -# 'en': """To what extent does the applicant clearly articulate why these priorities are significant for the AI4D research agenda of the organization and the contexts in which they work? +# 'en': """To what extent does the applicant clearly articulate why these priorities are significant for the AI4D research agenda of the organization and the contexts in which they work? # \[Total possible points = 10\] # **Scale:** @@ -657,7 +764,7 @@ def upgrade(): # 4 - 7 : Substantive clarity of research priorities # 8 - 10 : Superior/significant clarity of research priorities # """, -# 'fr': u"""Dans quelle mesure le candidat explique-t-il clairement pourquoi ces priorités sont importantes pour l’agenda de recherche IAPD de l’organisation et les contextes dans lesquels elles s’inscrivent? +# 'fr': u"""Dans quelle mesure le candidat explique-t-il clairement pourquoi ces priorités sont importantes pour l’agenda de recherche IAPD de l’organisation et les contextes dans lesquels elles s’inscrivent? # \[Total des points possibles = 10\] # **Scale:** @@ -671,7 +778,7 @@ def upgrade(): # 'en': 'Evaluating Question II.1', # 'fr': u'Évaluation de la question II.1.' # }, descriptions={ -# 'en': """To what extent does the applicant demonstrate the ability to draw upon evidence to support the background, context and arguments for why these are the priorities for their research? +# 'en': """To what extent does the applicant demonstrate the ability to draw upon evidence to support the background, context and arguments for why these are the priorities for their research? # \[Total possible points = 10\] # **Scale:** @@ -679,7 +786,7 @@ def upgrade(): # 4 - 7 : Substantive demonstrated ability # 8 - 10 : Superior/significant demonstrated ability # """, -# 'fr': u"""Dans quelle mesure le candidat démontre-t-il sa capacité à s’appuyer sur des preuves pour étayer le contexte et les arguments qui expliquent pourquoi il s’agit là des priorités de sa recherche? +# 'fr': u"""Dans quelle mesure le candidat démontre-t-il sa capacité à s’appuyer sur des preuves pour étayer le contexte et les arguments qui expliquent pourquoi il s’agit là des priorités de sa recherche? # \[Total des points possibles = 10\] # **Scale:** @@ -693,7 +800,7 @@ def upgrade(): # 'en': 'Evaluating Question II.1', # 'fr': u'Évaluation de la question II.1.' # }, descriptions={ -# 'en': """To what extent does the applicant demonstrate the ability to write on complex issues succinctly and intelligently? +# 'en': """To what extent does the applicant demonstrate the ability to write on complex issues succinctly and intelligently? # \[Total possible points = 5\] # **Scale:** @@ -701,10 +808,10 @@ def upgrade(): # 2 - 3 : Some ability to write on complex issues (some good ideas, structure may lack coherence) # 4 - 5 : Superior/significant ability to write on complex issues (exceptionally clear and coherent) # """, -# 'fr': u"""Dans quelle mesure le candidat démontre-t-il sa capacité à écrire sur des questions complexes de manière succincte et intelligente? +# 'fr': u"""Dans quelle mesure le candidat démontre-t-il sa capacité à écrire sur des questions complexes de manière succincte et intelligente? # \[Total des points possibles = 5\] -# Scale: +# Scale: # 0 - 1 : Capacité limitée à écrire sur des sujets complexes (confus, manque de logique et de cohérence) # 2 - 3 : Une certaine capacité à écrire sur des questions complexes (quelques bonnes idées, la structure peut manquer de cohérence) # 4 - 5 : Capacité supérieure à rédiger sur des questions complexes (exceptionnellement clair et cohérent) @@ -725,7 +832,7 @@ def upgrade(): # 'en': 'Evaluating Question II.2', # 'fr': u'Évaluation de la question II.2.' # }, descriptions={ -# 'en': """To what extent are the general and specific objectives appropriate and logically connected to problem statement and justification? Are they feasible? +# 'en': """To what extent are the general and specific objectives appropriate and logically connected to problem statement and justification? Are they feasible? # \[Total possible points = 10\] # **Scale:** @@ -733,10 +840,10 @@ def upgrade(): # 4 - 7 : Objectives need improvement, but are relatively appropriate and connected to problem statement # 8 - 10 : Objectives are coherent, flow logically, and are appropriate and feasible # """, -# 'fr': u"""Dans quelle mesure les objectifs généraux et spécifiques sont-ils appropriés et logiquement liés à l’énoncé et à la justification du problème? Sont-ils réalisables? +# 'fr': u"""Dans quelle mesure les objectifs généraux et spécifiques sont-ils appropriés et logiquement liés à l’énoncé et à la justification du problème? Sont-ils réalisables? # \[Total des points possibles = 10\] -# **Scale:** +# **Scale:** # 0 - 3 : Mal articulé et sans lien avec l’énoncé du problème, objectifs inappropriés et/ou irréalisables # 4 - 7 : Les objectifs doivent être améliorés, mais ils sont relativement appropriés et liés à l’énoncé du problème # 8 - 10 : Les objectifs sont cohérents et logiques, et sont appropriés et réalisables @@ -757,7 +864,7 @@ def upgrade(): # 'en': 'Evaluating Question II.3: approach to research', # 'fr': u'Évaluation de la sous-question II.3: Approche de la recherche' # }, descriptions={ -# 'en': """To what extent does the organization demonstrate a coherent approach to achieving their research objectives? +# 'en': """To what extent does the organization demonstrate a coherent approach to achieving their research objectives? # \[Total possible points = 10\] # **Scale:** @@ -765,7 +872,7 @@ def upgrade(): # 4 - 7 : Some demonstrated coherence # 8 - 10 : Significant demonstrated coherence # """, -# 'fr': u"""Dans quelle mesure l’organisme fait-il preuve d’une approche cohérente pour atteindre ses objectifs de recherche? +# 'fr': u"""Dans quelle mesure l’organisme fait-il preuve d’une approche cohérente pour atteindre ses objectifs de recherche? # \[Total des points possibles = 10\] # **Scale:** @@ -786,7 +893,7 @@ def upgrade(): # 'en': 'Evaluating Question II.3: policy research', # 'fr': u'Évaluation de la sous-question II.3: Recherche sur les politiques' # }, descriptions={ -# 'en': """To what extent is the conceptual and theoretical framework appropriate to the research question at hand? How well is the framework linked to the proposed research design? +# 'en': """To what extent is the conceptual and theoretical framework appropriate to the research question at hand? How well is the framework linked to the proposed research design? # \[Total possible points = 10\] # **Scale:** @@ -794,7 +901,7 @@ def upgrade(): # 4 - 7 : Some demonstrated strategy in linking framework to research design # 8 - 10 : Significant demonstrated strategy in linking framework to research design # """, -# 'fr': u"""Dans quelle mesure le cadre conceptuel et théorique est-il adapté à la question de recherche en question? Dans quelle mesure le cadre est-il lié à la conception de la recherche proposée? +# 'fr': u"""Dans quelle mesure le cadre conceptuel et théorique est-il adapté à la question de recherche en question? Dans quelle mesure le cadre est-il lié à la conception de la recherche proposée? # \[Total des points possibles = 10\] # **Scale:** @@ -813,7 +920,7 @@ def upgrade(): # 'en': 'Evaluating Question II.3: policy research', # 'fr': u'Évaluation de la sous-question II.3: Recherche sur les politiques' # }, descriptions={ -# 'en': """To what extent does the organization demonstrate a rigorous approach to answering their research questions? +# 'en': """To what extent does the organization demonstrate a rigorous approach to answering their research questions? # \[Total possible points = 10\] # **Scale:** @@ -821,7 +928,7 @@ def upgrade(): # 4 - 7 : Some demonstrated rigor # 8 - 10 : Significant/superior demonstrated rigor # """, -# 'fr': u"""Dans quelle mesure l’organisation fait-elle preuve de rigueur dans ses réponses aux questions de recherche? +# 'fr': u"""Dans quelle mesure l’organisation fait-elle preuve de rigueur dans ses réponses aux questions de recherche? # \[Total des points possibles = 10\] # **Scale:** @@ -843,7 +950,7 @@ def upgrade(): # 'en': 'Evaluating Question II.3: gender and inclusion', # 'fr': u'Évaluation de la sous-question II.3: Genre et inclusion' # }, descriptions={ -# 'en': """To what extent does the organization demonstrate in-depth knowledge of gender-related dimensions of AI4D? +# 'en': """To what extent does the organization demonstrate in-depth knowledge of gender-related dimensions of AI4D? # \[Total possible points = 10\] # **Scale:** @@ -851,7 +958,7 @@ def upgrade(): # 4 - 7 : Substantive demonstrated ability with gender related issues # 8 - 10 : Superior/significant demonstrated ability with gender related issues # """, -# 'fr': u"""Dans quelle mesure l’organisation fait-elle preuve d’une connaissance approfondie des dimensions de genre d’IAPD? +# 'fr': u"""Dans quelle mesure l’organisation fait-elle preuve d’une connaissance approfondie des dimensions de genre d’IAPD? # \[Total des points possibles = 10\] # **Scale:** @@ -865,7 +972,7 @@ def upgrade(): # 'en': 'Evaluating Question II.3: gender and inclusion', # 'fr': u'Évaluation de la sous-question II.3: Genre et inclusion' # }, descriptions={ -# 'en': """To what extent does the organization demonstrate in-depth knowledge of other inclusion-related dimensions of AI4D, such as socioeconomics, cultural/ethnic and broader social inclusion issues? +# 'en': """To what extent does the organization demonstrate in-depth knowledge of other inclusion-related dimensions of AI4D, such as socioeconomics, cultural/ethnic and broader social inclusion issues? # \[Total possible points = 10\] # **Scale:** @@ -873,7 +980,7 @@ def upgrade(): # 4 - 7 : Substantive demonstrated understanding of inclusion-related challenges # 8 - 10 : Significant demonstrated understanding of inclusion-related challenges # """, -# 'fr': u"""Dans quelle mesure l’organisation démontre-t-elle une connaissance approfondie d’autres dimensions de l’IAPD liées à l’inclusion, telles que la socio-économie, la culture/ethnie et les questions plus larges d’inclusion sociale? +# 'fr': u"""Dans quelle mesure l’organisation démontre-t-elle une connaissance approfondie d’autres dimensions de l’IAPD liées à l’inclusion, telles que la socio-économie, la culture/ethnie et les questions plus larges d’inclusion sociale? # \[Total des points possibles = 10\] # **Scale:** @@ -887,7 +994,7 @@ def upgrade(): # 'en': 'Evaluating Question II.3: gender and inclusion', # 'fr': u'Évaluation de la sous-question II.3: Genre et inclusion' # }, descriptions={ -# 'en': """Considering the answer on gender and inclusion and the examples provided by the applicant, to what extent does the organization demonstrate in-depth knowledge of gender and inclusion-related issues/challenges in AI and advanced technologies? +# 'en': """Considering the answer on gender and inclusion and the examples provided by the applicant, to what extent does the organization demonstrate in-depth knowledge of gender and inclusion-related issues/challenges in AI and advanced technologies? # \[Total possible points = 5\] # **Scale:** @@ -895,7 +1002,7 @@ def upgrade(): # 2 - 3 : Some knowledge of gender and inclusion-related issues # 4 - 5 : Superior/significant knowledge of gender and inclusion-related issues # """, -# 'fr': u"""Compte tenu de la réponse sur le genre et l’inclusion et des exemples fournis par le candidat, dans quelle mesure l’organisation démontre-t-elle une connaissance approfondie des questions et des défis liés au genre et à l’inclusion dans le domaine de l’IA et des technologies avancées? +# 'fr': u"""Compte tenu de la réponse sur le genre et l’inclusion et des exemples fournis par le candidat, dans quelle mesure l’organisation démontre-t-elle une connaissance approfondie des questions et des défis liés au genre et à l’inclusion dans le domaine de l’IA et des technologies avancées? # \[Total des points possibles = 5\] # **Scale:** @@ -916,7 +1023,7 @@ def upgrade(): # 'en': 'Evaluating Question II.3: ethics', # 'fr': u'Évaluation de la sous-question II.3: Éthique' # }, descriptions={ -# 'en': """Please assess the applicant’s ethics review process for their research. Does the applicant outline a clear and independent process for reviewing and ensuring the privacy, dignity, and integrity of individuals who are the subjects of their research? +# 'en': """Please assess the applicant’s ethics review process for their research. Does the applicant outline a clear and independent process for reviewing and ensuring the privacy, dignity, and integrity of individuals who are the subjects of their research? # \[Total possible points = 5\] # **Scale:** @@ -924,7 +1031,7 @@ def upgrade(): # 2 - 3 : Some research ethics protocols # 4 - 5 : Significant research ethics protocols # """, -# 'fr': u"""Veuillez évaluer le processus d’évaluation éthique du candidat pour sa recherche. Le candidat décrit-il un processus clair et indépendant pour évaluer et garantir la vie privée, la dignité et l’ntégrité des personnes qui font l’objet de ses recherches? +# 'fr': u"""Veuillez évaluer le processus d’évaluation éthique du candidat pour sa recherche. Le candidat décrit-il un processus clair et indépendant pour évaluer et garantir la vie privée, la dignité et l’ntégrité des personnes qui font l’objet de ses recherches? # \[Total des points possibles = 5\] # **Scale:** @@ -945,7 +1052,7 @@ def upgrade(): # 'en': 'Evaluating Question II.4', # 'fr': u'Évaluation de la question II.4.' # }, descriptions={ -# 'en': """To what extent does the applicant clearly identify where they need the most capacity building to improve work and outcomes to advance responsible AI in their country/region - and offer a coherent, reasonable strategy for addressing the gap? +# 'en': """To what extent does the applicant clearly identify where they need the most capacity building to improve work and outcomes to advance responsible AI in their country/region - and offer a coherent, reasonable strategy for addressing the gap? # \[Total possible points = 10\] # **Scale:** @@ -974,7 +1081,7 @@ def upgrade(): # 'en': 'Evaluating Question II.5', # 'fr': u'Évaluation de la question II.5.' # }, descriptions={ -# 'en': """Please review the applicant's description of their research communications strategy. To what extent does the organization demonstrate an ability or approach to communicate their research well to influence policy? +# 'en': """Please review the applicant's description of their research communications strategy. To what extent does the organization demonstrate an ability or approach to communicate their research well to influence policy? # \[Total possible points = 10\] # **Scale:** @@ -982,7 +1089,7 @@ def upgrade(): # 4 - 7 : Some demonstrated ability to communicate research # 8 - 10 : Significant/superior demonstrated ability to communicate research # """, -# 'fr': u"""Veuillez examiner la description par le candidat de sa stratégie de communication sur la recherche. Dans quelle mesure l’organisme démontre-t-il une capacité ou une approche pour bien communiquer sur sa recherche afin d’influencer les politiques? +# 'fr': u"""Veuillez examiner la description par le candidat de sa stratégie de communication sur la recherche. Dans quelle mesure l’organisme démontre-t-il une capacité ou une approche pour bien communiquer sur sa recherche afin d’influencer les politiques? # \[Total des points possibles = 10\] # **Scale:** @@ -1003,7 +1110,7 @@ def upgrade(): # 'en': 'Evaluating Question II.6', # 'fr': u'Évaluation de la question II.6.' # }, descriptions={ -# 'en': """To what extent are the proposed markers for measuring success for the organization in the context of this project specific, appropriate, achievable, timely and measurable? +# 'en': """To what extent are the proposed markers for measuring success for the organization in the context of this project specific, appropriate, achievable, timely and measurable? # \[Total possible points = 15\] # **Scale:** @@ -1011,7 +1118,7 @@ def upgrade(): # 6 - 10 : Some articulation of success, markers may lack clarity, specificity, appropriateness, feasibility, or measurability # 11- 15 : Clear vision of success matched with strong, measurable markers # """, -# 'fr': u"""Dans quelle mesure les marqueurs proposés pour mesurer le succès de l’organisation dans le cadre de ce projet sont-ils spécifiques, appropriés, réalisables, opportuns et mesurables? +# 'fr': u"""Dans quelle mesure les marqueurs proposés pour mesurer le succès de l’organisation dans le cadre de ce projet sont-ils spécifiques, appropriés, réalisables, opportuns et mesurables? # \[Total des points possibles = 15\] # **Scale:** @@ -1032,7 +1139,7 @@ def upgrade(): # 'en': 'Evaluating Question II.7', # 'fr': u'Évaluation de la question II.7.' # }, descriptions={ -# 'en': """To what extent does the applicant demonstrate a clear and realistic project schedule for achieving their objectives with outputs and milestones as they relate to the budget? +# 'en': """To what extent does the applicant demonstrate a clear and realistic project schedule for achieving their objectives with outputs and milestones as they relate to the budget? # \[Total possible points = 5\] # **Scale:** @@ -1040,10 +1147,10 @@ def upgrade(): # 2 - 3 : Some demonstration of how to achieve project objectives # 4 - 5 : Superior/significant demonstration of how to achieve project objectives # """, -# 'fr': u"""Dans quelle mesure le demandeur démontre-t-il un calendrier de projet clair et réaliste pour atteindre ses objectifs, avec des résultats et des étapes en rapport avec le budget? +# 'fr': u"""Dans quelle mesure le demandeur démontre-t-il un calendrier de projet clair et réaliste pour atteindre ses objectifs, avec des résultats et des étapes en rapport avec le budget? # \[Total des points possibles = 5\] -# Scale: +# Scale: # 0 - 1 : Démonstration limitée de la manière d’atteindre les objectifs du projet # 2 - 3 : Quelques démonstrations de la manière d’atteindre les objectifs du projet # 4 - 5 : Démonstration supérieure/significative de la manière d’atteindre les objectifs du projet @@ -1101,16 +1208,16 @@ def upgrade(): # 'en': 'Evaluating Question III.1', # 'fr': u'Évaluation de la question III.1.' # }, descriptions={ -# 'en': """To what extent does the organization demonstrate quality engagement with national governing institutions (e.g. government departments, bodies and agencies, as well as regulators, and other public-sector institutions) on advanced technology issues? \[Total possible points = 15\] +# 'en': """To what extent does the organization demonstrate quality engagement with national governing institutions (e.g. government departments, bodies and agencies, as well as regulators, and other public-sector institutions) on advanced technology issues? \[Total possible points = 15\] # *Note: For the purposes of this initiative, having national policy influence is critical.* # Applicant receives and responds to requests for input from governments/other regulatory institutions. # **Scale:** -# 0 - 5 : Limited quality engagement +# 0 - 5 : Limited quality engagement # 6 - 10 : Some quality engagement # 11- 15 : Significant quality engagement # """, -# 'fr': u"""Dans quelle mesure l’organisation fait-elle preuve d’un engagement de qualité avec les institutions nationales de gouvernance (par exemple, les ministères, les organismes et les agences gouvernementales, ainsi que les régulateurs et autres institutions du secteur public) sur les questions de technologie avancée? +# 'fr': u"""Dans quelle mesure l’organisation fait-elle preuve d’un engagement de qualité avec les institutions nationales de gouvernance (par exemple, les ministères, les organismes et les agences gouvernementales, ainsi que les régulateurs et autres institutions du secteur public) sur les questions de technologie avancée? # \[Total des points possibles = 15\] # *Note: Pour les besoins de cette initiative, il est essentiel d'avoir une influence sur les politiques nationales.* @@ -1126,16 +1233,16 @@ def upgrade(): # 'en': 'Evaluating Question III.1', # 'fr': u'Évaluation de la question III.1.' # }, descriptions={ -# 'en': """To what extent does the organization demonstrate quality engagement with national governing institutions (e.g. government departments, bodies and agencies, as well as regulators, and other public-sector institutions) on advanced technology issues? \[Total possible points = 15\] +# 'en': """To what extent does the organization demonstrate quality engagement with national governing institutions (e.g. government departments, bodies and agencies, as well as regulators, and other public-sector institutions) on advanced technology issues? \[Total possible points = 15\] # *Note: For the purposes of this initiative, having national policy influence is critical.* # Applicant proactively engages with policy makers (such as on steering committees, or on emerging policy issues). # **Scale:** -# 0 - 5 : Limited quality engagement +# 0 - 5 : Limited quality engagement # 6 - 10 : Some quality engagement # 11- 15 : Significant quality engagement # """, -# 'fr': u"""Dans quelle mesure l’organisation fait-elle preuve d’un engagement de qualité avec les institutions nationales de gouvernance (par exemple, les ministères, les organismes et les agences gouvernementales, ainsi que les régulateurs et autres institutions du secteur public) sur les questions de technologie avancée? +# 'fr': u"""Dans quelle mesure l’organisation fait-elle preuve d’un engagement de qualité avec les institutions nationales de gouvernance (par exemple, les ministères, les organismes et les agences gouvernementales, ainsi que les régulateurs et autres institutions du secteur public) sur les questions de technologie avancée? # \[Total des points possibles = 15\] # *Note: Pour les besoins de cette initiative, il est essentiel d'avoir une influence sur les politiques nationales.* @@ -1151,7 +1258,7 @@ def upgrade(): # 'en': 'Evaluating Question III.1', # 'fr': u'Évaluation de la question III.1.' # }, descriptions={ -# 'en': """Please review the three examples of policy engagement. To what extent does the applicant demonstrate an ability to engage with policy makers on substantive/technical policy issues? +# 'en': """Please review the three examples of policy engagement. To what extent does the applicant demonstrate an ability to engage with policy makers on substantive/technical policy issues? # \[Total possible points = 15\] # **Scale:** @@ -1159,7 +1266,7 @@ def upgrade(): # 6 - 10 : Some demonstrated ability to engage with policy makers on substantive/technical policy issues # 11- 15 : Significant/superior demonstrated ability to engage with policy makers on substantive/technical policy issues # """, -# 'fr': u"""Veuillez examiner les trois exemples d’engagement politique. Dans quelle mesure le candidat démontre-t-il une capacité à s’engager auprès des décideurs politiques sur des questions politiques de fond/techniques? +# 'fr': u"""Veuillez examiner les trois exemples d’engagement politique. Dans quelle mesure le candidat démontre-t-il une capacité à s’engager auprès des décideurs politiques sur des questions politiques de fond/techniques? # \[Total des points possibles = 15\] # **Scale:** @@ -1180,7 +1287,7 @@ def upgrade(): # 'en': 'Evaluating Question III.2', # 'fr': u'Évaluation de la question III.2.' # }, descriptions={ -# 'en': """To what extent does the applicant demonstrate an ability to inform public debate and policy processes nationally or regionally? +# 'en': """To what extent does the applicant demonstrate an ability to inform public debate and policy processes nationally or regionally? # \[Total possible points = 15\] # **Scale:** @@ -1188,7 +1295,7 @@ def upgrade(): # 6 - 10 : Some demonstrated ability # 11- 15 : Significant demonstrated ability # """, -# 'fr': u"""Dans quelle mesure le candidat démontre-t-il une capacité à informer le débat public et les processus politiques au niveau national ou régional? +# 'fr': u"""Dans quelle mesure le candidat démontre-t-il une capacité à informer le débat public et les processus politiques au niveau national ou régional? # \[Total des points possibles = 15\] # **Scale:** @@ -1205,15 +1312,15 @@ def upgrade(): # 'en': 'Evaluating Question III.3', # 'fr': u'Évaluation de la question III.3' # }, descriptions={ -# 'en': """To what extent does the organization demonstrate clear regional/international engagement on AI/advanced technology policy issues with other governments, public sector institutions, bi-lateral, multilateral or international institutions/organizations or multi stakeholder fora? +# 'en': """To what extent does the organization demonstrate clear regional/international engagement on AI/advanced technology policy issues with other governments, public sector institutions, bi-lateral, multilateral or international institutions/organizations or multi stakeholder fora? # \[Total possible points = 15\] -# Scale: +# Scale: # 0 - 5 : Limited regional/international engagement # 6 - 10 : Substantive regional/international engagement # 11- 15 : Significant regional/international engagement # """, -# 'fr': u"""Dans quelle mesure l’organisation fait-elle preuve d’un engagement régional/international clair sur les questions de politique technologique avancée et d’IA avec d’autres gouvernements, des institutions du secteur public, des institutions/organisations bilatérales, multilatérales ou internationales ou des forums multipartites? +# 'fr': u"""Dans quelle mesure l’organisation fait-elle preuve d’un engagement régional/international clair sur les questions de politique technologique avancée et d’IA avec d’autres gouvernements, des institutions du secteur public, des institutions/organisations bilatérales, multilatérales ou internationales ou des forums multipartites? # \[Total des points possibles = 15\] # **Scale:** @@ -1262,7 +1369,7 @@ def upgrade(): # 'fr': u'Évaluation du budget' # }, descriptions={ # 'en': """Please review the applicant’s budget and consider the coherence and clarity, the appropriateness of budget lines for proposed activities and the budget’s alignment with the research plan. -# In terms of overall coherence, appropriateness to research activities and fiscal management, please rate the applicant’s budget. +# In terms of overall coherence, appropriateness to research activities and fiscal management, please rate the applicant’s budget. # \[Total possible points = 30\] # **Scale:** @@ -1271,7 +1378,7 @@ def upgrade(): # 21 - 30 : Budget demonstrates superior coherence and appropriateness for research and policy impact # """, # 'fr': u"""Veuillez examiner le budget du candidat et considérer la cohérence et la clarté, la pertinence des lignes budgétaires pour les activités proposées et l’alignement du budget sur le plan de recherche. -# En termes de cohérence globale, d’adéquation aux activités de recherche et de gestion budgétaire, veuillez évaluer le budget du candidat. +# En termes de cohérence globale, d’adéquation aux activités de recherche et de gestion budgétaire, veuillez évaluer le budget du candidat. # \[Total des points possibles = 30\] # **Scale:** @@ -1288,7 +1395,7 @@ def upgrade(): # 'en': """IDRC’s grants administration team will be reviewing all of the budgets independently, and their assessment will be discussed during the final selection process. However, given the significance of budgets in terms of institutional strength, research planning and strategy, and risk mitigation, please provide feedback on the applicant’s proposed budget, and offer any input or insights you may have regarding the soundness of the applicant’s budget in relation to their research objectives and overall plan.""", # 'fr': u"""L’équipe d’administration des subventions du CRDI examinera tous les budgets de manière indépendante et leur évaluation sera discutée lors du processus de sélection final. Toutefois, étant donné l’importance des budgets en termes de force institutionnelle, de planification et de stratégie de recherche et d’atténuation des risques, veuillez donner votre avis sur le budget proposé par le candidat et faire part de vos commentaires ou de vos idées concernant""" # }) - + # print('Added section 4') # add_divider(form.id, 74, headlines={ @@ -1305,7 +1412,7 @@ def upgrade(): # 'en': 'Overall quality of application', # 'fr': u'Qualité générale de la demande' # }, descriptions={ -# 'en': """Is the overall application well-articulated and does it demonstrate clarity of purpose and potential impact for AI4D? +# 'en': """Is the overall application well-articulated and does it demonstrate clarity of purpose and potential impact for AI4D? # \[Total possible points = 10\] # **Scale:** @@ -1313,7 +1420,7 @@ def upgrade(): # 4 - 7 : Somewhat well-articulated and clear # 8 - 10 : Very well-articulated and clear # """, -# 'fr': u"""La demande globale est-elle bien articulée et démontre-t-elle la clarté de l’objectif et l’impact potentiel pour IAPD? +# 'fr': u"""La demande globale est-elle bien articulée et démontre-t-elle la clarté de l’objectif et l’impact potentiel pour IAPD? # \[Total des points possibles = 10\] # **Scale:** @@ -1327,7 +1434,7 @@ def upgrade(): # 'en': 'Overall quality of application', # 'fr': u'Qualité générale de la demande' # }, descriptions={ -# 'en': """Does the proposal demonstrate in-depth, well-rounded, and multi-disciplinary knowledge of AI and advanced technology policy themes and issues? +# 'en': """Does the proposal demonstrate in-depth, well-rounded, and multi-disciplinary knowledge of AI and advanced technology policy themes and issues? # \[Total possible points = 10\] # **Scale:** @@ -1335,7 +1442,7 @@ def upgrade(): # 4 - 7 : Some multi-disciplinary knowledge # 8 - 10 : Significant multi-disciplinary knowledge # """, -# 'fr': u"""La proposition démontre-t-elle une connaissance approfondie, bien équilibrée et pluridisciplinaire des thèmes et des questions de politique en matière d’IA et de technologies avancées? +# 'fr': u"""La proposition démontre-t-elle une connaissance approfondie, bien équilibrée et pluridisciplinaire des thèmes et des questions de politique en matière d’IA et de technologies avancées? # \[Total des points possibles = 10\] # **Scale:** @@ -1349,7 +1456,7 @@ def upgrade(): # 'en': 'Overall quality of application', # 'fr': u'Qualité générale de la demande' # }, descriptions={ -# 'en': """Does the proposal demonstrate a clear strategy of how the organization will benefit from the support offered by this initiative and add to debates on the AI and advanced technology policy themes in the region? +# 'en': """Does the proposal demonstrate a clear strategy of how the organization will benefit from the support offered by this initiative and add to debates on the AI and advanced technology policy themes in the region? # \[Total possible points = 5\] # **Scale:** @@ -1357,7 +1464,7 @@ def upgrade(): # 2 - 3 : Some # 4 - 5 : Significant # """, -# 'fr': u"""La proposition présente-t-elle une stratégie claire quant à la manière dont l’organisation bénéficiera du soutien offert par cette initiative et contribue-t-elle aux débats sur les thèmes de l’IA et de la politique en matière de technologies avancées dans la région? +# 'fr': u"""La proposition présente-t-elle une stratégie claire quant à la manière dont l’organisation bénéficiera du soutien offert par cette initiative et contribue-t-elle aux débats sur les thèmes de l’IA et de la politique en matière de technologies avancées dans la région? # \[Total des points possibles = 5\] # **Scale:** @@ -1377,19 +1484,28 @@ def upgrade(): # print('Added section 5') # print('FINISHED MIGRATION 347a14922cff') - + # db.session.commit() # db.session.flush() + def downgrade(): - event = db.session.query(Event).filter_by(key='prc').first() - application_form = db.session.query(ApplicationForm).filter_by(event_id=event.id).first() - form = db.session.query(ReviewForm).filter_by(application_form_id=application_form.id).first() + event = db.session.query(Event).filter_by(key="prc").first() + application_form = ( + db.session.query(ApplicationForm).filter_by(event_id=event.id).first() + ) + form = ( + db.session.query(ReviewForm) + .filter_by(application_form_id=application_form.id) + .first() + ) questions = db.session.query(ReviewQuestion).filter_by(review_form_id=form.id).all() for q in questions: - db.session.query(ReviewQuestionTranslation).filter_by(review_question_id=q.id).delete() + db.session.query(ReviewQuestionTranslation).filter_by( + review_question_id=q.id + ).delete() db.session.commit() db.session.query(ReviewQuestion).filter_by(review_form_id=form.id).delete() diff --git a/api/migrations/versions/34a5a1fce672_ai4d_prc_review_updates.py b/api/migrations/versions/34a5a1fce672_ai4d_prc_review_updates.py index d15bff9fd..803cd2c61 100644 --- a/api/migrations/versions/34a5a1fce672_ai4d_prc_review_updates.py +++ b/api/migrations/versions/34a5a1fce672_ai4d_prc_review_updates.py @@ -9,26 +9,27 @@ """ # revision identifiers, used by Alembic. -revision = '34a5a1fce672' -down_revision = 'e1f99a034c22' +revision = "34a5a1fce672" +down_revision = "e1f99a034c22" + +import datetime +from enum import Enum -from alembic import op import sqlalchemy as sa -from sqlalchemy import orm +from alembic import op +from sqlalchemy import orm, select from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy import orm -from sqlalchemy import select from sqlalchemy.orm import column_property + from app import db -import datetime -from enum import Enum Base = declarative_base() + class Organisation(Base): __tablename__ = "organisation" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(50), nullable=False) @@ -40,7 +41,17 @@ class Organisation(Base): email_from = db.Column(db.String(100), nullable=True) system_url = db.Column(db.String(100), nullable=False) - def __init__(self, name, system_name, small_logo, large_logo, domain, url, email_from, system_url): + def __init__( + self, + name, + system_name, + small_logo, + large_logo, + domain, + url, + email_from, + system_url, + ): self.name = name self.small_logo = small_logo self.large_logo = large_logo @@ -50,9 +61,10 @@ def __init__(self, name, system_name, small_logo, large_logo, domain, url, email self.email_from = email_from self.system_url = system_url + class Country(Base): __tablename__ = "country" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(100), nullable=False) @@ -62,21 +74,23 @@ def __init__(self, name): class EventType(Enum): - EVENT = 'event' - AWARD = 'award' - CALL = 'call' + EVENT = "event" + AWARD = "award" + CALL = "call" class Event(Base): __tablename__ = "event" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) start_date = db.Column(db.DateTime(), nullable=False) end_date = db.Column(db.DateTime(), nullable=False) key = db.Column(db.String(255), nullable=False, unique=True) - organisation_id = db.Column(db.Integer(), db.ForeignKey('organisation.id'), nullable=False) + organisation_id = db.Column( + db.Integer(), db.ForeignKey("organisation.id"), nullable=False + ) email_from = db.Column(db.String(255), nullable=False) url = db.Column(db.String(255), nullable=False) application_open = db.Column(db.DateTime(), nullable=False) @@ -93,7 +107,7 @@ class Event(Base): travel_grant = db.Column(db.Boolean(), nullable=False) miniconf_url = db.Column(db.String(100), nullable=True) - event_translations = db.relationship('EventTranslation', lazy='dynamic') + event_translations = db.relationship("EventTranslation", lazy="dynamic") def __init__( self, @@ -117,7 +131,7 @@ def __init__( registration_close, event_type, travel_grant, - miniconf_url=None + miniconf_url=None, ): self.start_date = start_date self.end_date = end_date @@ -149,10 +163,11 @@ def add_event_translations(self, names, descriptions): event_translation = EventTranslation(name, description, language) self.event_translations.append(event_translation) + class EventTranslation(Base): __tablename__ = "event_translation" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) event_id = db.Column(db.Integer(), db.ForeignKey("event.id"), nullable=False) @@ -160,19 +175,20 @@ class EventTranslation(Base): description = db.Column(db.String(255), nullable=False) language = db.Column(db.String(2)) - event = db.relationship('Event', foreign_keys=[event_id]) + event = db.relationship("Event", foreign_keys=[event_id]) def __init__(self, name, description, language): self.name = name self.description = description self.language = language + class ApplicationForm(Base): - __tablename__ = 'application_form' - __table_args__ = {'extend_existing': True} + __tablename__ = "application_form" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - event_id = db.Column(db.Integer(), db.ForeignKey('event.id'), nullable=False) + event_id = db.Column(db.Integer(), db.ForeignKey("event.id"), nullable=False) is_open = db.Column(db.Boolean(), nullable=False) nominations = db.Column(db.Boolean(), nullable=False) @@ -181,22 +197,29 @@ def __init__(self, event_id, is_open, nominations): self.is_open = is_open self.nominations = nominations + class Question(Base): - __tablename__ = 'question' - __table_args__ = {'extend_existing': True} + __tablename__ = "question" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) - section_id = db.Column(db.Integer(), db.ForeignKey('section.id'), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) + section_id = db.Column(db.Integer(), db.ForeignKey("section.id"), nullable=False) type = db.Column(db.String(), nullable=False) order = db.Column(db.Integer(), nullable=False) is_required = db.Column(db.Boolean(), nullable=False) - depends_on_question_id = db.Column(db.Integer(), db.ForeignKey('question.id'), nullable=True) + depends_on_question_id = db.Column( + db.Integer(), db.ForeignKey("question.id"), nullable=True + ) key = db.Column(db.String(255), nullable=True) - question_translations = db.relationship('QuestionTranslation', lazy='dynamic') + question_translations = db.relationship("QuestionTranslation", lazy="dynamic") - def __init__(self, application_form_id, section_id, order, questionType, is_required=True): + def __init__( + self, application_form_id, section_id, order, questionType, is_required=True + ): self.application_form_id = application_form_id self.section_id = section_id self.order = order @@ -204,15 +227,18 @@ def __init__(self, application_form_id, section_id, order, questionType, is_requ self.is_required = is_required def get_translation(self, language): - question_translation = self.question_translations.filter_by(language=language).first() + question_translation = self.question_translations.filter_by( + language=language + ).first() return question_translation + class QuestionTranslation(Base): - __tablename__ = 'question_translation' - __table_args__ = {'extend_existing': True} + __tablename__ = "question_translation" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - question_id = db.Column(db.Integer(), db.ForeignKey('question.id'), nullable=False) + question_id = db.Column(db.Integer(), db.ForeignKey("question.id"), nullable=False) language = db.Column(db.String(2), nullable=False) headline = db.Column(db.String(), nullable=False) description = db.Column(db.String(), nullable=True) @@ -222,7 +248,7 @@ class QuestionTranslation(Base): options = db.Column(db.JSON(), nullable=True) show_for_values = db.Column(db.JSON(), nullable=True) - question = db.relationship('Question', foreign_keys=[question_id]) + question = db.relationship("Question", foreign_keys=[question_id]) def __init__( self, @@ -234,7 +260,7 @@ def __init__( validation_regex=None, validation_text=None, options=None, - show_for_values=None + show_for_values=None, ): self.question_id = question_id self.language = language @@ -246,17 +272,22 @@ def __init__( self.options = options self.show_for_values = show_for_values + class ReviewForm(Base): - __tablename__ = 'review_form' - __table_args__ = {'extend_existing': True} + __tablename__ = "review_form" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) is_open = db.Column(db.Boolean(), nullable=False) deadline = db.Column(db.DateTime(), nullable=False) - application_form = db.relationship('ApplicationForm', foreign_keys=[application_form_id]) - review_questions = db.relationship('ReviewQuestion') + application_form = db.relationship( + "ApplicationForm", foreign_keys=[application_form_id] + ) + review_questions = db.relationship("ReviewQuestion") def __init__(self, application_form_id, deadline): self.application_form_id = application_form_id @@ -268,30 +299,26 @@ def close(self): class ReviewQuestion(Base): - __tablename__ = 'review_question' - __table_args__ = {'extend_existing': True} + __tablename__ = "review_question" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer, primary_key=True) - review_form_id = db.Column(db.Integer(), db.ForeignKey('review_form.id'), nullable=False) - question_id = db.Column(db.Integer(), db.ForeignKey('question.id'), nullable=True) - + review_form_id = db.Column( + db.Integer(), db.ForeignKey("review_form.id"), nullable=False + ) + question_id = db.Column(db.Integer(), db.ForeignKey("question.id"), nullable=True) + type = db.Column(db.String(), nullable=False) - + is_required = db.Column(db.Boolean(), nullable=False) order = db.Column(db.Integer(), nullable=False) weight = db.Column(db.Float(), nullable=False) - review_form = db.relationship('ReviewForm', foreign_keys=[review_form_id]) - question = db.relationship('Question', foreign_keys=[question_id]) - - translations = db.relationship('ReviewQuestionTranslation', lazy='dynamic') - - def __init__(self, - review_form_id, - question_id, - type, - is_required, - order, - weight): + review_form = db.relationship("ReviewForm", foreign_keys=[review_form_id]) + question = db.relationship("Question", foreign_keys=[question_id]) + + translations = db.relationship("ReviewQuestionTranslation", lazy="dynamic") + + def __init__(self, review_form_id, question_id, type, is_required, order, weight): self.review_form_id = review_form_id self.question_id = question_id self.type = type @@ -305,12 +332,20 @@ def get_translation(self, language): class ReviewQuestionTranslation(Base): - __tablename__ = 'review_question_translation' - __table_args__ = {'extend_existing': True} - __table_args__ = tuple([db.UniqueConstraint('review_question_id', 'language', name='uq_review_question_id_language')]) + __tablename__ = "review_question_translation" + __table_args__ = {"extend_existing": True} + __table_args__ = tuple( + [ + db.UniqueConstraint( + "review_question_id", "language", name="uq_review_question_id_language" + ) + ] + ) id = db.Column(db.Integer(), primary_key=True) - review_question_id = db.Column(db.Integer(), db.ForeignKey('review_question.id'), nullable=False) + review_question_id = db.Column( + db.Integer(), db.ForeignKey("review_question.id"), nullable=False + ) language = db.Column(db.String(2), nullable=False) description = db.Column(db.String(), nullable=True) @@ -320,15 +355,17 @@ class ReviewQuestionTranslation(Base): validation_regex = db.Column(db.String(), nullable=True) validation_text = db.Column(db.String(), nullable=True) - def __init__(self, - review_question_id, - language, - description=None, - headline=None, - placeholder=None, - options=None, - validation_regex=None, - validation_text=None): + def __init__( + self, + review_question_id, + language, + description=None, + headline=None, + placeholder=None, + options=None, + validation_regex=None, + validation_text=None, + ): self.review_question_id = review_question_id self.language = language self.description = description @@ -338,50 +375,59 @@ def __init__(self, self.validation_regex = validation_regex self.validation_text = validation_text + class ReviewConfiguration(Base): - __tablename__ = 'review_configuration' - __table_args__ = {'extend_existing': True} + __tablename__ = "review_configuration" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - review_form_id = db.Column(db.Integer(), db.ForeignKey('review_form.id'), nullable=False) + review_form_id = db.Column( + db.Integer(), db.ForeignKey("review_form.id"), nullable=False + ) num_reviews_required = db.Column(db.Integer(), nullable=False) num_optional_reviews = db.Column(db.Integer(), nullable=False) - drop_optional_question_id = db.Column(db.Integer(), db.ForeignKey('review_question.id'), nullable=True) + drop_optional_question_id = db.Column( + db.Integer(), db.ForeignKey("review_question.id"), nullable=True + ) drop_optional_agreement_values = db.Column(db.String(), nullable=True) + class ReviewScore(Base): - __tablename__ = 'review_score' - __table_args__ = {'extend_existing': True} + __tablename__ = "review_score" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - review_response_id = db.Column(db.Integer(), db.ForeignKey('review_response.id'), nullable=False) - review_question_id = db.Column(db.Integer(), db.ForeignKey('review_question.id'), nullable=False) + review_response_id = db.Column( + db.Integer(), db.ForeignKey("review_response.id"), nullable=False + ) + review_question_id = db.Column( + db.Integer(), db.ForeignKey("review_question.id"), nullable=False + ) value = db.Column(db.String(), nullable=False) - def __init__(self, - review_question_id, - value): + def __init__(self, review_question_id, value): self.review_question_id = review_question_id self.value = value + class ReviewResponse(Base): - __tablename__ = 'review_response' - __table_args__ = {'extend_existing': True} + __tablename__ = "review_response" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - review_form_id = db.Column(db.Integer(), db.ForeignKey('review_form.id'), nullable=False) - reviewer_user_id = db.Column(db.Integer(), db.ForeignKey('app_user.id'), nullable=False) - response_id = db.Column(db.Integer(), db.ForeignKey('response.id'), nullable=False) + review_form_id = db.Column( + db.Integer(), db.ForeignKey("review_form.id"), nullable=False + ) + reviewer_user_id = db.Column( + db.Integer(), db.ForeignKey("app_user.id"), nullable=False + ) + response_id = db.Column(db.Integer(), db.ForeignKey("response.id"), nullable=False) submitted_timestamp = db.Column(db.DateTime(), nullable=False) language = db.Column(db.String(2), nullable=False) is_submitted = db.Column(db.Boolean(), nullable=False) submitted_timestamp = db.Column(db.DateTime(), nullable=True) - def __init__(self, - review_form_id, - reviewer_user_id, - response_id, - language): + def __init__(self, review_form_id, reviewer_user_id, response_id, language): self.review_form_id = review_form_id self.reviewer_user_id = reviewer_user_id self.response_id = response_id @@ -392,33 +438,49 @@ def submit(self): self.is_submitted = True self.submitted_timestamp = datetime.now() + current_order = 1 -def add_question(review_form_id, type, is_required, weight=0, question_id=None, - descriptions=None, headlines=None, placeholders=None, options=None, - validation_regexs=None, validation_texts=None): + +def add_question( + review_form_id, + type, + is_required, + weight=0, + question_id=None, + descriptions=None, + headlines=None, + placeholders=None, + options=None, + validation_regexs=None, + validation_texts=None, +): global current_order - question = ReviewQuestion(review_form_id, question_id, type, is_required, current_order, weight) + question = ReviewQuestion( + review_form_id, question_id, type, is_required, current_order, weight + ) db.session.add(question) db.session.commit() en = ReviewQuestionTranslation( - question.id, - 'en', - None if descriptions is None else descriptions['en'], - None if headlines is None else headlines['en'], - None if placeholders is None else placeholders['en'], - None if options is None else options['en'], - None if validation_regexs is None else validation_regexs['en'], - None if validation_texts is None else validation_texts['en']) + question.id, + "en", + None if descriptions is None else descriptions["en"], + None if headlines is None else headlines["en"], + None if placeholders is None else placeholders["en"], + None if options is None else options["en"], + None if validation_regexs is None else validation_regexs["en"], + None if validation_texts is None else validation_texts["en"], + ) fr = ReviewQuestionTranslation( - question.id, - 'fr', - None if descriptions is None else descriptions['fr'], - None if headlines is None else headlines['fr'], - None if placeholders is None else placeholders['fr'], - None if options is None else options['fr'], - None if validation_regexs is None else validation_regexs['fr'], - None if validation_texts is None else validation_texts['fr']) + question.id, + "fr", + None if descriptions is None else descriptions["fr"], + None if headlines is None else headlines["fr"], + None if placeholders is None else placeholders["fr"], + None if options is None else options["fr"], + None if validation_regexs is None else validation_regexs["fr"], + None if validation_texts is None else validation_texts["fr"], + ) db.session.add_all([en, fr]) db.session.commit() @@ -426,53 +488,109 @@ def add_question(review_form_id, type, is_required, weight=0, question_id=None, return question, en, fr + def get_question(en_headline): - en = db.session.query(QuestionTranslation).filter_by(language='en', headline=en_headline).first() - fr = db.session.query(QuestionTranslation).filter_by(language='fr', question_id=en.question_id).first() + en = ( + db.session.query(QuestionTranslation) + .filter_by(language="en", headline=en_headline) + .first() + ) + fr = ( + db.session.query(QuestionTranslation) + .filter_by(language="fr", question_id=en.question_id) + .first() + ) return en.question, en, fr + def add_evaluation_question(form_id, headlines, descriptions, min_score, max_score): - validation_regex = '^({})$'.format('|'.join([str(i) for i in range(max_score, min_score-1, -1)])) + validation_regex = "^({})$".format( + "|".join([str(i) for i in range(max_score, min_score - 1, -1)]) + ) validation_text = { - 'en': 'Enter a number between {} and {}'.format(min_score, max_score), - 'fr': 'Entrez un nombre entre {} et {}'.format(min_score, max_score) + "en": "Enter a number between {} and {}".format(min_score, max_score), + "fr": "Entrez un nombre entre {} et {}".format(min_score, max_score), } placeholders = { - 'en': 'Enter a score between {} and {}'.format(min_score, max_score), - 'fr': 'Saisissez un score compris entre {} et {}'.format(min_score, max_score) + "en": "Enter a score between {} and {}".format(min_score, max_score), + "fr": "Saisissez un score compris entre {} et {}".format(min_score, max_score), } - return add_question(form_id, 'short-text', True, 1, headlines=headlines, - descriptions=descriptions, validation_regexs={'en': validation_regex, 'fr': validation_regex}, - validation_texts=validation_text, placeholders=placeholders) + return add_question( + form_id, + "short-text", + True, + 1, + headlines=headlines, + descriptions=descriptions, + validation_regexs={"en": validation_regex, "fr": validation_regex}, + validation_texts=validation_text, + placeholders=placeholders, + ) -def add_comment_question(form_id, headlines, descriptions, is_required=False): - return add_question(form_id, 'long-text', is_required, weight=1, descriptions=descriptions, headlines=headlines) -def add_information_question(form_id, en_headline, type='information', heading_override=False, description_override=None): +def add_comment_question(form_id, headlines, descriptions, is_required=False): + return add_question( + form_id, + "long-text", + is_required, + weight=1, + descriptions=descriptions, + headlines=headlines, + ) + + +def add_information_question( + form_id, + en_headline, + type="information", + heading_override=False, + description_override=None, +): question, en, fr = get_question(en_headline) - + headlines = { - 'en': None if heading_override else en.headline, - 'fr': None if heading_override else fr.headline + "en": None if heading_override else en.headline, + "fr": None if heading_override else fr.headline, } descriptions = { - 'en': description_override['en'] if description_override is not None else en.description, - 'fr': description_override['fr'] if description_override is not None else fr.description + "en": description_override["en"] + if description_override is not None + else en.description, + "fr": description_override["fr"] + if description_override is not None + else fr.description, } - if question.type == 'information': + if question.type == "information": return add_heading(form_id, headlines) - return add_question(form_id, type, False, question_id=question.id, headlines=headlines, descriptions=descriptions) + return add_question( + form_id, + type, + False, + question_id=question.id, + headlines=headlines, + descriptions=descriptions, + ) + def add_heading(form_id, headlines): - return add_question(form_id, 'heading', False, headlines=headlines) + return add_question(form_id, "heading", False, headlines=headlines) + def add_divider(form_id, headlines, descriptions): - return add_question(form_id, 'section-divider', False, headlines=headlines, descriptions=descriptions) + return add_question( + form_id, + "section-divider", + False, + headlines=headlines, + descriptions=descriptions, + ) + def upgrade(): - print('Start migration 34a5a1fce672') + print("Start migration 34a5a1fce672") + # event = db.session.query(Event).filter_by(key='prc').first() # application_form = db.session.query(ApplicationForm).filter_by(event_id=event.id).first() @@ -497,20 +615,20 @@ def upgrade(): # 'en': 'Evaluation of proposals', # 'fr': u'Évaluation des propositions' # }, descriptions={ -# 'en': """Below are a series of weighted, multiple choice questions intended to evaluate the applicant’s proposal. The evaluation is divided up into the sections from the application: -# I. Organization capacity and experience; -# II. Policy research; -# III. Policy engagement experience; and -# IV. Budget, followed by section -# V. Overall Assessment. +# 'en': """Below are a series of weighted, multiple choice questions intended to evaluate the applicant’s proposal. The evaluation is divided up into the sections from the application: +# I. Organization capacity and experience; +# II. Policy research; +# III. Policy engagement experience; and +# IV. Budget, followed by section +# V. Overall Assessment. # At the end of each question and/or section, please reflect in your own words what you think of the answers in that section. Each evaluation question corresponds with the section and numbered question in the proposal. # """, -# 'fr': u"""Vous trouverez ci-dessous une série de questions à choix multiples pondérées destinées à évaluer la proposition du candidat. L’évaluation est divisée en plusieurs sections à partir de la candidature: -# I. Capacité et expérience de l’organisation; +# 'fr': u"""Vous trouverez ci-dessous une série de questions à choix multiples pondérées destinées à évaluer la proposition du candidat. L’évaluation est divisée en plusieurs sections à partir de la candidature: +# I. Capacité et expérience de l’organisation; # II. Recherche sur les politiques; -# III. Expérience en matière d’engagement politique; et -# IV. Budget, suivi par la section -# V. Évaluation globale. +# III. Expérience en matière d’engagement politique; et +# IV. Budget, suivi par la section +# V. Évaluation globale. # À la fin de chaque question et/ou section, veuillez exprimer dans vos propres mots ce que vous pensez des réponses de cette section. Chaque question d'évaluation correspond à la section et à la question numérotée dans la proposition. # """ # }) @@ -547,7 +665,7 @@ def upgrade(): # review_q1 = add_evaluation_question(form.id, headlines=None, descriptions={ # 'en': """Please review the makeup of the organization in terms of staffing and answer the following question. -# Assessment of research team: After reviewing the CVs of the principal researchers, assess the strength of the research team with respect to achieving the goals and strategy of the proposed research agenda. +# Assessment of research team: After reviewing the CVs of the principal researchers, assess the strength of the research team with respect to achieving the goals and strategy of the proposed research agenda. # \[Total possible points = 10\] # **Scale:** @@ -555,7 +673,7 @@ def upgrade(): # 4 - 7 : Research team is adequate but needs some broad capacity development # 8 - 10 : Research team is strong (although they might still benefit from limited capacity development) # """, -# 'fr': u"""Évaluation de l’équipe de recherche: Après avoir examiné les CV des principaux chercheurs, évaluez la force de l’équipe de recherche en ce qui concerne la réalisation des objectifs et de la stratégie du programme de recherche proposé. +# 'fr': u"""Évaluation de l’équipe de recherche: Après avoir examiné les CV des principaux chercheurs, évaluez la force de l’équipe de recherche en ce qui concerne la réalisation des objectifs et de la stratégie du programme de recherche proposé. # \[Total des points possibles = 10\] # **Scale:** @@ -573,7 +691,7 @@ def upgrade(): # add_information_question(form.id, '2. Mission for Responsible AI') # add_evaluation_question(form.id, headlines=None, descriptions={ -# 'en': """To what extent does this mission statement of the organization relate to advancing the principles of responsible AI? +# 'en': """To what extent does this mission statement of the organization relate to advancing the principles of responsible AI? # \[Total possible points = 15\] # **Scale:** @@ -582,7 +700,7 @@ def upgrade(): # 11 - 15 : Superior demonstration of mission and the significance # """, # 'fr': u"""Dans quelle mesure cette déclaration de mission de l’organisation est-elle liée à la promotion des -# principes de l’IA responsable? +# principes de l’IA responsable? # \[Total des points possibles = 15\] # **Scale:** @@ -601,7 +719,7 @@ def upgrade(): # add_information_question(form.id, 'Examples') # add_evaluation_question(form.id, headlines=None, descriptions={ -# 'en': """To what extent does the applicant demonstrate knowledge and expertise with at least three of the key research areas? +# 'en': """To what extent does the applicant demonstrate knowledge and expertise with at least three of the key research areas? # \[Total possible points = 15\] # **Scale:** @@ -609,7 +727,7 @@ def upgrade(): # 6 - 10 : Some demonstrated knowledge and expertise in at least two areas # 11 - 15 : Superior/significant demonstrated knowledge and expertise in three areas # """, -# 'fr': u"""Dans quelle mesure le candidat démontre-t-il des connaissances et une expertise dans au moins trois des principaux domaines de recherche? +# 'fr': u"""Dans quelle mesure le candidat démontre-t-il des connaissances et une expertise dans au moins trois des principaux domaines de recherche? # \[Total des points possibles = 15\] # **Scale:** @@ -620,7 +738,7 @@ def upgrade(): # }, min_score=0, max_score=15) # add_evaluation_question(form.id, headlines=None, descriptions={ -# 'en': """Please review the examples provided by the applicant from the organization’s research portfolio. Based on what they’ve written and provided, to what extent does the organization demonstrate an ability to produce high quality policy research on these issues? +# 'en': """Please review the examples provided by the applicant from the organization’s research portfolio. Based on what they’ve written and provided, to what extent does the organization demonstrate an ability to produce high quality policy research on these issues? # \[Total possible points = 15\] # *High quality research is relevant, shows integrity, legitimacy, importance (originality) and is well positioned for use. Please refer to the [Research Quality Plus](https://www.idrc.ca/en/research-in-action/research-quality-plus) assessment instrument if you have questions.* @@ -629,7 +747,7 @@ def upgrade(): # 6 - 10 : Demonstrates variable research quality with some high-quality research # 11 - 15 : Significant experience producing high quality research # """, -# 'fr': u"""Veuillez examiner les exemples fournis par le candidat à partir du portefeuille de recherche de l’organisme. Sur la base de ce qu’ils ont écrit et fourni, dans quelle mesure l’organisme démontre-t-il sa capacité à produire des recherches politiques de haute qualité sur ces questions? +# 'fr': u"""Veuillez examiner les exemples fournis par le candidat à partir du portefeuille de recherche de l’organisme. Sur la base de ce qu’ils ont écrit et fourni, dans quelle mesure l’organisme démontre-t-il sa capacité à produire des recherches politiques de haute qualité sur ces questions? # \[Total des points possibles = 15\] # Une recherche de haute qualité est pertinente, fait preuve d’intégrité, de légitimité, d’importance (originalité) et est bien placée pour être utilisée. Veuillez-vous référer à l’instrument d’évaluation [Qualité de la recherche plus](https://idl-bnc-idrc.dspacedirect.org/bitstream/handle/10625/56600/IDL-56600.pdf?sequence=2&isAllowed=y) si vous avez des questions. @@ -640,7 +758,7 @@ def upgrade(): # 11 - 15 : Une expérience significative dans la production de recherches de haute qualité # """ # }, min_score=0, max_score=15) - + # add_comment_question(form.id, descriptions={ # 'en': "Comments", # 'fr': u"Des commentaires?" @@ -686,7 +804,7 @@ def upgrade(): # add_information_question(form.id, '1. Research background, problem statement and justification') # add_evaluation_question(form.id, headlines=None, descriptions={ -# 'en': """To what extent does the applicant clearly articulate why these priorities are significant for the AI4D research agenda of the organization and the contexts in which they work? +# 'en': """To what extent does the applicant clearly articulate why these priorities are significant for the AI4D research agenda of the organization and the contexts in which they work? # \[Total possible points = 10\] # **Scale:** @@ -694,7 +812,7 @@ def upgrade(): # 4 - 7 : Substantive clarity of research priorities # 8 - 10 : Superior/significant clarity of research priorities # """, -# 'fr': u"""Dans quelle mesure le candidat explique-t-il clairement pourquoi ces priorités sont importantes pour l’agenda de recherche IAPD de l’organisation et les contextes dans lesquels elles s’inscrivent? +# 'fr': u"""Dans quelle mesure le candidat explique-t-il clairement pourquoi ces priorités sont importantes pour l’agenda de recherche IAPD de l’organisation et les contextes dans lesquels elles s’inscrivent? # \[Total des points possibles = 10\] # **Scale:** @@ -705,7 +823,7 @@ def upgrade(): # }, min_score=0, max_score=10) # add_evaluation_question(form.id, headlines=None, descriptions={ -# 'en': """To what extent does the applicant demonstrate the ability to draw upon evidence to support the background, context and arguments for why these are the priorities for their research? +# 'en': """To what extent does the applicant demonstrate the ability to draw upon evidence to support the background, context and arguments for why these are the priorities for their research? # \[Total possible points = 10\] # **Scale:** @@ -713,7 +831,7 @@ def upgrade(): # 4 - 7 : Substantive demonstrated ability # 8 - 10 : Superior/significant demonstrated ability # """, -# 'fr': u"""Dans quelle mesure le candidat démontre-t-il sa capacité à s’appuyer sur des preuves pour étayer le contexte et les arguments qui expliquent pourquoi il s’agit là des priorités de sa recherche? +# 'fr': u"""Dans quelle mesure le candidat démontre-t-il sa capacité à s’appuyer sur des preuves pour étayer le contexte et les arguments qui expliquent pourquoi il s’agit là des priorités de sa recherche? # \[Total des points possibles = 10\] # **Scale:** @@ -724,7 +842,7 @@ def upgrade(): # }, min_score=0, max_score=10) # add_evaluation_question(form.id, headlines=None, descriptions={ -# 'en': """To what extent does the applicant demonstrate the ability to write on complex issues succinctly and intelligently? +# 'en': """To what extent does the applicant demonstrate the ability to write on complex issues succinctly and intelligently? # \[Total possible points = 5\] # **Scale:** @@ -732,10 +850,10 @@ def upgrade(): # 2 - 3 : Some ability to write on complex issues (some good ideas, structure may lack coherence) # 4 - 5 : Superior/significant ability to write on complex issues (exceptionally clear and coherent) # """, -# 'fr': u"""Dans quelle mesure le candidat démontre-t-il sa capacité à écrire sur des questions complexes de manière succincte et intelligente? +# 'fr': u"""Dans quelle mesure le candidat démontre-t-il sa capacité à écrire sur des questions complexes de manière succincte et intelligente? # \[Total des points possibles = 5\] -# Scale: +# Scale: # 0 - 1 : Capacité limitée à écrire sur des sujets complexes (confus, manque de logique et de cohérence) # 2 - 3 : Une certaine capacité à écrire sur des questions complexes (quelques bonnes idées, la structure peut manquer de cohérence) # 4 - 5 : Capacité supérieure à rédiger sur des questions complexes (exceptionnellement clair et cohérent) @@ -750,7 +868,7 @@ def upgrade(): # add_information_question(form.id, '2. General and specific objectives') # add_evaluation_question(form.id, headlines=None, descriptions={ -# 'en': """To what extent are the general and specific objectives appropriate and logically connected to problem statement and justification? Are they feasible? +# 'en': """To what extent are the general and specific objectives appropriate and logically connected to problem statement and justification? Are they feasible? # \[Total possible points = 10\] # **Scale:** @@ -758,10 +876,10 @@ def upgrade(): # 4 - 7 : Objectives need improvement, but are relatively appropriate and connected to problem statement # 8 - 10 : Objectives are coherent, flow logically, and are appropriate and feasible # """, -# 'fr': u"""Dans quelle mesure les objectifs généraux et spécifiques sont-ils appropriés et logiquement liés à l’énoncé et à la justification du problème? Sont-ils réalisables? +# 'fr': u"""Dans quelle mesure les objectifs généraux et spécifiques sont-ils appropriés et logiquement liés à l’énoncé et à la justification du problème? Sont-ils réalisables? # \[Total des points possibles = 10\] -# **Scale:** +# **Scale:** # 0 - 3 : Mal articulé et sans lien avec l’énoncé du problème, objectifs inappropriés et/ou irréalisables # 4 - 7 : Les objectifs doivent être améliorés, mais ils sont relativement appropriés et liés à l’énoncé du problème # 8 - 10 : Les objectifs sont cohérents et logiques, et sont appropriés et réalisables @@ -776,7 +894,7 @@ def upgrade(): # add_information_question(form.id, '3. Methodology and approach') # add_information_question(form.id, 'Approach') # add_evaluation_question(form.id, headlines=None, descriptions={ -# 'en': """To what extent does the organization demonstrate a coherent approach to achieving their research objectives? +# 'en': """To what extent does the organization demonstrate a coherent approach to achieving their research objectives? # \[Total possible points = 10\] # **Scale:** @@ -784,7 +902,7 @@ def upgrade(): # 4 - 7 : Some demonstrated coherence # 8 - 10 : Significant demonstrated coherence # """, -# 'fr': u"""Dans quelle mesure l’organisme fait-il preuve d’une approche cohérente pour atteindre ses objectifs de recherche? +# 'fr': u"""Dans quelle mesure l’organisme fait-il preuve d’une approche cohérente pour atteindre ses objectifs de recherche? # \[Total des points possibles = 10\] # **Scale:** @@ -802,7 +920,7 @@ def upgrade(): # add_information_question(form.id, 'Policy research') # add_evaluation_question(form.id, headlines=None, descriptions={ -# 'en': """To what extent is the conceptual and theoretical framework appropriate to the research question at hand? How well is the framework linked to the proposed research design? +# 'en': """To what extent is the conceptual and theoretical framework appropriate to the research question at hand? How well is the framework linked to the proposed research design? # \[Total possible points = 10\] # **Scale:** @@ -810,7 +928,7 @@ def upgrade(): # 4 - 7 : Some demonstrated strategy in linking framework to research design # 8 - 10 : Significant demonstrated strategy in linking framework to research design # """, -# 'fr': u"""Dans quelle mesure le cadre conceptuel et théorique est-il adapté à la question de recherche en question? Dans quelle mesure le cadre est-il lié à la conception de la recherche proposée? +# 'fr': u"""Dans quelle mesure le cadre conceptuel et théorique est-il adapté à la question de recherche en question? Dans quelle mesure le cadre est-il lié à la conception de la recherche proposée? # \[Total des points possibles = 10\] # **Scale:** @@ -826,7 +944,7 @@ def upgrade(): # }, headlines=None) # add_evaluation_question(form.id, headlines=None, descriptions={ -# 'en': """To what extent does the organization demonstrate a rigorous approach to answering their research questions? +# 'en': """To what extent does the organization demonstrate a rigorous approach to answering their research questions? # \[Total possible points = 10\] # **Scale:** @@ -834,7 +952,7 @@ def upgrade(): # 4 - 7 : Some demonstrated rigor # 8 - 10 : Significant/superior demonstrated rigor # """, -# 'fr': u"""Dans quelle mesure l’organisation fait-elle preuve de rigueur dans ses réponses aux questions de recherche? +# 'fr': u"""Dans quelle mesure l’organisation fait-elle preuve de rigueur dans ses réponses aux questions de recherche? # \[Total des points possibles = 10\] # **Scale:** @@ -850,9 +968,9 @@ def upgrade(): # }, headlines=None) # add_information_question(form.id, 'Gender and inclusion', description_override={ -# 'en': """How has your organization addressed gender-equality and other kinds of inclusion and diversity dimensions socioeconomic, political, cultural, ethnic in its policy research? -# How do you anticipate this will improve with this AI4D support? -# Please give a brief explanation of your strategy with gender, inclusion and diversity related issues as it relates to AI/advanced technologies and offer examples of the research work. +# 'en': """How has your organization addressed gender-equality and other kinds of inclusion and diversity dimensions socioeconomic, political, cultural, ethnic in its policy research? +# How do you anticipate this will improve with this AI4D support? +# Please give a brief explanation of your strategy with gender, inclusion and diversity related issues as it relates to AI/advanced technologies and offer examples of the research work. # Please provide examples PDF or links to material that demonstrates your strategy and research on gender, inclusion and diversity. @@ -862,7 +980,7 @@ def upgrade(): # add_information_question(form.id, 'Gender and inclusion: Supporting documents', type='multi-file') # add_evaluation_question(form.id, headlines=None, descriptions={ -# 'en': """To what extent does the organization demonstrate in-depth knowledge of gender-related dimensions of AI4D? +# 'en': """To what extent does the organization demonstrate in-depth knowledge of gender-related dimensions of AI4D? # \[Total possible points = 10\] # **Scale:** @@ -870,7 +988,7 @@ def upgrade(): # 4 - 7 : Substantive demonstrated ability with gender related issues # 8 - 10 : Superior/significant demonstrated ability with gender related issues # """, -# 'fr': u"""Dans quelle mesure l’organisation fait-elle preuve d’une connaissance approfondie des dimensions de genre d’IAPD? +# 'fr': u"""Dans quelle mesure l’organisation fait-elle preuve d’une connaissance approfondie des dimensions de genre d’IAPD? # \[Total des points possibles = 10\] # **Scale:** @@ -881,7 +999,7 @@ def upgrade(): # }, min_score=0, max_score=10) # add_evaluation_question(form.id, headlines=None, descriptions={ -# 'en': """To what extent does the organization demonstrate in-depth knowledge of other inclusion-related dimensions of AI4D, such as socioeconomics, cultural/ethnic and broader social inclusion issues? +# 'en': """To what extent does the organization demonstrate in-depth knowledge of other inclusion-related dimensions of AI4D, such as socioeconomics, cultural/ethnic and broader social inclusion issues? # \[Total possible points = 10\] # **Scale:** @@ -889,7 +1007,7 @@ def upgrade(): # 4 - 7 : Substantive demonstrated understanding of inclusion-related challenges # 8 - 10 : Significant demonstrated understanding of inclusion-related challenges # """, -# 'fr': u"""Dans quelle mesure l’organisation démontre-t-elle une connaissance approfondie d’autres dimensions de l’IAPD liées à l’inclusion, telles que la socio-économie, la culture/ethnie et les questions plus larges d’inclusion sociale? +# 'fr': u"""Dans quelle mesure l’organisation démontre-t-elle une connaissance approfondie d’autres dimensions de l’IAPD liées à l’inclusion, telles que la socio-économie, la culture/ethnie et les questions plus larges d’inclusion sociale? # \[Total des points possibles = 10\] # **Scale:** @@ -900,7 +1018,7 @@ def upgrade(): # }, min_score=0, max_score=10) # add_evaluation_question(form.id, headlines=None, descriptions={ -# 'en': """Considering the answer on gender and inclusion and the examples provided by the applicant, to what extent does the organization demonstrate in-depth knowledge of gender and inclusion-related issues/challenges in AI and advanced technologies? +# 'en': """Considering the answer on gender and inclusion and the examples provided by the applicant, to what extent does the organization demonstrate in-depth knowledge of gender and inclusion-related issues/challenges in AI and advanced technologies? # \[Total possible points = 5\] # **Scale:** @@ -908,7 +1026,7 @@ def upgrade(): # 2 - 3 : Some knowledge of gender and inclusion-related issues # 4 - 5 : Superior/significant knowledge of gender and inclusion-related issues # """, -# 'fr': u"""Compte tenu de la réponse sur le genre et l’inclusion et des exemples fournis par le candidat, dans quelle mesure l’organisation démontre-t-elle une connaissance approfondie des questions et des défis liés au genre et à l’inclusion dans le domaine de l’IA et des technologies avancées? +# 'fr': u"""Compte tenu de la réponse sur le genre et l’inclusion et des exemples fournis par le candidat, dans quelle mesure l’organisation démontre-t-elle une connaissance approfondie des questions et des défis liés au genre et à l’inclusion dans le domaine de l’IA et des technologies avancées? # \[Total des points possibles = 5\] # **Scale:** @@ -926,7 +1044,7 @@ def upgrade(): # add_information_question(form.id, 'Ethical considerations') # add_evaluation_question(form.id, headlines=None, descriptions={ -# 'en': """Please assess the applicant’s ethics review process for their research. Does the applicant outline a clear and independent process for reviewing and ensuring the privacy, dignity, and integrity of individuals who are the subjects of their research? +# 'en': """Please assess the applicant’s ethics review process for their research. Does the applicant outline a clear and independent process for reviewing and ensuring the privacy, dignity, and integrity of individuals who are the subjects of their research? # \[Total possible points = 5\] # **Scale:** @@ -934,7 +1052,7 @@ def upgrade(): # 2 - 3 : Some research ethics protocols # 4 - 5 : Significant research ethics protocols # """, -# 'fr': u"""Veuillez évaluer le processus d’évaluation éthique du candidat pour sa recherche. Le candidat décrit-il un processus clair et indépendant pour évaluer et garantir la vie privée, la dignité et l’ntégrité des personnes qui font l’objet de ses recherches? +# 'fr': u"""Veuillez évaluer le processus d’évaluation éthique du candidat pour sa recherche. Le candidat décrit-il un processus clair et indépendant pour évaluer et garantir la vie privée, la dignité et l’ntégrité des personnes qui font l’objet de ses recherches? # \[Total des points possibles = 5\] # **Scale:** @@ -952,7 +1070,7 @@ def upgrade(): # add_information_question(form.id, '4. Capacity development') # add_evaluation_question(form.id, headlines=None, descriptions={ -# 'en': """To what extent does the applicant clearly identify where they need the most capacity building to improve work and outcomes to advance responsible AI in their country/region - and offer a coherent, reasonable strategy for addressing the gap? +# 'en': """To what extent does the applicant clearly identify where they need the most capacity building to improve work and outcomes to advance responsible AI in their country/region - and offer a coherent, reasonable strategy for addressing the gap? # \[Total possible points = 10\] # **Scale:** @@ -978,7 +1096,7 @@ def upgrade(): # add_information_question(form.id, '5. Communicating for influence') # add_evaluation_question(form.id, headlines=None, descriptions={ -# 'en': """Please review the applicant's description of their research communications strategy. To what extent does the organization demonstrate an ability or approach to communicate their research well to influence policy? +# 'en': """Please review the applicant's description of their research communications strategy. To what extent does the organization demonstrate an ability or approach to communicate their research well to influence policy? # \[Total possible points = 10\] # **Scale:** @@ -986,7 +1104,7 @@ def upgrade(): # 4 - 7 : Some demonstrated ability to communicate research # 8 - 10 : Significant/superior demonstrated ability to communicate research # """, -# 'fr': u"""Veuillez examiner la description par le candidat de sa stratégie de communication sur la recherche. Dans quelle mesure l’organisme démontre-t-il une capacité ou une approche pour bien communiquer sur sa recherche afin d’influencer les politiques? +# 'fr': u"""Veuillez examiner la description par le candidat de sa stratégie de communication sur la recherche. Dans quelle mesure l’organisme démontre-t-il une capacité ou une approche pour bien communiquer sur sa recherche afin d’influencer les politiques? # \[Total des points possibles = 10\] # **Scale:** @@ -1004,7 +1122,7 @@ def upgrade(): # add_information_question(form.id, '6. Results') # add_evaluation_question(form.id, headlines=None, descriptions={ -# 'en': """To what extent are the proposed markers for measuring success for the organization in the context of this project specific, appropriate, achievable, timely and measurable? +# 'en': """To what extent are the proposed markers for measuring success for the organization in the context of this project specific, appropriate, achievable, timely and measurable? # \[Total possible points = 15\] # **Scale:** @@ -1012,7 +1130,7 @@ def upgrade(): # 6 - 10 : Some articulation of success, markers may lack clarity, specificity, appropriateness, feasibility, or measurability # 11- 15 : Clear vision of success matched with strong, measurable markers # """, -# 'fr': u"""Dans quelle mesure les marqueurs proposés pour mesurer le succès de l’organisation dans le cadre de ce projet sont-ils spécifiques, appropriés, réalisables, opportuns et mesurables? +# 'fr': u"""Dans quelle mesure les marqueurs proposés pour mesurer le succès de l’organisation dans le cadre de ce projet sont-ils spécifiques, appropriés, réalisables, opportuns et mesurables? # \[Total des points possibles = 15\] # **Scale:** @@ -1030,7 +1148,7 @@ def upgrade(): # add_information_question(form.id, '7. Project schedule') # add_evaluation_question(form.id, headlines=None, descriptions={ -# 'en': """To what extent does the applicant demonstrate a clear and realistic project schedule for achieving their objectives with outputs and milestones as they relate to the budget? +# 'en': """To what extent does the applicant demonstrate a clear and realistic project schedule for achieving their objectives with outputs and milestones as they relate to the budget? # \[Total possible points = 5\] # **Scale:** @@ -1038,10 +1156,10 @@ def upgrade(): # 2 - 3 : Some demonstration of how to achieve project objectives # 4 - 5 : Superior/significant demonstration of how to achieve project objectives # """, -# 'fr': u"""Dans quelle mesure le demandeur démontre-t-il un calendrier de projet clair et réaliste pour atteindre ses objectifs, avec des résultats et des étapes en rapport avec le budget? +# 'fr': u"""Dans quelle mesure le demandeur démontre-t-il un calendrier de projet clair et réaliste pour atteindre ses objectifs, avec des résultats et des étapes en rapport avec le budget? # \[Total des points possibles = 5\] -# Scale: +# Scale: # 0 - 1 : Démonstration limitée de la manière d’atteindre les objectifs du projet # 2 - 3 : Quelques démonstrations de la manière d’atteindre les objectifs du projet # 4 - 5 : Démonstration supérieure/significative de la manière d’atteindre les objectifs du projet @@ -1090,17 +1208,17 @@ def upgrade(): # add_information_question(form.id, '1. Policy engagement') # add_evaluation_question(form.id, headlines=None, descriptions={ -# 'en': """To what extent does the organization demonstrate quality engagement with national governing institutions (e.g. government departments, bodies and agencies, as well as regulators, and other public-sector institutions) on advanced technology issues? \[Total possible points = 15\] +# 'en': """To what extent does the organization demonstrate quality engagement with national governing institutions (e.g. government departments, bodies and agencies, as well as regulators, and other public-sector institutions) on advanced technology issues? \[Total possible points = 15\] # *Note: For the purposes of this initiative, having national policy influence is critical.* # Applicant receives and responds to requests for input from governments/other regulatory institutions. # **Scale:** -# 0 - 5 : Limited quality engagement +# 0 - 5 : Limited quality engagement # 6 - 10 : Some quality engagement # 11- 15 : Significant quality engagement # """, -# 'fr': u"""Dans quelle mesure l’organisation fait-elle preuve d’un engagement de qualité avec les institutions nationales de gouvernance (par exemple, les ministères, les organismes et les agences gouvernementales, ainsi que les régulateurs et autres institutions du secteur public) sur les questions de technologie avancée? +# 'fr': u"""Dans quelle mesure l’organisation fait-elle preuve d’un engagement de qualité avec les institutions nationales de gouvernance (par exemple, les ministères, les organismes et les agences gouvernementales, ainsi que les régulateurs et autres institutions du secteur public) sur les questions de technologie avancée? # \[Total des points possibles = 15\] # *Note: Pour les besoins de cette initiative, il est essentiel d'avoir une influence sur les politiques nationales.* @@ -1116,11 +1234,11 @@ def upgrade(): # 'en': """Applicant proactively engages with policy makers (such as on steering committees, or on emerging policy issues). # **Scale:** -# 0 - 5 : Limited quality engagement +# 0 - 5 : Limited quality engagement # 6 - 10 : Some quality engagement # 11- 15 : Significant quality engagement # """, -# 'fr': u"""Dans quelle mesure l’organisation fait-elle preuve d’un engagement de qualité avec les institutions nationales de gouvernance (par exemple, les ministères, les organismes et les agences gouvernementales, ainsi que les régulateurs et autres institutions du secteur public) sur les questions de technologie avancée? +# 'fr': u"""Dans quelle mesure l’organisation fait-elle preuve d’un engagement de qualité avec les institutions nationales de gouvernance (par exemple, les ministères, les organismes et les agences gouvernementales, ainsi que les régulateurs et autres institutions du secteur public) sur les questions de technologie avancée? # \[Total des points possibles = 15\] # *Note: Pour les besoins de cette initiative, il est essentiel d'avoir une influence sur les politiques nationales.* @@ -1133,7 +1251,7 @@ def upgrade(): # }, min_score=0, max_score=15) # add_evaluation_question(form.id, headlines=None, descriptions={ -# 'en': """Please review the three examples of policy engagement. To what extent does the applicant demonstrate an ability to engage with policy makers on substantive/technical policy issues? +# 'en': """Please review the three examples of policy engagement. To what extent does the applicant demonstrate an ability to engage with policy makers on substantive/technical policy issues? # \[Total possible points = 15\] # **Scale:** @@ -1141,7 +1259,7 @@ def upgrade(): # 6 - 10 : Some demonstrated ability to engage with policy makers on substantive/technical policy issues # 11- 15 : Significant/superior demonstrated ability to engage with policy makers on substantive/technical policy issues # """, -# 'fr': u"""Veuillez examiner les trois exemples d’engagement politique. Dans quelle mesure le candidat démontre-t-il une capacité à s’engager auprès des décideurs politiques sur des questions politiques de fond/techniques? +# 'fr': u"""Veuillez examiner les trois exemples d’engagement politique. Dans quelle mesure le candidat démontre-t-il une capacité à s’engager auprès des décideurs politiques sur des questions politiques de fond/techniques? # \[Total des points possibles = 15\] # **Scale:** @@ -1159,7 +1277,7 @@ def upgrade(): # add_information_question(form.id, '2. Informing debates and policies') # add_evaluation_question(form.id, headlines=None, descriptions={ -# 'en': """To what extent does the applicant demonstrate an ability to inform public debate and policy processes nationally or regionally? +# 'en': """To what extent does the applicant demonstrate an ability to inform public debate and policy processes nationally or regionally? # \[Total possible points = 15\] # **Scale:** @@ -1167,7 +1285,7 @@ def upgrade(): # 6 - 10 : Some demonstrated ability # 11- 15 : Significant demonstrated ability # """, -# 'fr': u"""Dans quelle mesure le candidat démontre-t-il une capacité à informer le débat public et les processus politiques au niveau national ou régional? +# 'fr': u"""Dans quelle mesure le candidat démontre-t-il une capacité à informer le débat public et les processus politiques au niveau national ou régional? # \[Total des points possibles = 15\] # **Scale:** @@ -1181,15 +1299,15 @@ def upgrade(): # add_information_question(form.id, 'Regional engagement', heading_override=True) # add_evaluation_question(form.id, headlines=None, descriptions={ -# 'en': """To what extent does the organization demonstrate clear regional/international engagement on AI/advanced technology policy issues with other governments, public sector institutions, bi-lateral, multilateral or international institutions/organizations or multi stakeholder fora? +# 'en': """To what extent does the organization demonstrate clear regional/international engagement on AI/advanced technology policy issues with other governments, public sector institutions, bi-lateral, multilateral or international institutions/organizations or multi stakeholder fora? # \[Total possible points = 15\] -# Scale: +# Scale: # 0 - 5 : Limited regional/international engagement # 6 - 10 : Substantive regional/international engagement # 11- 15 : Significant regional/international engagement # """, -# 'fr': u"""Dans quelle mesure l’organisation fait-elle preuve d’un engagement régional/international clair sur les questions de politique technologique avancée et d’IA avec d’autres gouvernements, des institutions du secteur public, des institutions/organisations bilatérales, multilatérales ou internationales ou des forums multipartites? +# 'fr': u"""Dans quelle mesure l’organisation fait-elle preuve d’un engagement régional/international clair sur les questions de politique technologique avancée et d’IA avec d’autres gouvernements, des institutions du secteur public, des institutions/organisations bilatérales, multilatérales ou internationales ou des forums multipartites? # \[Total des points possibles = 15\] # **Scale:** @@ -1232,7 +1350,7 @@ def upgrade(): # add_evaluation_question(form.id, headlines=None, descriptions={ # 'en': """Please review the applicant’s budget and consider the coherence and clarity, the appropriateness of budget lines for proposed activities and the budget’s alignment with the research plan. -# In terms of overall coherence, appropriateness to research activities and fiscal management, please rate the applicant’s budget. +# In terms of overall coherence, appropriateness to research activities and fiscal management, please rate the applicant’s budget. # \[Total possible points = 30\] # **Scale:** @@ -1241,7 +1359,7 @@ def upgrade(): # 21 - 30 : Budget demonstrates superior coherence and appropriateness for research and policy impact # """, # 'fr': u"""Veuillez examiner le budget du candidat et considérer la cohérence et la clarté, la pertinence des lignes budgétaires pour les activités proposées et l’alignement du budget sur le plan de recherche. -# En termes de cohérence globale, d’adéquation aux activités de recherche et de gestion budgétaire, veuillez évaluer le budget du candidat. +# En termes de cohérence globale, d’adéquation aux activités de recherche et de gestion budgétaire, veuillez évaluer le budget du candidat. # \[Total des points possibles = 30\] # **Scale:** @@ -1255,7 +1373,7 @@ def upgrade(): # 'en': """IDRC’s grants administration team will be reviewing all of the budgets independently, and their assessment will be discussed during the final selection process. However, given the significance of budgets in terms of institutional strength, research planning and strategy, and risk mitigation, please provide feedback on the applicant’s proposed budget, and offer any input or insights you may have regarding the soundness of the applicant’s budget in relation to their research objectives and overall plan.""", # 'fr': u"""L’équipe d’administration des subventions du CRDI examinera tous les budgets de manière indépendante et leur évaluation sera discutée lors du processus de sélection final. Toutefois, étant donné l’importance des budgets en termes de force institutionnelle, de planification et de stratégie de recherche et d’atténuation des risques, veuillez donner votre avis sur le budget proposé par le candidat et faire part de vos commentaires ou de vos idées concernant""" # }) - + # print('Finished configuring Section IV') # add_divider(form.id, headlines={ @@ -1269,7 +1387,7 @@ def upgrade(): # }) # add_evaluation_question(form.id, headlines=None, descriptions={ -# 'en': """Is the overall application well-articulated and does it demonstrate clarity of purpose and potential impact for AI4D? +# 'en': """Is the overall application well-articulated and does it demonstrate clarity of purpose and potential impact for AI4D? # \[Total possible points = 10\] # **Scale:** @@ -1277,7 +1395,7 @@ def upgrade(): # 4 - 7 : Somewhat well-articulated and clear # 8 - 10 : Very well-articulated and clear # """, -# 'fr': u"""La demande globale est-elle bien articulée et démontre-t-elle la clarté de l’objectif et l’impact potentiel pour IAPD? +# 'fr': u"""La demande globale est-elle bien articulée et démontre-t-elle la clarté de l’objectif et l’impact potentiel pour IAPD? # \[Total des points possibles = 10\] # **Scale:** @@ -1288,7 +1406,7 @@ def upgrade(): # }, min_score=0, max_score=10) # add_evaluation_question(form.id, headlines=None, descriptions={ -# 'en': """Does the proposal demonstrate in-depth, well-rounded, and multi-disciplinary knowledge of AI and advanced technology policy themes and issues? +# 'en': """Does the proposal demonstrate in-depth, well-rounded, and multi-disciplinary knowledge of AI and advanced technology policy themes and issues? # \[Total possible points = 10\] # **Scale:** @@ -1296,7 +1414,7 @@ def upgrade(): # 4 - 7 : Some multi-disciplinary knowledge # 8 - 10 : Significant multi-disciplinary knowledge # """, -# 'fr': u"""La proposition démontre-t-elle une connaissance approfondie, bien équilibrée et pluridisciplinaire des thèmes et des questions de politique en matière d’IA et de technologies avancées? +# 'fr': u"""La proposition démontre-t-elle une connaissance approfondie, bien équilibrée et pluridisciplinaire des thèmes et des questions de politique en matière d’IA et de technologies avancées? # \[Total des points possibles = 10\] # **Scale:** @@ -1307,7 +1425,7 @@ def upgrade(): # }, min_score=0, max_score=10) # add_evaluation_question(form.id, headlines=None, descriptions={ -# 'en': """Does the proposal demonstrate a clear strategy of how the organization will benefit from the support offered by this initiative and add to debates on the AI and advanced technology policy themes in the region? +# 'en': """Does the proposal demonstrate a clear strategy of how the organization will benefit from the support offered by this initiative and add to debates on the AI and advanced technology policy themes in the region? # \[Total possible points = 5\] # **Scale:** @@ -1315,7 +1433,7 @@ def upgrade(): # 2 - 3 : Some # 4 - 5 : Significant # """, -# 'fr': u"""La proposition présente-t-elle une stratégie claire quant à la manière dont l’organisation bénéficiera du soutien offert par cette initiative et contribue-t-elle aux débats sur les thèmes de l’IA et de la politique en matière de technologies avancées dans la région? +# 'fr': u"""La proposition présente-t-elle une stratégie claire quant à la manière dont l’organisation bénéficiera du soutien offert par cette initiative et contribue-t-elle aux débats sur les thèmes de l’IA et de la politique en matière de technologies avancées dans la région? # \[Total des points possibles = 5\] # **Scale:** diff --git a/api/migrations/versions/351c26992f23_.py b/api/migrations/versions/351c26992f23_.py index 0bebf3c97..b3a9ef528 100644 --- a/api/migrations/versions/351c26992f23_.py +++ b/api/migrations/versions/351c26992f23_.py @@ -7,38 +7,46 @@ """ # revision identifiers, used by Alembic. -revision = '351c26992f23' +revision = "351c26992f23" down_revision = None -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): ### commands auto generated by Alembic - please adjust! ### - op.create_table('app_user', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('email', sa.String(length=255), nullable=True), - sa.Column('password', sa.String(length=255), nullable=True), - sa.Column('active', sa.Boolean(), nullable=True), - sa.Column('is_admin', sa.Boolean(), nullable=True), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('email') + op.create_table( + "app_user", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("email", sa.String(length=255), nullable=True), + sa.Column("password", sa.String(length=255), nullable=True), + sa.Column("active", sa.Boolean(), nullable=True), + sa.Column("is_admin", sa.Boolean(), nullable=True), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("email"), ) - op.create_table('password_reset', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=True), - sa.Column('code', sa.String(length=255), nullable=True), - sa.Column('date', sa.DateTime(), nullable=True), - sa.ForeignKeyConstraint(['user_id'], ['app_user.id'], ), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('code') + op.create_table( + "password_reset", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("user_id", sa.Integer(), nullable=True), + sa.Column("code", sa.String(length=255), nullable=True), + sa.Column("date", sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint( + ["user_id"], + ["app_user.id"], + ), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("code"), ) ### end Alembic commands ### - op.create_index('unique_user_code', 'password_reset', ['user_id', 'code'], unique=True) + op.create_index( + "unique_user_code", "password_reset", ["user_id", "code"], unique=True + ) + def downgrade(): ### commands auto generated by Alembic - please adjust! ### - op.drop_table('password_reset') - op.drop_table('app_user') + op.drop_table("password_reset") + op.drop_table("app_user") ### end Alembic commands ### diff --git a/api/migrations/versions/358d0e0282bb_add_user_comment.py b/api/migrations/versions/358d0e0282bb_add_user_comment.py index d44d63154..01cff7f55 100644 --- a/api/migrations/versions/358d0e0282bb_add_user_comment.py +++ b/api/migrations/versions/358d0e0282bb_add_user_comment.py @@ -7,31 +7,41 @@ """ # revision identifiers, used by Alembic. -revision = '358d0e0282bb' -down_revision = '3dcef2e3c442' +revision = "358d0e0282bb" +down_revision = "3dcef2e3c442" -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.create_table('user_comment', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('event_id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=False), - sa.Column('comment_by_user_id', sa.Integer(), nullable=False), - sa.Column('timestamp', sa.DateTime(), nullable=False), - sa.Column('comment', sa.String(length=2000), nullable=True), - sa.ForeignKeyConstraint(['comment_by_user_id'], ['app_user.id'], ), - sa.ForeignKeyConstraint(['event_id'], ['event.id'], ), - sa.ForeignKeyConstraint(['user_id'], ['app_user.id'], ), - sa.PrimaryKeyConstraint('id') + op.create_table( + "user_comment", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("event_id", sa.Integer(), nullable=False), + sa.Column("user_id", sa.Integer(), nullable=False), + sa.Column("comment_by_user_id", sa.Integer(), nullable=False), + sa.Column("timestamp", sa.DateTime(), nullable=False), + sa.Column("comment", sa.String(length=2000), nullable=True), + sa.ForeignKeyConstraint( + ["comment_by_user_id"], + ["app_user.id"], + ), + sa.ForeignKeyConstraint( + ["event_id"], + ["event.id"], + ), + sa.ForeignKeyConstraint( + ["user_id"], + ["app_user.id"], + ), + sa.PrimaryKeyConstraint("id"), ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('user_comment') + op.drop_table("user_comment") # ### end Alembic commands ### diff --git a/api/migrations/versions/35cdc3c51184_add_validation_text.py b/api/migrations/versions/35cdc3c51184_add_validation_text.py index 6410947eb..c1aa364ba 100644 --- a/api/migrations/versions/35cdc3c51184_add_validation_text.py +++ b/api/migrations/versions/35cdc3c51184_add_validation_text.py @@ -7,26 +7,27 @@ """ # revision identifiers, used by Alembic. -revision = '35cdc3c51184' -down_revision = 'd4a62bfc5be5' +revision = "35cdc3c51184" +down_revision = "d4a62bfc5be5" -from alembic import op import sqlalchemy as sa +from alembic import op from sqlalchemy import orm from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy import orm -from app import db +from app import db Base = declarative_base() class Question(Base): - __tablename__ = 'question' - __table_args__ = {'extend_existing': True} + __tablename__ = "question" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) - section_id = db.Column(db.Integer(), db.ForeignKey('section.id'), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) + section_id = db.Column(db.Integer(), db.ForeignKey("section.id"), nullable=False) type = db.Column(db.String(), nullable=False) description = db.Column(db.String(), nullable=True) headline = db.Column(db.String(), nullable=False) @@ -36,7 +37,19 @@ class Question(Base): options = db.Column(db.JSON(), nullable=True) is_required = db.Column(db.Boolean(), nullable=False) - def __init__(self, application_form_id, section_id, headline, placeholder, order, questionType, validation_regex, is_required = True, description = None, options = None): + def __init__( + self, + application_form_id, + section_id, + headline, + placeholder, + order, + questionType, + validation_regex, + is_required=True, + description=None, + options=None, + ): self.application_form_id = application_form_id self.section_id = section_id self.headline = headline @@ -52,7 +65,7 @@ def __init__(self, application_form_id, section_id, headline, placeholder, order def upgrade(): Base.metadata.bind = op.get_bind() session = orm.Session(bind=Base.metadata.bind) - op.add_column('question', sa.Column('validation_text', sa.String(), nullable=True)) + op.add_column("question", sa.Column("validation_text", sa.String(), nullable=True)) update_question_data() @@ -64,15 +77,16 @@ def update_question(session, question_id, validation_text): def update_question_data(): bind = op.get_bind() session = orm.Session(bind=bind) - update_question(session, 1, 'Enter 50 to 150 words') + update_question(session, 1, "Enter 50 to 150 words") + + update_question(session, 2, "Enter 50 to 150 words") + update_question(session, 3, "Enter a maximum of 80 words") + update_question(session, 4, "Enter a maximum of 80 words") + update_question(session, 5, "Enter a maximum of 150 words") + update_question(session, 9, "Enter a maximum of 150 words") + update_question(session, 16, "Enter a maximum of 150 words") + update_question(session, 18, "Enter a maximum of 150 words") - update_question(session, 2, 'Enter 50 to 150 words') - update_question(session, 3, 'Enter a maximum of 80 words') - update_question(session, 4, 'Enter a maximum of 80 words') - update_question(session, 5, 'Enter a maximum of 150 words') - update_question(session, 9, 'Enter a maximum of 150 words') - update_question(session, 16, 'Enter a maximum of 150 words') - update_question(session, 18, 'Enter a maximum of 150 words') def downgrade(): - op.drop_column('question', 'validation_text') + op.drop_column("question", "validation_text") diff --git a/api/migrations/versions/36235e184d09_.py b/api/migrations/versions/36235e184d09_.py index 27f6e7ae4..8f262ab13 100644 --- a/api/migrations/versions/36235e184d09_.py +++ b/api/migrations/versions/36235e184d09_.py @@ -7,26 +7,58 @@ """ # revision identifiers, used by Alembic. -revision = '36235e184d09' -down_revision = '3ec3ed0eb3c6' +revision = "36235e184d09" +down_revision = "3ec3ed0eb3c6" -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('response', 'nomination_email') - op.drop_column('response', 'nomination_firstname') - op.drop_column('response', 'nomination_title') - op.drop_column('response', 'nomination_lastname') + op.drop_column("response", "nomination_email") + op.drop_column("response", "nomination_firstname") + op.drop_column("response", "nomination_title") + op.drop_column("response", "nomination_lastname") # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.add_column('response', sa.Column('nomination_lastname', sa.VARCHAR(length=100), autoincrement=False, nullable=True)) - op.add_column('response', sa.Column('nomination_title', sa.VARCHAR(length=20), autoincrement=False, nullable=True)) - op.add_column('response', sa.Column('nomination_firstname', sa.VARCHAR(length=100), autoincrement=False, nullable=True)) - op.add_column('response', sa.Column('nomination_email', sa.VARCHAR(length=255), autoincrement=False, nullable=True)) + op.add_column( + "response", + sa.Column( + "nomination_lastname", + sa.VARCHAR(length=100), + autoincrement=False, + nullable=True, + ), + ) + op.add_column( + "response", + sa.Column( + "nomination_title", + sa.VARCHAR(length=20), + autoincrement=False, + nullable=True, + ), + ) + op.add_column( + "response", + sa.Column( + "nomination_firstname", + sa.VARCHAR(length=100), + autoincrement=False, + nullable=True, + ), + ) + op.add_column( + "response", + sa.Column( + "nomination_email", + sa.VARCHAR(length=255), + autoincrement=False, + nullable=True, + ), + ) # ### end Alembic commands ### diff --git a/api/migrations/versions/3745cf17152b_.py b/api/migrations/versions/3745cf17152b_.py index 0dfed593c..3d843f0f4 100644 --- a/api/migrations/versions/3745cf17152b_.py +++ b/api/migrations/versions/3745cf17152b_.py @@ -7,28 +7,35 @@ """ # revision identifiers, used by Alembic. -revision = '3745cf17152b' -down_revision = '358d0e0282bb' +revision = "3745cf17152b" +down_revision = "358d0e0282bb" -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.create_table('invited_guest', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('event_id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=False), - sa.Column('role', sa.String(length=50), nullable=False), - sa.ForeignKeyConstraint(['event_id'], ['event.id'], ), - sa.ForeignKeyConstraint(['user_id'], ['app_user.id'], ), - sa.PrimaryKeyConstraint('id') + op.create_table( + "invited_guest", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("event_id", sa.Integer(), nullable=False), + sa.Column("user_id", sa.Integer(), nullable=False), + sa.Column("role", sa.String(length=50), nullable=False), + sa.ForeignKeyConstraint( + ["event_id"], + ["event.id"], + ), + sa.ForeignKeyConstraint( + ["user_id"], + ["app_user.id"], + ), + sa.PrimaryKeyConstraint("id"), ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('invited_guest') + op.drop_table("invited_guest") # ### end Alembic commands ### diff --git a/api/migrations/versions/37fbee2da7b7_.py b/api/migrations/versions/37fbee2da7b7_.py index e7dcfd2dc..ef9134d73 100644 --- a/api/migrations/versions/37fbee2da7b7_.py +++ b/api/migrations/versions/37fbee2da7b7_.py @@ -7,28 +7,32 @@ """ # revision identifiers, used by Alembic. -revision = '37fbee2da7b7' -down_revision = '72548fc390d4' +revision = "37fbee2da7b7" +down_revision = "72548fc390d4" -from alembic import op import sqlalchemy as sa +from alembic import op from sqlalchemy.dialects import postgresql - -event_type = postgresql.ENUM('AWARD', 'EVENT', name='event_type') +event_type = postgresql.ENUM("AWARD", "EVENT", name="event_type") def upgrade(): # ### commands auto generated by Alembic - please adjust! ### event_type.create(op.get_bind()) - op.add_column('event', sa.Column('event_type', sa.Enum('AWARD', 'EVENT', name='event_type'), nullable=True)) + op.add_column( + "event", + sa.Column( + "event_type", sa.Enum("AWARD", "EVENT", name="event_type"), nullable=True + ), + ) op.execute("UPDATE event SET event_type = 'EVENT'") - op.alter_column('event', 'event_type', nullable=False) + op.alter_column("event", "event_type", nullable=False) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('event', 'event_type') + op.drop_column("event", "event_type") event_type.drop(op.get_bind()) # ### end Alembic commands ### diff --git a/api/migrations/versions/38dde1052c5e_.py b/api/migrations/versions/38dde1052c5e_.py index b41afb3b5..539bafcd1 100644 --- a/api/migrations/versions/38dde1052c5e_.py +++ b/api/migrations/versions/38dde1052c5e_.py @@ -7,50 +7,55 @@ """ # revision identifiers, used by Alembic. -revision = '38dde1052c5e' -down_revision = 'c8d762cf6784' +revision = "38dde1052c5e" +down_revision = "c8d762cf6784" -from alembic import op import sqlalchemy as sa +from alembic import op from sqlalchemy.sql import table + def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - event = op.create_table('event', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('name', sa.String(length=255), nullable=False), - sa.Column('description', sa.String(length=255), nullable=False), - sa.Column('start_date', sa.DateTime(), nullable=False), - sa.Column('end_date', sa.DateTime(), nullable=False), - sa.PrimaryKeyConstraint('id') + event = op.create_table( + "event", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("name", sa.String(length=255), nullable=False), + sa.Column("description", sa.String(length=255), nullable=False), + sa.Column("start_date", sa.DateTime(), nullable=False), + sa.Column("end_date", sa.DateTime(), nullable=False), + sa.PrimaryKeyConstraint("id"), ) - op.create_table('event_role', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('event_id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=False), - sa.Column('role', sa.String(length=50), nullable=False), - sa.ForeignKeyConstraint(['user_id'], ['app_user.id'] ), - sa.ForeignKeyConstraint(['event_id'], ['event.id'] ), - sa.PrimaryKeyConstraint('id') + op.create_table( + "event_role", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("event_id", sa.Integer(), nullable=False), + sa.Column("user_id", sa.Integer(), nullable=False), + sa.Column("role", sa.String(length=50), nullable=False), + sa.ForeignKeyConstraint(["user_id"], ["app_user.id"]), + sa.ForeignKeyConstraint(["event_id"], ["event.id"]), + sa.PrimaryKeyConstraint("id"), ) ### end Alembic commands ### - op.bulk_insert(event, - [ - { - 'id': 1, - 'name': 'indaba 2019', - 'description': """ The Deep Learning Indaba 2019, Kenyatta University, Nairobi, Kenya """, - 'start_date': '25 August 2019', - 'end_date': '31 August 2019' - } - ]) + op.bulk_insert( + event, + [ + { + "id": 1, + "name": "indaba 2019", + "description": """ The Deep Learning Indaba 2019, Kenyatta University, Nairobi, Kenya """, + "start_date": "25 August 2019", + "end_date": "31 August 2019", + } + ], + ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('event_role') - op.drop_table('event') + op.drop_table("event_role") + op.drop_table("event") # ### end Alembic commands ### diff --git a/api/migrations/versions/3ab678fc66cd_populate_reg_form.py b/api/migrations/versions/3ab678fc66cd_populate_reg_form.py index 94b251ffd..556703592 100644 --- a/api/migrations/versions/3ab678fc66cd_populate_reg_form.py +++ b/api/migrations/versions/3ab678fc66cd_populate_reg_form.py @@ -7,86 +7,82 @@ """ # revision identifiers, used by Alembic. -revision = '3ab678fc66cd' -down_revision = 'a5480ca6646b' +revision = "3ab678fc66cd" +down_revision = "a5480ca6646b" -from alembic import op import sqlalchemy as sa -from sqlalchemy.sql import table, column +from alembic import op +from sqlalchemy.sql import column, table def upgrade(): - op.alter_column('registration_section', 'show_for_travel_award', - nullable=True) - op.alter_column('registration_section', 'show_for_accommodation_award', - nullable=True) - op.alter_column('registration_section', 'show_for_payment_required', - nullable=True) - - reg_form = table('registration_form', - column('id', sa.Integer()), - column('event_id', sa.Integer())) + op.alter_column("registration_section", "show_for_travel_award", nullable=True) + op.alter_column( + "registration_section", "show_for_accommodation_award", nullable=True + ) + op.alter_column("registration_section", "show_for_payment_required", nullable=True) - section = table('registration_section', - column('id', sa.Integer()), - column('registration_form_id', sa.Integer()), - column('name', sa.String()), - column('description', sa.String()), - column('order', sa.Integer()), - column('show_for_travel_award', sa.Boolean()), - column('show_for_accommodation_award', sa.Boolean()), - column('show_for_payment_required', sa.Boolean())) + reg_form = table( + "registration_form", + column("id", sa.Integer()), + column("event_id", sa.Integer()), + ) - question = table('registration_question', - column('id', sa.Integer()), - column('registration_form_id', sa.Integer()), - column('section_id', sa.Integer()), - column('type', sa.String()), - column('description', sa.String()), - column('headline', sa.String()), - column('placeholder', sa.String()), - column('validation_regex', sa.String()), - column('validation_text', sa.String()), - column('order', sa.Integer()), - column('options', sa.JSON()), - column('is_required', sa.Boolean()), - column('depends_on_question_id', sa.Integer()), - column('hide_for_dependent_value', sa.String()), - column('required_value', sa.String()) - ) + section = table( + "registration_section", + column("id", sa.Integer()), + column("registration_form_id", sa.Integer()), + column("name", sa.String()), + column("description", sa.String()), + column("order", sa.Integer()), + column("show_for_travel_award", sa.Boolean()), + column("show_for_accommodation_award", sa.Boolean()), + column("show_for_payment_required", sa.Boolean()), + ) - op.bulk_insert( - reg_form, - [ - { - 'id': 1, - 'event_id': 1 - } - ] + question = table( + "registration_question", + column("id", sa.Integer()), + column("registration_form_id", sa.Integer()), + column("section_id", sa.Integer()), + column("type", sa.String()), + column("description", sa.String()), + column("headline", sa.String()), + column("placeholder", sa.String()), + column("validation_regex", sa.String()), + column("validation_text", sa.String()), + column("order", sa.Integer()), + column("options", sa.JSON()), + column("is_required", sa.Boolean()), + column("depends_on_question_id", sa.Integer()), + column("hide_for_dependent_value", sa.String()), + column("required_value", sa.String()), ) + op.bulk_insert(reg_form, [{"id": 1, "event_id": 1}]) + op.bulk_insert( section, [ { - 'id': 1, - 'registration_form_id': 1, - 'name': 'Personal Details', - 'description': '''This is the registration form for candidates who have been accepted to attend the Deep Learning Indaba 2019. It should take about 5 minutes to complete. + "id": 1, + "registration_form_id": 1, + "name": "Personal Details", + "description": """This is the registration form for candidates who have been accepted to attend the Deep Learning Indaba 2019. It should take about 5 minutes to complete. Data usage: We will use the information you provide in this form for registration and coordination purposes for the Deep Learning Indaba 2019. This may involve sharing your contact details with third-party service providers for the purposes of event management and organisation. -Due Date: Please complete and submit this form by 20 July 2019.''', - 'order': 1, - 'show_for_travel_award': None, - 'show_for_accommodation_award': None, - 'show_for_payment_required': None +Due Date: Please complete and submit this form by 20 July 2019.""", + "order": 1, + "show_for_travel_award": None, + "show_for_accommodation_award": None, + "show_for_payment_required": None, }, { - 'id': 2, - 'registration_form_id': 1, - 'name': 'Code of Conduct', - 'description': '''We ask that you recall a central principle of African philosophy, whether you know it as ujamaa (Swahili), umuntu (Chichewa), ubuntu (Zulu), unhu (Shona), djema'a (Arabic), or through the many other words used across our continent: the philosophy of familyhood and unity. Our duty, as we come together at the Indaba, is to create a familyhood of people and cultures and learning, built on the principles of freedom, equality and unity; and towards the aim of strengthening African machine learning. + "id": 2, + "registration_form_id": 1, + "name": "Code of Conduct", + "description": """We ask that you recall a central principle of African philosophy, whether you know it as ujamaa (Swahili), umuntu (Chichewa), ubuntu (Zulu), unhu (Shona), djema'a (Arabic), or through the many other words used across our continent: the philosophy of familyhood and unity. Our duty, as we come together at the Indaba, is to create a familyhood of people and cultures and learning, built on the principles of freedom, equality and unity; and towards the aim of strengthening African machine learning. This means that every participant is responsible for providing a safe experience for all participants, regardless of gender, gender identity and expression, sexual orientation, ability, physical appearance, body size, race, ethnicity, nationality, age, religion, or socioeconomic status. As organisers, we are committed to these responsibilities and will not tolerate harassment of Indaba participants in any form. @@ -94,353 +90,381 @@ def upgrade(): If a participant engages in behaviour that breaks this code of conduct, the organisers retain the right to take any actions needed to keep the Indaba a welcoming environment for all participants. You can report a concern or an incident through an anonymous report, or in person. -For details of expected conduct, unacceptable behaviour, how to report an incident, how we maintain a respectful community, and how we enforce these standards, please go to deeplearningindaba.com/conduct or send a message to conduct@deeplearningindaba.com.''', - 'order': 2, - 'show_for_travel_award': None, - 'show_for_accommodation_award': None, - 'show_for_payment_required': None +For details of expected conduct, unacceptable behaviour, how to report an incident, how we maintain a respectful community, and how we enforce these standards, please go to deeplearningindaba.com/conduct or send a message to conduct@deeplearningindaba.com.""", + "order": 2, + "show_for_travel_award": None, + "show_for_accommodation_award": None, + "show_for_payment_required": None, }, { - 'id': 3, - 'registration_form_id': 1, - 'name': 'Posters', - 'description': 'If you would like to showcase some of the work you have been doing at your institution and solicit feedback from leading researchers and industry experts, we encourage you to take part in our poster sessions! ', - 'order': 3, - 'show_for_travel_award': None, - 'show_for_accommodation_award': None, - 'show_for_payment_required': None + "id": 3, + "registration_form_id": 1, + "name": "Posters", + "description": "If you would like to showcase some of the work you have been doing at your institution and solicit feedback from leading researchers and industry experts, we encourage you to take part in our poster sessions! ", + "order": 3, + "show_for_travel_award": None, + "show_for_accommodation_award": None, + "show_for_payment_required": None, }, { - 'id': 4, - 'registration_form_id': 1, - 'name': 'Attendance Information', - 'description': 'Some more information to help our planning.', - 'order': 10, - 'show_for_travel_award': None, - 'show_for_accommodation_award': None, - 'show_for_payment_required': None + "id": 4, + "registration_form_id": 1, + "name": "Attendance Information", + "description": "Some more information to help our planning.", + "order": 10, + "show_for_travel_award": None, + "show_for_accommodation_award": None, + "show_for_payment_required": None, }, { - 'id': 5, - 'registration_form_id': 1, - 'name': 'Registration Fee', - 'description': '''To reduce the barrier to participation, we ask no registration fees for students. We do charge a registration fee of USD 350 for participants from industry and academic faculty. + "id": 5, + "registration_form_id": 1, + "name": "Registration Fee", + "description": """To reduce the barrier to participation, we ask no registration fees for students. We do charge a registration fee of USD 350 for participants from industry and academic faculty. -Payments will be taken through a secure payment portal, and receipts provided for all payments. Payment details will be sent in a separate email, or you can check for deeplearningindaba.com/payments for updates.''', - 'order': 5, - 'show_for_travel_award': None, - 'show_for_accommodation_award': None, - 'show_for_payment_required': True +Payments will be taken through a secure payment portal, and receipts provided for all payments. Payment details will be sent in a separate email, or you can check for deeplearningindaba.com/payments for updates.""", + "order": 5, + "show_for_travel_award": None, + "show_for_accommodation_award": None, + "show_for_payment_required": True, }, { - 'id': 6, - 'registration_form_id': 1, - 'name': 'Registration Fee', - 'description': 'To reduce the barrier to participation, we ask no registration fees for students. We do charge a registration fee of USD 350 for participants from industry and academic faculty. ', - 'order': 6, - 'show_for_travel_award': None, - 'show_for_accommodation_award': None, - 'show_for_payment_required': False + "id": 6, + "registration_form_id": 1, + "name": "Registration Fee", + "description": "To reduce the barrier to participation, we ask no registration fees for students. We do charge a registration fee of USD 350 for participants from industry and academic faculty. ", + "order": 6, + "show_for_travel_award": None, + "show_for_accommodation_award": None, + "show_for_payment_required": False, }, { - 'id': 7, - 'registration_form_id': 1, - 'name': 'Travel Award', - 'description': 'You have accepted a travel award and the Indaba will fund your travel to and from Nairobi. We will book these flights on your behalf. AIrport transfers will also be available for those arriving on certain days, more information about this will be available on the website closer to the event. If you choose to travel on different dates you will be responsibile for your own airport transfers.', - 'order': 7, - 'show_for_travel_award': True, - 'show_for_accommodation_award': None, - 'show_for_payment_required': None + "id": 7, + "registration_form_id": 1, + "name": "Travel Award", + "description": "You have accepted a travel award and the Indaba will fund your travel to and from Nairobi. We will book these flights on your behalf. AIrport transfers will also be available for those arriving on certain days, more information about this will be available on the website closer to the event. If you choose to travel on different dates you will be responsibile for your own airport transfers.", + "order": 7, + "show_for_travel_award": True, + "show_for_accommodation_award": None, + "show_for_payment_required": None, }, { - 'id': 8, - 'registration_form_id': 1, - 'name': 'Accommodation Award', - 'description': 'You have accepted an accommodation award in a shared room at Nyayo Hostel on the Kenyatta University Campus. You will only share with someone of the same gender as indicated in your user profile. The Indaba will provide accommodation for the nights of the 25th of August to the 31st of August. You will be responsible for your own accommodation outside of these dates should you decide to arrive earlier or leave later. ', - 'order': 8, - 'show_for_travel_award': None, - 'show_for_accommodation_award': False, - 'show_for_payment_required': None + "id": 8, + "registration_form_id": 1, + "name": "Accommodation Award", + "description": "You have accepted an accommodation award in a shared room at Nyayo Hostel on the Kenyatta University Campus. You will only share with someone of the same gender as indicated in your user profile. The Indaba will provide accommodation for the nights of the 25th of August to the 31st of August. You will be responsible for your own accommodation outside of these dates should you decide to arrive earlier or leave later. ", + "order": 8, + "show_for_travel_award": None, + "show_for_accommodation_award": False, + "show_for_payment_required": None, }, - ] + ], ) op.bulk_insert( question, [ { - 'id': 1, - 'registration_form_id': 1, - 'section_id': 1, - 'type': 'short-text', - 'description': 'Including country code.', - 'headline': 'Cellphone Number', - 'placeholder': 'Cellphone Number', - 'validation_regex': None, - 'validation_text': None, - 'order': 1, - 'options': None, - 'is_required': True, - 'depends_on_question_id': None, - 'hide_for_dependent_value': None, - 'required_value': None, + "id": 1, + "registration_form_id": 1, + "section_id": 1, + "type": "short-text", + "description": "Including country code.", + "headline": "Cellphone Number", + "placeholder": "Cellphone Number", + "validation_regex": None, + "validation_text": None, + "order": 1, + "options": None, + "is_required": True, + "depends_on_question_id": None, + "hide_for_dependent_value": None, + "required_value": None, }, { - 'id': 2, - 'registration_form_id': 1, - 'section_id': 2, - 'type': 'single-choice', - 'description': '', - 'headline': 'I have read, understood and agree with the Code of Conduct', - 'placeholder': '', - 'validation_regex': '1', - 'validation_text': 'You must agree to the Code of Conduct.', - 'order': 1, - 'options': None, - 'is_required': True, - 'depends_on_question_id': None, - 'hide_for_dependent_value': None, - 'required_value': '1', + "id": 2, + "registration_form_id": 1, + "section_id": 2, + "type": "single-choice", + "description": "", + "headline": "I have read, understood and agree with the Code of Conduct", + "placeholder": "", + "validation_regex": "1", + "validation_text": "You must agree to the Code of Conduct.", + "order": 1, + "options": None, + "is_required": True, + "depends_on_question_id": None, + "hide_for_dependent_value": None, + "required_value": "1", }, { - 'id': 3, - 'registration_form_id': 1, - 'section_id': 3, - 'type': 'multi-choice', - 'description': '', - 'headline': 'Will you be bringing a poster?', - 'placeholder': '', - 'validation_regex': None, - 'validation_text': None, - 'order': 1, - 'options': [{"value": "yes", "label": "Yes"}, {"value": "no", "label": "No"}], - 'is_required': True, - 'depends_on_question_id': None, - 'hide_for_dependent_value': None, - 'required_value': None, + "id": 3, + "registration_form_id": 1, + "section_id": 3, + "type": "multi-choice", + "description": "", + "headline": "Will you be bringing a poster?", + "placeholder": "", + "validation_regex": None, + "validation_text": None, + "order": 1, + "options": [ + {"value": "yes", "label": "Yes"}, + {"value": "no", "label": "No"}, + ], + "is_required": True, + "depends_on_question_id": None, + "hide_for_dependent_value": None, + "required_value": None, }, { - 'id': 4, - 'registration_form_id': 1, - 'section_id': 3, - 'type': 'short-text', - 'description': "You can come back and complete this later if you don't have a title yet.", - 'headline': 'What is the provisional title of your poster?', - 'placeholder': 'Poster Title', - 'validation_regex': None, - 'validation_text': None, - 'order': 2, - 'options': None, - 'is_required': False, - 'depends_on_question_id': 3, - 'hide_for_dependent_value': 'no', - 'required_value': None, + "id": 4, + "registration_form_id": 1, + "section_id": 3, + "type": "short-text", + "description": "You can come back and complete this later if you don't have a title yet.", + "headline": "What is the provisional title of your poster?", + "placeholder": "Poster Title", + "validation_regex": None, + "validation_text": None, + "order": 2, + "options": None, + "is_required": False, + "depends_on_question_id": 3, + "hide_for_dependent_value": "no", + "required_value": None, }, { - 'id': 5, - 'registration_form_id': 1, - 'section_id': 3, - 'type': 'file', - 'description': "", - 'headline': 'Upload your poster PDF', - 'placeholder': '', - 'validation_regex': None, - 'validation_text': None, - 'order': 3, - 'options': None, - 'is_required': False, - 'depends_on_question_id': 3, - 'hide_for_dependent_value': 'no', - 'required_value': None, + "id": 5, + "registration_form_id": 1, + "section_id": 3, + "type": "file", + "description": "", + "headline": "Upload your poster PDF", + "placeholder": "", + "validation_regex": None, + "validation_text": None, + "order": 3, + "options": None, + "is_required": False, + "depends_on_question_id": 3, + "hide_for_dependent_value": "no", + "required_value": None, }, { - 'id': 6, - 'registration_form_id': 1, - 'section_id': 4, - 'type': 'multi-choice', - 'description': "", - 'headline': 'Do you have any dietary requirements?', - 'placeholder': '', - 'validation_regex': None, - 'validation_text': None, - 'order': 1, - 'options': [{"value": "vegetarian", "label": "Vegetarian"}, {"value": "vegan", "label": "Vegan"}, {"value": "halaal", "label": "Halaal"}, {"value": "kosher", "label": "Kosher"}, {"value": "gluten-free", "label": "Gluten Free"}, {"value": "other", "label": "Other"}], - 'is_required': False, - 'depends_on_question_id': None, - 'hide_for_dependent_value': None, - 'required_value': None, + "id": 6, + "registration_form_id": 1, + "section_id": 4, + "type": "multi-choice", + "description": "", + "headline": "Do you have any dietary requirements?", + "placeholder": "", + "validation_regex": None, + "validation_text": None, + "order": 1, + "options": [ + {"value": "vegetarian", "label": "Vegetarian"}, + {"value": "vegan", "label": "Vegan"}, + {"value": "halaal", "label": "Halaal"}, + {"value": "kosher", "label": "Kosher"}, + {"value": "gluten-free", "label": "Gluten Free"}, + {"value": "other", "label": "Other"}, + ], + "is_required": False, + "depends_on_question_id": None, + "hide_for_dependent_value": None, + "required_value": None, }, { - 'id': 7, - 'registration_form_id': 1, - 'section_id': 4, - 'type': 'short-text', - 'description': 'If you answered "Other" to the dietary requirements question, please give us more details.', - 'headline': 'Other Details', - 'placeholder': '', - 'validation_regex': None, - 'validation_text': None, - 'order': 2, - 'options': None, - 'is_required': False, - 'depends_on_question_id': None, - 'hide_for_dependent_value': None, - 'required_value': None, + "id": 7, + "registration_form_id": 1, + "section_id": 4, + "type": "short-text", + "description": 'If you answered "Other" to the dietary requirements question, please give us more details.', + "headline": "Other Details", + "placeholder": "", + "validation_regex": None, + "validation_text": None, + "order": 2, + "options": None, + "is_required": False, + "depends_on_question_id": None, + "hide_for_dependent_value": None, + "required_value": None, }, { - 'id': 8, - 'registration_form_id': 1, - 'section_id': 4, - 'type': 'multi-choice', - 'description': '', - 'headline': 'T-Shirt Size', - 'placeholder': '', - 'validation_regex': None, - 'validation_text': None, - 'order': 3, - 'options': [{"value": "S", "label": "S"}, {"value": "M", "label": "M"}, {"value": "L", "label": "L"}, {"value": "XL", "label": "XL"}], - 'is_required': True, - 'depends_on_question_id': None, - 'hide_for_dependent_value': None, - 'required_value': None, + "id": 8, + "registration_form_id": 1, + "section_id": 4, + "type": "multi-choice", + "description": "", + "headline": "T-Shirt Size", + "placeholder": "", + "validation_regex": None, + "validation_text": None, + "order": 3, + "options": [ + {"value": "S", "label": "S"}, + {"value": "M", "label": "M"}, + {"value": "L", "label": "L"}, + {"value": "XL", "label": "XL"}, + ], + "is_required": True, + "depends_on_question_id": None, + "hide_for_dependent_value": None, + "required_value": None, }, { - 'id': 9, - 'registration_form_id': 1, - 'section_id': 5, - 'type': 'single-choice', - 'description': '', - 'headline': 'I agree to the payment of a USD 350 registration fee.', - 'placeholder': '', - 'validation_regex': '1', - 'validation_text': 'You must agree to the payment of USD 350 in order to register.', - 'order': 1, - 'options': None, - 'is_required': True, - 'depends_on_question_id': None, - 'hide_for_dependent_value': None, - 'required_value': '1', + "id": 9, + "registration_form_id": 1, + "section_id": 5, + "type": "single-choice", + "description": "", + "headline": "I agree to the payment of a USD 350 registration fee.", + "placeholder": "", + "validation_regex": "1", + "validation_text": "You must agree to the payment of USD 350 in order to register.", + "order": 1, + "options": None, + "is_required": True, + "depends_on_question_id": None, + "hide_for_dependent_value": None, + "required_value": "1", }, { - 'id': 10, - 'registration_form_id': 1, - 'section_id': 6, - 'type': 'single-choice', - 'description': 'In order to waive the registration fee, please confirm that you are currently a STUDENT or Unemployed. If your status has changed, please get in touch with us at info@deeplearningindaba.com', - 'headline': 'Are you currently a student or unemployed?', - 'placeholder': '', - 'validation_regex': '1', - 'validation_text': 'You must confirm you are student or contact us at info@deeplearningindaba.com', - 'order': 1, - 'options': None, - 'is_required': True, - 'depends_on_question_id': None, - 'hide_for_dependent_value': None, - 'required_value': None, + "id": 10, + "registration_form_id": 1, + "section_id": 6, + "type": "single-choice", + "description": "In order to waive the registration fee, please confirm that you are currently a STUDENT or Unemployed. If your status has changed, please get in touch with us at info@deeplearningindaba.com", + "headline": "Are you currently a student or unemployed?", + "placeholder": "", + "validation_regex": "1", + "validation_text": "You must confirm you are student or contact us at info@deeplearningindaba.com", + "order": 1, + "options": None, + "is_required": True, + "depends_on_question_id": None, + "hide_for_dependent_value": None, + "required_value": None, }, { - 'id': 11, - 'registration_form_id': 1, - 'section_id': 7, - 'type': 'file', - 'description': '', - 'headline': 'Upload a copy of the photo page of your passport', - 'placeholder': '', - 'validation_regex': None, - 'validation_text': None, - 'order': 1, - 'options': None, - 'is_required': True, - 'depends_on_question_id': None, - 'hide_for_dependent_value': None, - 'required_value': None, + "id": 11, + "registration_form_id": 1, + "section_id": 7, + "type": "file", + "description": "", + "headline": "Upload a copy of the photo page of your passport", + "placeholder": "", + "validation_regex": None, + "validation_text": None, + "order": 1, + "options": None, + "is_required": True, + "depends_on_question_id": None, + "hide_for_dependent_value": None, + "required_value": None, }, { - 'id': 12, - 'registration_form_id': 1, - 'section_id': 7, - 'type': 'short-text', - 'description': '(e.g. O.R. Tambo International Airport)', - 'headline': 'Please confirm your chosen airport of departure.', - 'placeholder': 'Airport of Departure', - 'validation_regex': None, - 'validation_text': None, - 'order': 2, - 'options': None, - 'is_required': True, - 'depends_on_question_id': None, - 'hide_for_dependent_value': None, - 'required_value': None, + "id": 12, + "registration_form_id": 1, + "section_id": 7, + "type": "short-text", + "description": "(e.g. O.R. Tambo International Airport)", + "headline": "Please confirm your chosen airport of departure.", + "placeholder": "Airport of Departure", + "validation_regex": None, + "validation_text": None, + "order": 2, + "options": None, + "is_required": True, + "depends_on_question_id": None, + "hide_for_dependent_value": None, + "required_value": None, }, { - 'id': 13, - 'registration_form_id': 1, - 'section_id': 7, - 'type': 'date', - 'description': "If you'd like to stay in Nairobi for longer, and not arrive on Sunday the 25th of August, please indicate your preferred arrival date.", - 'headline': 'Preferred Arrival Date in Nairobi.', - 'placeholder': 'Arrival Date', - 'validation_regex': None, - 'validation_text': None, - 'order': 3, - 'options': None, - 'is_required': False, - 'depends_on_question_id': None, - 'hide_for_dependent_value': None, - 'required_value': None, + "id": 13, + "registration_form_id": 1, + "section_id": 7, + "type": "date", + "description": "If you'd like to stay in Nairobi for longer, and not arrive on Sunday the 25th of August, please indicate your preferred arrival date.", + "headline": "Preferred Arrival Date in Nairobi.", + "placeholder": "Arrival Date", + "validation_regex": None, + "validation_text": None, + "order": 3, + "options": None, + "is_required": False, + "depends_on_question_id": None, + "hide_for_dependent_value": None, + "required_value": None, }, { - 'id': 14, - 'registration_form_id': 1, - 'section_id': 7, - 'type': 'date', - 'description': "If you'd like to stay in Nairobi for longer, and not depart on Saturday 31st August, please indicate your preferred departure date.", - 'headline': 'Preferred Departure Date.', - 'placeholder': 'Departure Date', - 'validation_regex': None, - 'validation_text': None, - 'order': 4, - 'options': None, - 'is_required': False, - 'depends_on_question_id': None, - 'hide_for_dependent_value': None, - 'required_value': None, + "id": 14, + "registration_form_id": 1, + "section_id": 7, + "type": "date", + "description": "If you'd like to stay in Nairobi for longer, and not depart on Saturday 31st August, please indicate your preferred departure date.", + "headline": "Preferred Departure Date.", + "placeholder": "Departure Date", + "validation_regex": None, + "validation_text": None, + "order": 4, + "options": None, + "is_required": False, + "depends_on_question_id": None, + "hide_for_dependent_value": None, + "required_value": None, }, { - 'id': 15, - 'registration_form_id': 1, - 'section_id': 8, - 'type': 'long-text', - 'description': "", - 'headline': 'Please let us know if you have any special needs relating to your accommodation that we should be aware of?', - 'placeholder': 'Special Accommodation Requirements', - 'validation_regex': None, - 'validation_text': None, - 'order': 1, - 'options': None, - 'is_required': False, - 'depends_on_question_id': None, - 'hide_for_dependent_value': None, - 'required_value': None, + "id": 15, + "registration_form_id": 1, + "section_id": 8, + "type": "long-text", + "description": "", + "headline": "Please let us know if you have any special needs relating to your accommodation that we should be aware of?", + "placeholder": "Special Accommodation Requirements", + "validation_regex": None, + "validation_text": None, + "order": 1, + "options": None, + "is_required": False, + "depends_on_question_id": None, + "hide_for_dependent_value": None, + "required_value": None, }, - ] + ], ) - op.get_bind().execute("""SELECT setval('registration_form_id_seq', (SELECT max(id) FROM registration_form));""") - op.get_bind().execute("""SELECT setval('registration_section_id_seq', (SELECT max(id) FROM registration_section));""") - op.get_bind().execute("""SELECT setval('registration_question_id_seq', (SELECT max(id) FROM registration_question));""") + op.get_bind().execute( + """SELECT setval('registration_form_id_seq', (SELECT max(id) FROM registration_form));""" + ) + op.get_bind().execute( + """SELECT setval('registration_section_id_seq', (SELECT max(id) FROM registration_section));""" + ) + op.get_bind().execute( + """SELECT setval('registration_question_id_seq', (SELECT max(id) FROM registration_question));""" + ) def downgrade(): - op.get_bind().execute('DELETE FROM registration_question; DELETE FROM registration_section; DELETE FROM registration_form;') + op.get_bind().execute( + "DELETE FROM registration_question; DELETE FROM registration_section; DELETE FROM registration_form;" + ) - op.get_bind().execute("""SELECT setval('registration_form_id_seq', (SELECT max(id) FROM registration_form));""") - op.get_bind().execute("""SELECT setval('registration_section_id_seq', (SELECT max(id) FROM registration_section));""") - op.get_bind().execute("""SELECT setval('registration_question_id_seq', (SELECT max(id) FROM registration_question));""") + op.get_bind().execute( + """SELECT setval('registration_form_id_seq', (SELECT max(id) FROM registration_form));""" + ) + op.get_bind().execute( + """SELECT setval('registration_section_id_seq', (SELECT max(id) FROM registration_section));""" + ) + op.get_bind().execute( + """SELECT setval('registration_question_id_seq', (SELECT max(id) FROM registration_question));""" + ) - op.alter_column('registration_section', 'show_for_travel_award', - nullable=False) - op.alter_column('registration_section', 'show_for_accommodation_award', - nullable=False) - op.alter_column('registration_section', 'show_for_payment_required', - nullable=False) + op.alter_column("registration_section", "show_for_travel_award", nullable=False) + op.alter_column( + "registration_section", "show_for_accommodation_award", nullable=False + ) + op.alter_column("registration_section", "show_for_payment_required", nullable=False) diff --git a/api/migrations/versions/3bc5355f159c_add_org_url_and_event_fields.py b/api/migrations/versions/3bc5355f159c_add_org_url_and_event_fields.py index 73a82cc91..d1c552819 100644 --- a/api/migrations/versions/3bc5355f159c_add_org_url_and_event_fields.py +++ b/api/migrations/versions/3bc5355f159c_add_org_url_and_event_fields.py @@ -7,44 +7,56 @@ """ # revision identifiers, used by Alembic. -revision = '3bc5355f159c' -down_revision = '99cfdad9869c' +revision = "3bc5355f159c" +down_revision = "99cfdad9869c" -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.add_column('event', sa.Column('email_from', sa.String(length=255), nullable=True)) - op.add_column('event', sa.Column('key', sa.String(length=255), nullable=True)) - op.add_column('event', sa.Column('organisation_id', sa.Integer(), nullable=True)) - op.add_column('event', sa.Column('url', sa.String(length=255), nullable=True)) - op.create_unique_constraint('unique_event_key', 'event', ['key']) - op.create_foreign_key('fk_event_organisation', 'event', 'organisation', ['organisation_id'], ['id']) - op.add_column('organisation', sa.Column('url', sa.String(length=100), nullable=True)) - - op.execute("""UPDATE organisation SET url = 'http://www.deeplearningindaba.com' WHERE id = 1""") + op.add_column( + "event", sa.Column("email_from", sa.String(length=255), nullable=True) + ) + op.add_column("event", sa.Column("key", sa.String(length=255), nullable=True)) + op.add_column("event", sa.Column("organisation_id", sa.Integer(), nullable=True)) + op.add_column("event", sa.Column("url", sa.String(length=255), nullable=True)) + op.create_unique_constraint("unique_event_key", "event", ["key"]) + op.create_foreign_key( + "fk_event_organisation", "event", "organisation", ["organisation_id"], ["id"] + ) + op.add_column( + "organisation", sa.Column("url", sa.String(length=100), nullable=True) + ) + + op.execute( + """UPDATE organisation SET url = 'http://www.deeplearningindaba.com' WHERE id = 1""" + ) op.execute("""UPDATE organisation SET url = 'http://eeml.eu' WHERE id = 2""") - op.execute("""UPDATE organisation SET url = 'http://www.deeplearningindaba.com' WHERE id = 3""") - - op.execute("""UPDATE event SET email_from = 'baobab@deeplearningindaba.com', key='indaba2019', organisation_id=1, url='www.deeplearningindaba.com' WHERE id = 1""") - - op.alter_column('event', 'email_from', nullable=False) - op.alter_column('event', 'key', nullable=False) - op.alter_column('event', 'organisation_id', nullable=False) - op.alter_column('event', 'url', nullable=False) - op.alter_column('organisation', 'url', nullable=False) + op.execute( + """UPDATE organisation SET url = 'http://www.deeplearningindaba.com' WHERE id = 3""" + ) + + op.execute( + """UPDATE event SET email_from = 'baobab@deeplearningindaba.com', key='indaba2019', organisation_id=1, url='www.deeplearningindaba.com' WHERE id = 1""" + ) + + op.alter_column("event", "email_from", nullable=False) + op.alter_column("event", "key", nullable=False) + op.alter_column("event", "organisation_id", nullable=False) + op.alter_column("event", "url", nullable=False) + op.alter_column("organisation", "url", nullable=False) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('organisation', 'url') - op.drop_constraint('fk_event_organisation', 'event', type_='foreignkey') - op.drop_constraint('unique_event_key', 'event', type_='unique') - op.drop_column('event', 'url') - op.drop_column('event', 'organisation_id') - op.drop_column('event', 'key') - op.drop_column('event', 'email_from') + op.drop_column("organisation", "url") + op.drop_constraint("fk_event_organisation", "event", type_="foreignkey") + op.drop_constraint("unique_event_key", "event", type_="unique") + op.drop_column("event", "url") + op.drop_column("event", "organisation_id") + op.drop_column("event", "key") + op.drop_column("event", "email_from") # ### end Alembic commands ### diff --git a/api/migrations/versions/3dcef2e3c442_.py b/api/migrations/versions/3dcef2e3c442_.py index 27dd83429..8e2065568 100644 --- a/api/migrations/versions/3dcef2e3c442_.py +++ b/api/migrations/versions/3dcef2e3c442_.py @@ -7,22 +7,22 @@ """ # revision identifiers, used by Alembic. -revision = '3dcef2e3c442' -down_revision = '1349a2c924f4' +revision = "3dcef2e3c442" +down_revision = "1349a2c924f4" -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_constraint('section_name_key', 'section', type_='unique') - op.drop_constraint('uq_section_order', 'section', type_='unique') + op.drop_constraint("section_name_key", "section", type_="unique") + op.drop_constraint("uq_section_order", "section", type_="unique") # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.create_unique_constraint('uq_section_order', 'section', ['order']) - op.create_unique_constraint('section_name_key', 'section', ['name']) + op.create_unique_constraint("uq_section_order", "section", ["order"]) + op.create_unique_constraint("section_name_key", "section", ["name"]) # ### end Alembic commands ### diff --git a/api/migrations/versions/3e7bdff8af80_eeml_form_updates.py b/api/migrations/versions/3e7bdff8af80_eeml_form_updates.py index e525df11b..b72c1c188 100644 --- a/api/migrations/versions/3e7bdff8af80_eeml_form_updates.py +++ b/api/migrations/versions/3e7bdff8af80_eeml_form_updates.py @@ -7,26 +7,30 @@ """ # revision identifiers, used by Alembic. -revision = '3e7bdff8af80' -down_revision = '20a94c6b2952' +revision = "3e7bdff8af80" +down_revision = "20a94c6b2952" + +import datetime -from alembic import op import sqlalchemy as sa +from alembic import op from sqlalchemy import orm from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy import orm + from app import db -import datetime Base = declarative_base() + class Question(Base): - __tablename__ = 'question' - __table_args__ = {'extend_existing': True} + __tablename__ = "question" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) - section_id = db.Column(db.Integer(), db.ForeignKey('section.id'), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) + section_id = db.Column(db.Integer(), db.ForeignKey("section.id"), nullable=False) type = db.Column(db.String(), nullable=False) description = db.Column(db.String(), nullable=True) headline = db.Column(db.String(), nullable=False) @@ -37,7 +41,20 @@ class Question(Base): options = db.Column(db.JSON(), nullable=True) is_required = db.Column(db.Boolean(), nullable=False) - def __init__(self, application_form_id, section_id, headline, placeholder, order, questionType, validation_regex, validation_text=None, is_required = True, description = None, options = None): + def __init__( + self, + application_form_id, + section_id, + headline, + placeholder, + order, + questionType, + validation_regex, + validation_text=None, + is_required=True, + description=None, + options=None, + ): self.application_form_id = application_form_id self.section_id = section_id self.headline = headline @@ -55,7 +72,8 @@ def upgrade(): Base.metadata.bind = op.get_bind() session = orm.Session(bind=Base.metadata.bind) - op.execute(""" + op.execute( + """ UPDATE email_template SET template = 'Dear {title} {firstname} {lastname}, This email serves to confirm that you have withdrawn your application to attend {event_name}. @@ -64,20 +82,37 @@ def upgrade(): The {event_name} Team' WHERE key='withdrawal' AND event_id IS NULL -""") +""" + ) - op.execute("UPDATE question SET description='Your university or company' WHERE headline='Affiliation' and application_form_id=2") + op.execute( + "UPDATE question SET description='Your university or company' WHERE headline='Affiliation' and application_form_id=2" + ) - company_question = session.query(Question).filter_by(headline='Institution or Company', application_form_id=2).first() + company_question = ( + session.query(Question) + .filter_by(headline="Institution or Company", application_form_id=2) + .first() + ) if company_question: - op.execute("DELETE FROM answer WHERE question_id={}".format(company_question.id)) + op.execute( + "DELETE FROM answer WHERE question_id={}".format(company_question.id) + ) op.execute("DELETE FROM question WHERE id={}".format(company_question.id)) - op.execute("UPDATE question set description='The country where you work or study.' WHERE headline='Country' AND application_form_id=2") - op.execute("UPDATE question set is_required=True WHERE headline='Extended abstract type' and application_form_id=2") - op.execute("UPDATE question set is_required=True WHERE headline='Extended abstract' and application_form_id=2") - op.execute("""UPDATE question set options='[{"value": "TMLSS2018", "label": "TMLSS 2018"}, {"value": "EEML2019", "label": "EEML 2019"}, {"value": "TMLSS2018-EEML2019", "label": "Both"}, {"value": "none", "label": "Neither"}]' WHERE headline='Previous attendance' AND application_form_id=2""") + op.execute( + "UPDATE question set description='The country where you work or study.' WHERE headline='Country' AND application_form_id=2" + ) + op.execute( + "UPDATE question set is_required=True WHERE headline='Extended abstract type' and application_form_id=2" + ) + op.execute( + "UPDATE question set is_required=True WHERE headline='Extended abstract' and application_form_id=2" + ) + op.execute( + """UPDATE question set options='[{"value": "TMLSS2018", "label": "TMLSS 2018"}, {"value": "EEML2019", "label": "EEML 2019"}, {"value": "TMLSS2018-EEML2019", "label": "Both"}, {"value": "none", "label": "Neither"}]' WHERE headline='Previous attendance' AND application_form_id=2""" + ) op.execute("""UPDATE organisation set name='EEML' WHERE id=2""") diff --git a/api/migrations/versions/3ec3ed0eb3c6_.py b/api/migrations/versions/3ec3ed0eb3c6_.py index fdcdc8d2c..667efaa67 100644 --- a/api/migrations/versions/3ec3ed0eb3c6_.py +++ b/api/migrations/versions/3ec3ed0eb3c6_.py @@ -7,11 +7,11 @@ """ # revision identifiers, used by Alembic. -revision = '3ec3ed0eb3c6' -down_revision = ('1c3fda18bad6', '838a2bd9a618') +revision = "3ec3ed0eb3c6" +down_revision = ("1c3fda18bad6", "838a2bd9a618") -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): diff --git a/api/migrations/versions/3fb92272089d_add_section_and_question_translations.py b/api/migrations/versions/3fb92272089d_add_section_and_question_translations.py index e9b81ef04..b9c9a09c6 100644 --- a/api/migrations/versions/3fb92272089d_add_section_and_question_translations.py +++ b/api/migrations/versions/3fb92272089d_add_section_and_question_translations.py @@ -7,28 +7,34 @@ """ # revision identifiers, used by Alembic. -revision = '3fb92272089d' -down_revision = '40ee6f91f6f3' +revision = "3fb92272089d" +down_revision = "40ee6f91f6f3" -from alembic import op import sqlalchemy as sa +from alembic import op from sqlalchemy.dialects import postgresql + def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.create_table('section_translation', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('section_id', sa.Integer(), nullable=False), - sa.Column('language', sa.String(length=2), nullable=False), - sa.Column('name', sa.String(length=255), nullable=False), - sa.Column('description', sa.String(), nullable=False), - sa.Column('show_for_values', sa.JSON(), nullable=True), - sa.ForeignKeyConstraint(['section_id'], ['section.id'], ), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('section_id', 'language', name='uq_section_id_language') - ) - - op.execute(""" + op.create_table( + "section_translation", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("section_id", sa.Integer(), nullable=False), + sa.Column("language", sa.String(length=2), nullable=False), + sa.Column("name", sa.String(length=255), nullable=False), + sa.Column("description", sa.String(), nullable=False), + sa.Column("show_for_values", sa.JSON(), nullable=True), + sa.ForeignKeyConstraint( + ["section_id"], + ["section.id"], + ), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("section_id", "language", name="uq_section_id_language"), + ) + + op.execute( + """ INSERT INTO section_translation ( section_id, language, @@ -43,29 +49,35 @@ def upgrade(): description, show_for_values FROM section - """) - - op.drop_column('section', 'name') - op.drop_column('section', 'description') - op.drop_column('section', 'show_for_values') - - op.create_table('question_translation', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('question_id', sa.Integer(), nullable=False), - sa.Column('language', sa.String(length=2), nullable=False), - sa.Column('description', sa.String(), nullable=True), - sa.Column('headline', sa.String(), nullable=False), - sa.Column('placeholder', sa.String(), nullable=True), - sa.Column('validation_regex', sa.String(), nullable=True), - sa.Column('validation_text', sa.String(), nullable=True), - sa.Column('options', sa.JSON(), nullable=True), - sa.Column('show_for_values', sa.JSON(), nullable=True), - sa.ForeignKeyConstraint(['question_id'], ['question.id'], ), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('question_id', 'language', name='uq_question_id_language') - ) - - op.execute(""" + """ + ) + + op.drop_column("section", "name") + op.drop_column("section", "description") + op.drop_column("section", "show_for_values") + + op.create_table( + "question_translation", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("question_id", sa.Integer(), nullable=False), + sa.Column("language", sa.String(length=2), nullable=False), + sa.Column("description", sa.String(), nullable=True), + sa.Column("headline", sa.String(), nullable=False), + sa.Column("placeholder", sa.String(), nullable=True), + sa.Column("validation_regex", sa.String(), nullable=True), + sa.Column("validation_text", sa.String(), nullable=True), + sa.Column("options", sa.JSON(), nullable=True), + sa.Column("show_for_values", sa.JSON(), nullable=True), + sa.ForeignKeyConstraint( + ["question_id"], + ["question.id"], + ), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("question_id", "language", name="uq_question_id_language"), + ) + + op.execute( + """ INSERT INTO question_translation ( question_id, language, @@ -87,46 +99,94 @@ def upgrade(): options, show_for_values FROM question - """) - - op.drop_column('question', 'description') - op.drop_column('question', 'headline') - op.drop_column('question', 'placeholder') - op.drop_column('question', 'validation_regex') - op.drop_column('question', 'validation_text') - op.drop_column('question', 'options') - op.drop_column('question', 'show_for_values') + """ + ) + + op.drop_column("question", "description") + op.drop_column("question", "headline") + op.drop_column("question", "placeholder") + op.drop_column("question", "validation_regex") + op.drop_column("question", "validation_text") + op.drop_column("question", "options") + op.drop_column("question", "show_for_values") # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.add_column('section', sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=True)) - op.add_column('section', sa.Column('description', sa.VARCHAR(), autoincrement=False, nullable=True)) - op.add_column('section', sa.Column('show_for_values', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=True)) + op.add_column( + "section", sa.Column("name", sa.VARCHAR(), autoincrement=False, nullable=True) + ) + op.add_column( + "section", + sa.Column("description", sa.VARCHAR(), autoincrement=False, nullable=True), + ) + op.add_column( + "section", + sa.Column( + "show_for_values", + postgresql.JSON(astext_type=sa.Text()), + autoincrement=False, + nullable=True, + ), + ) - op.execute(""" + op.execute( + """ UPDATE section SET name = (SELECT name from section_translation where section.id = section_translation.section_id AND section_translation.language = 'en'), description = (SELECT description from section_translation where section.id = section_translation.section_id AND section_translation.language = 'en'), show_for_values = (SELECT show_for_values from section_translation where section.id = section_translation.section_id AND section_translation.language = 'en') - """) + """ + ) - op.alter_column('section', 'name', nullable=False) - op.alter_column('section', 'description', nullable=False) + op.alter_column("section", "name", nullable=False) + op.alter_column("section", "description", nullable=False) - op.drop_table('section_translation') + op.drop_table("section_translation") - op.add_column('question', sa.Column('description', sa.VARCHAR(), autoincrement=False, nullable=True)) - op.add_column('question', sa.Column('headline', sa.VARCHAR(), autoincrement=False, nullable=True)) - op.add_column('question', sa.Column('placeholder', sa.VARCHAR(), autoincrement=False, nullable=True)) - op.add_column('question', sa.Column('validation_regex', sa.VARCHAR(), autoincrement=False, nullable=True)) - op.add_column('question', sa.Column('validation_text', sa.VARCHAR(), autoincrement=False, nullable=True)) - op.add_column('question', sa.Column('options', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=True)) - op.add_column('question', sa.Column('show_for_values', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=True)) + op.add_column( + "question", + sa.Column("description", sa.VARCHAR(), autoincrement=False, nullable=True), + ) + op.add_column( + "question", + sa.Column("headline", sa.VARCHAR(), autoincrement=False, nullable=True), + ) + op.add_column( + "question", + sa.Column("placeholder", sa.VARCHAR(), autoincrement=False, nullable=True), + ) + op.add_column( + "question", + sa.Column("validation_regex", sa.VARCHAR(), autoincrement=False, nullable=True), + ) + op.add_column( + "question", + sa.Column("validation_text", sa.VARCHAR(), autoincrement=False, nullable=True), + ) + op.add_column( + "question", + sa.Column( + "options", + postgresql.JSON(astext_type=sa.Text()), + autoincrement=False, + nullable=True, + ), + ) + op.add_column( + "question", + sa.Column( + "show_for_values", + postgresql.JSON(astext_type=sa.Text()), + autoincrement=False, + nullable=True, + ), + ) - op.execute(""" + op.execute( + """ UPDATE question SET description = (SELECT description from question_translation where question.id = question_translation.question_id AND question_translation.language = 'en'), @@ -136,9 +196,10 @@ def downgrade(): validation_text = (SELECT validation_text from question_translation where question.id = question_translation.question_id AND question_translation.language = 'en'), options = (SELECT options from question_translation where question.id = question_translation.question_id AND question_translation.language = 'en'), show_for_values = (SELECT show_for_values from question_translation where question.id = question_translation.question_id AND question_translation.language = 'en') - """) + """ + ) - op.alter_column('question', 'headline', nullable=False) + op.alter_column("question", "headline", nullable=False) - op.drop_table('question_translation') + op.drop_table("question_translation") # ### end Alembic commands ### diff --git a/api/migrations/versions/405d106c8292_.py b/api/migrations/versions/405d106c8292_.py index 337de1041..c93073ad0 100644 --- a/api/migrations/versions/405d106c8292_.py +++ b/api/migrations/versions/405d106c8292_.py @@ -7,11 +7,11 @@ """ # revision identifiers, used by Alembic. -revision = '405d106c8292' -down_revision = ('2e81ebc8d90a', '62352569fb5d') +revision = "405d106c8292" +down_revision = ("2e81ebc8d90a", "62352569fb5d") -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): diff --git a/api/migrations/versions/40ee6f91f6f3_.py b/api/migrations/versions/40ee6f91f6f3_.py index 0293decef..237a07bdb 100644 --- a/api/migrations/versions/40ee6f91f6f3_.py +++ b/api/migrations/versions/40ee6f91f6f3_.py @@ -7,11 +7,11 @@ """ # revision identifiers, used by Alembic. -revision = '40ee6f91f6f3' -down_revision = ('44df297ad858', '88cc4397bdf5') +revision = "40ee6f91f6f3" +down_revision = ("44df297ad858", "88cc4397bdf5") -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): diff --git a/api/migrations/versions/43597f3ea193_redo_ai4d_call.py b/api/migrations/versions/43597f3ea193_redo_ai4d_call.py index 0ca89844d..e7dcc0df7 100644 --- a/api/migrations/versions/43597f3ea193_redo_ai4d_call.py +++ b/api/migrations/versions/43597f3ea193_redo_ai4d_call.py @@ -9,139 +9,140 @@ """ # revision identifiers, used by Alembic. -revision = '43597f3ea193' -down_revision = '62c7711123a8' +revision = "43597f3ea193" +down_revision = "62c7711123a8" + +import datetime +from enum import Enum -from alembic import op import sqlalchemy as sa +from alembic import op from sqlalchemy import orm from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy import orm + from app import db -import datetime -from enum import Enum Base = declarative_base() en_countries = [ - 'Algeria', - 'Angola', - 'Benin', - 'Botswana', - 'Burkina Faso', - 'Burundi', - 'Cameroon', - 'Cape Verde', - 'Central African Republic', - 'Chad', - 'Comoros', - 'Democratic Republic of the Congo', - 'Republic of the Congo', - 'Côte d’Ivoire', - 'Djibouti', - 'Egypt', - 'Equatorial Guinea', - 'Eritrea', - 'Eswatini', - 'Ethiopia', - 'Gabon', - 'The Gambia', - 'Ghana', - 'Guinea', - 'Guinea-Bissau', - 'Kenya', - 'Lesotho', - 'Liberia', - 'Libya', - 'Madagascar', - 'Malawi', - 'Mali', - 'Mauritania', - 'Mauritius', - 'Morocco', - 'Mozambique', - 'Namibia', - 'Niger', - 'Nigeria', - 'Rwanda', - 'Sao Tome and Principe', - 'Senegal', - 'Seychelles', - 'Sierra Leone', - 'Somalia', - 'South Africa', - 'South Sudan', - 'Sudan', - 'Tanzania', - 'Togo', - 'Tunisia', - 'Uganda', - 'Zambia', - 'Zimbabwe' + "Algeria", + "Angola", + "Benin", + "Botswana", + "Burkina Faso", + "Burundi", + "Cameroon", + "Cape Verde", + "Central African Republic", + "Chad", + "Comoros", + "Democratic Republic of the Congo", + "Republic of the Congo", + "Côte d’Ivoire", + "Djibouti", + "Egypt", + "Equatorial Guinea", + "Eritrea", + "Eswatini", + "Ethiopia", + "Gabon", + "The Gambia", + "Ghana", + "Guinea", + "Guinea-Bissau", + "Kenya", + "Lesotho", + "Liberia", + "Libya", + "Madagascar", + "Malawi", + "Mali", + "Mauritania", + "Mauritius", + "Morocco", + "Mozambique", + "Namibia", + "Niger", + "Nigeria", + "Rwanda", + "Sao Tome and Principe", + "Senegal", + "Seychelles", + "Sierra Leone", + "Somalia", + "South Africa", + "South Sudan", + "Sudan", + "Tanzania", + "Togo", + "Tunisia", + "Uganda", + "Zambia", + "Zimbabwe", ] fr_countries = [ - 'Algérie', - 'Angola', - 'Bénin', - 'Botswana', - 'Burkina Faso', - 'Burundi', - 'Cameroun', - 'Cap-Vert', - 'République centrafricaine', - 'Tchad', - 'Comores', - 'République démocratique du Congo', - 'République du Congo', - 'Côte d’Ivoire', - 'Djibouti', - 'Égypte', - 'Guinée équatoriale', - 'Érythrée', - 'Eswatini', - 'Éthiopie', - 'Gabon', - 'Gambie', - 'Ghana', - 'Guinée', - 'Guinée-Bissau', - 'Kenya', - 'Lesotho', - 'Libéria', - 'Libye', - 'Madagascar', - 'Malawi', - 'Mali', - 'Maroc', - 'Mauritanie', - 'île Maurice', - 'Mozambique', - 'Namibie', - 'Niger', - 'Nigeria', - 'Rwanda', - 'Sao Tomé-et-Principe', - 'Sénégal', - 'Seychelles', - 'Sierra Leone', - 'Somalie', - 'Afrique du Sud', - 'Soudan du Sud', - 'Soudan', - 'Tanzanie', - 'Togo', - 'Tunisie', - 'Ouganda', - 'Zambie', - 'Zimbabwe' + "Algérie", + "Angola", + "Bénin", + "Botswana", + "Burkina Faso", + "Burundi", + "Cameroun", + "Cap-Vert", + "République centrafricaine", + "Tchad", + "Comores", + "République démocratique du Congo", + "République du Congo", + "Côte d’Ivoire", + "Djibouti", + "Égypte", + "Guinée équatoriale", + "Érythrée", + "Eswatini", + "Éthiopie", + "Gabon", + "Gambie", + "Ghana", + "Guinée", + "Guinée-Bissau", + "Kenya", + "Lesotho", + "Libéria", + "Libye", + "Madagascar", + "Malawi", + "Mali", + "Maroc", + "Mauritanie", + "île Maurice", + "Mozambique", + "Namibie", + "Niger", + "Nigeria", + "Rwanda", + "Sao Tomé-et-Principe", + "Sénégal", + "Seychelles", + "Sierra Leone", + "Somalie", + "Afrique du Sud", + "Soudan du Sud", + "Soudan", + "Tanzanie", + "Togo", + "Tunisie", + "Ouganda", + "Zambie", + "Zimbabwe", ] class Organisation(Base): __tablename__ = "organisation" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(50), nullable=False) @@ -153,7 +154,17 @@ class Organisation(Base): email_from = db.Column(db.String(100), nullable=True) system_url = db.Column(db.String(100), nullable=False) - def __init__(self, name, system_name, small_logo, large_logo, domain, url, email_from, system_url): + def __init__( + self, + name, + system_name, + small_logo, + large_logo, + domain, + url, + email_from, + system_url, + ): self.name = name self.small_logo = small_logo self.large_logo = large_logo @@ -163,9 +174,10 @@ def __init__(self, name, system_name, small_logo, large_logo, domain, url, email self.email_from = email_from self.system_url = system_url + class Country(Base): __tablename__ = "country" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(100), nullable=False) @@ -173,22 +185,25 @@ class Country(Base): def __init__(self, name): self.name = name + class EventType(Enum): - EVENT = 'event' - AWARD = 'award' - CALL = 'call' + EVENT = "event" + AWARD = "award" + CALL = "call" class Event(Base): __tablename__ = "event" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) start_date = db.Column(db.DateTime(), nullable=False) end_date = db.Column(db.DateTime(), nullable=False) key = db.Column(db.String(255), nullable=False, unique=True) - organisation_id = db.Column(db.Integer(), db.ForeignKey('organisation.id'), nullable=False) + organisation_id = db.Column( + db.Integer(), db.ForeignKey("organisation.id"), nullable=False + ) email_from = db.Column(db.String(255), nullable=False) url = db.Column(db.String(255), nullable=False) application_open = db.Column(db.DateTime(), nullable=False) @@ -205,7 +220,7 @@ class Event(Base): travel_grant = db.Column(db.Boolean(), nullable=False) miniconf_url = db.Column(db.String(100), nullable=True) - event_translations = db.relationship('EventTranslation', lazy='dynamic') + event_translations = db.relationship("EventTranslation", lazy="dynamic") def __init__( self, @@ -229,7 +244,7 @@ def __init__( registration_close, event_type, travel_grant, - miniconf_url=None + miniconf_url=None, ): self.start_date = start_date self.end_date = end_date @@ -261,10 +276,11 @@ def add_event_translations(self, names, descriptions): event_translation = EventTranslation(name, description, language) self.event_translations.append(event_translation) + class EventTranslation(Base): __tablename__ = "event_translation" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) event_id = db.Column(db.Integer(), db.ForeignKey("event.id"), nullable=False) @@ -272,19 +288,20 @@ class EventTranslation(Base): description = db.Column(db.String(255), nullable=False) language = db.Column(db.String(2)) - event = db.relationship('Event', foreign_keys=[event_id]) + event = db.relationship("Event", foreign_keys=[event_id]) def __init__(self, name, description, language): self.name = name self.description = description self.language = language + class ApplicationForm(Base): - __tablename__ = 'application_form' - __table_args__ = {'extend_existing': True} + __tablename__ = "application_form" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - event_id = db.Column(db.Integer(), db.ForeignKey('event.id'), nullable=False) + event_id = db.Column(db.Integer(), db.ForeignKey("event.id"), nullable=False) is_open = db.Column(db.Boolean(), nullable=False) nominations = db.Column(db.Boolean(), nullable=False) @@ -293,22 +310,29 @@ def __init__(self, event_id, is_open, nominations): self.is_open = is_open self.nominations = nominations + class Question(Base): - __tablename__ = 'question' - __table_args__ = {'extend_existing': True} + __tablename__ = "question" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) - section_id = db.Column(db.Integer(), db.ForeignKey('section.id'), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) + section_id = db.Column(db.Integer(), db.ForeignKey("section.id"), nullable=False) type = db.Column(db.String(), nullable=False) order = db.Column(db.Integer(), nullable=False) is_required = db.Column(db.Boolean(), nullable=False) - depends_on_question_id = db.Column(db.Integer(), db.ForeignKey('question.id'), nullable=True) + depends_on_question_id = db.Column( + db.Integer(), db.ForeignKey("question.id"), nullable=True + ) key = db.Column(db.String(255), nullable=True) - question_translations = db.relationship('QuestionTranslation', lazy='dynamic') + question_translations = db.relationship("QuestionTranslation", lazy="dynamic") - def __init__(self, application_form_id, section_id, order, questionType, is_required=True): + def __init__( + self, application_form_id, section_id, order, questionType, is_required=True + ): self.application_form_id = application_form_id self.section_id = section_id self.order = order @@ -317,18 +341,24 @@ def __init__(self, application_form_id, section_id, order, questionType, is_requ class Section(Base): - __tablename__ = 'section' - __table_args__ = {'extend_existing': True} + __tablename__ = "section" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) order = db.Column(db.Integer(), nullable=False) - depends_on_question_id = db.Column(db.Integer(), db.ForeignKey('question.id', use_alter=True), nullable=True) + depends_on_question_id = db.Column( + db.Integer(), db.ForeignKey("question.id", use_alter=True), nullable=True + ) key = db.Column(db.String(255), nullable=True) - section_translations = db.relationship('SectionTranslation', lazy='dynamic') + section_translations = db.relationship("SectionTranslation", lazy="dynamic") - def __init__(self, application_form_id, order, depends_on_question_id=None, key=None): + def __init__( + self, application_form_id, order, depends_on_question_id=None, key=None + ): self.application_form_id = application_form_id self.order = order self.depends_on_question_id = depends_on_question_id @@ -336,11 +366,11 @@ def __init__(self, application_form_id, order, depends_on_question_id=None, key= class SectionTranslation(Base): - __tablename__ = 'section_translation' - __table_args__ = {'extend_existing': True} + __tablename__ = "section_translation" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - section_id = db.Column(db.Integer(), db.ForeignKey('section.id'), nullable=False) + section_id = db.Column(db.Integer(), db.ForeignKey("section.id"), nullable=False) language = db.Column(db.String(2), nullable=False) name = db.Column(db.String(255), nullable=False) description = db.Column(db.String(), nullable=False) @@ -355,11 +385,11 @@ def __init__(self, section_id, language, name, description, show_for_values=None class QuestionTranslation(Base): - __tablename__ = 'question_translation' - __table_args__ = {'extend_existing': True} + __tablename__ = "question_translation" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - question_id = db.Column(db.Integer(), db.ForeignKey('question.id'), nullable=False) + question_id = db.Column(db.Integer(), db.ForeignKey("question.id"), nullable=False) language = db.Column(db.String(2), nullable=False) headline = db.Column(db.String(), nullable=False) description = db.Column(db.String(), nullable=True) @@ -379,7 +409,7 @@ def __init__( validation_regex=None, validation_text=None, options=None, - show_for_values=None + show_for_values=None, ): self.question_id = question_id self.language = language @@ -396,7 +426,7 @@ def upgrade(): Base.metadata.bind = op.get_bind() session = orm.Session(bind=Base.metadata.bind) - event = session.query(Event).filter_by(key='prc').first() + event = session.query(Event).filter_by(key="prc").first() app_form = session.query(ApplicationForm).filter_by(event_id=event.id).first() questions = session.query(Question).filter_by(application_form_id=app_form.id).all() @@ -415,42 +445,44 @@ def upgrade(): session.query(ApplicationForm).filter_by(event_id=event.id).delete() - event = session.query(Event).filter_by(key='prc').first() + event = session.query(Event).filter_by(key="prc").first() session.query(EventTranslation).filter_by(event_id=event.id).delete() - session.query(Event).filter_by(key='prc').delete() + session.query(Event).filter_by(key="prc").delete() session.commit() - ai4d = session.query(Organisation).filter_by(name='AI4D Africa').first() + ai4d = session.query(Organisation).filter_by(name="AI4D Africa").first() # Add Event event = Event( { - 'en': 'Call for Proposals for Policy Research Centres', - 'fr': 'Appel à Propositions pour les Centres de Recherche Politique' + "en": "Call for Proposals for Policy Research Centres", + "fr": "Appel à Propositions pour les Centres de Recherche Politique", }, { - 'en': 'Policy Research Centres on Artificial Intelligence for Development in Africa: Getting to responsible AI through research and policy development', - 'fr': 'Centres de Recherche sur les Politiques concernant l’intelligence artificielle (IA) pour le développement en Afrique: Vers une IA responsable grâce à la recherche et à l’élaboration de politiques' + "en": "Policy Research Centres on Artificial Intelligence for Development in Africa: Getting to responsible AI through research and policy development", + "fr": "Centres de Recherche sur les Politiques concernant l’intelligence artificielle (IA) pour le développement en Afrique: Vers une IA responsable grâce à la recherche et à l’élaboration de politiques", }, start_date=datetime.date(2020, 11, 6), end_date=datetime.date(2020, 11, 6), - key='prc', + key="prc", organisation_id=ai4d.id, - email_from='calls@ai4d.ai', - url='ai4d.ai', - application_open=datetime.date(2020,9,1), - application_close=datetime.datetime(2020,10,17,5,0,0), # Equivalent to midnight EST - review_open=datetime.date(2020,10,17), - review_close=datetime.date(2020,10,30), - selection_open=datetime.date(2020,11,1), - selection_close=datetime.date(2020,11,6), - offer_open=datetime.date(2020,12,31), - offer_close=datetime.date(2020,12,31), - registration_open=datetime.date(2020,12,31), - registration_close=datetime.date(2020,12,31), + email_from="calls@ai4d.ai", + url="ai4d.ai", + application_open=datetime.date(2020, 9, 1), + application_close=datetime.datetime( + 2020, 10, 17, 5, 0, 0 + ), # Equivalent to midnight EST + review_open=datetime.date(2020, 10, 17), + review_close=datetime.date(2020, 10, 30), + selection_open=datetime.date(2020, 11, 1), + selection_close=datetime.date(2020, 11, 6), + offer_open=datetime.date(2020, 12, 31), + offer_close=datetime.date(2020, 12, 31), + registration_open=datetime.date(2020, 12, 31), + registration_close=datetime.date(2020, 12, 31), event_type=EventType.CALL, - travel_grant=False + travel_grant=False, ) session.add(event) @@ -460,24 +492,40 @@ def upgrade(): session.add(form) session.commit() - def add_section(names, descriptions, order, depends_on_question_id=None, key=None, show_for_values=None): + def add_section( + names, + descriptions, + order, + depends_on_question_id=None, + key=None, + show_for_values=None, + ): section = Section(form.id, order) session.add(section) session.commit() translations = [] for language in names: - translations.append(SectionTranslation( - section.id, - language, - names[language], - '' if descriptions is None else descriptions[language], - show_for_values=None if show_for_values is None else show_for_values[language])) + translations.append( + SectionTranslation( + section.id, + language, + names[language], + "" if descriptions is None else descriptions[language], + show_for_values=None + if show_for_values is None + else show_for_values[language], + ) + ) session.add_all(translations) session.commit() return section - def add_question(section, order, questionType, headlines, + def add_question( + section, + order, + questionType, + headlines, descriptions=None, placeholders=None, validation_regexs=None, @@ -485,8 +533,11 @@ def add_question(section, order, questionType, headlines, options=None, show_for_values=None, is_required=True, - depends_on_question_id=None): - question = Question(form.id, section.id, order, questionType, is_required=is_required) + depends_on_question_id=None, + ): + question = Question( + form.id, section.id, order, questionType, is_required=is_required + ) if depends_on_question_id is not None: question.depends_on_question_id = depends_on_question_id @@ -496,13 +547,26 @@ def add_question(section, order, questionType, headlines, translations = [] for language in headlines: - translations.append(QuestionTranslation(question.id, language, - headline=headlines[language], - description=None if descriptions is None else descriptions[language], - placeholder=None if placeholders is None else placeholders[language], - validation_regex=None if validation_regexs is None else validation_regexs[language], - options=None if options is None else options[language], - show_for_values=None if show_for_values is None else show_for_values[language])) + translations.append( + QuestionTranslation( + question.id, + language, + headline=headlines[language], + description=None + if descriptions is None + else descriptions[language], + placeholder=None + if placeholders is None + else placeholders[language], + validation_regex=None + if validation_regexs is None + else validation_regexs[language], + options=None if options is None else options[language], + show_for_values=None + if show_for_values is None + else show_for_values[language], + ) + ) session.add_all(translations) session.commit() return question @@ -527,470 +591,602 @@ def add_question(section, order, questionType, headlines, Pour toute question, veuillez envoyer un e-mail à calls@ai4d.ai""" # (Description Page) - add_section({ - 'en': 'Call for Proposals for Policy Research Centres', - 'fr': 'Appel à Propositions pour les Centres de Recherche Politique' - }, - {'en': en_description, 'fr': fr_description}, - 1) + add_section( + { + "en": "Call for Proposals for Policy Research Centres", + "fr": "Appel à Propositions pour les Centres de Recherche Politique", + }, + {"en": en_description, "fr": fr_description}, + 1, + ) # Organisation and lead contact details - org_section = add_section({ - 'en': 'Organisation and Lead Applicant Contact details', - 'fr': "Coordonnées de l'organisation et de la personne-ressource pour cette demande" - }, None, 2) - - add_question(org_section, 1, 'information', { - 'en': 'Organization Information', - 'fr': 'Renseignements sur l’organisation' - }, is_required=False) - - add_question(org_section, 2, 'short-text', { - 'en': 'Name of Organisation', - 'fr': 'Nom de l’organisation' - }, is_required=True) - - add_question(org_section, 3, 'short-text', { - 'en': 'Director/CEO', - 'fr': 'Directeur ou premier dirigeant' - }, is_required=True) - - add_question(org_section, 4, 'short-text', { - 'en': 'Mailing address', - 'fr': 'Adresse postale' - }, is_required=True) - - add_question(org_section, 5, 'short-text', { - 'en': 'City', - 'fr': 'Ville' - }, is_required=True) + org_section = add_section( + { + "en": "Organisation and Lead Applicant Contact details", + "fr": "Coordonnées de l'organisation et de la personne-ressource pour cette demande", + }, + None, + 2, + ) - en_options = [ - {'label': c, 'value': c} for c in en_countries - ] - fr_options = [ - {'label': f, 'value': e} for e, f in zip(en_countries, fr_countries) - ] + add_question( + org_section, + 1, + "information", + {"en": "Organization Information", "fr": "Renseignements sur l’organisation"}, + is_required=False, + ) + + add_question( + org_section, + 2, + "short-text", + {"en": "Name of Organisation", "fr": "Nom de l’organisation"}, + is_required=True, + ) + + add_question( + org_section, + 3, + "short-text", + {"en": "Director/CEO", "fr": "Directeur ou premier dirigeant"}, + is_required=True, + ) + + add_question( + org_section, + 4, + "short-text", + {"en": "Mailing address", "fr": "Adresse postale"}, + is_required=True, + ) + + add_question( + org_section, 5, "short-text", {"en": "City", "fr": "Ville"}, is_required=True + ) + + en_options = [{"label": c, "value": c} for c in en_countries] + fr_options = [{"label": f, "value": e} for e, f in zip(en_countries, fr_countries)] + + add_question( + org_section, + 6, + "multi-choice", + {"en": "Country", "fr": "Pays"}, + descriptions={ + "en": "Eligible countries in sub-Saharan and North Africa", + "fr": "Pays admissibles de l’Afrique subsaharienne et de l’Afrique du Nord", + }, + placeholders={"en": "Select a Country", "fr": "Choisissez un Pays"}, + options={"en": en_options, "fr": fr_options}, + is_required=True, + ) + + add_question( + org_section, + 7, + "short-text", + {"en": "Telephone", "fr": "Numéro de téléphone"}, + is_required=True, + ) + + add_question( + org_section, + 8, + "short-text", + {"en": "Mobile (Optional)", "fr": "Numéro de téléphone mobile (facultatif)"}, + is_required=False, + ) + + add_question( + org_section, + 9, + "short-text", + {"en": "Email Address", "fr": "Adresse de courriel"}, + is_required=True, + ) + + add_question( + org_section, + 10, + "short-text", + {"en": "Website", "fr": "Site Web"}, + is_required=True, + ) + + add_question( + org_section, + 11, + "short-text", + {"en": "Social media handles", "fr": "Pseudonymes sur les médias sociaux"}, + is_required=True, + ) + + add_question( + org_section, + 12, + "information", + { + "en": "Name and contact details of principal contact for the application", + "fr": "Nom et coordonnées de la personne-ressource pour cette demande", + }, + is_required=False, + ) + + add_question( + org_section, + 13, + "short-text", + {"en": "Name of principal contact", "fr": "Nom du contact principal"}, + is_required=True, + ) + + add_question( + org_section, + 14, + "short-text", + { + "en": "Email Address of principal contact", + "fr": "Adresse de courriel du contact principal", + }, + is_required=True, + ) - add_question(org_section, 6, 'multi-choice', { - 'en': 'Country', - 'fr': 'Pays' - }, - descriptions={ - 'en': 'Eligible countries in sub-Saharan and North Africa', - 'fr': 'Pays admissibles de l’Afrique subsaharienne et de l’Afrique du Nord' - }, - placeholders={ - 'en': 'Select a Country', - 'fr': 'Choisissez un Pays' - }, - options={ - 'en': en_options, - 'fr': fr_options - }, - is_required=True) - - add_question(org_section, 7, 'short-text', { - 'en': 'Telephone', - 'fr': 'Numéro de téléphone' - }, is_required=True) - - add_question(org_section, 8, 'short-text', { - 'en': 'Mobile (Optional)', - 'fr': 'Numéro de téléphone mobile (facultatif)' - }, is_required=False) - - add_question(org_section, 9, 'short-text', { - 'en': 'Email Address', - 'fr': 'Adresse de courriel' - }, is_required=True) - - add_question(org_section, 10, 'short-text', { - 'en': 'Website', - 'fr': 'Site Web' - }, is_required=True) - - add_question(org_section, 11, 'short-text', { - 'en': 'Social media handles', - 'fr': 'Pseudonymes sur les médias sociaux' - }, is_required=True) - - add_question(org_section, 12, 'information', { - 'en': 'Name and contact details of principal contact for the application', - 'fr': 'Nom et coordonnées de la personne-ressource pour cette demande' - }, is_required=False) - - add_question(org_section, 13, 'short-text', { - 'en': 'Name of principal contact', - 'fr': 'Nom du contact principal' - }, is_required=True) - - add_question(org_section, 14, 'short-text', { - 'en': 'Email Address of principal contact', - 'fr': 'Adresse de courriel du contact principal' - }, is_required=True) - - add_question(org_section, 15, 'short-text', { - 'en': 'Telephone/Mobile of principal contact', - 'fr': 'Numéro de téléphone ou de téléphone mobile du contact principal' - }, is_required=True) + add_question( + org_section, + 15, + "short-text", + { + "en": "Telephone/Mobile of principal contact", + "fr": "Numéro de téléphone ou de téléphone mobile du contact principal", + }, + is_required=True, + ) # Section 1 - section1 = add_section({ - 'en': 'Section 1: Organisation Experience and Capacity', - 'fr': 'Section 1: Expérience et capacités de l’organisation' - }, None, 3) - - add_question(section1, 1, 'short-text', { - 'en': '1. How many people in total work in your organization?', - 'fr': '1. Combien d’employés compte votre organisation en tout?' - }, descriptions={ - 'en': 'Total number of employees', - 'fr': 'Nombre total d’employés' - }, - is_required=True) - - add_question(section1, 2, 'short-text', { - 'en': '1a. Of these, how many are full time policy researchers?', - 'fr': '1a. Combien d’entre eux occupent un poste de chercheur en matière de politiques à temps plein?' - }, is_required=True) - - add_question(section1, 3, 'multi-file', { - 'en': "1b. Please provide the names and CVs of your organization's principal researchers (up to five)", - 'fr': '1b. Veuillez fournir les noms et les CV des principaux chercheurs de votre organisation (maximum de cinq)' - }, descriptions={ - 'en': 'Upload the CVs of relevant researchers in the organization here.', - 'fr': 'Téléchargez ici les CV des chercheurs concernés de votre organisation' - }, is_required=True) - - add_question(section1, 4, 'long-text', { - 'en': '2. Mission for Responsible AI', - 'fr': '2. Mission pour une IA responsable' - }, descriptions={ - 'en': 'Responsible AI is an approach to artificial intelligence that is inclusive, sustainable, ethical and which supports human rights. Please describe the mission of your organization as it relates to the principles of responsible AI.', - 'fr': "L’intelligence artificielle (IA) responsable est une approche de l’IA à la fois inclusive, durable, éthique et fondée sur les droits de la personne. Veuillez décrire la mission de votre organisation par rapport aux principes de l’IA responsable." - }, placeholders={ - 'en': 'Enter up to 250 words', - 'fr': "Entrez jusqu'à 250 mots" - }, validation_regexs={ - 'en': r'^\s*(\S+(\s+|$)){0,250}$', - 'fr': r'^\s*(\S+(\s+|$)){0,250}$', - }, validation_texts={ - 'en': 'Maximum 250 words', - 'fr': 'Maximum 250 mots' - }, is_required=True) + section1 = add_section( + { + "en": "Section 1: Organisation Experience and Capacity", + "fr": "Section 1: Expérience et capacités de l’organisation", + }, + None, + 3, + ) + + add_question( + section1, + 1, + "short-text", + { + "en": "1. How many people in total work in your organization?", + "fr": "1. Combien d’employés compte votre organisation en tout?", + }, + descriptions={ + "en": "Total number of employees", + "fr": "Nombre total d’employés", + }, + is_required=True, + ) + + add_question( + section1, + 2, + "short-text", + { + "en": "1a. Of these, how many are full time policy researchers?", + "fr": "1a. Combien d’entre eux occupent un poste de chercheur en matière de politiques à temps plein?", + }, + is_required=True, + ) + + add_question( + section1, + 3, + "multi-file", + { + "en": "1b. Please provide the names and CVs of your organization's principal researchers (up to five)", + "fr": "1b. Veuillez fournir les noms et les CV des principaux chercheurs de votre organisation (maximum de cinq)", + }, + descriptions={ + "en": "Upload the CVs of relevant researchers in the organization here.", + "fr": "Téléchargez ici les CV des chercheurs concernés de votre organisation", + }, + is_required=True, + ) + + add_question( + section1, + 4, + "long-text", + { + "en": "2. Mission for Responsible AI", + "fr": "2. Mission pour une IA responsable", + }, + descriptions={ + "en": "Responsible AI is an approach to artificial intelligence that is inclusive, sustainable, ethical and which supports human rights. Please describe the mission of your organization as it relates to the principles of responsible AI.", + "fr": "L’intelligence artificielle (IA) responsable est une approche de l’IA à la fois inclusive, durable, éthique et fondée sur les droits de la personne. Veuillez décrire la mission de votre organisation par rapport aux principes de l’IA responsable.", + }, + placeholders={"en": "Enter up to 250 words", "fr": "Entrez jusqu'à 250 mots"}, + validation_regexs={ + "en": r"^\s*(\S+(\s+|$)){0,250}$", + "fr": r"^\s*(\S+(\s+|$)){0,250}$", + }, + validation_texts={"en": "Maximum 250 words", "fr": "Maximum 250 mots"}, + is_required=True, + ) en_options = [ - 'Enabling beneficial AI research and development', - 'Economic impacts, labor shifts, inequality, and technological unemployment', - 'Accountability, transparency, and explainability', - 'Surveillance, privacy, and civil liberties', - 'Fairness, ethics, and human rights', - 'Diversity and gender equality', - 'Data capacity, analytics and governance', - 'Political manipulation and computational propaganda', - 'Human dignity, autonomy, and psychological impact', - 'AI safety' + "Enabling beneficial AI research and development", + "Economic impacts, labor shifts, inequality, and technological unemployment", + "Accountability, transparency, and explainability", + "Surveillance, privacy, and civil liberties", + "Fairness, ethics, and human rights", + "Diversity and gender equality", + "Data capacity, analytics and governance", + "Political manipulation and computational propaganda", + "Human dignity, autonomy, and psychological impact", + "AI safety", ] fr_options = [ - 'Recherche-développement utile en lien avec l’IA', - 'Répercussions économiques, mouvements de main-d’oeuvre, inégalité et chômage technologique', - 'Responsabilité, transparence et explicabilité', - 'Surveillance, vie privée et libertés civiles', - 'Équité, éthique et droits de l’homme', - 'Diversité et égalité des genres', - 'Capacités en matière de données, d’analyses et de gouvernance', - 'Manipulation politique et propagande virtuelle', - 'Dignité humaine, autonomie et répercussions psychologiques', - 'Sécurité de l’IA' + "Recherche-développement utile en lien avec l’IA", + "Répercussions économiques, mouvements de main-d’oeuvre, inégalité et chômage technologique", + "Responsabilité, transparence et explicabilité", + "Surveillance, vie privée et libertés civiles", + "Équité, éthique et droits de l’homme", + "Diversité et égalité des genres", + "Capacités en matière de données, d’analyses et de gouvernance", + "Manipulation politique et propagande virtuelle", + "Dignité humaine, autonomie et répercussions psychologiques", + "Sécurité de l’IA", ] - fr_options = [ - {'label': f, 'value': e} for e, f in zip(en_options, fr_options) - ] - en_options = [ - {'label': o, 'value': o} for o in en_options - ] + fr_options = [{"label": f, "value": e} for e, f in zip(en_options, fr_options)] + en_options = [{"label": o, "value": o} for o in en_options] + + add_question( + section1, + 5, + "multi-checkbox", + {"en": "3. Demonstrated expertise.", "fr": "3. Une expertise reconnue."}, + descriptions={ + "en": "Please indicate the subject areas that your organization has experience in developing policy research and frameworks for, as they relate to artificial intelligence or any advanced technologies. Select all that apply.", + "fr": "Veuillez préciser les domaines dans lesquels votre organisation a acquis de l’expérience en matière de recherches et de cadres stratégiques liés à l’IA ou à d’autres technologies de pointe. Cochez toutes les réponses qui s’appliquent.", + }, + options={"en": en_options, "fr": fr_options}, + is_required=True, + ) - add_question(section1, 5, 'multi-checkbox', { - 'en': '3. Demonstrated expertise.', - 'fr': '3. Une expertise reconnue.' - }, descriptions={ - 'en': 'Please indicate the subject areas that your organization has experience in developing policy research and frameworks for, as they relate to artificial intelligence or any advanced technologies. Select all that apply.', - 'fr': 'Veuillez préciser les domaines dans lesquels votre organisation a acquis de l’expérience en matière de recherches et de cadres stratégiques liés à l’IA ou à d’autres technologies de pointe. Cochez toutes les réponses qui s’appliquent.' - }, options={ - 'en': en_options, - 'fr': fr_options - }, is_required=True) - - add_question(section1, 6, 'long-text', { - 'en': '3a. Examples.', - 'fr': '3a. Exemples.' - }, descriptions={ - 'en': 'Please provide three to five examples here that demonstrate your research and policy analysis conducted in these areas. You may provide links, or upload documents and/or PDFs at the end of the application.', - 'fr': 'Veuillez fournir trois à cinq exemples qui témoignent des recherches et des analyses des politiques que vous avez effectuées dans ces domaines. Vous pouvez fournir des liens ou télécharger des documents ou des documents PDF à la fin de la demande.' - }, is_required=True) + add_question( + section1, + 6, + "long-text", + {"en": "3a. Examples.", "fr": "3a. Exemples."}, + descriptions={ + "en": "Please provide three to five examples here that demonstrate your research and policy analysis conducted in these areas. You may provide links, or upload documents and/or PDFs at the end of the application.", + "fr": "Veuillez fournir trois à cinq exemples qui témoignent des recherches et des analyses des politiques que vous avez effectuées dans ces domaines. Vous pouvez fournir des liens ou télécharger des documents ou des documents PDF à la fin de la demande.", + }, + is_required=True, + ) # Section 2 - section2 = add_section({ - 'en': 'Section 2: Policy Research', - 'fr': 'Section 2: Recherche sur les politiques' - }, None, 4) - - add_question(section2, 1, 'long-text', { - 'en': '1. Research background, problem statement and justification.', - 'fr': '1. Contexte, énoncé du problème et justification' - }, descriptions={ - 'en': 'Please briefly outline what the top three priorities of your research agenda and what the key research questions will be as an AI Policy Center, and explain why you are selecting these priorities. Provide a background and review of the local context which describes these priorities within the country/region and how the focus of this project clearly links with the local needs of the country and region. It should also indicate whether others are addressing this problem and why these proposed priorities are justified, or whether the project derives some of its importance from involving research which is largely neglected.', - 'fr': "Veuillez décrire brièvement les trois principales priorités de votre programme de recherche et les principales questions de recherche que vous aborderez en tant que centre des politiques sur l’IA, puis expliquer pourquoi vous avez sélectionné ces priorités. Donnez une vue d’ensemble et effectuez un examen du contexte local décrivant ces priorités dans le pays ou la région et expliquez le lien établi entre l’axe thématique du projet et les besoins particuliers du pays ou de la région. Vous devez également préciser s’il existe d’autres parties concernées qui se penchent sur le problème énoncé, les raisons justifiant les priorités proposées, et si l’importance du projet tient au fait qu’il porte sur un sujet de recherche grandement négligé" - }, placeholders={ - 'en': 'Enter up to 1000 words', - 'fr': "Entrez jusqu'à 1000 mots" - }, validation_regexs={ - 'en': r'^\s*(\S+(\s+|$)){0,1000}$', - 'fr': r'^\s*(\S+(\s+|$)){0,1000}$', - }, validation_texts={ - 'en': 'Maximum 1000 words', - 'fr': 'Maximum 1000 mots' - }, is_required=True) - - add_question(section2, 2, 'long-text', { - 'en': '2. General and specific objectives.', - 'fr': '2. Objectifs généraux et précis.' - }, descriptions={ - 'en': 'The general objective should state the development goal being pursued by the research. The specific objectives should indicate the specific types of knowledge to be produced, the audiences to be reached, and forms of capacity to be reinforced. These are the objectives against which the success of the project will be judged. Use only active verbs (no passive). Typical objectives focus on 1) deepening understanding of a topic through research; 2) influencing policy and practice; and 3) building capacity.', - 'fr': "L’objectif général correspond à l’objectif de développement que l’on souhaite atteindre au moyen de la recherche. Les objectifs particuliers doivent signaler les types précis de connaissances recherchées, les destinataires et les capacités à renforcer. C’est en fonction de ces objectifs que sera jaugée la réussite du projet. Utilisez seulement des verbes d’action (évitez les verbes d’état). En règle générale, les objectifs permettent : 1) de comprendre un sujet en profondeur grâce à la recherche; 2) d’influencer les politiques et les pratiques; et 3) de favoriser le renforcement des capacités." - }, placeholders={ - 'en': 'Enter up to 250 words', - 'fr': "Entrez jusqu'à 250 mots" - }, validation_regexs={ - 'en': r'^\s*(\S+(\s+|$)){0,250}$', - 'fr': r'^\s*(\S+(\s+|$)){0,250}$', - }, validation_texts={ - 'en': 'Maximum 250 words', - 'fr': 'Maximum 250 mots' - }, is_required=True) - - add_question(section2, 3, 'information', { - 'en': '3. Methodology and approach.', - 'fr': '3. Méthodologie et approche.' - }, is_required=False) - - add_question(section2, 4, 'long-text', { - 'en': '3a. Approach.', - 'fr': '3a. Approche.' - }, descriptions={ - 'en': 'Briefly describe the overall approach/logic/pathway to achieve the above objectives.', - 'fr': "Décrivez brièvement l’approche, la logique ou le cheminement permettant d’atteindre les objectifs mentionnés précédemment." - }, placeholders={ - 'en': 'Enter up to 350 words', - 'fr': "Entrez jusqu'à 350 mots" - }, validation_regexs={ - 'en': r'^\s*(\S+(\s+|$)){0,350}$', - 'fr': r'^\s*(\S+(\s+|$)){0,350}$', - }, validation_texts={ - 'en': 'Maximum 350 words', - 'fr': 'Maximum 350 mots' - }, is_required=True) - - add_question(section2, 5, 'long-text', { - 'en': '3b. Policy research.', - 'fr': '3b. Recherche sur les politiques.' - }, descriptions={ - 'en': 'How will you answer the proposed research questions in the most rigorous way possible? This includes conceptual and theoretical frameworks, user participation, data collection and analysis.', - 'fr': "Comment comptez-vous répondre aux questions de recherche proposées de la façon la plus rigoureuse possible ? Veuillez notamment fournir des précisions à l’égard des cadres conceptuels et théoriques, de la participation des utilisateurs, et de la collecte et de l’analyse des données." - }, placeholders={ - 'en': 'Enter up to 750 words', - 'fr': "Entrez jusqu'à 750 mots" - }, validation_regexs={ - 'en': r'^\s*(\S+(\s+|$)){0,750}$', - 'fr': r'^\s*(\S+(\s+|$)){0,750}$', - }, validation_texts={ - 'en': 'Maximum 750 words', - 'fr': 'Maximum 750 mots' - }, is_required=True) - - add_question(section2, 6, 'long-text', { - 'en': '3c. Gender and inclusion.', - 'fr': '3c. Sexospécificité et inclusion.' - }, descriptions={ - 'en': """i. How has your organization addressed gender-equality and other kinds of inclusion and diversity dimensions [socioeconomic, political, cultural, ethnic] in its policy research? + section2 = add_section( + { + "en": "Section 2: Policy Research", + "fr": "Section 2: Recherche sur les politiques", + }, + None, + 4, + ) + + add_question( + section2, + 1, + "long-text", + { + "en": "1. Research background, problem statement and justification.", + "fr": "1. Contexte, énoncé du problème et justification", + }, + descriptions={ + "en": "Please briefly outline what the top three priorities of your research agenda and what the key research questions will be as an AI Policy Center, and explain why you are selecting these priorities. Provide a background and review of the local context which describes these priorities within the country/region and how the focus of this project clearly links with the local needs of the country and region. It should also indicate whether others are addressing this problem and why these proposed priorities are justified, or whether the project derives some of its importance from involving research which is largely neglected.", + "fr": "Veuillez décrire brièvement les trois principales priorités de votre programme de recherche et les principales questions de recherche que vous aborderez en tant que centre des politiques sur l’IA, puis expliquer pourquoi vous avez sélectionné ces priorités. Donnez une vue d’ensemble et effectuez un examen du contexte local décrivant ces priorités dans le pays ou la région et expliquez le lien établi entre l’axe thématique du projet et les besoins particuliers du pays ou de la région. Vous devez également préciser s’il existe d’autres parties concernées qui se penchent sur le problème énoncé, les raisons justifiant les priorités proposées, et si l’importance du projet tient au fait qu’il porte sur un sujet de recherche grandement négligé", + }, + placeholders={"en": "Enter up to 1000 words", "fr": "Entrez jusqu'à 1000 mots"}, + validation_regexs={ + "en": r"^\s*(\S+(\s+|$)){0,1000}$", + "fr": r"^\s*(\S+(\s+|$)){0,1000}$", + }, + validation_texts={"en": "Maximum 1000 words", "fr": "Maximum 1000 mots"}, + is_required=True, + ) + + add_question( + section2, + 2, + "long-text", + { + "en": "2. General and specific objectives.", + "fr": "2. Objectifs généraux et précis.", + }, + descriptions={ + "en": "The general objective should state the development goal being pursued by the research. The specific objectives should indicate the specific types of knowledge to be produced, the audiences to be reached, and forms of capacity to be reinforced. These are the objectives against which the success of the project will be judged. Use only active verbs (no passive). Typical objectives focus on 1) deepening understanding of a topic through research; 2) influencing policy and practice; and 3) building capacity.", + "fr": "L’objectif général correspond à l’objectif de développement que l’on souhaite atteindre au moyen de la recherche. Les objectifs particuliers doivent signaler les types précis de connaissances recherchées, les destinataires et les capacités à renforcer. C’est en fonction de ces objectifs que sera jaugée la réussite du projet. Utilisez seulement des verbes d’action (évitez les verbes d’état). En règle générale, les objectifs permettent : 1) de comprendre un sujet en profondeur grâce à la recherche; 2) d’influencer les politiques et les pratiques; et 3) de favoriser le renforcement des capacités.", + }, + placeholders={"en": "Enter up to 250 words", "fr": "Entrez jusqu'à 250 mots"}, + validation_regexs={ + "en": r"^\s*(\S+(\s+|$)){0,250}$", + "fr": r"^\s*(\S+(\s+|$)){0,250}$", + }, + validation_texts={"en": "Maximum 250 words", "fr": "Maximum 250 mots"}, + is_required=True, + ) + + add_question( + section2, + 3, + "information", + {"en": "3. Methodology and approach.", "fr": "3. Méthodologie et approche."}, + is_required=False, + ) + + add_question( + section2, + 4, + "long-text", + {"en": "3a. Approach.", "fr": "3a. Approche."}, + descriptions={ + "en": "Briefly describe the overall approach/logic/pathway to achieve the above objectives.", + "fr": "Décrivez brièvement l’approche, la logique ou le cheminement permettant d’atteindre les objectifs mentionnés précédemment.", + }, + placeholders={"en": "Enter up to 350 words", "fr": "Entrez jusqu'à 350 mots"}, + validation_regexs={ + "en": r"^\s*(\S+(\s+|$)){0,350}$", + "fr": r"^\s*(\S+(\s+|$)){0,350}$", + }, + validation_texts={"en": "Maximum 350 words", "fr": "Maximum 350 mots"}, + is_required=True, + ) + + add_question( + section2, + 5, + "long-text", + {"en": "3b. Policy research.", "fr": "3b. Recherche sur les politiques."}, + descriptions={ + "en": "How will you answer the proposed research questions in the most rigorous way possible? This includes conceptual and theoretical frameworks, user participation, data collection and analysis.", + "fr": "Comment comptez-vous répondre aux questions de recherche proposées de la façon la plus rigoureuse possible ? Veuillez notamment fournir des précisions à l’égard des cadres conceptuels et théoriques, de la participation des utilisateurs, et de la collecte et de l’analyse des données.", + }, + placeholders={"en": "Enter up to 750 words", "fr": "Entrez jusqu'à 750 mots"}, + validation_regexs={ + "en": r"^\s*(\S+(\s+|$)){0,750}$", + "fr": r"^\s*(\S+(\s+|$)){0,750}$", + }, + validation_texts={"en": "Maximum 750 words", "fr": "Maximum 750 mots"}, + is_required=True, + ) + + add_question( + section2, + 6, + "long-text", + {"en": "3c. Gender and inclusion.", "fr": "3c. Sexospécificité et inclusion."}, + descriptions={ + "en": """i. How has your organization addressed gender-equality and other kinds of inclusion and diversity dimensions [socioeconomic, political, cultural, ethnic] in its policy research? ii. How do you anticipate this will improve with this AI4D support? iii. Please give a brief explanation of your strategy with gender, inclusion and diversity related issues as it relates to AI/advanced technologies and offer examples of the research work. Please provide examples [PDF or links] to material that demonstrates your strategy and research on gender, inclusion and diversity. You may upload supporting documents at the end of the application. """, - 'fr': """i. Comment votre organisation a-t-elle tenu compte de l’égalité des genres et d’autres dimensions (p. ex. socio-économiques, politiques, culturelles, ethniques) liées à l’inclusion et à la diversité dans sa recherche sur les politiques? + "fr": """i. Comment votre organisation a-t-elle tenu compte de l’égalité des genres et d’autres dimensions (p. ex. socio-économiques, politiques, culturelles, ethniques) liées à l’inclusion et à la diversité dans sa recherche sur les politiques? ii. Dans quelle mesure pensez-vous que ces éléments s’amélioreront grâce au soutien de l’initiative Intelligence artificielle pour le développement (IAPD) ? iii. Veuillez expliquer brièvement votre stratégie en ce qui a trait aux questions liées à la sexospécificité, à l’inclusion et à la diversité, ainsi qu’à l’IA et aux technologies de pointe, et fournir des exemples de travaux de recherche. Veuillez fournir des exemples (c.-à-d. des documents en format PDF, ou des liens menant vers des fichiers) qui démontrent votre stratégie et vos recherches sur la sexospécificité, l’inclusion et la diversité. -""" - }, placeholders={ - 'en': 'Enter up to 750 words', - 'fr': "Entrez jusqu'à 750 mots" - }, validation_regexs={ - 'en': r'^\s*(\S+(\s+|$)){0,750}$', - 'fr': r'^\s*(\S+(\s+|$)){0,750}$', - }, validation_texts={ - 'en': 'Maximum 750 words', - 'fr': 'Maximum 750 mots' - }, is_required=True) - - add_question(section2, 7, 'long-text', { - 'en': '3d. Ethical considerations.', - 'fr': '3d. Considérations d’ordre éthique.' - }, descriptions={ - 'en': 'All projects that include human subjects must ensure that their privacy, dignity, and integrity are protected. An independent ethical review committee must approve the protocols. Please describe the key ethical considerations for the proposed research and the research ethics protocols your organization uses.', - 'fr': "Dans le cadre de tout projet de recherche portant sur des sujets humains, il faut garantir la protection de la vie privée, de la dignité et de l’intégrité de la personne. Un comité d’éthique indépendant doit autoriser les protocoles. Veuillez décrire les principales considérations d’ordre éthique pour la recherche proposée, ainsi que les protocoles d’éthique de la recherche utilisés par votre organisation." - }, placeholders={ - 'en': 'Enter up to 350 words', - 'fr': "Entrez jusqu'à 350 mots" - }, validation_regexs={ - 'en': r'^\s*(\S+(\s+|$)){0,350}$', - 'fr': r'^\s*(\S+(\s+|$)){0,350}$', - }, validation_texts={ - 'en': 'Maximum 350 words', - 'fr': 'Maximum 350 mots' - }, is_required=True) - - add_question(section2, 8, 'long-text', { - 'en': '4. Capacity development.', - 'fr': '4. Développement des capacités.' - }, descriptions={ - 'en': 'In terms of developing capacity, how would your organization use this grant to advance the capacity of your research team in any of the above subject areas? What would you need to do to build capacity to improve your work and outcomes to advance responsible artificial intelligence in your country/region?', - 'fr': "En matière de développement des capacités, comment votre organisation utiliserait-elle cette subvention pour renforcer les capacités de votre équipe de recherche dans l’un des domaines susmentionnés ? Que devriez- vous faire pour renforcer vos capacités, améliorer la qualité de votre travail, obtenir de meilleurs résultats, et faire progresser l’IA responsable dans votre pays ou votre région?" - }, placeholders={ - 'en': 'Enter up to 350 words', - 'fr': "Entrez jusqu'à 350 mots" - }, validation_regexs={ - 'en': r'^\s*(\S+(\s+|$)){0,350}$', - 'fr': r'^\s*(\S+(\s+|$)){0,350}$', - }, validation_texts={ - 'en': 'Maximum 350 words', - 'fr': 'Maximum 350 mots' - }, is_required=True) - - add_question(section2, 9, 'long-text', { - 'en': '5. Communicating for influence.', - 'fr': '5. Communication en vue d’exercer une influence.' - }, descriptions={ - 'en': 'Please provide a description of your intended research communications strategy to influence policy in the context of this AI4D Africa funding.', - 'fr': "Veuillez fournir une description de la stratégie de communication que vous comptez adopter dans le cadre de votre recherche pour exercer une influence sur les politiques, grâce à ce financement de l’initiative IAPD Afrique." - }, placeholders={ - 'en': 'Enter up to 350 words', - 'fr': "Entrez jusqu'à 350 mots" - }, validation_regexs={ - 'en': r'^\s*(\S+(\s+|$)){0,350}$', - 'fr': r'^\s*(\S+(\s+|$)){0,350}$', - }, validation_texts={ - 'en': 'Maximum 350 words', - 'fr': 'Maximum 350 mots' - }, is_required=True) - - add_question(section2, 10, 'long-text', { - 'en': '6. Results.', - 'fr': '6. Résultats.' - }, descriptions={ - 'en': 'Please describe what success will look like at the end of the three years. What markers will you use to gauge your success in AI4D policy and research? What are your expected research outputs and outcomes?', - 'fr': "Veuillez décrire ce à quoi ressemblera la réussite à la fin de la période de trois années. Quels marqueurs utiliserez-vous pour évaluer votre réussite pour les politiques et les recherches liées à l’initiative IAPD Afrique? Quels sont les résultats escomptés de votre recherche?" - }, placeholders={ - 'en': 'Enter up to 500 words', - 'fr': "Entrez jusqu'à 500 mots" - }, validation_regexs={ - 'en': r'^\s*(\S+(\s+|$)){0,500}$', - 'fr': r'^\s*(\S+(\s+|$)){0,500}$', - }, validation_texts={ - 'en': 'Maximum 500 words', - 'fr': 'Maximum 500 mots' - }, is_required=True) - - add_question(section2, 11, 'long-text', { - 'en': '7. Project schedule.', - 'fr': '7. Calendrier du projet.' - }, descriptions={ - 'en': 'Please provide a draft schedule for how you envision the activities will be organized according to the specific objectives of the project. The project schedule should include a list of key outputs and milestones (key events or deliverables). These need to be related to the items included in the project budget (Section 4).', - 'fr': "Veuillez fournir un calendrier provisoire pour expliquer la façon dont vous comptez organiser les activités, compte tenu des objectifs spécifiques du projet. Le calendrier du projet doit comprendre une liste des principaux extrants et jalons (événements ou produits livrables clés). Ils doivent être liés aux éléments qui figurent dans le budget du projet (section 4)." - }, placeholders={ - 'en': 'Enter up to 350 words', - 'fr': "Entrez jusqu'à 350 mots" - }, validation_regexs={ - 'en': r'^\s*(\S+(\s+|$)){0,350}$', - 'fr': r'^\s*(\S+(\s+|$)){0,350}$', - }, validation_texts={ - 'en': 'Maximum 350 words', - 'fr': 'Maximum 350 mots' - }, is_required=True) - - add_question(section2, 12, 'multi-file', { - 'en': '8. Supplemental documentation (optional).', - 'fr': '8. Documents supplémentaires (facultatifs).' - }, descriptions={ - 'en': 'Attach any supplemental documentation that will help clarify your application (e.g., Theory of Change, supporting graphics)', - 'fr': "Veuillez joindre toute documentation supplémentaire qui aidera à clarifier votre demande (p. ex. théorie du changement, graphiques à l’appui)." - }, is_required=False) +""", + }, + placeholders={"en": "Enter up to 750 words", "fr": "Entrez jusqu'à 750 mots"}, + validation_regexs={ + "en": r"^\s*(\S+(\s+|$)){0,750}$", + "fr": r"^\s*(\S+(\s+|$)){0,750}$", + }, + validation_texts={"en": "Maximum 750 words", "fr": "Maximum 750 mots"}, + is_required=True, + ) + + add_question( + section2, + 7, + "long-text", + { + "en": "3d. Ethical considerations.", + "fr": "3d. Considérations d’ordre éthique.", + }, + descriptions={ + "en": "All projects that include human subjects must ensure that their privacy, dignity, and integrity are protected. An independent ethical review committee must approve the protocols. Please describe the key ethical considerations for the proposed research and the research ethics protocols your organization uses.", + "fr": "Dans le cadre de tout projet de recherche portant sur des sujets humains, il faut garantir la protection de la vie privée, de la dignité et de l’intégrité de la personne. Un comité d’éthique indépendant doit autoriser les protocoles. Veuillez décrire les principales considérations d’ordre éthique pour la recherche proposée, ainsi que les protocoles d’éthique de la recherche utilisés par votre organisation.", + }, + placeholders={"en": "Enter up to 350 words", "fr": "Entrez jusqu'à 350 mots"}, + validation_regexs={ + "en": r"^\s*(\S+(\s+|$)){0,350}$", + "fr": r"^\s*(\S+(\s+|$)){0,350}$", + }, + validation_texts={"en": "Maximum 350 words", "fr": "Maximum 350 mots"}, + is_required=True, + ) + + add_question( + section2, + 8, + "long-text", + {"en": "4. Capacity development.", "fr": "4. Développement des capacités."}, + descriptions={ + "en": "In terms of developing capacity, how would your organization use this grant to advance the capacity of your research team in any of the above subject areas? What would you need to do to build capacity to improve your work and outcomes to advance responsible artificial intelligence in your country/region?", + "fr": "En matière de développement des capacités, comment votre organisation utiliserait-elle cette subvention pour renforcer les capacités de votre équipe de recherche dans l’un des domaines susmentionnés ? Que devriez- vous faire pour renforcer vos capacités, améliorer la qualité de votre travail, obtenir de meilleurs résultats, et faire progresser l’IA responsable dans votre pays ou votre région?", + }, + placeholders={"en": "Enter up to 350 words", "fr": "Entrez jusqu'à 350 mots"}, + validation_regexs={ + "en": r"^\s*(\S+(\s+|$)){0,350}$", + "fr": r"^\s*(\S+(\s+|$)){0,350}$", + }, + validation_texts={"en": "Maximum 350 words", "fr": "Maximum 350 mots"}, + is_required=True, + ) + + add_question( + section2, + 9, + "long-text", + { + "en": "5. Communicating for influence.", + "fr": "5. Communication en vue d’exercer une influence.", + }, + descriptions={ + "en": "Please provide a description of your intended research communications strategy to influence policy in the context of this AI4D Africa funding.", + "fr": "Veuillez fournir une description de la stratégie de communication que vous comptez adopter dans le cadre de votre recherche pour exercer une influence sur les politiques, grâce à ce financement de l’initiative IAPD Afrique.", + }, + placeholders={"en": "Enter up to 350 words", "fr": "Entrez jusqu'à 350 mots"}, + validation_regexs={ + "en": r"^\s*(\S+(\s+|$)){0,350}$", + "fr": r"^\s*(\S+(\s+|$)){0,350}$", + }, + validation_texts={"en": "Maximum 350 words", "fr": "Maximum 350 mots"}, + is_required=True, + ) + + add_question( + section2, + 10, + "long-text", + {"en": "6. Results.", "fr": "6. Résultats."}, + descriptions={ + "en": "Please describe what success will look like at the end of the three years. What markers will you use to gauge your success in AI4D policy and research? What are your expected research outputs and outcomes?", + "fr": "Veuillez décrire ce à quoi ressemblera la réussite à la fin de la période de trois années. Quels marqueurs utiliserez-vous pour évaluer votre réussite pour les politiques et les recherches liées à l’initiative IAPD Afrique? Quels sont les résultats escomptés de votre recherche?", + }, + placeholders={"en": "Enter up to 500 words", "fr": "Entrez jusqu'à 500 mots"}, + validation_regexs={ + "en": r"^\s*(\S+(\s+|$)){0,500}$", + "fr": r"^\s*(\S+(\s+|$)){0,500}$", + }, + validation_texts={"en": "Maximum 500 words", "fr": "Maximum 500 mots"}, + is_required=True, + ) + + add_question( + section2, + 11, + "long-text", + {"en": "7. Project schedule.", "fr": "7. Calendrier du projet."}, + descriptions={ + "en": "Please provide a draft schedule for how you envision the activities will be organized according to the specific objectives of the project. The project schedule should include a list of key outputs and milestones (key events or deliverables). These need to be related to the items included in the project budget (Section 4).", + "fr": "Veuillez fournir un calendrier provisoire pour expliquer la façon dont vous comptez organiser les activités, compte tenu des objectifs spécifiques du projet. Le calendrier du projet doit comprendre une liste des principaux extrants et jalons (événements ou produits livrables clés). Ils doivent être liés aux éléments qui figurent dans le budget du projet (section 4).", + }, + placeholders={"en": "Enter up to 350 words", "fr": "Entrez jusqu'à 350 mots"}, + validation_regexs={ + "en": r"^\s*(\S+(\s+|$)){0,350}$", + "fr": r"^\s*(\S+(\s+|$)){0,350}$", + }, + validation_texts={"en": "Maximum 350 words", "fr": "Maximum 350 mots"}, + is_required=True, + ) + + add_question( + section2, + 12, + "multi-file", + { + "en": "8. Supplemental documentation (optional).", + "fr": "8. Documents supplémentaires (facultatifs).", + }, + descriptions={ + "en": "Attach any supplemental documentation that will help clarify your application (e.g., Theory of Change, supporting graphics)", + "fr": "Veuillez joindre toute documentation supplémentaire qui aidera à clarifier votre demande (p. ex. théorie du changement, graphiques à l’appui).", + }, + is_required=False, + ) # Section 3 - section3 = add_section({ - 'en': 'Section 3: Policy engagement experience', - 'fr': 'Section 3: Expérience en matière d’interaction avec la sphère des politiques' - }, None, 5) - - add_question(section3, 1, 'long-text', { - 'en': '1. Please describe the kinds of engagement your organization has had with national governing institutions (e.g. government departments, bodies and agencies, commissions, parliaments, regulators, and other public-sector institutions) on any of the above topics (from Section 1, question 3)? This can include a reflection on what you have done well with communications, and what you have done not so well.', - 'fr': 'Veuillez décrire les types d’interactions que votre organisation a eus avec des institutions gouvernementales nationales (p. ex. ministères et organismes gouvernementaux, commissions, parlements, organismes de réglementation et autres institutions du secteur public) en ce qui concerne les sujets susmentionnés (à la section 1, question 3). Vous pouvez notamment faire part de vos réflexions sur ce que vous avez bien fait en matière de communication, ainsi que sur les éléments que vous devez améliorer.' - }, descriptions={ - 'en': 'Please give up to three examples of this engagement, such as requests for input, sitting on policy steering committees and meetings with policy makers.', - 'fr': 'Veuillez fournir un maximum de trois exemples de ces interactions (p. ex. demandes de consultation, participation à des comités directeurs, réunions avec des décideurs politiques).' - }, placeholders={ - 'en': 'Enter up to 350 words', - 'fr': "Entrez jusqu'à 350 mots" - }, validation_regexs={ - 'en': r'^\s*(\S+(\s+|$)){0,350}$', - 'fr': r'^\s*(\S+(\s+|$)){0,350}$', - }, validation_texts={ - 'en': 'Maximum 350 words', - 'fr': 'Maximum 350 mots' - }, is_required=True) - - add_question(section3, 2, 'long-text', { - 'en': '2. Describe and give examples of how your research has informed public debate or policy processes nationally and/or regionally. ', - 'fr': 'Décrivez votre recherche et donnez des exemples de la façon dont elle a exercé une influence sur le débat public ou les processus politiques à l’échelle nationale ou régionale.' - }, descriptions=None, placeholders={ - 'en': 'Enter up to 350 words', - 'fr': "Entrez jusqu'à 350 mots" - }, validation_regexs={ - 'en': r'^\s*(\S+(\s+|$)){0,350}$', - 'fr': r'^\s*(\S+(\s+|$)){0,350}$', - }, validation_texts={ - 'en': 'Maximum 350 words', - 'fr': 'Maximum 350 mots' - }, is_required=True) - - q3 = add_question(section3, 3, 'multi-choice', { - 'en': '3. Does your organization also work regionally and/or globally on technology policy related issues?', - 'fr': '3. Votre organisation travaille-t-elle aussi sur des questions liées aux politiques technologiques à l’échelle régionale ou mondiale?' - }, options={ - 'en': [{'label': 'Yes', 'value': 'Yes'}, {'label': 'No', 'value': 'No'}], - 'fr': [{'label': 'Oui', 'value': 'Yes'}, {'label': 'Non', 'value': 'No'}] - }, is_required=True) - - add_question(section3, 4, 'long-text', { - 'en': '3a. If yes, please describe this engagement. List any regional and global networks/association memberships if appropriate.', - 'fr': '3a. Si oui, veuillez décrire la nature de ces travaux. Dressez une liste des associations ou des réseaux auxquels vous appartenez à l’échelle régionale et mondiale, le cas échéant.' - }, descriptions=None, placeholders={ - 'en': 'Enter up to 250 words', - 'fr': "Entrez jusqu'à 250 mots" - }, validation_regexs={ - 'en': r'^\s*(\S+(\s+|$)){0,250}$', - 'fr': r'^\s*(\S+(\s+|$)){0,250}$', - }, validation_texts={ - 'en': 'Maximum 250 words', - 'fr': 'Maximum 250 mots' - }, is_required=True, depends_on_question_id=q3.id, show_for_values={ - 'en': 'Yes', - 'fr': 'Yes' - }) + section3 = add_section( + { + "en": "Section 3: Policy engagement experience", + "fr": "Section 3: Expérience en matière d’interaction avec la sphère des politiques", + }, + None, + 5, + ) + + add_question( + section3, + 1, + "long-text", + { + "en": "1. Please describe the kinds of engagement your organization has had with national governing institutions (e.g. government departments, bodies and agencies, commissions, parliaments, regulators, and other public-sector institutions) on any of the above topics (from Section 1, question 3)? This can include a reflection on what you have done well with communications, and what you have done not so well.", + "fr": "Veuillez décrire les types d’interactions que votre organisation a eus avec des institutions gouvernementales nationales (p. ex. ministères et organismes gouvernementaux, commissions, parlements, organismes de réglementation et autres institutions du secteur public) en ce qui concerne les sujets susmentionnés (à la section 1, question 3). Vous pouvez notamment faire part de vos réflexions sur ce que vous avez bien fait en matière de communication, ainsi que sur les éléments que vous devez améliorer.", + }, + descriptions={ + "en": "Please give up to three examples of this engagement, such as requests for input, sitting on policy steering committees and meetings with policy makers.", + "fr": "Veuillez fournir un maximum de trois exemples de ces interactions (p. ex. demandes de consultation, participation à des comités directeurs, réunions avec des décideurs politiques).", + }, + placeholders={"en": "Enter up to 350 words", "fr": "Entrez jusqu'à 350 mots"}, + validation_regexs={ + "en": r"^\s*(\S+(\s+|$)){0,350}$", + "fr": r"^\s*(\S+(\s+|$)){0,350}$", + }, + validation_texts={"en": "Maximum 350 words", "fr": "Maximum 350 mots"}, + is_required=True, + ) + + add_question( + section3, + 2, + "long-text", + { + "en": "2. Describe and give examples of how your research has informed public debate or policy processes nationally and/or regionally. ", + "fr": "Décrivez votre recherche et donnez des exemples de la façon dont elle a exercé une influence sur le débat public ou les processus politiques à l’échelle nationale ou régionale.", + }, + descriptions=None, + placeholders={"en": "Enter up to 350 words", "fr": "Entrez jusqu'à 350 mots"}, + validation_regexs={ + "en": r"^\s*(\S+(\s+|$)){0,350}$", + "fr": r"^\s*(\S+(\s+|$)){0,350}$", + }, + validation_texts={"en": "Maximum 350 words", "fr": "Maximum 350 mots"}, + is_required=True, + ) + + q3 = add_question( + section3, + 3, + "multi-choice", + { + "en": "3. Does your organization also work regionally and/or globally on technology policy related issues?", + "fr": "3. Votre organisation travaille-t-elle aussi sur des questions liées aux politiques technologiques à l’échelle régionale ou mondiale?", + }, + options={ + "en": [{"label": "Yes", "value": "Yes"}, {"label": "No", "value": "No"}], + "fr": [{"label": "Oui", "value": "Yes"}, {"label": "Non", "value": "No"}], + }, + is_required=True, + ) + + add_question( + section3, + 4, + "long-text", + { + "en": "3a. If yes, please describe this engagement. List any regional and global networks/association memberships if appropriate.", + "fr": "3a. Si oui, veuillez décrire la nature de ces travaux. Dressez une liste des associations ou des réseaux auxquels vous appartenez à l’échelle régionale et mondiale, le cas échéant.", + }, + descriptions=None, + placeholders={"en": "Enter up to 250 words", "fr": "Entrez jusqu'à 250 mots"}, + validation_regexs={ + "en": r"^\s*(\S+(\s+|$)){0,250}$", + "fr": r"^\s*(\S+(\s+|$)){0,250}$", + }, + validation_texts={"en": "Maximum 250 words", "fr": "Maximum 250 mots"}, + is_required=True, + depends_on_question_id=q3.id, + show_for_values={"en": "Yes", "fr": "Yes"}, + ) # Section 4 en_desc = """Please submit a full three year budget with notes using the Excel sheet available on the IDRC public website: https://www.idrc.ca/en/resources/guides-and-forms @@ -1007,21 +1203,27 @@ def add_question(section, order, questionType, headlines, Insérez ce budget en tant que pièce jointe à votre demande. Dans le cas contraire, elle sera rejetée. """ - section4 = add_section({ - 'en': 'Section 4: Budget', - 'fr': 'Section 4: Budget' - }, { - 'en': en_desc, - 'fr': fr_desc - }, 6) - - add_question(section4, 1, 'file', { - 'en': 'Budget', - 'fr': 'Budget' - }, is_required=True, options={ - 'en': {"accept": ".csv, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"}, - 'fr': {"accept": ".csv, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"}, - }) + section4 = add_section( + {"en": "Section 4: Budget", "fr": "Section 4: Budget"}, + {"en": en_desc, "fr": fr_desc}, + 6, + ) + + add_question( + section4, + 1, + "file", + {"en": "Budget", "fr": "Budget"}, + is_required=True, + options={ + "en": { + "accept": ".csv, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel" + }, + "fr": { + "accept": ".csv, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel" + }, + }, + ) def downgrade(): diff --git a/api/migrations/versions/438ca9e1c2e8_.py b/api/migrations/versions/438ca9e1c2e8_.py index 47d67d42d..99c7cfc97 100644 --- a/api/migrations/versions/438ca9e1c2e8_.py +++ b/api/migrations/versions/438ca9e1c2e8_.py @@ -7,11 +7,11 @@ """ # revision identifiers, used by Alembic. -revision = '438ca9e1c2e8' -down_revision = ('ab8b7ff61eee', '57e55dc645c1') +revision = "438ca9e1c2e8" +down_revision = ("ab8b7ff61eee", "57e55dc645c1") -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): diff --git a/api/migrations/versions/44df297ad858_.py b/api/migrations/versions/44df297ad858_.py index 50e96b8ff..a9d1a9cde 100644 --- a/api/migrations/versions/44df297ad858_.py +++ b/api/migrations/versions/44df297ad858_.py @@ -7,32 +7,42 @@ """ # revision identifiers, used by Alembic. -revision = '44df297ad858' -down_revision = '910e74b87f06' +revision = "44df297ad858" +down_revision = "910e74b87f06" -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('event', 'name') - op.drop_column('event', 'description') + op.drop_column("event", "name") + op.drop_column("event", "description") # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.add_column('event', sa.Column('description', sa.VARCHAR(length=255), autoincrement=False, nullable=True)) - op.add_column('event', sa.Column('name', sa.VARCHAR(length=255), autoincrement=False, nullable=True)) - - op.execute(""" + op.add_column( + "event", + sa.Column( + "description", sa.VARCHAR(length=255), autoincrement=False, nullable=True + ), + ) + op.add_column( + "event", + sa.Column("name", sa.VARCHAR(length=255), autoincrement=False, nullable=True), + ) + + op.execute( + """ UPDATE event SET name = (SELECT name FROM event_translation WHERE event.id = event_translation.event_id AND event_translation.language = 'en'), description = (SELECT description FROM event_translation WHERE event.id = event_translation.event_id AND event_translation.language = 'en') - """) + """ + ) - op.alter_column('event', 'name', nullable=False) - op.alter_column('event', 'description', nullable=False) + op.alter_column("event", "name", nullable=False) + op.alter_column("event", "description", nullable=False) # ### end Alembic commands ### diff --git a/api/migrations/versions/4605cce59919_add_ai4d.py b/api/migrations/versions/4605cce59919_add_ai4d.py index 2be1d847d..aecf2259e 100644 --- a/api/migrations/versions/4605cce59919_add_ai4d.py +++ b/api/migrations/versions/4605cce59919_add_ai4d.py @@ -7,24 +7,26 @@ """ # revision identifiers, used by Alembic. -revision = '4605cce59919' -down_revision = '2279e1fa2e49' +revision = "4605cce59919" +down_revision = "2279e1fa2e49" + +import datetime +from enum import Enum -from alembic import op import sqlalchemy as sa +from alembic import op from sqlalchemy import orm from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy import orm + from app import db -import datetime -from enum import Enum Base = declarative_base() + class Organisation(Base): __tablename__ = "organisation" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(50), nullable=False) @@ -36,7 +38,17 @@ class Organisation(Base): email_from = db.Column(db.String(100), nullable=True) system_url = db.Column(db.String(100), nullable=False) - def __init__(self, name, system_name, small_logo, large_logo, domain, url, email_from, system_url): + def __init__( + self, + name, + system_name, + small_logo, + large_logo, + domain, + url, + email_from, + system_url, + ): self.name = name self.small_logo = small_logo self.large_logo = large_logo @@ -46,9 +58,10 @@ def __init__(self, name, system_name, small_logo, large_logo, domain, url, email self.email_from = email_from self.system_url = system_url + class Country(Base): __tablename__ = "country" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(100), nullable=False) @@ -58,14 +71,14 @@ def __init__(self, name): class EventType(Enum): - EVENT = 'event' - AWARD = 'award' + EVENT = "event" + AWARD = "award" class Event(Base): __tablename__ = "event" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(50), nullable=False) @@ -73,8 +86,9 @@ class Event(Base): start_date = db.Column(db.DateTime(), nullable=False) end_date = db.Column(db.DateTime(), nullable=False) key = db.Column(db.String(255), nullable=False, unique=True) - organisation_id = db.Column(db.Integer(), db.ForeignKey( - 'organisation.id'), nullable=False) + organisation_id = db.Column( + db.Integer(), db.ForeignKey("organisation.id"), nullable=False + ) email_from = db.Column(db.String(255), nullable=False) url = db.Column(db.String(255), nullable=False) travel_grant = db.Column(db.Boolean(), nullable=False) @@ -91,28 +105,29 @@ class Event(Base): registration_close = db.Column(db.DateTime(), nullable=False) event_type = db.Column(db.Enum(EventType), nullable=False) - def __init__(self, - name, - description, - start_date, - end_date, - key, - organisation_id, - email_from, - url, - application_open, - application_close, - review_open, - review_close, - selection_open, - selection_close, - offer_open, - offer_close, - registration_open, - registration_close, - event_type, - travel_grant - ): + def __init__( + self, + name, + description, + start_date, + end_date, + key, + organisation_id, + email_from, + url, + application_open, + application_close, + review_open, + review_close, + selection_open, + selection_close, + offer_open, + offer_close, + registration_open, + registration_close, + event_type, + travel_grant, + ): self.name = name self.description = description @@ -135,14 +150,15 @@ def __init__(self, self.event_type = event_type self.travel_grant = travel_grant + class ApplicationForm(Base): - __tablename__ = 'application_form' - __table_args__ = {'extend_existing': True} + __tablename__ = "application_form" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - event_id = db.Column(db.Integer(), db.ForeignKey('event.id'), nullable=False) + event_id = db.Column(db.Integer(), db.ForeignKey("event.id"), nullable=False) is_open = db.Column(db.Boolean(), nullable=False) - nominations = db.Column(db.Boolean(), nullable=False) + nominations = db.Column(db.Boolean(), nullable=False) def __init__(self, event_id, is_open, nominations): self.event_id = event_id @@ -151,15 +167,19 @@ def __init__(self, event_id, is_open, nominations): class Section(Base): - __tablename__ = 'section' - __table_args__ = {'extend_existing': True} + __tablename__ = "section" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) name = db.Column(db.String(255), nullable=False) description = db.Column(db.String(255), nullable=False) order = db.Column(db.Integer(), nullable=False) - depends_on_question_id = db.Column(db.Integer(), db.ForeignKey('question.id', use_alter=True), nullable=True) + depends_on_question_id = db.Column( + db.Integer(), db.ForeignKey("question.id", use_alter=True), nullable=True + ) show_for_values = db.Column(db.JSON(), nullable=True) key = db.Column(db.String(255), nullable=True) @@ -169,13 +189,16 @@ def __init__(self, application_form_id, name, description, order): self.description = description self.order = order + class Question(Base): - __tablename__ = 'question' - __table_args__ = {'extend_existing': True} + __tablename__ = "question" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) - section_id = db.Column(db.Integer(), db.ForeignKey('section.id'), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) + section_id = db.Column(db.Integer(), db.ForeignKey("section.id"), nullable=False) type = db.Column(db.String(), nullable=False) description = db.Column(db.String(), nullable=True) headline = db.Column(db.String(), nullable=False) @@ -185,11 +208,26 @@ class Question(Base): order = db.Column(db.Integer(), nullable=False) options = db.Column(db.JSON(), nullable=True) is_required = db.Column(db.Boolean(), nullable=False) - depends_on_question_id = db.Column(db.Integer(), db.ForeignKey('question.id'), nullable=True) + depends_on_question_id = db.Column( + db.Integer(), db.ForeignKey("question.id"), nullable=True + ) show_for_values = db.Column(db.JSON(), nullable=True) key = db.Column(db.String(255), nullable=True) - def __init__(self, application_form_id, section_id, headline, placeholder, order, questionType, validation_regex, validation_text=None, is_required = True, description = None, options = None): + def __init__( + self, + application_form_id, + section_id, + headline, + placeholder, + order, + questionType, + validation_regex, + validation_text=None, + is_required=True, + description=None, + options=None, + ): self.application_form_id = application_form_id self.section_id = section_id self.headline = headline @@ -207,10 +245,7 @@ def get_country_list(session): countries = session.query(Country).all() country_list = [] for country in countries: - country_list.append({ - 'label': country.name, - 'value': country.name - }) + country_list.append({"label": country.name, "value": country.name}) return country_list @@ -219,13 +254,29 @@ def upgrade(): session = orm.Session(bind=Base.metadata.bind) # Add event - ai4d = Event('AI4D Research Innovation', 'AI4D-Africa Community-Based Research Innovation', - datetime.date(2021, 1, 31), datetime.date(2021, 1, 31), 'ai4d2020', 1, 'baobab@deeplearningindaba.com', - 'https://deeplearningindaba.com/2020/grand-challenge', datetime.date(2020, 5, 27), datetime.date(2020, 6, 20), datetime.date(2020, 12, 31), - datetime.date(2020, 12, 31), datetime.date(2020, 12, 31), datetime.date(2020, 12, 31), datetime.date(2020, 12, 31), - datetime.date(2020, 12, 31), datetime.date(2020, 12, 31), datetime.date(2020, 12, 31), EventType.EVENT, - False) - + ai4d = Event( + "AI4D Research Innovation", + "AI4D-Africa Community-Based Research Innovation", + datetime.date(2021, 1, 31), + datetime.date(2021, 1, 31), + "ai4d2020", + 1, + "baobab@deeplearningindaba.com", + "https://deeplearningindaba.com/2020/grand-challenge", + datetime.date(2020, 5, 27), + datetime.date(2020, 6, 20), + datetime.date(2020, 12, 31), + datetime.date(2020, 12, 31), + datetime.date(2020, 12, 31), + datetime.date(2020, 12, 31), + datetime.date(2020, 12, 31), + datetime.date(2020, 12, 31), + datetime.date(2020, 12, 31), + datetime.date(2020, 12, 31), + EventType.EVENT, + False, + ) + session.add(ai4d) session.commit() @@ -233,7 +284,10 @@ def upgrade(): session.add(app_form) session.commit() - main_section = Section(app_form.id, 'AI4D-Africa Community-Based Research Innovation: Call for Proposals', """ + main_section = Section( + app_form.id, + "AI4D-Africa Community-Based Research Innovation: Call for Proposals", + """ We are looking to strengthen AI research communities and the work they do during the Covid pandemic, by funding research projects across Africa that are collaborative at heart. This Call for Proposals invites individuals, grassroots organizations, initiatives, academic, and civil society institutions to apply for funding for mini-projects. These could be new or existing projects at various stages, including projects that create and analyse new data sets around a research hypothesis, to later stage projects that require a "final push". New initiatives might also focus on a Research Grand Challenge, as set out below. @@ -247,23 +301,71 @@ def upgrade(): APPLICATOIN DEADLINE: 19 June 2020 - """, 1) + """, + 1, + ) session.add(main_section) session.commit() - q1 = Question(app_form.id, main_section.id, 'Project Name', 'Project Name', 1, 'short-text', None) - q2 = Question(app_form.id, main_section.id, 'Affiliation of Project Lead', 'Affiliation', 2, 'short-text', None) - q3 = Question(app_form.id, main_section.id, 'Country of Project Lead', 'Country', 3, 'multi-choice', None, options=get_country_list(session)) - q4 = Question(app_form.id, main_section.id, 'Preferred method of funding', 'Method of funding', 4, 'multi-choice', None, options=[ - {'label': '(a) university/research institution/partner charity', 'value': 'partner'}, - {'label': '(b) through the project lead directly', 'value': 'project-lead'}, - ], description=""" + q1 = Question( + app_form.id, + main_section.id, + "Project Name", + "Project Name", + 1, + "short-text", + None, + ) + q2 = Question( + app_form.id, + main_section.id, + "Affiliation of Project Lead", + "Affiliation", + 2, + "short-text", + None, + ) + q3 = Question( + app_form.id, + main_section.id, + "Country of Project Lead", + "Country", + 3, + "multi-choice", + None, + options=get_country_list(session), + ) + q4 = Question( + app_form.id, + main_section.id, + "Preferred method of funding", + "Method of funding", + 4, + "multi-choice", + None, + options=[ + { + "label": "(a) university/research institution/partner charity", + "value": "partner", + }, + {"label": "(b) through the project lead directly", "value": "project-lead"}, + ], + description=""" There are two avenues to fund a project: (a) through a university or research institution or local partner charity; (b) or through the project lead directly. In case (b), the project lead would be contracted as a consultant for Knowledge4All (K4A) until the project's completion. The project lead would receive the project funding directly as a personal research allowance, and be accountable to K4A for how the research allowance is used toward the project. We expect that income tax would be payable if the project is funded as a personal research allowance. The project lead would be personally responsible for declaring it in their yearly tax return. We don't want project leads to unexpectedly pay additional income tax on the research allowance after it is spent! Project leads are advised to include the income tax percentage and amount upfront in their budget. - """) - q5 = Question(app_form.id, main_section.id, 'Your Proposal', 'Proposal', 5, 'file', None, description=""" + """, + ) + q5 = Question( + app_form.id, + main_section.id, + "Your Proposal", + "Proposal", + 5, + "file", + None, + description=""" Please submit a PDF of at most 4 pages according to the following template: 1) Names of collaborators, with brief profiles of the team involved, giving relevant information as to demonstrate your ability to undertake and deliver on the proposed research; 2) Research hypothesis and expected outcomes; @@ -273,28 +375,36 @@ def upgrade(): 6) Suggestions of what other support you would require from us to make your research work a success; 7) A preferred method of funding (details below); 8) A budget detailing the total amount requested and expenditure allocation (limit USD 8,000 per project). The budget may account for income tax on a personal research allowance, if option (b) below is pursued. - """) - q6 = Question(app_form.id, main_section.id, 'Mentorship', 'Mentorship', 6, 'multi-choice', None, - options=[{'label': 'Yes', 'value': 'yes'}, {'label': 'No', 'value': 'no'}], - description=""" + """, + ) + q6 = Question( + app_form.id, + main_section.id, + "Mentorship", + "Mentorship", + 6, + "multi-choice", + None, + options=[{"label": "Yes", "value": "yes"}, {"label": "No", "value": "no"}], + description=""" Would you appreciate project mentorship from the Deep Learning Indaba network? - """) + """, + ) session.add_all([q1, q2, q3, q4, q5, q6]) session.commit() - def downgrade(): Base.metadata.bind = op.get_bind() session = orm.Session(bind=Base.metadata.bind) - event = session.query(Event).filter_by(key='ai4d2020').first() + event = session.query(Event).filter_by(key="ai4d2020").first() app_form = session.query(ApplicationForm).filter_by(event_id=event.id).first() session.query(Question).filter_by(application_form_id=app_form.id).delete() session.query(Section).filter_by(application_form_id=app_form.id).delete() session.query(ApplicationForm).filter_by(event_id=event.id).delete() - session.query(Event).filter_by(key='ai4d2020').delete() + session.query(Event).filter_by(key="ai4d2020").delete() session.commit() diff --git a/api/migrations/versions/472bf293e0a1_.py b/api/migrations/versions/472bf293e0a1_.py index b9d88ac37..11a1c3f30 100644 --- a/api/migrations/versions/472bf293e0a1_.py +++ b/api/migrations/versions/472bf293e0a1_.py @@ -7,11 +7,11 @@ """ # revision identifiers, used by Alembic. -revision = '472bf293e0a1' -down_revision = ('79c61673a487', '7e8bffa88454') +revision = "472bf293e0a1" +down_revision = ("79c61673a487", "7e8bffa88454") -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): diff --git a/api/migrations/versions/492546998537_.py b/api/migrations/versions/492546998537_.py index 5496d5b15..2b7bc87a7 100644 --- a/api/migrations/versions/492546998537_.py +++ b/api/migrations/versions/492546998537_.py @@ -7,11 +7,11 @@ """ # revision identifiers, used by Alembic. -revision = '492546998537' -down_revision = ('36235e184d09', 'cb36953371ab') +revision = "492546998537" +down_revision = ("36235e184d09", "cb36953371ab") -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): diff --git a/api/migrations/versions/49d55031108e_ai4d_scholarships.py b/api/migrations/versions/49d55031108e_ai4d_scholarships.py index 559c9afa9..ea22a47ef 100644 --- a/api/migrations/versions/49d55031108e_ai4d_scholarships.py +++ b/api/migrations/versions/49d55031108e_ai4d_scholarships.py @@ -9,139 +9,140 @@ """ # revision identifiers, used by Alembic. -revision = '49d55031108e' -down_revision = 'd7dfd413e768' +revision = "49d55031108e" +down_revision = "d7dfd413e768" + +import datetime +from enum import Enum -from alembic import op import sqlalchemy as sa +from alembic import op from sqlalchemy import orm from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy import orm + from app import db -import datetime -from enum import Enum Base = declarative_base() en_countries = [ - 'Algeria', - 'Angola', - 'Benin', - 'Botswana', - 'Burkina Faso', - 'Burundi', - 'Cameroon', - 'Cape Verde', - 'Central African Republic', - 'Chad', - 'Comoros', - 'Democratic Republic of the Congo', - 'Republic of the Congo', - 'Côte d’Ivoire', - 'Djibouti', - 'Egypt', - 'Equatorial Guinea', - 'Eritrea', - 'Eswatini', - 'Ethiopia', - 'Gabon', - 'The Gambia', - 'Ghana', - 'Guinea', - 'Guinea-Bissau', - 'Kenya', - 'Lesotho', - 'Liberia', - 'Libya', - 'Madagascar', - 'Malawi', - 'Mali', - 'Mauritania', - 'Mauritius', - 'Morocco', - 'Mozambique', - 'Namibia', - 'Niger', - 'Nigeria', - 'Rwanda', - 'Sao Tome and Principe', - 'Senegal', - 'Seychelles', - 'Sierra Leone', - 'Somalia', - 'South Africa', - 'South Sudan', - 'Sudan', - 'Tanzania', - 'Togo', - 'Tunisia', - 'Uganda', - 'Zambia', - 'Zimbabwe' + "Algeria", + "Angola", + "Benin", + "Botswana", + "Burkina Faso", + "Burundi", + "Cameroon", + "Cape Verde", + "Central African Republic", + "Chad", + "Comoros", + "Democratic Republic of the Congo", + "Republic of the Congo", + "Côte d’Ivoire", + "Djibouti", + "Egypt", + "Equatorial Guinea", + "Eritrea", + "Eswatini", + "Ethiopia", + "Gabon", + "The Gambia", + "Ghana", + "Guinea", + "Guinea-Bissau", + "Kenya", + "Lesotho", + "Liberia", + "Libya", + "Madagascar", + "Malawi", + "Mali", + "Mauritania", + "Mauritius", + "Morocco", + "Mozambique", + "Namibia", + "Niger", + "Nigeria", + "Rwanda", + "Sao Tome and Principe", + "Senegal", + "Seychelles", + "Sierra Leone", + "Somalia", + "South Africa", + "South Sudan", + "Sudan", + "Tanzania", + "Togo", + "Tunisia", + "Uganda", + "Zambia", + "Zimbabwe", ] fr_countries = [ - 'Algérie', - 'Angola', - 'Bénin', - 'Botswana', - 'Burkina Faso', - 'Burundi', - 'Cameroun', - 'Cap-Vert', - 'République centrafricaine', - 'Tchad', - 'Comores', - 'République démocratique du Congo', - 'République du Congo', - 'Côte d’Ivoire', - 'Djibouti', - 'Égypte', - 'Guinée équatoriale', - 'Érythrée', - 'Eswatini', - 'Éthiopie', - 'Gabon', - 'Gambie', - 'Ghana', - 'Guinée', - 'Guinée-Bissau', - 'Kenya', - 'Lesotho', - 'Libéria', - 'Libye', - 'Madagascar', - 'Malawi', - 'Mali', - 'Maroc', - 'Mauritanie', - 'île Maurice', - 'Mozambique', - 'Namibie', - 'Niger', - 'Nigeria', - 'Rwanda', - 'Sao Tomé-et-Principe', - 'Sénégal', - 'Seychelles', - 'Sierra Leone', - 'Somalie', - 'Afrique du Sud', - 'Soudan du Sud', - 'Soudan', - 'Tanzanie', - 'Togo', - 'Tunisie', - 'Ouganda', - 'Zambie', - 'Zimbabwe' + "Algérie", + "Angola", + "Bénin", + "Botswana", + "Burkina Faso", + "Burundi", + "Cameroun", + "Cap-Vert", + "République centrafricaine", + "Tchad", + "Comores", + "République démocratique du Congo", + "République du Congo", + "Côte d’Ivoire", + "Djibouti", + "Égypte", + "Guinée équatoriale", + "Érythrée", + "Eswatini", + "Éthiopie", + "Gabon", + "Gambie", + "Ghana", + "Guinée", + "Guinée-Bissau", + "Kenya", + "Lesotho", + "Libéria", + "Libye", + "Madagascar", + "Malawi", + "Mali", + "Maroc", + "Mauritanie", + "île Maurice", + "Mozambique", + "Namibie", + "Niger", + "Nigeria", + "Rwanda", + "Sao Tomé-et-Principe", + "Sénégal", + "Seychelles", + "Sierra Leone", + "Somalie", + "Afrique du Sud", + "Soudan du Sud", + "Soudan", + "Tanzanie", + "Togo", + "Tunisie", + "Ouganda", + "Zambie", + "Zimbabwe", ] class Organisation(Base): __tablename__ = "organisation" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(50), nullable=False) @@ -153,7 +154,17 @@ class Organisation(Base): email_from = db.Column(db.String(100), nullable=True) system_url = db.Column(db.String(100), nullable=False) - def __init__(self, name, system_name, small_logo, large_logo, domain, url, email_from, system_url): + def __init__( + self, + name, + system_name, + small_logo, + large_logo, + domain, + url, + email_from, + system_url, + ): self.name = name self.small_logo = small_logo self.large_logo = large_logo @@ -163,9 +174,10 @@ def __init__(self, name, system_name, small_logo, large_logo, domain, url, email self.email_from = email_from self.system_url = system_url + class Country(Base): __tablename__ = "country" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(100), nullable=False) @@ -173,22 +185,25 @@ class Country(Base): def __init__(self, name): self.name = name + class EventType(Enum): - EVENT = 'event' - AWARD = 'award' - CALL = 'call' + EVENT = "event" + AWARD = "award" + CALL = "call" class Event(Base): __tablename__ = "event" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) start_date = db.Column(db.DateTime(), nullable=False) end_date = db.Column(db.DateTime(), nullable=False) key = db.Column(db.String(255), nullable=False, unique=True) - organisation_id = db.Column(db.Integer(), db.ForeignKey('organisation.id'), nullable=False) + organisation_id = db.Column( + db.Integer(), db.ForeignKey("organisation.id"), nullable=False + ) email_from = db.Column(db.String(255), nullable=False) url = db.Column(db.String(255), nullable=False) application_open = db.Column(db.DateTime(), nullable=False) @@ -205,7 +220,7 @@ class Event(Base): travel_grant = db.Column(db.Boolean(), nullable=False) miniconf_url = db.Column(db.String(100), nullable=True) - event_translations = db.relationship('EventTranslation', lazy='dynamic') + event_translations = db.relationship("EventTranslation", lazy="dynamic") def __init__( self, @@ -229,7 +244,7 @@ def __init__( registration_close, event_type, travel_grant, - miniconf_url=None + miniconf_url=None, ): self.start_date = start_date self.end_date = end_date @@ -261,10 +276,11 @@ def add_event_translations(self, names, descriptions): event_translation = EventTranslation(name, description, language) self.event_translations.append(event_translation) + class EventTranslation(Base): __tablename__ = "event_translation" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) event_id = db.Column(db.Integer(), db.ForeignKey("event.id"), nullable=False) @@ -272,19 +288,20 @@ class EventTranslation(Base): description = db.Column(db.String(255), nullable=False) language = db.Column(db.String(2)) - event = db.relationship('Event', foreign_keys=[event_id]) + event = db.relationship("Event", foreign_keys=[event_id]) def __init__(self, name, description, language): self.name = name self.description = description self.language = language + class ApplicationForm(Base): - __tablename__ = 'application_form' - __table_args__ = {'extend_existing': True} + __tablename__ = "application_form" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - event_id = db.Column(db.Integer(), db.ForeignKey('event.id'), nullable=False) + event_id = db.Column(db.Integer(), db.ForeignKey("event.id"), nullable=False) is_open = db.Column(db.Boolean(), nullable=False) nominations = db.Column(db.Boolean(), nullable=False) @@ -293,22 +310,29 @@ def __init__(self, event_id, is_open, nominations): self.is_open = is_open self.nominations = nominations + class Question(Base): - __tablename__ = 'question' - __table_args__ = {'extend_existing': True} + __tablename__ = "question" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) - section_id = db.Column(db.Integer(), db.ForeignKey('section.id'), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) + section_id = db.Column(db.Integer(), db.ForeignKey("section.id"), nullable=False) type = db.Column(db.String(), nullable=False) order = db.Column(db.Integer(), nullable=False) is_required = db.Column(db.Boolean(), nullable=False) - depends_on_question_id = db.Column(db.Integer(), db.ForeignKey('question.id'), nullable=True) + depends_on_question_id = db.Column( + db.Integer(), db.ForeignKey("question.id"), nullable=True + ) key = db.Column(db.String(255), nullable=True) - question_translations = db.relationship('QuestionTranslation', lazy='dynamic') + question_translations = db.relationship("QuestionTranslation", lazy="dynamic") - def __init__(self, application_form_id, section_id, order, questionType, is_required=True): + def __init__( + self, application_form_id, section_id, order, questionType, is_required=True + ): self.application_form_id = application_form_id self.section_id = section_id self.order = order @@ -317,18 +341,24 @@ def __init__(self, application_form_id, section_id, order, questionType, is_requ class Section(Base): - __tablename__ = 'section' - __table_args__ = {'extend_existing': True} + __tablename__ = "section" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) order = db.Column(db.Integer(), nullable=False) - depends_on_question_id = db.Column(db.Integer(), db.ForeignKey('question.id', use_alter=True), nullable=True) + depends_on_question_id = db.Column( + db.Integer(), db.ForeignKey("question.id", use_alter=True), nullable=True + ) key = db.Column(db.String(255), nullable=True) - section_translations = db.relationship('SectionTranslation', lazy='dynamic') + section_translations = db.relationship("SectionTranslation", lazy="dynamic") - def __init__(self, application_form_id, order, depends_on_question_id=None, key=None): + def __init__( + self, application_form_id, order, depends_on_question_id=None, key=None + ): self.application_form_id = application_form_id self.order = order self.depends_on_question_id = depends_on_question_id @@ -336,11 +366,11 @@ def __init__(self, application_form_id, order, depends_on_question_id=None, key= class SectionTranslation(Base): - __tablename__ = 'section_translation' - __table_args__ = {'extend_existing': True} + __tablename__ = "section_translation" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - section_id = db.Column(db.Integer(), db.ForeignKey('section.id'), nullable=False) + section_id = db.Column(db.Integer(), db.ForeignKey("section.id"), nullable=False) language = db.Column(db.String(2), nullable=False) name = db.Column(db.String(255), nullable=False) description = db.Column(db.String(), nullable=False) @@ -355,11 +385,11 @@ def __init__(self, section_id, language, name, description, show_for_values=None class QuestionTranslation(Base): - __tablename__ = 'question_translation' - __table_args__ = {'extend_existing': True} + __tablename__ = "question_translation" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - question_id = db.Column(db.Integer(), db.ForeignKey('question.id'), nullable=False) + question_id = db.Column(db.Integer(), db.ForeignKey("question.id"), nullable=False) language = db.Column(db.String(2), nullable=False) headline = db.Column(db.String(), nullable=False) description = db.Column(db.String(), nullable=True) @@ -379,7 +409,7 @@ def __init__( validation_regex=None, validation_text=None, options=None, - show_for_values=None + show_for_values=None, ): self.question_id = question_id self.language = language @@ -396,40 +426,45 @@ def upgrade(): Base.metadata.bind = op.get_bind() session = orm.Session(bind=Base.metadata.bind) - ai4d = session.query(Organisation).filter_by(name='AI4D Africa').first() + ai4d = session.query(Organisation).filter_by(name="AI4D Africa").first() - op.alter_column('event_translation', 'description', - existing_type=sa.String(length=255), - type_=sa.String(length=500)) + op.alter_column( + "event_translation", + "description", + existing_type=sa.String(length=255), + type_=sa.String(length=500), + ) # Add Event event = Event( { - 'en': 'AI4D Call for Proposals: Scholarships program manager', - 'fr': 'Appel à propositions IAPD: Gestionnaire des programmes de bourses' + "en": "AI4D Call for Proposals: Scholarships program manager", + "fr": "Appel à propositions IAPD: Gestionnaire des programmes de bourses", }, { - 'en': 'Academic capacity building to foster African talent around Artificial Intelligence: Seeking a program and financial manager for the African Artificial Intelligence for Development (AI4D) Scholarships program', - 'fr': 'Renforcement des capacités dans les milieux universitaires pour favoriser le développement des talents africains autour de l’intelligence artificielle : Recherche d’un responsable de programme et directeur financier pour le programme de bourses d’études africaines en intelligence artificielle pour le développement (IAPD).' + "en": "Academic capacity building to foster African talent around Artificial Intelligence: Seeking a program and financial manager for the African Artificial Intelligence for Development (AI4D) Scholarships program", + "fr": "Renforcement des capacités dans les milieux universitaires pour favoriser le développement des talents africains autour de l’intelligence artificielle : Recherche d’un responsable de programme et directeur financier pour le programme de bourses d’études africaines en intelligence artificielle pour le développement (IAPD).", }, start_date=datetime.date(2020, 12, 8), end_date=datetime.date(2020, 12, 8), - key='spm', + key="spm", organisation_id=ai4d.id, - email_from='calls@ai4d.ai', - url='https://ai4d.ai/calls/scholarships-manager/', - application_open=datetime.date(2020,11,3), - application_close=datetime.datetime(2020,12,9,5,0,0), # Equivalent to midnight EST - review_open=datetime.date(2020,12,9), - review_close=datetime.date(2020,12,17), - selection_open=datetime.date(2020,12,18), - selection_close=datetime.date(2020,12,31), - offer_open=datetime.date(2020,12,18), - offer_close=datetime.date(2020,12,31), - registration_open=datetime.date(2020,12,31), - registration_close=datetime.date(2020,12,31), + email_from="calls@ai4d.ai", + url="https://ai4d.ai/calls/scholarships-manager/", + application_open=datetime.date(2020, 11, 3), + application_close=datetime.datetime( + 2020, 12, 9, 5, 0, 0 + ), # Equivalent to midnight EST + review_open=datetime.date(2020, 12, 9), + review_close=datetime.date(2020, 12, 17), + selection_open=datetime.date(2020, 12, 18), + selection_close=datetime.date(2020, 12, 31), + offer_open=datetime.date(2020, 12, 18), + offer_close=datetime.date(2020, 12, 31), + registration_open=datetime.date(2020, 12, 31), + registration_close=datetime.date(2020, 12, 31), event_type=EventType.CALL, - travel_grant=False + travel_grant=False, ) session.add(event) @@ -439,24 +474,40 @@ def upgrade(): session.add(form) session.commit() - def add_section(names, descriptions, order, depends_on_question_id=None, key=None, show_for_values=None): + def add_section( + names, + descriptions, + order, + depends_on_question_id=None, + key=None, + show_for_values=None, + ): section = Section(form.id, order) session.add(section) session.commit() translations = [] for language in names: - translations.append(SectionTranslation( - section.id, - language, - names[language], - '' if descriptions is None else descriptions[language], - show_for_values=None if show_for_values is None else show_for_values[language])) + translations.append( + SectionTranslation( + section.id, + language, + names[language], + "" if descriptions is None else descriptions[language], + show_for_values=None + if show_for_values is None + else show_for_values[language], + ) + ) session.add_all(translations) session.commit() return section - def add_question(section, order, questionType, headlines, + def add_question( + section, + order, + questionType, + headlines, descriptions=None, placeholders=None, validation_regexs=None, @@ -464,8 +515,11 @@ def add_question(section, order, questionType, headlines, options=None, show_for_values=None, is_required=True, - depends_on_question_id=None): - question = Question(form.id, section.id, order, questionType, is_required=is_required) + depends_on_question_id=None, + ): + question = Question( + form.id, section.id, order, questionType, is_required=is_required + ) if depends_on_question_id is not None: question.depends_on_question_id = depends_on_question_id @@ -475,13 +529,26 @@ def add_question(section, order, questionType, headlines, translations = [] for language in headlines: - translations.append(QuestionTranslation(question.id, language, - headline=headlines[language], - description=None if descriptions is None else descriptions[language], - placeholder=None if placeholders is None else placeholders[language], - validation_regex=None if validation_regexs is None else validation_regexs[language], - options=None if options is None else options[language], - show_for_values=None if show_for_values is None else show_for_values[language])) + translations.append( + QuestionTranslation( + question.id, + language, + headline=headlines[language], + description=None + if descriptions is None + else descriptions[language], + placeholder=None + if placeholders is None + else placeholders[language], + validation_regex=None + if validation_regexs is None + else validation_regexs[language], + options=None if options is None else options[language], + show_for_values=None + if show_for_values is None + else show_for_values[language], + ) + ) session.add_all(translations) session.commit() return question @@ -508,487 +575,610 @@ def add_question(section, order, questionType, headlines, """ # (Description Page) - add_section({ - 'en': 'AI4D Call for Proposals: Scholarships program manager', - 'fr': 'Appel à propositions IAPD: Gestionnaire des programmes de bourses' - }, - {'en': en_description, 'fr': fr_description}, - 1) + add_section( + { + "en": "AI4D Call for Proposals: Scholarships program manager", + "fr": "Appel à propositions IAPD: Gestionnaire des programmes de bourses", + }, + {"en": en_description, "fr": fr_description}, + 1, + ) # Organisation and lead contact details - org_section = add_section({ - 'en': 'Organisation and Lead Applicant Contact details', - 'fr': "Coordonnées de l'organisation et de la personne-ressource pour cette demande" - }, None, 2) - - add_question(org_section, 1, 'information', { - 'en': 'Organization Information', - 'fr': 'Renseignements sur l’organisation' - }, is_required=False) - - add_question(org_section, 2, 'short-text', { - 'en': 'Name of Organisation', - 'fr': 'Nom de l’organisation' - }, is_required=True) - - add_question(org_section, 3, 'short-text', { - 'en': 'Director/CEO', - 'fr': 'Directeur ou premier dirigeant' - }, is_required=True) - - add_question(org_section, 4, 'short-text', { - 'en': 'Mailing address', - 'fr': 'Adresse postale' - }, is_required=True) - - add_question(org_section, 5, 'short-text', { - 'en': 'City', - 'fr': 'Ville' - }, is_required=True) - - en_options = [ - {'label': c, 'value': c} for c in en_countries - ] - fr_options = [ - {'label': f, 'value': e} for e, f in zip(en_countries, fr_countries) - ] - - add_question(org_section, 6, 'multi-choice', { - 'en': 'Country', - 'fr': 'Pays' - }, - descriptions={ - 'en': 'Eligible countries in sub-Saharan and North Africa', - 'fr': 'Pays admissibles de l’Afrique subsaharienne et de l’Afrique du Nord' - }, - placeholders={ - 'en': 'Select a Country', - 'fr': 'Choisissez un Pays' - }, - options={ - 'en': en_options, - 'fr': fr_options - }, - is_required=True) - - add_question(org_section, 7, 'short-text', { - 'en': 'Telephone', - 'fr': 'Numéro de téléphone' - }, is_required=True) - - add_question(org_section, 8, 'short-text', { - 'en': 'Mobile (Optional)', - 'fr': 'Numéro de téléphone mobile (facultatif)' - }, is_required=False) - - add_question(org_section, 9, 'short-text', { - 'en': 'Email Address', - 'fr': 'Adresse de courriel' - }, is_required=True) - - add_question(org_section, 10, 'short-text', { - 'en': 'Website', - 'fr': 'Site Web' - }, is_required=True) - - add_question(org_section, 11, 'short-text', { - 'en': 'Social media handles (optional)', - 'fr': 'Pseudonymes sur les médias sociaux (facultatif)' - }, is_required=False) - - add_question(org_section, 12, 'information', { - 'en': 'Name and contact details of principal contact for the application', - 'fr': 'Nom et coordonnées de la personne-ressource pour cette demande' - }, is_required=False) - - add_question(org_section, 13, 'short-text', { - 'en': 'Name of principal contact', - 'fr': 'Nom du contact principal' - }, is_required=True) - - add_question(org_section, 14, 'short-text', { - 'en': 'Email Address of principal contact', - 'fr': 'Adresse de courriel du contact principal' - }, is_required=True) - - add_question(org_section, 15, 'short-text', { - 'en': 'Telephone/Mobile of principal contact', - 'fr': 'Numéro de téléphone ou de téléphone mobile du contact principal' - }, is_required=True) + org_section = add_section( + { + "en": "Organisation and Lead Applicant Contact details", + "fr": "Coordonnées de l'organisation et de la personne-ressource pour cette demande", + }, + None, + 2, + ) + + add_question( + org_section, + 1, + "information", + {"en": "Organization Information", "fr": "Renseignements sur l’organisation"}, + is_required=False, + ) + + add_question( + org_section, + 2, + "short-text", + {"en": "Name of Organisation", "fr": "Nom de l’organisation"}, + is_required=True, + ) + + add_question( + org_section, + 3, + "short-text", + {"en": "Director/CEO", "fr": "Directeur ou premier dirigeant"}, + is_required=True, + ) + + add_question( + org_section, + 4, + "short-text", + {"en": "Mailing address", "fr": "Adresse postale"}, + is_required=True, + ) + + add_question( + org_section, 5, "short-text", {"en": "City", "fr": "Ville"}, is_required=True + ) + + en_options = [{"label": c, "value": c} for c in en_countries] + fr_options = [{"label": f, "value": e} for e, f in zip(en_countries, fr_countries)] + + add_question( + org_section, + 6, + "multi-choice", + {"en": "Country", "fr": "Pays"}, + descriptions={ + "en": "Eligible countries in sub-Saharan and North Africa", + "fr": "Pays admissibles de l’Afrique subsaharienne et de l’Afrique du Nord", + }, + placeholders={"en": "Select a Country", "fr": "Choisissez un Pays"}, + options={"en": en_options, "fr": fr_options}, + is_required=True, + ) + + add_question( + org_section, + 7, + "short-text", + {"en": "Telephone", "fr": "Numéro de téléphone"}, + is_required=True, + ) + + add_question( + org_section, + 8, + "short-text", + {"en": "Mobile (Optional)", "fr": "Numéro de téléphone mobile (facultatif)"}, + is_required=False, + ) + + add_question( + org_section, + 9, + "short-text", + {"en": "Email Address", "fr": "Adresse de courriel"}, + is_required=True, + ) + + add_question( + org_section, + 10, + "short-text", + {"en": "Website", "fr": "Site Web"}, + is_required=True, + ) + + add_question( + org_section, + 11, + "short-text", + { + "en": "Social media handles (optional)", + "fr": "Pseudonymes sur les médias sociaux (facultatif)", + }, + is_required=False, + ) + + add_question( + org_section, + 12, + "information", + { + "en": "Name and contact details of principal contact for the application", + "fr": "Nom et coordonnées de la personne-ressource pour cette demande", + }, + is_required=False, + ) + + add_question( + org_section, + 13, + "short-text", + {"en": "Name of principal contact", "fr": "Nom du contact principal"}, + is_required=True, + ) + + add_question( + org_section, + 14, + "short-text", + { + "en": "Email Address of principal contact", + "fr": "Adresse de courriel du contact principal", + }, + is_required=True, + ) + + add_question( + org_section, + 15, + "short-text", + { + "en": "Telephone/Mobile of principal contact", + "fr": "Numéro de téléphone ou de téléphone mobile du contact principal", + }, + is_required=True, + ) # Section 1 - section1 = add_section({ - 'en': 'Section I: Organizational capacity and program fit of scholarships manager', - 'fr': 'Section I: Capacité organisationnelle et compatibilité avec le programme du gestionnaire de bourses' - }, None, 3) - - add_question(section1, 1, 'short-text', { - 'en': '1. How many people in total work in your organization?', - 'fr': '1. Combien d’employés compte votre organisation en tout?' - }, descriptions={ - 'en': 'Total number of employees', - 'fr': 'Nombre total d’employés' - }, - is_required=True) - - add_question(section1, 2, 'short-text', { - 'en': '1a. Of these, how many are full time employees?', - 'fr': '1a. Combien d’entre eux occupent un poste d’employé à temps plein?' - }, is_required=True) - - add_question(section1, 3, 'multi-file', { - 'en': "1b. Please provide the names and CVs of your institution's primary officers and administrators who will be responsible for developing and managing the African AI4D Scholarships program (up to five)", - 'fr': '1b. Veuillez fournir le nom et le CV des principaux responsables et administrateurs de votre établissement qui seront chargés d’élaborer et de gérer le programme de bourses africaines d’AI4D (cinq au maximum)' - }, descriptions={ - 'en': 'Upload the five CVs of relevant staff in the organization here.', - 'fr': 'Téléchargez ici les CV des cinq employés concernés de votre organisation.' - }, is_required=True, - options={ - 'en': {"num_uploads": 5}, - 'fr': {"num_uploads": 5} - }) - - add_question(section1, 4, 'long-text', { - 'en': '2. Demonstrated experience and capacity', - 'fr': '2. Expérience et capacité démontrées' - }, descriptions={ - 'en': """Please provide examples of past or current related experience of your institution in implementing scholarship or grant programs to individuals in Africa. + section1 = add_section( + { + "en": "Section I: Organizational capacity and program fit of scholarships manager", + "fr": "Section I: Capacité organisationnelle et compatibilité avec le programme du gestionnaire de bourses", + }, + None, + 3, + ) + + add_question( + section1, + 1, + "short-text", + { + "en": "1. How many people in total work in your organization?", + "fr": "1. Combien d’employés compte votre organisation en tout?", + }, + descriptions={ + "en": "Total number of employees", + "fr": "Nombre total d’employés", + }, + is_required=True, + ) + + add_question( + section1, + 2, + "short-text", + { + "en": "1a. Of these, how many are full time employees?", + "fr": "1a. Combien d’entre eux occupent un poste d’employé à temps plein?", + }, + is_required=True, + ) + + add_question( + section1, + 3, + "multi-file", + { + "en": "1b. Please provide the names and CVs of your institution's primary officers and administrators who will be responsible for developing and managing the African AI4D Scholarships program (up to five)", + "fr": "1b. Veuillez fournir le nom et le CV des principaux responsables et administrateurs de votre établissement qui seront chargés d’élaborer et de gérer le programme de bourses africaines d’AI4D (cinq au maximum)", + }, + descriptions={ + "en": "Upload the five CVs of relevant staff in the organization here.", + "fr": "Téléchargez ici les CV des cinq employés concernés de votre organisation.", + }, + is_required=True, + options={"en": {"num_uploads": 5}, "fr": {"num_uploads": 5}}, + ) + + add_question( + section1, + 4, + "long-text", + { + "en": "2. Demonstrated experience and capacity", + "fr": "2. Expérience et capacité démontrées", + }, + descriptions={ + "en": """Please provide examples of past or current related experience of your institution in implementing scholarship or grant programs to individuals in Africa. (Max. 1000 words)""", - 'fr': """Veuillez fournir des exemples d’expériences passées ou actuelles de mise en oeuvre par votre établissement de programmes de bourses ou de subventions destinées à des personnes en Afrique -(1000 mots au maximum).""" - }, placeholders={ - 'en': 'Enter up to 1000 words', - 'fr': "Entrez jusqu'à 1000 mots" - }, validation_regexs={ - 'en': r'^\s*(\S+(\s+|$)){0,1000}$', - 'fr': r'^\s*(\S+(\s+|$)){0,1000}$', - }, validation_texts={ - 'en': 'Maximum 1000 words', - 'fr': 'Maximum 1000 mots' - }, is_required=True) - - add_question(section1, 5, 'long-text', { - 'en': '2a. Available resources ', - 'fr': '2a. Ressources disponibles' - }, descriptions={ - 'en': """Discuss the resources and capacities that your institution already has to successfully implement the African AI4D Scholarships program. (Max.750 words). Note: The scholarship manager is encouraged to use existing resources, including leveraging past or active grants and/or scholarships, to support the development and management of the scholarships. + "fr": """Veuillez fournir des exemples d’expériences passées ou actuelles de mise en oeuvre par votre établissement de programmes de bourses ou de subventions destinées à des personnes en Afrique +(1000 mots au maximum).""", + }, + placeholders={"en": "Enter up to 1000 words", "fr": "Entrez jusqu'à 1000 mots"}, + validation_regexs={ + "en": r"^\s*(\S+(\s+|$)){0,1000}$", + "fr": r"^\s*(\S+(\s+|$)){0,1000}$", + }, + validation_texts={"en": "Maximum 1000 words", "fr": "Maximum 1000 mots"}, + is_required=True, + ) + + add_question( + section1, + 5, + "long-text", + {"en": "2a. Available resources ", "fr": "2a. Ressources disponibles"}, + descriptions={ + "en": """Discuss the resources and capacities that your institution already has to successfully implement the African AI4D Scholarships program. (Max.750 words). Note: The scholarship manager is encouraged to use existing resources, including leveraging past or active grants and/or scholarships, to support the development and management of the scholarships. (Max. 750 words)""", - 'fr': """Indiquez les ressources et les capacités dont dispose déjà votre établissement pour mettre en oeuvre avec succès le programme de bourses africaines d’AI4D (750 mots au maximum). Remarque : Le gestionnaire des bourses est encouragé à utiliser les ressources existantes, notamment en tirant parti des subventions ou bourses antérieures ou actives, pour appuyer le développement et la gestion des bourses. -(750 mots au maximum).""" - }, placeholders={ - 'en': 'Enter up to 750 words', - 'fr': "Entrez jusqu'à 750 mots" - }, validation_regexs={ - 'en': r'^\s*(\S+(\s+|$)){0,750}$', - 'fr': r'^\s*(\S+(\s+|$)){0,750}$', - }, validation_texts={ - 'en': 'Maximum 750 words', - 'fr': 'Maximum 750 mots' - }, is_required=True) - - q2b = add_question(section1, 6, 'multi-choice', { - 'en': '2b. Partnerships plan', - 'fr': '2b. Plan de partenariats' - }, descriptions={ - 'en': "The host institution may choose to supplement the scholarships, including the complementary activities, through different partnerships. Do you plan to partner with other institutes or groups to support the scholarships?", - 'fr': "L’établissement d’accueil peut choisir de compléter les bourses, y compris les activités complémentaires, au moyen de différents partenariats. Prévoyez-vous de vous associer à d’autres instituts ou groupes pour soutenir les bourses?" - }, options={ - 'en': [{'label': 'Yes', 'value': 'Yes'}, {'label': 'No', 'value': 'No'}], - 'fr': [{'label': 'Oui', 'value': 'Yes'}, {'label': 'Non', 'value': 'No'}] - }, is_required=True) - - add_question(section1, 7, 'long-text', { - 'en': '2c. Please provide a brief overview of your plan for coordinating those partnerships', - 'fr': '2c. Dans l’affirmative, veuillez brièvement décrire comment vous prévoyez coordonner ces partenariats' - }, descriptions={ - 'en': '(Max. 350 words)', - 'fr': '(350 mots au maximum)' - }, placeholders={ - 'en': 'Enter up to 350 words', - 'fr': "Entrez jusqu'à 350 mots" - }, validation_regexs={ - 'en': r'^\s*(\S+(\s+|$)){0,350}$', - 'fr': r'^\s*(\S+(\s+|$)){0,350}$', - }, validation_texts={ - 'en': 'Maximum 350 words', - 'fr': 'Maximum 350 mots' - }, is_required=True, depends_on_question_id=q2b.id, show_for_values={ - 'en': ["Yes"], - 'fr': ["Yes"] - }) - - add_question(section1, 8, 'long-text', { - 'en': '3. Relevant expertise of staff', - 'fr': '3. Expertise pertinente du personnel' - }, descriptions={ - 'en': """Describe the relevant expertise and skills that are in-house and can be deployed to evaluate AI for development scholarship proposals across a variety of domains. What expertise gaps do you have and how will you identify and mobilize the appropriate expertise to fill them? + "fr": """Indiquez les ressources et les capacités dont dispose déjà votre établissement pour mettre en oeuvre avec succès le programme de bourses africaines d’AI4D (750 mots au maximum). Remarque : Le gestionnaire des bourses est encouragé à utiliser les ressources existantes, notamment en tirant parti des subventions ou bourses antérieures ou actives, pour appuyer le développement et la gestion des bourses. +(750 mots au maximum).""", + }, + placeholders={"en": "Enter up to 750 words", "fr": "Entrez jusqu'à 750 mots"}, + validation_regexs={ + "en": r"^\s*(\S+(\s+|$)){0,750}$", + "fr": r"^\s*(\S+(\s+|$)){0,750}$", + }, + validation_texts={"en": "Maximum 750 words", "fr": "Maximum 750 mots"}, + is_required=True, + ) + + q2b = add_question( + section1, + 6, + "multi-choice", + {"en": "2b. Partnerships plan", "fr": "2b. Plan de partenariats"}, + descriptions={ + "en": "The host institution may choose to supplement the scholarships, including the complementary activities, through different partnerships. Do you plan to partner with other institutes or groups to support the scholarships?", + "fr": "L’établissement d’accueil peut choisir de compléter les bourses, y compris les activités complémentaires, au moyen de différents partenariats. Prévoyez-vous de vous associer à d’autres instituts ou groupes pour soutenir les bourses?", + }, + options={ + "en": [{"label": "Yes", "value": "Yes"}, {"label": "No", "value": "No"}], + "fr": [{"label": "Oui", "value": "Yes"}, {"label": "Non", "value": "No"}], + }, + is_required=True, + ) + + add_question( + section1, + 7, + "long-text", + { + "en": "2c. Please provide a brief overview of your plan for coordinating those partnerships", + "fr": "2c. Dans l’affirmative, veuillez brièvement décrire comment vous prévoyez coordonner ces partenariats", + }, + descriptions={"en": "(Max. 350 words)", "fr": "(350 mots au maximum)"}, + placeholders={"en": "Enter up to 350 words", "fr": "Entrez jusqu'à 350 mots"}, + validation_regexs={ + "en": r"^\s*(\S+(\s+|$)){0,350}$", + "fr": r"^\s*(\S+(\s+|$)){0,350}$", + }, + validation_texts={"en": "Maximum 350 words", "fr": "Maximum 350 mots"}, + is_required=True, + depends_on_question_id=q2b.id, + show_for_values={"en": ["Yes"], "fr": ["Yes"]}, + ) + + add_question( + section1, + 8, + "long-text", + { + "en": "3. Relevant expertise of staff", + "fr": "3. Expertise pertinente du personnel", + }, + descriptions={ + "en": """Describe the relevant expertise and skills that are in-house and can be deployed to evaluate AI for development scholarship proposals across a variety of domains. What expertise gaps do you have and how will you identify and mobilize the appropriate expertise to fill them? (Max. 500 words)""", - 'fr': """Décrivez l’expertise et les compétences pertinentes qui existent en interne et qui peuvent être déployées pour évaluer les propositions de bourses de développement de l’IA dans divers domaines. Quelles sont vos lacunes en matière d’expertise et comment allez-vous repérer et mobiliser l’expertise appropriée pour les combler? -(500 mots au maximum)""" - }, placeholders={ - 'en': 'Enter up to 500 words', - 'fr': "Entrez jusqu'à 500 mots" - }, validation_regexs={ - 'en': r'^\s*(\S+(\s+|$)){0,500}$', - 'fr': r'^\s*(\S+(\s+|$)){0,500}$', - }, validation_texts={ - 'en': 'Maximum 500 words', - 'fr': 'Maximum 500 mots' - }, is_required=True) + "fr": """Décrivez l’expertise et les compétences pertinentes qui existent en interne et qui peuvent être déployées pour évaluer les propositions de bourses de développement de l’IA dans divers domaines. Quelles sont vos lacunes en matière d’expertise et comment allez-vous repérer et mobiliser l’expertise appropriée pour les combler? +(500 mots au maximum)""", + }, + placeholders={"en": "Enter up to 500 words", "fr": "Entrez jusqu'à 500 mots"}, + validation_regexs={ + "en": r"^\s*(\S+(\s+|$)){0,500}$", + "fr": r"^\s*(\S+(\s+|$)){0,500}$", + }, + validation_texts={"en": "Maximum 500 words", "fr": "Maximum 500 mots"}, + is_required=True, + ) # Section 2 - section2 = add_section({ - 'en': 'Section II: Scholarship Plan and Proposal', - 'fr': """Section II: Plan et proposition de les programmes de bourses d'études""" - }, None, 4) - - add_question(section2, 1, 'long-text', { - 'en': '1. General and specific objectives for the proposed AI4D Scholarships Management', - 'fr': '1. Objectifs généraux et précis.' - }, descriptions={ - 'en': """The general objective should state the goals being pursued by the project overall. The specific objectives should indicate the specific types of knowledge to be produced, the audiences to be reached, and forms of capacity to be reinforced. These are the objectives against which the success of the project will be judged. Use only active verbs (no passive). + section2 = add_section( + { + "en": "Section II: Scholarship Plan and Proposal", + "fr": """Section II: Plan et proposition de les programmes de bourses d'études""", + }, + None, + 4, + ) + + add_question( + section2, + 1, + "long-text", + { + "en": "1. General and specific objectives for the proposed AI4D Scholarships Management", + "fr": "1. Objectifs généraux et précis.", + }, + descriptions={ + "en": """The general objective should state the goals being pursued by the project overall. The specific objectives should indicate the specific types of knowledge to be produced, the audiences to be reached, and forms of capacity to be reinforced. These are the objectives against which the success of the project will be judged. Use only active verbs (no passive). (Max. 250 words)""", - 'fr': """L’objectif général correspond à l’objectif de développement que l’on souhaite atteindre au moyen de la recherche. Les objectifs particuliers doivent signaler les types précis de connaissances recherchées, les destinataires et les capacités à renforcer. C’est en fonction de ces objectifs que sera jaugée la réussite du projet. Utilisez seulement des verbes d’action (évitez les verbes d’état). -(250 mots au maximum)""" - }, placeholders={ - 'en': 'Enter up to 250 words', - 'fr': "Entrez jusqu'à 250 mots" - }, validation_regexs={ - 'en': r'^\s*(\S+(\s+|$)){0,250}$', - 'fr': r'^\s*(\S+(\s+|$)){0,250}$', - }, validation_texts={ - 'en': 'Maximum 250 words', - 'fr': 'Maximum 250 mots' - }, is_required=True) - - add_question(section2, 2, 'long-text', { - 'en': '2. Summary of the proposed approach to the financial and administrative management of the AI4D scholarships', - 'fr': '2. Résumé de l’approche proposée pour la gestion financière et administrative de la bourse d’AI4D' - }, descriptions={ - 'en': """Briefly outline your proposal to manage the AI4D scholarship program according to the requirements outlined in section 2.1 of the Call for Proposals Background Document. What approaches, disciplines and modalities will you draw upon to support this + "fr": """L’objectif général correspond à l’objectif de développement que l’on souhaite atteindre au moyen de la recherche. Les objectifs particuliers doivent signaler les types précis de connaissances recherchées, les destinataires et les capacités à renforcer. C’est en fonction de ces objectifs que sera jaugée la réussite du projet. Utilisez seulement des verbes d’action (évitez les verbes d’état). +(250 mots au maximum)""", + }, + placeholders={"en": "Enter up to 250 words", "fr": "Entrez jusqu'à 250 mots"}, + validation_regexs={ + "en": r"^\s*(\S+(\s+|$)){0,250}$", + "fr": r"^\s*(\S+(\s+|$)){0,250}$", + }, + validation_texts={"en": "Maximum 250 words", "fr": "Maximum 250 mots"}, + is_required=True, + ) + + add_question( + section2, + 2, + "long-text", + { + "en": "2. Summary of the proposed approach to the financial and administrative management of the AI4D scholarships", + "fr": "2. Résumé de l’approche proposée pour la gestion financière et administrative de la bourse d’AI4D", + }, + descriptions={ + "en": """Briefly outline your proposal to manage the AI4D scholarship program according to the requirements outlined in section 2.1 of the Call for Proposals Background Document. What approaches, disciplines and modalities will you draw upon to support this Be sure to outline the key priorities, background, problems and justifications for supporting AI talent and capacity building work in the context of Africa. Briefly describe the overall approach/logic/pathway to achieve the project objectives. (Max. 1000 words) """, - 'fr': """Décrivez brièvement comment vous proposez de gérer le programme de bourses d’AI4D conformément aux exigences décrites dans la section 2.1 du document de référence de l’appel à propositions. Quelles méthodes, disciplines et modalités allez-vous utiliser à l’appui? + "fr": """Décrivez brièvement comment vous proposez de gérer le programme de bourses d’AI4D conformément aux exigences décrites dans la section 2.1 du document de référence de l’appel à propositions. Quelles méthodes, disciplines et modalités allez-vous utiliser à l’appui? Veillez à présenter les principales priorités, le contexte, les problèmes et les justifications du soutien aux talents et au travail de renforcement des capacités d’IA dans le contexte de l’Afrique. Décrivez brièvement l’approche, la logique ou le cheminement permettant d’atteindre les objectifs du projet -(1 000 mots au maximum).""" - }, placeholders={ - 'en': 'Enter up to 1000 words', - 'fr': "Entrez jusqu'à 1000 mots" - }, validation_regexs={ - 'en': r'^\s*(\S+(\s+|$)){0,1000}$', - 'fr': r'^\s*(\S+(\s+|$)){0,1000}$', - }, validation_texts={ - 'en': 'Maximum 1000 words', - 'fr': 'Maximum 1000 mots' - }, is_required=True) - - add_question(section2, 3, 'long-text', { - 'en': '2a.Communication strategies', - 'fr': '2a. Stratégies de communication' - }, descriptions={ - 'en': """Discuss the plan for launching the African AI4D PhD Scholarship and the African AI4D Advanced Scholars Program. How does the plan ensure that the program will be as inclusive as possible? +(1 000 mots au maximum).""", + }, + placeholders={"en": "Enter up to 1000 words", "fr": "Entrez jusqu'à 1000 mots"}, + validation_regexs={ + "en": r"^\s*(\S+(\s+|$)){0,1000}$", + "fr": r"^\s*(\S+(\s+|$)){0,1000}$", + }, + validation_texts={"en": "Maximum 1000 words", "fr": "Maximum 1000 mots"}, + is_required=True, + ) + + add_question( + section2, + 3, + "long-text", + {"en": "2a.Communication strategies", "fr": "2a. Stratégies de communication"}, + descriptions={ + "en": """Discuss the plan for launching the African AI4D PhD Scholarship and the African AI4D Advanced Scholars Program. How does the plan ensure that the program will be as inclusive as possible? What will be the approach to spotlight the research of scholarship recipients and disseminate their project results widely, targeting both academics and practitioners? (Max. 750 words) """, - 'fr': """Discutez du plan de lancement de la bourse de doctorat africaine d’AI4D et du programme de bourses avancées africaines d’AI4D. Comment le plan garantira-t-il que le programme sera aussi inclusif que possible? + "fr": """Discutez du plan de lancement de la bourse de doctorat africaine d’AI4D et du programme de bourses avancées africaines d’AI4D. Comment le plan garantira-t-il que le programme sera aussi inclusif que possible? Quelle sera l’approche adoptée pour mettre en lumière les recherches des bénéficiaires de bourses et diffuser largement les résultats de leurs projets, en ciblant à la fois les universitaires et les praticiens? -(750 mots au maximum).""" - }, placeholders={ - 'en': 'Enter up to 750 words', - 'fr': "Entrez jusqu'à 750 mots" - }, validation_regexs={ - 'en': r'^\s*(\S+(\s+|$)){0,750}$', - 'fr': r'^\s*(\S+(\s+|$)){0,750}$', - }, validation_texts={ - 'en': 'Maximum 750 words', - 'fr': 'Maximum 750 mots' - }, is_required=True) - - add_question(section2, 4, 'long-text', { - 'en': '2b. Monitoring, evaluation, and learning (MEL)', - 'fr': '2b. Suivi, évaluation et apprentissage' - }, descriptions={ - 'en': """Discuss your strategy for ongoing monitoring of the scholarship programs, adapting in response to lessons learned and emergent issues, learning to improve future scholarship calls, and evaluating the impact of the program? +(750 mots au maximum).""", + }, + placeholders={"en": "Enter up to 750 words", "fr": "Entrez jusqu'à 750 mots"}, + validation_regexs={ + "en": r"^\s*(\S+(\s+|$)){0,750}$", + "fr": r"^\s*(\S+(\s+|$)){0,750}$", + }, + validation_texts={"en": "Maximum 750 words", "fr": "Maximum 750 mots"}, + is_required=True, + ) + + add_question( + section2, + 4, + "long-text", + { + "en": "2b. Monitoring, evaluation, and learning (MEL)", + "fr": "2b. Suivi, évaluation et apprentissage", + }, + descriptions={ + "en": """Discuss your strategy for ongoing monitoring of the scholarship programs, adapting in response to lessons learned and emergent issues, learning to improve future scholarship calls, and evaluating the impact of the program? (Max. 750 words)""", - 'fr': """Discutez de votre stratégie de suivi permanent des programmes de bourses, d’adaptation en fonction des enseignements tirés et des questions émergentes, d’apprentissage pour améliorer les futurs appels à propositions de bourses et d’évaluation de l’impact du programme -(750 mots au maximum).""" - }, placeholders={ - 'en': 'Enter up to 750 words', - 'fr': "Entrez jusqu'à 750 mots" - }, validation_regexs={ - 'en': r'^\s*(\S+(\s+|$)){0,750}$', - 'fr': r'^\s*(\S+(\s+|$)){0,750}$', - }, validation_texts={ - 'en': 'Maximum 750 words', - 'fr': 'Maximum 750 mots' - }, is_required=True) - - add_question(section2, 5, 'long-text', { - 'en': '3. Design of the scholarship plans', - 'fr': '3. Conception des plans de bourses ' - }, descriptions={ - 'en': """Briefly outline your proposed plan for the design of the (i) PhD Scholarship and (ii) Early Career Scholars Program. Include details on how funds will be disbursed and explain how the scholarships would support research of the winning PhD students and Early Career Academics in practise. + "fr": """Discutez de votre stratégie de suivi permanent des programmes de bourses, d’adaptation en fonction des enseignements tirés et des questions émergentes, d’apprentissage pour améliorer les futurs appels à propositions de bourses et d’évaluation de l’impact du programme +(750 mots au maximum).""", + }, + placeholders={"en": "Enter up to 750 words", "fr": "Entrez jusqu'à 750 mots"}, + validation_regexs={ + "en": r"^\s*(\S+(\s+|$)){0,750}$", + "fr": r"^\s*(\S+(\s+|$)){0,750}$", + }, + validation_texts={"en": "Maximum 750 words", "fr": "Maximum 750 mots"}, + is_required=True, + ) + + add_question( + section2, + 5, + "long-text", + { + "en": "3. Design of the scholarship plans", + "fr": "3. Conception des plans de bourses ", + }, + descriptions={ + "en": """Briefly outline your proposed plan for the design of the (i) PhD Scholarship and (ii) Early Career Scholars Program. Include details on how funds will be disbursed and explain how the scholarships would support research of the winning PhD students and Early Career Academics in practise. How will your proposed approach help meet the requirements for the calls outlined in section 2? (Max. 1000 words)""", - 'fr': """Décrivez brièvement le plan que vous proposez pour la conception (i) de la bourse de doctorat et (ii) du programme de bourses d’études en début de carrière. Donnez des détails sur la manière dont les fonds seront déboursés et expliquez comment les bourses appuieront la recherche des étudiants en doctorat et des universitaires en début de carrière bénéficiaires des bourses dans la pratique. + "fr": """Décrivez brièvement le plan que vous proposez pour la conception (i) de la bourse de doctorat et (ii) du programme de bourses d’études en début de carrière. Donnez des détails sur la manière dont les fonds seront déboursés et expliquez comment les bourses appuieront la recherche des étudiants en doctorat et des universitaires en début de carrière bénéficiaires des bourses dans la pratique. Comment l’approche que vous proposez contribuera-t-elle à répondre aux exigences des appels décrites dans la section 2? (1 000 mots au maximum) -""" - }, placeholders={ - 'en': 'Enter up to 1000 words', - 'fr': "Entrez jusqu'à 1000 mots" - }, validation_regexs={ - 'en': r'^\s*(\S+(\s+|$)){0,1000}$', - 'fr': r'^\s*(\S+(\s+|$)){0,1000}$', - }, validation_texts={ - 'en': 'Maximum 1000 words', - 'fr': 'Maximum 1000 mots' - }, is_required=True) - - add_question(section2, 6, 'long-text', { - 'en': '3a. Gender and Inclusion Considerations', - 'fr': '3a. Considérations liées à la sexospécificité et à l’inclusion' - }, descriptions={ - 'en': """How will your proposed approach promote increased diversity and equity in AI research? What literature or experience will you draw upon to support gender equity and linguistic equity goals in the design and execution of the calls? How will you process integrate best practices in marketing and outreach, engagement, and support for recipients? +""", + }, + placeholders={"en": "Enter up to 1000 words", "fr": "Entrez jusqu'à 1000 mots"}, + validation_regexs={ + "en": r"^\s*(\S+(\s+|$)){0,1000}$", + "fr": r"^\s*(\S+(\s+|$)){0,1000}$", + }, + validation_texts={"en": "Maximum 1000 words", "fr": "Maximum 1000 mots"}, + is_required=True, + ) + + add_question( + section2, + 6, + "long-text", + { + "en": "3a. Gender and Inclusion Considerations", + "fr": "3a. Considérations liées à la sexospécificité et à l’inclusion", + }, + descriptions={ + "en": """How will your proposed approach promote increased diversity and equity in AI research? What literature or experience will you draw upon to support gender equity and linguistic equity goals in the design and execution of the calls? How will you process integrate best practices in marketing and outreach, engagement, and support for recipients? (Max. 750 words)""", - 'fr': """Comment l’approche que vous proposez va-t-elle promouvoir une plus grande diversité et équité dans la recherche en IA? Quelle documentation ou expérience allez-vous utiliser pour soutenir les objectifs d’équité entre les sexes et d’équité linguistique dans la conception et l’exécution des appels? Comment allez-vous procéder pour intégrer les meilleures pratiques en matière de marketing et de sensibilisation, d’engagement et de soutien aux bénéficiaires? + "fr": """Comment l’approche que vous proposez va-t-elle promouvoir une plus grande diversité et équité dans la recherche en IA? Quelle documentation ou expérience allez-vous utiliser pour soutenir les objectifs d’équité entre les sexes et d’équité linguistique dans la conception et l’exécution des appels? Comment allez-vous procéder pour intégrer les meilleures pratiques en matière de marketing et de sensibilisation, d’engagement et de soutien aux bénéficiaires? (750 mots au maximum) -""" - }, placeholders={ - 'en': 'Enter up to 750 words', - 'fr': "Entrez jusqu'à 750 mots" - }, validation_regexs={ - 'en': r'^\s*(\S+(\s+|$)){0,750}$', - 'fr': r'^\s*(\S+(\s+|$)){0,750}$', - }, validation_texts={ - 'en': 'Maximum 750 words', - 'fr': 'Maximum 750 mots' - }, is_required=True) - - add_question(section2, 7, 'long-text', { - 'en': '3b. Selection process and evaluation', - 'fr': '3b. Processus de sélection et évaluation' - }, descriptions={ - 'en': """Broadly discuss the evaluation process for the two scholarships activities +""", + }, + placeholders={"en": "Enter up to 750 words", "fr": "Entrez jusqu'à 750 mots"}, + validation_regexs={ + "en": r"^\s*(\S+(\s+|$)){0,750}$", + "fr": r"^\s*(\S+(\s+|$)){0,750}$", + }, + validation_texts={"en": "Maximum 750 words", "fr": "Maximum 750 mots"}, + is_required=True, + ) + + add_question( + section2, + 7, + "long-text", + { + "en": "3b. Selection process and evaluation", + "fr": "3b. Processus de sélection et évaluation", + }, + descriptions={ + "en": """Broadly discuss the evaluation process for the two scholarships activities How will the evaluation process ensure that the projects funded will be relevant to the AI4D Africa program’s responsible AI mandate, address ethical concerns and gender dimensions, and assess the capacity of applicants to carry out the proposed research? (Max. 1000 words)""", - 'fr': """Présentez dans les grandes lignes le processus d’évaluation des deux activités de bourses. + "fr": """Présentez dans les grandes lignes le processus d’évaluation des deux activités de bourses. Comment le processus d’évaluation garantira-t-il que les projets financés auront un rapport direct avec le mandat responsable en IA d’AI4D Africa, qu’ils répondront aux préoccupations éthiques et aux dimensions sexospécifiques, et qu’ils évalueront la capacité des candidats à mener à bien la recherche proposée? (1000 mots au maximum) -""" - }, placeholders={ - 'en': 'Enter up to 1000 words', - 'fr': "Entrez jusqu'à 1000 mots" - }, validation_regexs={ - 'en': r'^\s*(\S+(\s+|$)){0,1000}$', - 'fr': r'^\s*(\S+(\s+|$)){0,1000}$', - }, validation_texts={ - 'en': 'Maximum 1000 words', - 'fr': 'Maximum 1000 mots' - }, is_required=True) - - add_question(section2, 8, 'long-text', { - 'en': '4. Complementary Activities Plan', - 'fr': '4. Plan d’activités complémentaires' - }, descriptions={ - 'en': """Briefly outline the plan for the complementary activities that will be developed in relation to the two scholarship programs. +""", + }, + placeholders={"en": "Enter up to 1000 words", "fr": "Entrez jusqu'à 1000 mots"}, + validation_regexs={ + "en": r"^\s*(\S+(\s+|$)){0,1000}$", + "fr": r"^\s*(\S+(\s+|$)){0,1000}$", + }, + validation_texts={"en": "Maximum 1000 words", "fr": "Maximum 1000 mots"}, + is_required=True, + ) + + add_question( + section2, + 8, + "long-text", + { + "en": "4. Complementary Activities Plan", + "fr": "4. Plan d’activités complémentaires", + }, + descriptions={ + "en": """Briefly outline the plan for the complementary activities that will be developed in relation to the two scholarship programs. How will the complementary activities enable PhD students to develop professionally and connect with their peers and publicize their work? How will the complementary activities help in forming links between Early Career Academics and add value in an aggregate way? (Max. 1000 words)""", - 'fr': """Décrivez brièvement le plan des activités complémentaires qui seront élaborées en relation avec les deux programmes de bourses. + "fr": """Décrivez brièvement le plan des activités complémentaires qui seront élaborées en relation avec les deux programmes de bourses. Comment les activités complémentaires vont-elles permettre aux étudiants en doctorat de se développer professionnellement, d’entrer en contact avec leurs pairs et de faire connaître leurs travaux? Comment les activités complémentaires aideront-elles à établir des liens entre les universitaires en début de carrière et à apporter une valeur ajoutée dans l’ensemble? (1000 mots au maximum) -""" - }, placeholders={ - 'en': 'Enter up to 1000 words', - 'fr': "Entrez jusqu'à 1000 mots" - }, validation_regexs={ - 'en': r'^\s*(\S+(\s+|$)){0,1000}$', - 'fr': r'^\s*(\S+(\s+|$)){0,1000}$', - }, validation_texts={ - 'en': 'Maximum 1000 words', - 'fr': 'Maximum 1000 mots' - }, is_required=True) - - add_question(section2, 9, 'long-text', { - 'en': '5. Ethical considerations', - 'fr': '5. Considérations d’ordre éthique' - }, descriptions={ - 'en': """All projects that include human subjects must ensure that their privacy, dignity, and integrity are protected. An independent ethical review committee must approve the protocols. Please describe the key ethical considerations for the proposed research and the research ethics protocols your organization uses. +""", + }, + placeholders={"en": "Enter up to 1000 words", "fr": "Entrez jusqu'à 1000 mots"}, + validation_regexs={ + "en": r"^\s*(\S+(\s+|$)){0,1000}$", + "fr": r"^\s*(\S+(\s+|$)){0,1000}$", + }, + validation_texts={"en": "Maximum 1000 words", "fr": "Maximum 1000 mots"}, + is_required=True, + ) + + add_question( + section2, + 9, + "long-text", + {"en": "5. Ethical considerations", "fr": "5. Considérations d’ordre éthique"}, + descriptions={ + "en": """All projects that include human subjects must ensure that their privacy, dignity, and integrity are protected. An independent ethical review committee must approve the protocols. Please describe the key ethical considerations for the proposed research and the research ethics protocols your organization uses. (Max. 350 words)""", - 'fr': """Dans le cadre de tout projet de recherche portant sur des sujets humains, il faut garantir la protection de la vie privée, de la dignité et de l’intégrité de la personne. Un comité d’éthique indépendant doit autoriser les protocoles. Veuillez décrire les principales considérations d’ordre éthique pour la recherche proposée, ainsi que les protocoles d’éthique de la recherche utilisés par votre organisation. + "fr": """Dans le cadre de tout projet de recherche portant sur des sujets humains, il faut garantir la protection de la vie privée, de la dignité et de l’intégrité de la personne. Un comité d’éthique indépendant doit autoriser les protocoles. Veuillez décrire les principales considérations d’ordre éthique pour la recherche proposée, ainsi que les protocoles d’éthique de la recherche utilisés par votre organisation. (350 mots au maximum) -""" - }, placeholders={ - 'en': 'Enter up to 350 words', - 'fr': "Entrez jusqu'à 350 mots" - }, validation_regexs={ - 'en': r'^\s*(\S+(\s+|$)){0,350}$', - 'fr': r'^\s*(\S+(\s+|$)){0,350}$', - }, validation_texts={ - 'en': 'Maximum 350 words', - 'fr': 'Maximum 350 mots' - }, is_required=True) - - add_question(section2, 10, 'long-text', { - 'en': '6. Results', - 'fr': '6. Résultats' - }, descriptions={ - 'en': """Please describe what success will look like at the end of the three years. What markers will you use to gauge your success in the AI4D scholarships? What are your expected outputs and outcomes? +""", + }, + placeholders={"en": "Enter up to 350 words", "fr": "Entrez jusqu'à 350 mots"}, + validation_regexs={ + "en": r"^\s*(\S+(\s+|$)){0,350}$", + "fr": r"^\s*(\S+(\s+|$)){0,350}$", + }, + validation_texts={"en": "Maximum 350 words", "fr": "Maximum 350 mots"}, + is_required=True, + ) + + add_question( + section2, + 10, + "long-text", + {"en": "6. Results", "fr": "6. Résultats"}, + descriptions={ + "en": """Please describe what success will look like at the end of the three years. What markers will you use to gauge your success in the AI4D scholarships? What are your expected outputs and outcomes? (Max. 500 words)""", - 'fr': """Veuillez décrire ce à quoi ressemblera la réussite à la fin de la période de trois années. Quels marqueurs utiliserez-vous pour évaluer votrel’initiative IAPD Afrique? Quels sont les résultats escomptés de votre recherche? + "fr": """Veuillez décrire ce à quoi ressemblera la réussite à la fin de la période de trois années. Quels marqueurs utiliserez-vous pour évaluer votrel’initiative IAPD Afrique? Quels sont les résultats escomptés de votre recherche? (500 mots au maximum) -""" - }, placeholders={ - 'en': 'Enter up to 500 words', - 'fr': "Entrez jusqu'à 500 mots" - }, validation_regexs={ - 'en': r'^\s*(\S+(\s+|$)){0,500}$', - 'fr': r'^\s*(\S+(\s+|$)){0,500}$', - }, validation_texts={ - 'en': 'Maximum 500 words', - 'fr': 'Maximum 500 mots' - }, is_required=True) - - add_question(section2, 11, 'long-text', { - 'en': '7. Project schedule', - 'fr': '7. Calendrier du projet' - }, descriptions={ - 'en': """Please provide a draft schedule for how you envision the activities will be organized according to the specific objectives of the project. The project schedule should include a list of key outputs and milestones (key events or deliverables). These need to be related to the items included in the project budget (Section IV). +""", + }, + placeholders={"en": "Enter up to 500 words", "fr": "Entrez jusqu'à 500 mots"}, + validation_regexs={ + "en": r"^\s*(\S+(\s+|$)){0,500}$", + "fr": r"^\s*(\S+(\s+|$)){0,500}$", + }, + validation_texts={"en": "Maximum 500 words", "fr": "Maximum 500 mots"}, + is_required=True, + ) + + add_question( + section2, + 11, + "long-text", + {"en": "7. Project schedule", "fr": "7. Calendrier du projet"}, + descriptions={ + "en": """Please provide a draft schedule for how you envision the activities will be organized according to the specific objectives of the project. The project schedule should include a list of key outputs and milestones (key events or deliverables). These need to be related to the items included in the project budget (Section IV). (Max. 350 words)""", - 'fr': """Veuillez fournir un calendrier provisoire pour expliquer la façon dont vous comptez organiser les activités, compte tenu des objectifs spécifiques du projet. Le calendrier du projet doit comprendre une liste des principaux extrants et jalons (événements ou produits livrables clés). Ils doivent être liés aux éléments qui figurent dans le budget du projet (section 4). + "fr": """Veuillez fournir un calendrier provisoire pour expliquer la façon dont vous comptez organiser les activités, compte tenu des objectifs spécifiques du projet. Le calendrier du projet doit comprendre une liste des principaux extrants et jalons (événements ou produits livrables clés). Ils doivent être liés aux éléments qui figurent dans le budget du projet (section 4). (350 mots au maximum) -""" - }, placeholders={ - 'en': 'Enter up to 350 words', - 'fr': "Entrez jusqu'à 350 mots" - }, validation_regexs={ - 'en': r'^\s*(\S+(\s+|$)){0,350}$', - 'fr': r'^\s*(\S+(\s+|$)){0,350}$', - }, validation_texts={ - 'en': 'Maximum 350 words', - 'fr': 'Maximum 350 mots' - }, is_required=True) - - add_question(section2, 12, 'multi-file', { - 'en': '8. Supplemental documentation (optional).', - 'fr': '8. Documents supplémentaires (facultatifs).' - }, descriptions={ - 'en': 'Attach any supplemental documentation that will help clarify your application.', - 'fr': "Veuillez joindre toute documentation supplémentaire qui aidera à clarifier votre demande." - }, is_required=False) +""", + }, + placeholders={"en": "Enter up to 350 words", "fr": "Entrez jusqu'à 350 mots"}, + validation_regexs={ + "en": r"^\s*(\S+(\s+|$)){0,350}$", + "fr": r"^\s*(\S+(\s+|$)){0,350}$", + }, + validation_texts={"en": "Maximum 350 words", "fr": "Maximum 350 mots"}, + is_required=True, + ) + + add_question( + section2, + 12, + "multi-file", + { + "en": "8. Supplemental documentation (optional).", + "fr": "8. Documents supplémentaires (facultatifs).", + }, + descriptions={ + "en": "Attach any supplemental documentation that will help clarify your application.", + "fr": "Veuillez joindre toute documentation supplémentaire qui aidera à clarifier votre demande.", + }, + is_required=False, + ) # Section 3 - section3 = add_section({ - 'en': 'Section III', - 'fr': """Section III""" - }, - { - 'en': r"""IDRC Open Access Policy + section3 = add_section( + {"en": "Section III", "fr": """Section III"""}, + { + "en": r"""IDRC Open Access Policy [https://www.idrc.ca/en/open-access-policy-idrc-funded-project-outputs](https://www.idrc.ca/en/open-access-policy-idrc-funded-project-outputs) IDRC Open Data Statement of Principles @@ -999,7 +1189,7 @@ def add_question(section, order, questionType, headlines, IDRC’s Standard Terms and Conditions for a Grant Agreement [https://www.idrc.ca/sites/default/files/sp/Documents%20EN/resources/idrc-general-terms-and-conditions-of-agreement.pdf](https://www.idrc.ca/sites/default/files/sp/Documents%20EN/resources/idrc-general-terms-and-conditions-of-agreement.pdf)""", - 'fr': r"""Politique de libre accès aux extrants des projets financés par le CRDI + "fr": r"""Politique de libre accès aux extrants des projets financés par le CRDI [https://www.idrc.ca/fr/politique-de-libre-acces-aux-extrants-des-projets-finances-par-le-crdi](https://www.idrc.ca/fr/politique-de-libre-acces-aux-extrants-des-projets-finances-par-le-crdi) Énoncé des principes des données ouvertes @@ -1010,16 +1200,25 @@ def add_question(section, order, questionType, headlines, Conditions générales de l’accord de subvention [https://www.idrc.ca/sites/default/files/sp/Documents%20FR/grant_agreement_fr.pdf](https://www.idrc.ca/sites/default/files/sp/Documents%20FR/grant_agreement_fr.pdf) -""" - }, 5) +""", + }, + 5, + ) - add_question(section3, 1, 'multi-choice', { - 'en': """I confirm that I am aware of IDRC's applicable policies and that my institution and the scholarship recipients will have to comply with them.""", - 'fr': """Je confirme que je connais les politiques applicables du CRDI et que mon établissement et les bénéficiaires des bourses devront s’y conformer.""" - }, options={ - 'en': [{'label': 'Yes', 'value': 'Yes'}, {'label': 'No', 'value': 'No'}], - 'fr': [{'label': 'Oui', 'value': 'Yes'}, {'label': 'Non', 'value': 'No'}] - }, is_required=True) + add_question( + section3, + 1, + "multi-choice", + { + "en": """I confirm that I am aware of IDRC's applicable policies and that my institution and the scholarship recipients will have to comply with them.""", + "fr": """Je confirme que je connais les politiques applicables du CRDI et que mon établissement et les bénéficiaires des bourses devront s’y conformer.""", + }, + options={ + "en": [{"label": "Yes", "value": "Yes"}, {"label": "No", "value": "No"}], + "fr": [{"label": "Oui", "value": "Yes"}, {"label": "Non", "value": "No"}], + }, + is_required=True, + ) # Section 4 en_desc = """Please submit a full three year budget with notes using the Excel template available on the IDRC public website: [https://www.idrc.ca/en/resources/guides-and-forms](https://www.idrc.ca/en/resources/guides-and-forms) @@ -1035,28 +1234,34 @@ def add_question(section, order, questionType, headlines, Insérez ce budget en tant que pièce jointe à votre demande. Dans le cas contraire, elle sera rejetée. """ - section4 = add_section({ - 'en': 'Section IV: Budget', - 'fr': 'Section IV: Budget' - }, { - 'en': en_desc, - 'fr': fr_desc - }, 6) + section4 = add_section( + {"en": "Section IV: Budget", "fr": "Section IV: Budget"}, + {"en": en_desc, "fr": fr_desc}, + 6, + ) - add_question(section4, 1, 'file', { - 'en': 'Budget', - 'fr': 'Budget' - }, is_required=True, options={ - 'en': {"accept": ".csv, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"}, - 'fr': {"accept": ".csv, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"}, - }) + add_question( + section4, + 1, + "file", + {"en": "Budget", "fr": "Budget"}, + is_required=True, + options={ + "en": { + "accept": ".csv, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel" + }, + "fr": { + "accept": ".csv, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel" + }, + }, + ) def downgrade(): Base.metadata.bind = op.get_bind() session = orm.Session(bind=Base.metadata.bind) - event = session.query(Event).filter_by(key='spm').first() + event = session.query(Event).filter_by(key="spm").first() app_form = session.query(ApplicationForm).filter_by(event_id=event.id).first() questions = session.query(Question).filter_by(application_form_id=app_form.id).all() @@ -1075,14 +1280,17 @@ def downgrade(): session.query(ApplicationForm).filter_by(event_id=event.id).delete() - event = session.query(Event).filter_by(key='spm').first() + event = session.query(Event).filter_by(key="spm").first() session.query(EventTranslation).filter_by(event_id=event.id).delete() - session.query(Event).filter_by(key='spm').delete() + session.query(Event).filter_by(key="spm").delete() session.commit() - op.alter_column('event_translation', 'description', - existing_type=sa.String(length=500), - type_=sa.String(length=255)) + op.alter_column( + "event_translation", + "description", + existing_type=sa.String(length=500), + type_=sa.String(length=255), + ) - session.commit() \ No newline at end of file + session.commit() diff --git a/api/migrations/versions/4b5d67699684_ai4d_scholarship_call_updates.py b/api/migrations/versions/4b5d67699684_ai4d_scholarship_call_updates.py index a0201dc7e..ec618a04f 100644 --- a/api/migrations/versions/4b5d67699684_ai4d_scholarship_call_updates.py +++ b/api/migrations/versions/4b5d67699684_ai4d_scholarship_call_updates.py @@ -9,26 +9,27 @@ """ # revision identifiers, used by Alembic. -revision = '4b5d67699684' -down_revision = '49d55031108e' +revision = "4b5d67699684" +down_revision = "49d55031108e" + +import datetime +from enum import Enum -from alembic import op import sqlalchemy as sa -from sqlalchemy import orm +from alembic import op +from sqlalchemy import orm, select from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy import orm -from sqlalchemy import select from sqlalchemy.orm import column_property + from app import db -import datetime -from enum import Enum Base = declarative_base() + class Organisation(Base): __tablename__ = "organisation" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(50), nullable=False) @@ -40,7 +41,17 @@ class Organisation(Base): email_from = db.Column(db.String(100), nullable=True) system_url = db.Column(db.String(100), nullable=False) - def __init__(self, name, system_name, small_logo, large_logo, domain, url, email_from, system_url): + def __init__( + self, + name, + system_name, + small_logo, + large_logo, + domain, + url, + email_from, + system_url, + ): self.name = name self.small_logo = small_logo self.large_logo = large_logo @@ -50,9 +61,10 @@ def __init__(self, name, system_name, small_logo, large_logo, domain, url, email self.email_from = email_from self.system_url = system_url + class Country(Base): __tablename__ = "country" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(100), nullable=False) @@ -60,22 +72,25 @@ class Country(Base): def __init__(self, name): self.name = name + class EventType(Enum): - EVENT = 'event' - AWARD = 'award' - CALL = 'call' + EVENT = "event" + AWARD = "award" + CALL = "call" class Event(Base): __tablename__ = "event" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) start_date = db.Column(db.DateTime(), nullable=False) end_date = db.Column(db.DateTime(), nullable=False) key = db.Column(db.String(255), nullable=False, unique=True) - organisation_id = db.Column(db.Integer(), db.ForeignKey('organisation.id'), nullable=False) + organisation_id = db.Column( + db.Integer(), db.ForeignKey("organisation.id"), nullable=False + ) email_from = db.Column(db.String(255), nullable=False) url = db.Column(db.String(255), nullable=False) application_open = db.Column(db.DateTime(), nullable=False) @@ -92,7 +107,7 @@ class Event(Base): travel_grant = db.Column(db.Boolean(), nullable=False) miniconf_url = db.Column(db.String(100), nullable=True) - event_translations = db.relationship('EventTranslation', lazy='dynamic') + event_translations = db.relationship("EventTranslation", lazy="dynamic") def __init__( self, @@ -116,7 +131,7 @@ def __init__( registration_close, event_type, travel_grant, - miniconf_url=None + miniconf_url=None, ): self.start_date = start_date self.end_date = end_date @@ -148,10 +163,11 @@ def add_event_translations(self, names, descriptions): event_translation = EventTranslation(name, description, language) self.event_translations.append(event_translation) + class EventTranslation(Base): __tablename__ = "event_translation" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) event_id = db.Column(db.Integer(), db.ForeignKey("event.id"), nullable=False) @@ -159,19 +175,20 @@ class EventTranslation(Base): description = db.Column(db.String(255), nullable=False) language = db.Column(db.String(2)) - event = db.relationship('Event', foreign_keys=[event_id]) + event = db.relationship("Event", foreign_keys=[event_id]) def __init__(self, name, description, language): self.name = name self.description = description self.language = language + class ApplicationForm(Base): - __tablename__ = 'application_form' - __table_args__ = {'extend_existing': True} + __tablename__ = "application_form" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - event_id = db.Column(db.Integer(), db.ForeignKey('event.id'), nullable=False) + event_id = db.Column(db.Integer(), db.ForeignKey("event.id"), nullable=False) is_open = db.Column(db.Boolean(), nullable=False) nominations = db.Column(db.Boolean(), nullable=False) @@ -180,22 +197,29 @@ def __init__(self, event_id, is_open, nominations): self.is_open = is_open self.nominations = nominations + class Question(Base): - __tablename__ = 'question' - __table_args__ = {'extend_existing': True} + __tablename__ = "question" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) - section_id = db.Column(db.Integer(), db.ForeignKey('section.id'), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) + section_id = db.Column(db.Integer(), db.ForeignKey("section.id"), nullable=False) type = db.Column(db.String(), nullable=False) order = db.Column(db.Integer(), nullable=False) is_required = db.Column(db.Boolean(), nullable=False) - depends_on_question_id = db.Column(db.Integer(), db.ForeignKey('question.id'), nullable=True) + depends_on_question_id = db.Column( + db.Integer(), db.ForeignKey("question.id"), nullable=True + ) key = db.Column(db.String(255), nullable=True) - question_translations = db.relationship('QuestionTranslation', lazy='dynamic') + question_translations = db.relationship("QuestionTranslation", lazy="dynamic") - def __init__(self, application_form_id, section_id, order, questionType, is_required=True): + def __init__( + self, application_form_id, section_id, order, questionType, is_required=True + ): self.application_form_id = application_form_id self.section_id = section_id self.order = order @@ -203,45 +227,55 @@ def __init__(self, application_form_id, section_id, order, questionType, is_requ self.is_required = is_required def get_translation(self, language): - question_translation = self.question_translations.filter_by(language=language).first() + question_translation = self.question_translations.filter_by( + language=language + ).first() return question_translation class Section(Base): - __tablename__ = 'section' - __table_args__ = {'extend_existing': True} + __tablename__ = "section" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) order = db.Column(db.Integer(), nullable=False) - depends_on_question_id = db.Column(db.Integer(), db.ForeignKey('question.id', use_alter=True), nullable=True) + depends_on_question_id = db.Column( + db.Integer(), db.ForeignKey("question.id", use_alter=True), nullable=True + ) key = db.Column(db.String(255), nullable=True) - section_translations = db.relationship('SectionTranslation', lazy='dynamic') + section_translations = db.relationship("SectionTranslation", lazy="dynamic") - def __init__(self, application_form_id, order, depends_on_question_id=None, key=None): + def __init__( + self, application_form_id, order, depends_on_question_id=None, key=None + ): self.application_form_id = application_form_id self.order = order self.depends_on_question_id = depends_on_question_id self.key = key def get_translation(self, language): - section_translation = self.section_translations.filter_by(language=language).first() + section_translation = self.section_translations.filter_by( + language=language + ).first() return section_translation class SectionTranslation(Base): - __tablename__ = 'section_translation' - __table_args__ = {'extend_existing': True} + __tablename__ = "section_translation" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - section_id = db.Column(db.Integer(), db.ForeignKey('section.id'), nullable=False) + section_id = db.Column(db.Integer(), db.ForeignKey("section.id"), nullable=False) language = db.Column(db.String(2), nullable=False) name = db.Column(db.String(255), nullable=False) description = db.Column(db.String(), nullable=False) show_for_values = db.Column(db.JSON(), nullable=True) - section = db.relationship('Section', foreign_keys=[section_id]) + section = db.relationship("Section", foreign_keys=[section_id]) def __init__(self, section_id, language, name, description, show_for_values=None): self.section_id = section_id @@ -252,11 +286,11 @@ def __init__(self, section_id, language, name, description, show_for_values=None class QuestionTranslation(Base): - __tablename__ = 'question_translation' - __table_args__ = {'extend_existing': True} + __tablename__ = "question_translation" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - question_id = db.Column(db.Integer(), db.ForeignKey('question.id'), nullable=False) + question_id = db.Column(db.Integer(), db.ForeignKey("question.id"), nullable=False) language = db.Column(db.String(2), nullable=False) headline = db.Column(db.String(), nullable=False) description = db.Column(db.String(), nullable=True) @@ -266,7 +300,7 @@ class QuestionTranslation(Base): options = db.Column(db.JSON(), nullable=True) show_for_values = db.Column(db.JSON(), nullable=True) - question = db.relationship('Question', foreign_keys=[question_id]) + question = db.relationship("Question", foreign_keys=[question_id]) def __init__( self, @@ -278,7 +312,7 @@ def __init__( validation_regex=None, validation_text=None, options=None, - show_for_values=None + show_for_values=None, ): self.question_id = question_id self.language = language @@ -295,16 +329,25 @@ def upgrade(): Base.metadata.bind = op.get_bind() session = orm.Session(bind=Base.metadata.bind) - event = session.query(Event).filter_by(key='prc').first() + event = session.query(Event).filter_by(key="prc").first() form = session.query(ApplicationForm).filter_by(event_id=event.id).first() - en = (session.query(SectionTranslation) - .filter_by(language='en', name='AI4D Call for Proposals: Scholarships program manager') - .first()) - - fr = (session.query(SectionTranslation) - .filter_by(language='fr', name='Appel à propositions IAPD: Gestionnaire des programmes de bourses') - .first()) + en = ( + session.query(SectionTranslation) + .filter_by( + language="en", name="AI4D Call for Proposals: Scholarships program manager" + ) + .first() + ) + + fr = ( + session.query(SectionTranslation) + .filter_by( + language="fr", + name="Appel à propositions IAPD: Gestionnaire des programmes de bourses", + ) + .first() + ) en.description = """The International Development Research Centre (IDRC) and the Swedish International Development Agency (Sida) invite proposals from qualified institutions to manage the African Artificial Intelligence for Development (AI4D) Scholarships program. @@ -334,43 +377,64 @@ def upgrade(): session.commit() - q = session.query(QuestionTranslation).filter_by(language='en', headline='Telephone').first() + q = ( + session.query(QuestionTranslation) + .filter_by(language="en", headline="Telephone") + .first() + ) if q: q_id = q.id op.execute("""DELETE FROM Answer WHERE question_id={}""".format(q_id)) session.query(QuestionTranslation).filter_by(question_id=q_id).delete() session.query(Question).filter_by(id=q_id).delete() - q = session.query(QuestionTranslation).filter_by(language='en', headline='Mobile (Optional)').first() + q = ( + session.query(QuestionTranslation) + .filter_by(language="en", headline="Mobile (Optional)") + .first() + ) if q: q_id = q.id op.execute("""DELETE FROM Answer WHERE question_id={}""".format(q_id)) session.query(QuestionTranslation).filter_by(question_id=q_id).delete() session.query(Question).filter_by(id=q_id).delete() - q = session.query(QuestionTranslation).filter_by(language='en', headline='Email Address').first() + q = ( + session.query(QuestionTranslation) + .filter_by(language="en", headline="Email Address") + .first() + ) if q: q_id = q.id op.execute("""DELETE FROM Answer WHERE question_id={}""".format(q_id)) session.query(QuestionTranslation).filter_by(question_id=q_id).delete() session.query(Question).filter_by(id=q_id).delete() - - en = session.query(QuestionTranslation).filter_by(language='en', headline='2. Summary of the proposed approach to the financial and administrative management of the AI4D scholarships') + + en = session.query(QuestionTranslation).filter_by( + language="en", + headline="2. Summary of the proposed approach to the financial and administrative management of the AI4D scholarships", + ) en.description = """Briefly outline your proposal to manage the AI4D scholarship program according to the requirements outlined in section 2.1 of the Call for Proposals Background Document. What approaches, disciplines and modalities will you draw upon to support this?""" session.commit() - en = session.query(QuestionTranslation).filter_by(language='en', headline='2a.Communication strategies') + en = session.query(QuestionTranslation).filter_by( + language="en", headline="2a.Communication strategies" + ) en.description = """Discuss your plan for launching the African AI4D PhD Scholarship and the African AI4D Advanced Scholars Program. How does your plan ensure that the program will be as inclusive as possible?""" session.commit() - en = session.query(QuestionTranslation).filter_by(language='en', headline='3a. Gender and Inclusion Considerations') + en = session.query(QuestionTranslation).filter_by( + language="en", headline="3a. Gender and Inclusion Considerations" + ) en.description = """How will your proposed approach promote increased diversity and equity in AI research? What literature or experience will you draw upon to support gender equity and linguistic equity goals in the design and execution of the calls? How will your process integrate best practices in marketing and outreach, engagement, and support for recipients?""" session.commit() - en = session.query(QuestionTranslation).filter_by(language='en', headline='3b. Selection process and evaluation') + en = session.query(QuestionTranslation).filter_by( + language="en", headline="3b. Selection process and evaluation" + ) en.description = """Broadly discuss the evaluation process for the two scholarships activities. How will the evaluation process ensure that the projects funded will be relevant to the AI4D Africa program’s responsible AI mandate, address ethical concerns and gender dimensions, and assess the capacity of applicants to carry out the proposed research?""" diff --git a/api/migrations/versions/4b881c1c6dc2_add_email_templates.py b/api/migrations/versions/4b881c1c6dc2_add_email_templates.py index 44ded1c65..4727b89a9 100644 --- a/api/migrations/versions/4b881c1c6dc2_add_email_templates.py +++ b/api/migrations/versions/4b881c1c6dc2_add_email_templates.py @@ -7,25 +7,25 @@ """ # revision identifiers, used by Alembic. -revision = '4b881c1c6dc2' -down_revision = 'fc82c8e54fd7' +revision = "4b881c1c6dc2" +down_revision = "fc82c8e54fd7" + +import datetime -from alembic import op import sqlalchemy as sa +from alembic import op from sqlalchemy import orm from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy import orm + from app import db -import datetime Base = declarative_base() - class EmailTemplate(Base): - __tablename__ = 'email_template' - __table_args__ = {'extend_existing': True} + __tablename__ = "email_template" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) key = db.Column(db.String(50), nullable=False) @@ -46,7 +46,9 @@ def upgrade(): Base.metadata.bind = op.get_bind() session = orm.Session(bind=Base.metadata.bind) - op.execute("""SELECT setval('email_template_id_seq', (SELECT max(id) FROM email_template));""") + op.execute( + """SELECT setval('email_template_id_seq', (SELECT max(id) FROM email_template));""" + ) template = """Dear {title} {firstname} {lastname}, @@ -55,7 +57,15 @@ def upgrade(): Best wishes, {event_name} Organisers""" - session.add(EmailTemplate('outcome-rejected', None, '{event_name} Application Status Update', template, 'en')) + session.add( + EmailTemplate( + "outcome-rejected", + None, + "{event_name} Application Status Update", + template, + "en", + ) + ) template = """Dear {title} {firstname} {lastname}, @@ -64,7 +74,15 @@ def upgrade(): Kind Regards, {event_name} Organisers""" - session.add(EmailTemplate('outcome-waitlist', None, '{event_name} Application Status Update', template, 'en')) + session.add( + EmailTemplate( + "outcome-waitlist", + None, + "{event_name} Application Status Update", + template, + "en", + ) + ) template = """Dear {title} {firstname} {lastname}, @@ -76,7 +94,11 @@ def upgrade(): The {event_name} Team """ - session.add(EmailTemplate('attendance-confirmation', None, 'Welcome to {event_name}!', template, 'en')) + session.add( + EmailTemplate( + "attendance-confirmation", None, "Welcome to {event_name}!", template, "en" + ) + ) template = """Dear {title} {firstname} {lastname}, @@ -89,7 +111,15 @@ def upgrade(): The {event_name} Team """ - session.add(EmailTemplate('guest-registration-confirmation', None, '{event_name} Registration Confirmation', template, 'en')) + session.add( + EmailTemplate( + "guest-registration-confirmation", + None, + "{event_name} Registration Confirmation", + template, + "en", + ) + ) template = """Dear {title} {firstname} {lastname}, @@ -99,7 +129,15 @@ def upgrade(): The {event_name} Team """ - session.add(EmailTemplate('invitation-letter', None, 'Invitation Letter to {event_name}', template, 'en')) + session.add( + EmailTemplate( + "invitation-letter", + None, + "Invitation Letter to {event_name}", + template, + "en", + ) + ) template = """Dear {title} {firstname} {lastname}, @@ -109,7 +147,11 @@ def upgrade(): Kind Regards, The {event_name} Organisers""" - session.add(EmailTemplate('guest-invitation', None, 'Your invitation to {event_name}', template, 'en')) + session.add( + EmailTemplate( + "guest-invitation", None, "Your invitation to {event_name}", template, "en" + ) + ) template = """Dear {title} {firstname} {lastname}, @@ -120,7 +162,15 @@ def upgrade(): Kind Regards, The {event_name} Organisers""" - session.add(EmailTemplate('guest-invitation-with-registration', None, 'Your invitation to {event_name}', template, 'en')) + session.add( + EmailTemplate( + "guest-invitation-with-registration", + None, + "Your invitation to {event_name}", + template, + "en", + ) + ) template = """Dear {title} {firstname} {lastname}, @@ -136,7 +186,15 @@ def upgrade(): Kind Regards, The {event_name} Organisers """ - session.add(EmailTemplate('new-guest-no-registration', None, 'Your invitation to {event_name}', template, 'en')) + session.add( + EmailTemplate( + "new-guest-no-registration", + None, + "Your invitation to {event_name}", + template, + "en", + ) + ) template = """Dear {title} {firstname} {lastname}, @@ -153,7 +211,15 @@ def upgrade(): Kind Regards, The {event_name} Organisers """ - session.add(EmailTemplate('new-guest-registration', None, 'Your invitation to {event_name}', template, 'en')) + session.add( + EmailTemplate( + "new-guest-registration", + None, + "Your invitation to {event_name}", + template, + "en", + ) + ) template = """Dear {title} {firstname} {lastname}, @@ -168,7 +234,15 @@ def upgrade(): Kind regards, The {event_name} team. """ - session.add(EmailTemplate('reference-request-self-nomination', None, 'Reference Request for {event_name}', template, 'en')) + session.add( + EmailTemplate( + "reference-request-self-nomination", + None, + "Reference Request for {event_name}", + template, + "en", + ) + ) template = """Dear {title} {firstname} {lastname}, @@ -183,7 +257,15 @@ def upgrade(): Kind regards, The {event_name} team. """ - session.add(EmailTemplate('reference-request', None, 'Reference Request for {event_name}', template, 'en')) + session.add( + EmailTemplate( + "reference-request", + None, + "Reference Request for {event_name}", + template, + "en", + ) + ) template = """Dear {title} {firstname} {lastname}, @@ -200,7 +282,11 @@ def upgrade(): Kind Regards, The {event_name} organisers """ - session.add(EmailTemplate('offer', None, '{event_name} Application Status Update', template, 'en')) + session.add( + EmailTemplate( + "offer", None, "{event_name} Application Status Update", template, "en" + ) + ) template = """Dear {title} {firstname} {lastname}, @@ -213,7 +299,15 @@ def upgrade(): Kind Regards, The {event_name} Organisers """ - session.add(EmailTemplate('registration-pending-confirmation', None, '{event_name} Registration', template, 'en')) + session.add( + EmailTemplate( + "registration-pending-confirmation", + None, + "{event_name} Registration", + template, + "en", + ) + ) template = """Dear {title} {firstname} {lastname}, @@ -226,7 +320,15 @@ def upgrade(): Kind Regards, The {event_name} Organisers """ - session.add(EmailTemplate('registration-with-confirmation', None, '{event_name} Registration', template, 'en')) + session.add( + EmailTemplate( + "registration-with-confirmation", + None, + "{event_name} Registration", + template, + "en", + ) + ) template = """Dear {title} {firstname} {lastname}, @@ -237,7 +339,15 @@ def upgrade(): Kind Regards, The {event_name} Organisers """ - session.add(EmailTemplate('registration-confirmed', None, '{event_name} Registration Confirmation', template, 'en')) + session.add( + EmailTemplate( + "registration-confirmed", + None, + "{event_name} Registration Confirmation", + template, + "en", + ) + ) template = """Dear {title} {firstname} {lastname}, @@ -249,7 +359,15 @@ def upgrade(): Kind Regards, The {event_name} Organisers """ - session.add(EmailTemplate('reviews-assigned', None, 'You have been assigned reviews for {event_name}', template, 'en')) + session.add( + EmailTemplate( + "reviews-assigned", + None, + "You have been assigned reviews for {event_name}", + template, + "en", + ) + ) template = """ Dear {title} {firstname} {lastname}, @@ -261,7 +379,11 @@ def upgrade(): Kind Regards, {organisation} """ - session.add(EmailTemplate('verify-email', None, '{system} Email Verficiation', template, 'en')) + session.add( + EmailTemplate( + "verify-email", None, "{system} Email Verficiation", template, "en" + ) + ) template = """Dear {title} {firstname} {lastname}, @@ -273,18 +395,39 @@ def upgrade(): Kind Regards, {organisation} """ - session.add(EmailTemplate('password-reset', None, '{system} Password Reset', template, 'en')) + session.add( + EmailTemplate("password-reset", None, "{system} Password Reset", template, "en") + ) session.commit() - def downgrade(): - added_keys = ['outcome-rejected', 'outcome-waitlist', 'attendance-confirmation', 'guest-registration-confirmation', - 'invitation-letter', 'guest-invitation', 'guest-invitation-with-registration', 'new-guest-no-registration', - 'new-guest-registration', 'reference-request-self-nomination', 'reference-request', 'offer', - 'registration-pending-confirmation', 'registration-with-confirmation', 'registration-confirmed', - 'reviews-assigned', 'verify-email', 'password-reset'] - op.execute("""DELETE FROM email_template WHERE key in ({})""".format(', '.join(["'" + k + "'" for k in added_keys]))) - op.execute("""SELECT setval('email_template_id_seq', (SELECT max(id) FROM email_template));""") - + added_keys = [ + "outcome-rejected", + "outcome-waitlist", + "attendance-confirmation", + "guest-registration-confirmation", + "invitation-letter", + "guest-invitation", + "guest-invitation-with-registration", + "new-guest-no-registration", + "new-guest-registration", + "reference-request-self-nomination", + "reference-request", + "offer", + "registration-pending-confirmation", + "registration-with-confirmation", + "registration-confirmed", + "reviews-assigned", + "verify-email", + "password-reset", + ] + op.execute( + """DELETE FROM email_template WHERE key in ({})""".format( + ", ".join(["'" + k + "'" for k in added_keys]) + ) + ) + op.execute( + """SELECT setval('email_template_id_seq', (SELECT max(id) FROM email_template));""" + ) diff --git a/api/migrations/versions/4d8b254d7e7e_link_user_to_organisation.py b/api/migrations/versions/4d8b254d7e7e_link_user_to_organisation.py index c7bf9d45f..a373d23fb 100644 --- a/api/migrations/versions/4d8b254d7e7e_link_user_to_organisation.py +++ b/api/migrations/versions/4d8b254d7e7e_link_user_to_organisation.py @@ -7,13 +7,14 @@ """ # revision identifiers, used by Alembic. -revision = '4d8b254d7e7e' -down_revision = '016571f41a20' +revision = "4d8b254d7e7e" +down_revision = "016571f41a20" -from alembic import op import sqlalchemy as sa -from sqlalchemy.ext.declarative import declarative_base +from alembic import op from sqlalchemy import orm +from sqlalchemy.ext.declarative import declarative_base + from app import db Base = declarative_base() @@ -23,13 +24,17 @@ def upgrade(): Base.metadata.bind = op.get_bind() session = orm.Session(bind=Base.metadata.bind) - op.add_column('app_user', sa.Column('organisation_id', sa.Integer(), nullable=True)) - op.create_foreign_key('fk_user_organisation', 'app_user', 'organisation', ['organisation_id'], ['id']) + op.add_column("app_user", sa.Column("organisation_id", sa.Integer(), nullable=True)) + op.create_foreign_key( + "fk_user_organisation", "app_user", "organisation", ["organisation_id"], ["id"] + ) op.execute("""UPDATE app_user SET organisation_id = 1""") - op.alter_column('app_user', 'organisation_id', nullable=False) + op.alter_column("app_user", "organisation_id", nullable=False) - op.add_column('organisation', sa.Column('system_name', sa.String(length=50), nullable=True)) + op.add_column( + "organisation", sa.Column("system_name", sa.String(length=50), nullable=True) + ) op.execute("""UPDATE organisation SET system_name = 'Baobab' WHERE id = 1""") op.execute("""UPDATE organisation SET system_name = 'EEML Portal' WHERE id = 2""") @@ -38,7 +43,7 @@ def upgrade(): def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('organisation', 'system_name') - op.drop_constraint('fk_user_organisation', 'app_user', type_='foreignkey') - op.drop_column('app_user', 'organisation_id') + op.drop_column("organisation", "system_name") + op.drop_constraint("fk_user_organisation", "app_user", type_="foreignkey") + op.drop_column("app_user", "organisation_id") # ### end Alembic commands ### diff --git a/api/migrations/versions/4ec21502ae82_add_call_event_type.py b/api/migrations/versions/4ec21502ae82_add_call_event_type.py index a78e6866c..c2f5f2fb5 100644 --- a/api/migrations/versions/4ec21502ae82_add_call_event_type.py +++ b/api/migrations/versions/4ec21502ae82_add_call_event_type.py @@ -7,11 +7,11 @@ """ # revision identifiers, used by Alembic. -revision = '4ec21502ae82' -down_revision = '9b181cdd3fa1' +revision = "4ec21502ae82" +down_revision = "9b181cdd3fa1" -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): @@ -20,8 +20,10 @@ def upgrade(): def downgrade(): - op.execute("""DELETE FROM pg_enum + op.execute( + """DELETE FROM pg_enum WHERE enumlabel = 'CALL' AND enumtypid = ( SELECT oid FROM pg_type WHERE typname = 'event_type' -)""") +)""" + ) diff --git a/api/migrations/versions/539d0567cd6c_.py b/api/migrations/versions/539d0567cd6c_.py index 5fdaee05c..999960046 100644 --- a/api/migrations/versions/539d0567cd6c_.py +++ b/api/migrations/versions/539d0567cd6c_.py @@ -8,20 +8,20 @@ """ # revision identifiers, used by Alembic. -revision = '539d0567cd6c' -down_revision = 'c0885e531d29' +revision = "539d0567cd6c" +down_revision = "c0885e531d29" -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.add_column('offer', sa.Column('accepted', sa.Boolean(), nullable=True)) + op.add_column("offer", sa.Column("accepted", sa.Boolean(), nullable=True)) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('offer', 'accepted') + op.drop_column("offer", "accepted") # ### end Alembic commands ### diff --git a/api/migrations/versions/57e55dc645c1_set_travel_accomm_columns.py b/api/migrations/versions/57e55dc645c1_set_travel_accomm_columns.py index 99e0063b1..4bd6f3a4e 100644 --- a/api/migrations/versions/57e55dc645c1_set_travel_accomm_columns.py +++ b/api/migrations/versions/57e55dc645c1_set_travel_accomm_columns.py @@ -7,24 +7,24 @@ """ # revision identifiers, used by Alembic. -revision = '57e55dc645c1' -down_revision = '3ab678fc66cd' +revision = "57e55dc645c1" +down_revision = "3ab678fc66cd" -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): - op.drop_column('offer', 'travel_award') - op.add_column('offer', sa.Column('travel_award', sa.Boolean(), nullable=False)) - op.alter_column('offer', 'accommodation_award', - existing_type=sa.BOOLEAN(), - nullable=False) + op.drop_column("offer", "travel_award") + op.add_column("offer", sa.Column("travel_award", sa.Boolean(), nullable=False)) + op.alter_column( + "offer", "accommodation_award", existing_type=sa.BOOLEAN(), nullable=False + ) def downgrade(): - op.drop_column('offer', 'travel_award') - op.add_column('offer', sa.Column('travel_award', sa.String(), nullable=False)) - op.alter_column('offer', 'accommodation_award', - existing_type=sa.BOOLEAN(), - nullable=True) + op.drop_column("offer", "travel_award") + op.add_column("offer", sa.Column("travel_award", sa.String(), nullable=False)) + op.alter_column( + "offer", "accommodation_award", existing_type=sa.BOOLEAN(), nullable=True + ) diff --git a/api/migrations/versions/613df2d7a759_update_email_template.py b/api/migrations/versions/613df2d7a759_update_email_template.py index 0eec951fc..5cc751fdf 100644 --- a/api/migrations/versions/613df2d7a759_update_email_template.py +++ b/api/migrations/versions/613df2d7a759_update_email_template.py @@ -7,16 +7,20 @@ """ # revision identifiers, used by Alembic. -revision = '613df2d7a759' -down_revision = '10b4b888c16b' +revision = "613df2d7a759" +down_revision = "10b4b888c16b" -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): - op.execute("""UPDATE email_template SET subject = '{system_name} Password Reset' where key = 'password-reset' and language='en'""") + op.execute( + """UPDATE email_template SET subject = '{system_name} Password Reset' where key = 'password-reset' and language='en'""" + ) def downgrade(): - op.execute("""UPDATE email_template SET subject = '{system} Password Reset' where key = 'password-reset' and language='en'""") + op.execute( + """UPDATE email_template SET subject = '{system} Password Reset' where key = 'password-reset' and language='en'""" + ) diff --git a/api/migrations/versions/62352569fb5d_.py b/api/migrations/versions/62352569fb5d_.py index 9e7df8cf7..36dd10ac3 100644 --- a/api/migrations/versions/62352569fb5d_.py +++ b/api/migrations/versions/62352569fb5d_.py @@ -7,11 +7,11 @@ """ # revision identifiers, used by Alembic. -revision = '62352569fb5d' -down_revision = '04188674173e' +revision = "62352569fb5d" +down_revision = "04188674173e" -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): diff --git a/api/migrations/versions/62c7711123a8_french_email_templates.py b/api/migrations/versions/62c7711123a8_french_email_templates.py index f3cfaa86d..9f91981a2 100644 --- a/api/migrations/versions/62c7711123a8_french_email_templates.py +++ b/api/migrations/versions/62c7711123a8_french_email_templates.py @@ -9,25 +9,25 @@ """ # revision identifiers, used by Alembic. -revision = '62c7711123a8' -down_revision = 'ca538998737f' +revision = "62c7711123a8" +down_revision = "ca538998737f" + +import datetime -from alembic import op import sqlalchemy as sa +from alembic import op from sqlalchemy import orm from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy import orm + from app import db -import datetime Base = declarative_base() - class EmailTemplate(Base): - __tablename__ = 'email_template' - __table_args__ = {'extend_existing': True} + __tablename__ = "email_template" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) key = db.Column(db.String(50), nullable=False) @@ -55,10 +55,17 @@ def upgrade(): Cordialement, Les organisateurs de {event_name}""" - session.add(EmailTemplate('outcome-rejected', None, 'Mise à jour de l’état de la demande pour {event_name}', template, 'fr')) - -######### - + session.add( + EmailTemplate( + "outcome-rejected", + None, + "Mise à jour de l’état de la demande pour {event_name}", + template, + "fr", + ) + ) + + ######### template = """Madame / Monsieur {lastname}, @@ -67,10 +74,17 @@ def upgrade(): Cordialement, Les organisateurs de {event_name}""" - session.add(EmailTemplate('outcome-waitlist', None, 'Mise à jour de l’état de la demande pour {event_name}', template, 'fr')) - -######### - + session.add( + EmailTemplate( + "outcome-waitlist", + None, + "Mise à jour de l’état de la demande pour {event_name}", + template, + "fr", + ) + ) + + ######### template = """Madame / Monsieur {lastname}, @@ -82,9 +96,17 @@ def upgrade(): Les organisateurs de {event_name} """ - session.add(EmailTemplate('attendance-confirmation', None, 'Bienvenue à {event_name}!', template, 'fr')) + session.add( + EmailTemplate( + "attendance-confirmation", + None, + "Bienvenue à {event_name}!", + template, + "fr", + ) + ) -######### + ######### template = """Madame / Monsieur {lastname}, @@ -97,9 +119,17 @@ def upgrade(): l’équipe {event_name} """ - session.add(EmailTemplate('guest-registration-confirmation', None, 'Inscription à {event_name}', template, 'fr')) + session.add( + EmailTemplate( + "guest-registration-confirmation", + None, + "Inscription à {event_name}", + template, + "fr", + ) + ) -######### + ######### template = """Madame / Monsieur {lastname}, @@ -109,10 +139,17 @@ def upgrade(): l’équipe {event_name} """ - session.add(EmailTemplate('invitation-letter', None, "Lettre d'invitation à {event_name}", template, 'fr')) - -######### + session.add( + EmailTemplate( + "invitation-letter", + None, + "Lettre d'invitation à {event_name}", + template, + "fr", + ) + ) + ######### template = """Madame / Monsieur {lastname}, @@ -122,10 +159,13 @@ def upgrade(): Cordialement, Les organisateurs de {event_name}""" - session.add(EmailTemplate('guest-invitation', None, 'Votre invitation à {event_name}', template, 'fr')) - -######### + session.add( + EmailTemplate( + "guest-invitation", None, "Votre invitation à {event_name}", template, "fr" + ) + ) + ######### template = """Madame / Monsieur {lastname}, @@ -139,10 +179,17 @@ def upgrade(): Cordialement, Les organisateurs de {event_name}""" - session.add(EmailTemplate('guest-invitation-with-registration', None, 'Votre invitation à {event_name}', template, 'fr')) - -######### - + session.add( + EmailTemplate( + "guest-invitation-with-registration", + None, + "Votre invitation à {event_name}", + template, + "fr", + ) + ) + + ######### template = """Madame / Monsieur {lastname}, @@ -160,10 +207,17 @@ def upgrade(): Cordialement, Les organisateurs de {event_name} """ - session.add(EmailTemplate('new-guest-no-registration', None, 'Votre invitation à {event_name}', template, 'fr')) - -######### - + session.add( + EmailTemplate( + "new-guest-no-registration", + None, + "Votre invitation à {event_name}", + template, + "fr", + ) + ) + + ######### template = """Madame / Monsieur {lastname}, @@ -182,10 +236,17 @@ def upgrade(): Cordialement, Les organisateurs de {event_name} """ - session.add(EmailTemplate('new-guest-registration', None, 'Votre invitation à {event_name}', template, 'fr')) - -######### - + session.add( + EmailTemplate( + "new-guest-registration", + None, + "Votre invitation à {event_name}", + template, + "fr", + ) + ) + + ######### template = """Madame / Monsieur {lastname}, @@ -204,10 +265,17 @@ def upgrade(): L’équipe de {event_name} """ - session.add(EmailTemplate('reference-request-self-nomination', None, 'DEMANDE DE RÉFÉRENCE – {event_name}', template, 'fr')) - -######### - + session.add( + EmailTemplate( + "reference-request-self-nomination", + None, + "DEMANDE DE RÉFÉRENCE – {event_name}", + template, + "fr", + ) + ) + + ######### template = """Madame / Monsieur {lastname}, @@ -225,10 +293,17 @@ def upgrade(): Cordialement, L’équipe de {event_name} """ - session.add(EmailTemplate('reference-request', None, 'DEMANDE DE RÉFÉRENCE – {event_name}', template, 'fr')) - -######### - + session.add( + EmailTemplate( + "reference-request", + None, + "DEMANDE DE RÉFÉRENCE – {event_name}", + template, + "fr", + ) + ) + + ######### template = """Madame / Monsieur {lastname}, @@ -245,10 +320,17 @@ def upgrade(): Les organisateurs de {event_name} """ - session.add(EmailTemplate('offer', None, 'Mise à jour de l’état de la demande pour {event_name}', template, 'fr')) - -######### - + session.add( + EmailTemplate( + "offer", + None, + "Mise à jour de l’état de la demande pour {event_name}", + template, + "fr", + ) + ) + + ######### template = """Madame / Monsieur {lastname}, @@ -262,11 +344,17 @@ def upgrade(): Cordialement, Les organisateurs de {event_name} """ - session.add(EmailTemplate('registration-pending-confirmation', None, 'Inscription à {event_name}', template, 'fr')) - -######### - - + session.add( + EmailTemplate( + "registration-pending-confirmation", + None, + "Inscription à {event_name}", + template, + "fr", + ) + ) + + ######### template = """Madame / Monsieur {lastname}, @@ -280,10 +368,17 @@ def upgrade(): Cordialement, Les organisateurs de {event_name} """ - session.add(EmailTemplate('registration-with-confirmation', None, 'Inscription à {event_name}', template, 'fr')) - -######### - + session.add( + EmailTemplate( + "registration-with-confirmation", + None, + "Inscription à {event_name}", + template, + "fr", + ) + ) + + ######### template = """Madame / Monsieur {lastname}, @@ -294,10 +389,17 @@ def upgrade(): Cordialement, Les organisateurs de {event_name} """ - session.add(EmailTemplate('registration-confirmed', None, "La confirmation d'inscription à {event_name}", template, 'fr')) - -######### - + session.add( + EmailTemplate( + "registration-confirmed", + None, + "La confirmation d'inscription à {event_name}", + template, + "fr", + ) + ) + + ######### template = """Madame / Monsieur {lastname}, @@ -310,10 +412,22 @@ def upgrade(): Cordialement, Les organisateurs de {event_name} """ - session.add(EmailTemplate('reviews-assigned', None, 'Des tâches de révision vous ont été attribuées dans {system_name}', template, 'fr')) - + session.add( + EmailTemplate( + "reviews-assigned", + None, + "Des tâches de révision vous ont été attribuées dans {system_name}", + template, + "fr", + ) + ) + # Update the English template - en_template = session.query(EmailTemplate).filter_by(key='reviews-assigned', language='en').first() + en_template = ( + session.query(EmailTemplate) + .filter_by(key="reviews-assigned", language="en") + .first() + ) en_template.template = """Dear {title} {firstname} {lastname}, You have been assigned {num_reviews} reviews on {system_name}. Please visit {baobab_host}/{event_key}/review to begin. @@ -325,8 +439,7 @@ def upgrade(): The {event_name} Organisers """ -######### - + ######### template = """Madame / Monsieur {lastname}, @@ -338,10 +451,13 @@ def upgrade(): Cordialement, {organisation} """ - session.add(EmailTemplate('verify-email', None, 'Courriel {system} Vérification', template, 'fr')) - -######### + session.add( + EmailTemplate( + "verify-email", None, "Courriel {system} Vérification", template, "fr" + ) + ) + ######### template = """Madame / Monsieur {lastname}, @@ -353,15 +469,42 @@ def upgrade(): Cordialement, {organisation} """ - session.add(EmailTemplate('password-reset', None, 'Réinitialisation du mot de passe pour {system_name}', template, 'fr')) + session.add( + EmailTemplate( + "password-reset", + None, + "Réinitialisation du mot de passe pour {system_name}", + template, + "fr", + ) + ) session.commit() def downgrade(): - added_keys = ['outcome-rejected', 'outcome-waitlist', 'attendance-confirmation', 'guest-registration-confirmation', - 'invitation-letter', 'guest-invitation', 'guest-invitation-with-registration', 'new-guest-no-registration', - 'new-guest-registration', 'reference-request-self-nomination', 'reference-request', 'offer', - 'registration-pending-confirmation', 'registration-with-confirmation', 'registration-confirmed', - 'reviews-assigned', 'verify-email', 'password-reset'] - op.execute("""DELETE FROM email_template WHERE language='fr' and key in ({})""".format(', '.join(["'" + k + "'" for k in added_keys]))) + added_keys = [ + "outcome-rejected", + "outcome-waitlist", + "attendance-confirmation", + "guest-registration-confirmation", + "invitation-letter", + "guest-invitation", + "guest-invitation-with-registration", + "new-guest-no-registration", + "new-guest-registration", + "reference-request-self-nomination", + "reference-request", + "offer", + "registration-pending-confirmation", + "registration-with-confirmation", + "registration-confirmed", + "reviews-assigned", + "verify-email", + "password-reset", + ] + op.execute( + """DELETE FROM email_template WHERE language='fr' and key in ({})""".format( + ", ".join(["'" + k + "'" for k in added_keys]) + ) + ) diff --git a/api/migrations/versions/65f15544a656_.py b/api/migrations/versions/65f15544a656_.py index f26727f62..df36552e1 100644 --- a/api/migrations/versions/65f15544a656_.py +++ b/api/migrations/versions/65f15544a656_.py @@ -7,39 +7,43 @@ """ # revision identifiers, used by Alembic. -revision = '65f15544a656' -down_revision = '438ca9e1c2e8' +revision = "65f15544a656" +down_revision = "438ca9e1c2e8" -from alembic import op import sqlalchemy as sa +from alembic import op from sqlalchemy.dialects import postgresql + def upgrade(): - op.create_table('guest_registration', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=False), - sa.Column('registration_form_id', sa.Integer(), nullable=False), - sa.Column('confirmed', sa.Boolean(), nullable=False), - sa.Column('created_at', sa.DateTime(), nullable=False), - sa.Column('confirmation_email_sent_at', sa.DateTime(), nullable=False), - sa.ForeignKeyConstraint(['user_id'], ['app_user.id']), - sa.ForeignKeyConstraint(['registration_form_id'], ['registration_form.id']), - sa.PrimaryKeyConstraint('id') - ) - - op.create_table('guest_registration_answer', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('guest_registration_id', sa.Integer(), nullable=False), - sa.Column('registration_question_id', sa.Integer(), nullable=False), - sa.Column('value', sa.String(), nullable=False), - sa.ForeignKeyConstraint(['guest_registration_id'], ['guest_registration.id']), - sa.ForeignKeyConstraint(['registration_question_id'], ['registration_question.id']), - sa.PrimaryKeyConstraint('id') - ) + op.create_table( + "guest_registration", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("user_id", sa.Integer(), nullable=False), + sa.Column("registration_form_id", sa.Integer(), nullable=False), + sa.Column("confirmed", sa.Boolean(), nullable=False), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.Column("confirmation_email_sent_at", sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(["user_id"], ["app_user.id"]), + sa.ForeignKeyConstraint(["registration_form_id"], ["registration_form.id"]), + sa.PrimaryKeyConstraint("id"), + ) + + op.create_table( + "guest_registration_answer", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("guest_registration_id", sa.Integer(), nullable=False), + sa.Column("registration_question_id", sa.Integer(), nullable=False), + sa.Column("value", sa.String(), nullable=False), + sa.ForeignKeyConstraint(["guest_registration_id"], ["guest_registration.id"]), + sa.ForeignKeyConstraint( + ["registration_question_id"], ["registration_question.id"] + ), + sa.PrimaryKeyConstraint("id"), + ) def downgrade(): - op.drop_table('guest_registration_answer') - op.drop_table('guest_registration') - + op.drop_table("guest_registration_answer") + op.drop_table("guest_registration") diff --git a/api/migrations/versions/6b64b8037b7b_prc_review_update2.py b/api/migrations/versions/6b64b8037b7b_prc_review_update2.py index 4bf401509..609895254 100644 --- a/api/migrations/versions/6b64b8037b7b_prc_review_update2.py +++ b/api/migrations/versions/6b64b8037b7b_prc_review_update2.py @@ -9,26 +9,27 @@ """ # revision identifiers, used by Alembic. -revision = '6b64b8037b7b' -down_revision = '111c4f9eab84' +revision = "6b64b8037b7b" +down_revision = "111c4f9eab84" + +import datetime +from enum import Enum -from alembic import op import sqlalchemy as sa -from sqlalchemy import orm +from alembic import op +from sqlalchemy import orm, select from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy import orm -from sqlalchemy import select from sqlalchemy.orm import column_property + from app import db -import datetime -from enum import Enum Base = declarative_base() + class Organisation(Base): __tablename__ = "organisation" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(50), nullable=False) @@ -40,7 +41,17 @@ class Organisation(Base): email_from = db.Column(db.String(100), nullable=True) system_url = db.Column(db.String(100), nullable=False) - def __init__(self, name, system_name, small_logo, large_logo, domain, url, email_from, system_url): + def __init__( + self, + name, + system_name, + small_logo, + large_logo, + domain, + url, + email_from, + system_url, + ): self.name = name self.small_logo = small_logo self.large_logo = large_logo @@ -50,9 +61,10 @@ def __init__(self, name, system_name, small_logo, large_logo, domain, url, email self.email_from = email_from self.system_url = system_url + class Country(Base): __tablename__ = "country" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(100), nullable=False) @@ -62,21 +74,23 @@ def __init__(self, name): class EventType(Enum): - EVENT = 'event' - AWARD = 'award' - CALL = 'call' + EVENT = "event" + AWARD = "award" + CALL = "call" class Event(Base): __tablename__ = "event" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) start_date = db.Column(db.DateTime(), nullable=False) end_date = db.Column(db.DateTime(), nullable=False) key = db.Column(db.String(255), nullable=False, unique=True) - organisation_id = db.Column(db.Integer(), db.ForeignKey('organisation.id'), nullable=False) + organisation_id = db.Column( + db.Integer(), db.ForeignKey("organisation.id"), nullable=False + ) email_from = db.Column(db.String(255), nullable=False) url = db.Column(db.String(255), nullable=False) application_open = db.Column(db.DateTime(), nullable=False) @@ -93,7 +107,7 @@ class Event(Base): travel_grant = db.Column(db.Boolean(), nullable=False) miniconf_url = db.Column(db.String(100), nullable=True) - event_translations = db.relationship('EventTranslation', lazy='dynamic') + event_translations = db.relationship("EventTranslation", lazy="dynamic") def __init__( self, @@ -117,7 +131,7 @@ def __init__( registration_close, event_type, travel_grant, - miniconf_url=None + miniconf_url=None, ): self.start_date = start_date self.end_date = end_date @@ -149,10 +163,11 @@ def add_event_translations(self, names, descriptions): event_translation = EventTranslation(name, description, language) self.event_translations.append(event_translation) + class EventTranslation(Base): __tablename__ = "event_translation" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) event_id = db.Column(db.Integer(), db.ForeignKey("event.id"), nullable=False) @@ -160,19 +175,20 @@ class EventTranslation(Base): description = db.Column(db.String(255), nullable=False) language = db.Column(db.String(2)) - event = db.relationship('Event', foreign_keys=[event_id]) + event = db.relationship("Event", foreign_keys=[event_id]) def __init__(self, name, description, language): self.name = name self.description = description self.language = language + class ApplicationForm(Base): - __tablename__ = 'application_form' - __table_args__ = {'extend_existing': True} + __tablename__ = "application_form" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - event_id = db.Column(db.Integer(), db.ForeignKey('event.id'), nullable=False) + event_id = db.Column(db.Integer(), db.ForeignKey("event.id"), nullable=False) is_open = db.Column(db.Boolean(), nullable=False) nominations = db.Column(db.Boolean(), nullable=False) @@ -181,22 +197,29 @@ def __init__(self, event_id, is_open, nominations): self.is_open = is_open self.nominations = nominations + class Question(Base): - __tablename__ = 'question' - __table_args__ = {'extend_existing': True} + __tablename__ = "question" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) - section_id = db.Column(db.Integer(), db.ForeignKey('section.id'), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) + section_id = db.Column(db.Integer(), db.ForeignKey("section.id"), nullable=False) type = db.Column(db.String(), nullable=False) order = db.Column(db.Integer(), nullable=False) is_required = db.Column(db.Boolean(), nullable=False) - depends_on_question_id = db.Column(db.Integer(), db.ForeignKey('question.id'), nullable=True) + depends_on_question_id = db.Column( + db.Integer(), db.ForeignKey("question.id"), nullable=True + ) key = db.Column(db.String(255), nullable=True) - question_translations = db.relationship('QuestionTranslation', lazy='dynamic') + question_translations = db.relationship("QuestionTranslation", lazy="dynamic") - def __init__(self, application_form_id, section_id, order, questionType, is_required=True): + def __init__( + self, application_form_id, section_id, order, questionType, is_required=True + ): self.application_form_id = application_form_id self.section_id = section_id self.order = order @@ -204,15 +227,18 @@ def __init__(self, application_form_id, section_id, order, questionType, is_requ self.is_required = is_required def get_translation(self, language): - question_translation = self.question_translations.filter_by(language=language).first() + question_translation = self.question_translations.filter_by( + language=language + ).first() return question_translation + class QuestionTranslation(Base): - __tablename__ = 'question_translation' - __table_args__ = {'extend_existing': True} + __tablename__ = "question_translation" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - question_id = db.Column(db.Integer(), db.ForeignKey('question.id'), nullable=False) + question_id = db.Column(db.Integer(), db.ForeignKey("question.id"), nullable=False) language = db.Column(db.String(2), nullable=False) headline = db.Column(db.String(), nullable=False) description = db.Column(db.String(), nullable=True) @@ -222,7 +248,7 @@ class QuestionTranslation(Base): options = db.Column(db.JSON(), nullable=True) show_for_values = db.Column(db.JSON(), nullable=True) - question = db.relationship('Question', foreign_keys=[question_id]) + question = db.relationship("Question", foreign_keys=[question_id]) def __init__( self, @@ -234,7 +260,7 @@ def __init__( validation_regex=None, validation_text=None, options=None, - show_for_values=None + show_for_values=None, ): self.question_id = question_id self.language = language @@ -246,17 +272,22 @@ def __init__( self.options = options self.show_for_values = show_for_values + class ReviewForm(Base): - __tablename__ = 'review_form' - __table_args__ = {'extend_existing': True} + __tablename__ = "review_form" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) is_open = db.Column(db.Boolean(), nullable=False) deadline = db.Column(db.DateTime(), nullable=False) - application_form = db.relationship('ApplicationForm', foreign_keys=[application_form_id]) - review_questions = db.relationship('ReviewQuestion') + application_form = db.relationship( + "ApplicationForm", foreign_keys=[application_form_id] + ) + review_questions = db.relationship("ReviewQuestion") def __init__(self, application_form_id, deadline): self.application_form_id = application_form_id @@ -268,30 +299,26 @@ def close(self): class ReviewQuestion(Base): - __tablename__ = 'review_question' - __table_args__ = {'extend_existing': True} + __tablename__ = "review_question" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer, primary_key=True) - review_form_id = db.Column(db.Integer(), db.ForeignKey('review_form.id'), nullable=False) - question_id = db.Column(db.Integer(), db.ForeignKey('question.id'), nullable=True) - + review_form_id = db.Column( + db.Integer(), db.ForeignKey("review_form.id"), nullable=False + ) + question_id = db.Column(db.Integer(), db.ForeignKey("question.id"), nullable=True) + type = db.Column(db.String(), nullable=False) - + is_required = db.Column(db.Boolean(), nullable=False) order = db.Column(db.Integer(), nullable=False) weight = db.Column(db.Float(), nullable=False) - review_form = db.relationship('ReviewForm', foreign_keys=[review_form_id]) - question = db.relationship('Question', foreign_keys=[question_id]) - - translations = db.relationship('ReviewQuestionTranslation', lazy='dynamic') - - def __init__(self, - review_form_id, - question_id, - type, - is_required, - order, - weight): + review_form = db.relationship("ReviewForm", foreign_keys=[review_form_id]) + question = db.relationship("Question", foreign_keys=[question_id]) + + translations = db.relationship("ReviewQuestionTranslation", lazy="dynamic") + + def __init__(self, review_form_id, question_id, type, is_required, order, weight): self.review_form_id = review_form_id self.question_id = question_id self.type = type @@ -305,12 +332,20 @@ def get_translation(self, language): class ReviewQuestionTranslation(Base): - __tablename__ = 'review_question_translation' - __table_args__ = {'extend_existing': True} - __table_args__ = tuple([db.UniqueConstraint('review_question_id', 'language', name='uq_review_question_id_language')]) + __tablename__ = "review_question_translation" + __table_args__ = {"extend_existing": True} + __table_args__ = tuple( + [ + db.UniqueConstraint( + "review_question_id", "language", name="uq_review_question_id_language" + ) + ] + ) id = db.Column(db.Integer(), primary_key=True) - review_question_id = db.Column(db.Integer(), db.ForeignKey('review_question.id'), nullable=False) + review_question_id = db.Column( + db.Integer(), db.ForeignKey("review_question.id"), nullable=False + ) language = db.Column(db.String(2), nullable=False) description = db.Column(db.String(), nullable=True) @@ -320,15 +355,17 @@ class ReviewQuestionTranslation(Base): validation_regex = db.Column(db.String(), nullable=True) validation_text = db.Column(db.String(), nullable=True) - def __init__(self, - review_question_id, - language, - description=None, - headline=None, - placeholder=None, - options=None, - validation_regex=None, - validation_text=None): + def __init__( + self, + review_question_id, + language, + description=None, + headline=None, + placeholder=None, + options=None, + validation_regex=None, + validation_text=None, + ): self.review_question_id = review_question_id self.language = language self.description = description @@ -338,50 +375,59 @@ def __init__(self, self.validation_regex = validation_regex self.validation_text = validation_text + class ReviewConfiguration(Base): - __tablename__ = 'review_configuration' - __table_args__ = {'extend_existing': True} + __tablename__ = "review_configuration" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - review_form_id = db.Column(db.Integer(), db.ForeignKey('review_form.id'), nullable=False) + review_form_id = db.Column( + db.Integer(), db.ForeignKey("review_form.id"), nullable=False + ) num_reviews_required = db.Column(db.Integer(), nullable=False) num_optional_reviews = db.Column(db.Integer(), nullable=False) - drop_optional_question_id = db.Column(db.Integer(), db.ForeignKey('review_question.id'), nullable=True) + drop_optional_question_id = db.Column( + db.Integer(), db.ForeignKey("review_question.id"), nullable=True + ) drop_optional_agreement_values = db.Column(db.String(), nullable=True) + class ReviewScore(Base): - __tablename__ = 'review_score' - __table_args__ = {'extend_existing': True} + __tablename__ = "review_score" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - review_response_id = db.Column(db.Integer(), db.ForeignKey('review_response.id'), nullable=False) - review_question_id = db.Column(db.Integer(), db.ForeignKey('review_question.id'), nullable=False) + review_response_id = db.Column( + db.Integer(), db.ForeignKey("review_response.id"), nullable=False + ) + review_question_id = db.Column( + db.Integer(), db.ForeignKey("review_question.id"), nullable=False + ) value = db.Column(db.String(), nullable=False) - def __init__(self, - review_question_id, - value): + def __init__(self, review_question_id, value): self.review_question_id = review_question_id self.value = value + class ReviewResponse(Base): - __tablename__ = 'review_response' - __table_args__ = {'extend_existing': True} + __tablename__ = "review_response" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - review_form_id = db.Column(db.Integer(), db.ForeignKey('review_form.id'), nullable=False) - reviewer_user_id = db.Column(db.Integer(), db.ForeignKey('app_user.id'), nullable=False) - response_id = db.Column(db.Integer(), db.ForeignKey('response.id'), nullable=False) + review_form_id = db.Column( + db.Integer(), db.ForeignKey("review_form.id"), nullable=False + ) + reviewer_user_id = db.Column( + db.Integer(), db.ForeignKey("app_user.id"), nullable=False + ) + response_id = db.Column(db.Integer(), db.ForeignKey("response.id"), nullable=False) submitted_timestamp = db.Column(db.DateTime(), nullable=False) language = db.Column(db.String(2), nullable=False) is_submitted = db.Column(db.Boolean(), nullable=False) submitted_timestamp = db.Column(db.DateTime(), nullable=True) - def __init__(self, - review_form_id, - reviewer_user_id, - response_id, - language): + def __init__(self, review_form_id, reviewer_user_id, response_id, language): self.review_form_id = review_form_id self.reviewer_user_id = reviewer_user_id self.response_id = response_id @@ -392,33 +438,49 @@ def submit(self): self.is_submitted = True self.submitted_timestamp = datetime.now() + current_order = 1 -def add_question(review_form_id, type, is_required, weight=0, question_id=None, - descriptions=None, headlines=None, placeholders=None, options=None, - validation_regexs=None, validation_texts=None): + +def add_question( + review_form_id, + type, + is_required, + weight=0, + question_id=None, + descriptions=None, + headlines=None, + placeholders=None, + options=None, + validation_regexs=None, + validation_texts=None, +): global current_order - question = ReviewQuestion(review_form_id, question_id, type, is_required, current_order, weight) + question = ReviewQuestion( + review_form_id, question_id, type, is_required, current_order, weight + ) db.session.add(question) db.session.commit() en = ReviewQuestionTranslation( - question.id, - 'en', - None if descriptions is None else descriptions['en'], - None if headlines is None else headlines['en'], - None if placeholders is None else placeholders['en'], - None if options is None else options['en'], - None if validation_regexs is None else validation_regexs['en'], - None if validation_texts is None else validation_texts['en']) + question.id, + "en", + None if descriptions is None else descriptions["en"], + None if headlines is None else headlines["en"], + None if placeholders is None else placeholders["en"], + None if options is None else options["en"], + None if validation_regexs is None else validation_regexs["en"], + None if validation_texts is None else validation_texts["en"], + ) fr = ReviewQuestionTranslation( - question.id, - 'fr', - None if descriptions is None else descriptions['fr'], - None if headlines is None else headlines['fr'], - None if placeholders is None else placeholders['fr'], - None if options is None else options['fr'], - None if validation_regexs is None else validation_regexs['fr'], - None if validation_texts is None else validation_texts['fr']) + question.id, + "fr", + None if descriptions is None else descriptions["fr"], + None if headlines is None else headlines["fr"], + None if placeholders is None else placeholders["fr"], + None if options is None else options["fr"], + None if validation_regexs is None else validation_regexs["fr"], + None if validation_texts is None else validation_texts["fr"], + ) db.session.add_all([en, fr]) db.session.commit() @@ -426,65 +488,127 @@ def add_question(review_form_id, type, is_required, weight=0, question_id=None, return question, en, fr + def get_question(application_form_id, en_headline): - en = (db.session.query(QuestionTranslation) - .filter_by(language='en', headline=en_headline) - .join(Question, QuestionTranslation.question_id == Question.id) - .filter_by(application_form_id=application_form_id) - .first()) - fr = db.session.query(QuestionTranslation).filter_by(language='fr', question_id=en.question_id).first() + en = ( + db.session.query(QuestionTranslation) + .filter_by(language="en", headline=en_headline) + .join(Question, QuestionTranslation.question_id == Question.id) + .filter_by(application_form_id=application_form_id) + .first() + ) + fr = ( + db.session.query(QuestionTranslation) + .filter_by(language="fr", question_id=en.question_id) + .first() + ) return en.question, en, fr + def add_evaluation_question(form_id, headlines, descriptions, min_score, max_score): - validation_regex = '^({})$'.format('|'.join([str(i) for i in range(max_score, min_score-1, -1)])) + validation_regex = "^({})$".format( + "|".join([str(i) for i in range(max_score, min_score - 1, -1)]) + ) validation_text = { - 'en': 'Enter a number between {} and {}'.format(min_score, max_score), - 'fr': 'Entrez un nombre entre {} et {}'.format(min_score, max_score) + "en": "Enter a number between {} and {}".format(min_score, max_score), + "fr": "Entrez un nombre entre {} et {}".format(min_score, max_score), } placeholders = { - 'en': 'Enter a score between {} and {}'.format(min_score, max_score), - 'fr': 'Saisissez un score compris entre {} et {}'.format(min_score, max_score) + "en": "Enter a score between {} and {}".format(min_score, max_score), + "fr": "Saisissez un score compris entre {} et {}".format(min_score, max_score), } - return add_question(form_id, 'short-text', True, 1, headlines=headlines, - descriptions=descriptions, validation_regexs={'en': validation_regex, 'fr': validation_regex}, - validation_texts=validation_text, placeholders=placeholders) + return add_question( + form_id, + "short-text", + True, + 1, + headlines=headlines, + descriptions=descriptions, + validation_regexs={"en": validation_regex, "fr": validation_regex}, + validation_texts=validation_text, + placeholders=placeholders, + ) -def add_comment_question(form_id, headlines, descriptions, is_required=False): - return add_question(form_id, 'long-text', is_required, weight=1, descriptions=descriptions, headlines=headlines) -def add_information_question(form_id, application_form_id, en_headline, type='information', heading_override=False, description_override=None): +def add_comment_question(form_id, headlines, descriptions, is_required=False): + return add_question( + form_id, + "long-text", + is_required, + weight=1, + descriptions=descriptions, + headlines=headlines, + ) + + +def add_information_question( + form_id, + application_form_id, + en_headline, + type="information", + heading_override=False, + description_override=None, +): question, en, fr = get_question(application_form_id, en_headline) - + headlines = { - 'en': None if heading_override else en.headline, - 'fr': None if heading_override else fr.headline + "en": None if heading_override else en.headline, + "fr": None if heading_override else fr.headline, } descriptions = { - 'en': description_override['en'] if description_override is not None else en.description, - 'fr': description_override['fr'] if description_override is not None else fr.description + "en": description_override["en"] + if description_override is not None + else en.description, + "fr": description_override["fr"] + if description_override is not None + else fr.description, } - if question.type == 'information': + if question.type == "information": return add_heading(form_id, headlines) - return add_question(form_id, type, False, question_id=question.id, headlines=headlines, descriptions=descriptions) + return add_question( + form_id, + type, + False, + question_id=question.id, + headlines=headlines, + descriptions=descriptions, + ) + def add_heading(form_id, headlines): - return add_question(form_id, 'heading', False, headlines=headlines) + return add_question(form_id, "heading", False, headlines=headlines) + def add_divider(form_id, headlines, descriptions): - return add_question(form_id, 'section-divider', False, headlines=headlines, descriptions=descriptions) + return add_question( + form_id, + "section-divider", + False, + headlines=headlines, + descriptions=descriptions, + ) + def upgrade(): - print('Starting 6b64b8037b7b') - event = db.session.query(Event).filter_by(key='prc').first() - application_form = db.session.query(ApplicationForm).filter_by(event_id=event.id).first() - form = db.session.query(ReviewForm).filter_by(application_form_id=application_form.id).first() + print("Starting 6b64b8037b7b") + event = db.session.query(Event).filter_by(key="prc").first() + application_form = ( + db.session.query(ApplicationForm).filter_by(event_id=event.id).first() + ) + form = ( + db.session.query(ReviewForm) + .filter_by(application_form_id=application_form.id) + .first() + ) questions = db.session.query(ReviewQuestion).filter_by(review_form_id=form.id).all() for q in questions: - db.session.query(ReviewQuestionTranslation).filter_by(review_question_id=q.id).delete() + db.session.query(ReviewQuestionTranslation).filter_by( + review_question_id=q.id + ).delete() db.session.query(ReviewScore).filter_by(review_question_id=q.id).delete() db.session.commit() @@ -493,16 +617,20 @@ def upgrade(): # db.session.query(ReviewResponse).filter_by(review_form_id=form.id).delete() - print('Removed existing form, repopulating...') + print("Removed existing form, repopulating...") # Set up questions - - divider1 = add_question(form.id, 'section-divider', False, headlines={ - 'en': 'Evaluation of proposals', - 'fr': 'Évaluation des propositions' - }, descriptions={ - 'en': """Below are a series of weighted, multiple choice questions intended to evaluate the applicant’s proposal. The evaluation is divided up into the sections from the application: + divider1 = add_question( + form.id, + "section-divider", + False, + headlines={ + "en": "Evaluation of proposals", + "fr": "Évaluation des propositions", + }, + descriptions={ + "en": """Below are a series of weighted, multiple choice questions intended to evaluate the applicant’s proposal. The evaluation is divided up into the sections from the application: I. Organization capacity and experience; II. Policy research; III. Policy engagement experience; and @@ -510,21 +638,27 @@ def upgrade(): V. Overall Assessment. At the end of each question and/or section, please reflect in your own words what you think of the answers in that section. Each evaluation question corresponds with the section and numbered question in the proposal. """, - 'fr': """Vous trouverez ci-dessous une série de questions à choix multiples pondérées destinées à évaluer la proposition du candidat. L’évaluation est divisée en plusieurs sections à partir de la candidature: + "fr": """Vous trouverez ci-dessous une série de questions à choix multiples pondérées destinées à évaluer la proposition du candidat. L’évaluation est divisée en plusieurs sections à partir de la candidature: I. Capacité et expérience de l’organisation; II. Recherche sur les politiques; III. Expérience en matière d’engagement politique; et IV. Budget, suivi par la section V. Évaluation globale. À la fin de chaque question et/ou section, veuillez exprimer dans vos propres mots ce que vous pensez des réponses de cette section. Chaque question d'évaluation correspond à la section et à la question numérotée dans la proposition. -""" - }) - - divider2 = add_question(form.id, 'section-divider', False, headlines={ - 'en': 'Section I: Organization capacity and experience', - 'fr': 'Section I: Capacité et expérience de l’organisation' - }, descriptions={ - 'en': """Please review and evaluate the applicant’s answers on organizational capacity, experience and mission. +""", + }, + ) + + divider2 = add_question( + form.id, + "section-divider", + False, + headlines={ + "en": "Section I: Organization capacity and experience", + "fr": "Section I: Capacité et expérience de l’organisation", + }, + descriptions={ + "en": """Please review and evaluate the applicant’s answers on organizational capacity, experience and mission. \[Total possible points = 55\] Evaluation criteria: @@ -533,7 +667,7 @@ def upgrade(): - Fit between proposed research agenda and the organization’s mission. - Demonstrated research quality (output, citations, research metrics) that addresses numerous facets of advanced technology policy. Please see the [Research Quality Plus](https://www.idrc.ca/en/research-in-action/research-quality-plus) assessment instrument for guidance. """, - 'fr': """Veuillez examiner et évaluer les réponses du candidat sur la capacité organisationnelle, l’expérience et la mission. + "fr": """Veuillez examiner et évaluer les réponses du candidat sur la capacité organisationnelle, l’expérience et la mission. \[Total des points possibles = 55\] Critères d’évaluation: @@ -541,16 +675,35 @@ def upgrade(): - Expérience/années de travail sur la politique d’IA, ou sur des questions connexes (telles que les TIC, les données numériques/grandes/ouvertes, les communications, les questions de cyberpolitique - en particulier en ce qui concerne le développement humain et les droits de l’homme - mais aussi les infrastructures, les capacités/compétences), avec des réalisations significatives. - Adéquation entre le programme de recherche proposé et la mission de l’organisation. - Qualité démontrée de la recherche (résultats, citations, paramètres de recherche) qui aborde de nombreuses facettes de la politique en matière de technologies avancées. Veuillez consulter l’instrument d’évaluation [Qualité de la recherche plus](https://idl-bnc-idrc.dspacedirect.org/bitstream/handle/10625/56600/IDL-56600.pdf?sequence=2&isAllowed=y) pour obtenir des conseils. -""" - }) - - add_information_question(form.id, form.application_form_id, '1. Organizational capacity and experience') - add_information_question(form.id, form.application_form_id, 'How many people in total work in your organization?') - add_information_question(form.id, form.application_form_id, 'Of these, how many are full time policy researchers?') - add_information_question(form.id, form.application_form_id, "Please provide the names and CVs of your organization's principal researchers (up to five)", 'multi-file') - - review_q1 = add_evaluation_question(form.id, headlines=None, descriptions={ - 'en': """Please review the makeup of the organization in terms of staffing and answer the following question. +""", + }, + ) + + add_information_question( + form.id, form.application_form_id, "1. Organizational capacity and experience" + ) + add_information_question( + form.id, + form.application_form_id, + "How many people in total work in your organization?", + ) + add_information_question( + form.id, + form.application_form_id, + "Of these, how many are full time policy researchers?", + ) + add_information_question( + form.id, + form.application_form_id, + "Please provide the names and CVs of your organization's principal researchers (up to five)", + "multi-file", + ) + + review_q1 = add_evaluation_question( + form.id, + headlines=None, + descriptions={ + "en": """Please review the makeup of the organization in terms of staffing and answer the following question. Assessment of research team: After reviewing the CVs of the principal researchers, assess the strength of the research team with respect to achieving the goals and strategy of the proposed research agenda. \[Total possible points = 10\] @@ -560,25 +713,34 @@ def upgrade(): 4 - 7 : Research team is adequate but needs some broad capacity development 8 - 10 : Research team is strong (although they might still benefit from limited capacity development) """, - 'fr': """Évaluation de l’équipe de recherche: Après avoir examiné les CV des principaux chercheurs, évaluez la force de l’équipe de recherche en ce qui concerne la réalisation des objectifs et de la stratégie du programme de recherche proposé. + "fr": """Évaluation de l’équipe de recherche: Après avoir examiné les CV des principaux chercheurs, évaluez la force de l’équipe de recherche en ce qui concerne la réalisation des objectifs et de la stratégie du programme de recherche proposé. \[Total des points possibles = 10\] **Scale:** 0 - 3 : L’équipe de recherche est insuffisante et aurait besoin d’être agrandie et développée 4 - 7 : L’équipe de recherche est adéquate mais a besoin d’un large développement des capacités 8 - 10 : L’équipe de recherche est solide (bien qu’elle puisse encore bénéficier d’un développement limité des capacités) -""" - }, min_score=0, max_score=10) - - add_comment_question(form.id, descriptions={ - 'en': "Comments", - 'fr': "Des commentaires?" - }, headlines=None) - - add_information_question(form.id,form.application_form_id, '2. Mission for Responsible AI') - - add_evaluation_question(form.id, headlines=None, descriptions={ - 'en': """To what extent does this mission statement of the organization relate to advancing the principles of responsible AI? +""", + }, + min_score=0, + max_score=10, + ) + + add_comment_question( + form.id, + descriptions={"en": "Comments", "fr": "Des commentaires?"}, + headlines=None, + ) + + add_information_question( + form.id, form.application_form_id, "2. Mission for Responsible AI" + ) + + add_evaluation_question( + form.id, + headlines=None, + descriptions={ + "en": """To what extent does this mission statement of the organization relate to advancing the principles of responsible AI? \[Total possible points = 15\] **Scale:** @@ -586,7 +748,7 @@ def upgrade(): 6 - 10 : Somewhat demonstrates mission and the significance 11 - 15 : Superior demonstration of mission and the significance """, - 'fr': """Dans quelle mesure cette déclaration de mission de l’organisation est-elle liée à la promotion des + "fr": """Dans quelle mesure cette déclaration de mission de l’organisation est-elle liée à la promotion des principes de l’IA responsable? \[Total des points possibles = 15\] @@ -594,19 +756,28 @@ def upgrade(): 0 - 5 : Démonstration limitée de la mission et de sa signification 6 - 10 : Démontre quelque peu la mission et la signification 11 - 15 : Démonstration supérieure de la mission et de sa signification -""" - }, min_score=0, max_score=15) - - add_comment_question(form.id, descriptions={ - 'en': "Comments", - 'fr': "Des commentaires?" - }, headlines=None) - - add_information_question(form.id, form.application_form_id, '3. Demonstrated expertise') - add_information_question(form.id, form.application_form_id, 'Examples') - - add_evaluation_question(form.id, headlines=None, descriptions={ - 'en': """To what extent does the applicant demonstrate knowledge and expertise with at least three of the key research areas? +""", + }, + min_score=0, + max_score=15, + ) + + add_comment_question( + form.id, + descriptions={"en": "Comments", "fr": "Des commentaires?"}, + headlines=None, + ) + + add_information_question( + form.id, form.application_form_id, "3. Demonstrated expertise" + ) + add_information_question(form.id, form.application_form_id, "Examples") + + add_evaluation_question( + form.id, + headlines=None, + descriptions={ + "en": """To what extent does the applicant demonstrate knowledge and expertise with at least three of the key research areas? \[Total possible points = 15\] **Scale:** @@ -614,18 +785,24 @@ def upgrade(): 6 - 10 : Some demonstrated knowledge and expertise in at least two areas 11 - 15 : Superior/significant demonstrated knowledge and expertise in three areas """, - 'fr': """Dans quelle mesure le candidat démontre-t-il des connaissances et une expertise dans au moins trois des principaux domaines de recherche? + "fr": """Dans quelle mesure le candidat démontre-t-il des connaissances et une expertise dans au moins trois des principaux domaines de recherche? \[Total des points possibles = 15\] **Scale:** 0 - 5 : Connaissances et compétences démontrées limitées 6 - 10 : Certains aspects font preuve de connaissances et d’expertise dans au moins deux domaines 11 - 15 : Connaissances et expertise supérieures/significatives démontrées dans trois domaines -""" - }, min_score=0, max_score=15) - - add_evaluation_question(form.id, headlines=None, descriptions={ - 'en': """Please review the examples provided by the applicant from the organization’s research portfolio. Based on what they’ve written and provided, to what extent does the organization demonstrate an ability to produce high quality policy research on these issues? +""", + }, + min_score=0, + max_score=15, + ) + + add_evaluation_question( + form.id, + headlines=None, + descriptions={ + "en": """Please review the examples provided by the applicant from the organization’s research portfolio. Based on what they’ve written and provided, to what extent does the organization demonstrate an ability to produce high quality policy research on these issues? \[Total possible points = 15\] *High quality research is relevant, shows integrity, legitimacy, importance (originality) and is well positioned for use. Please refer to the [Research Quality Plus](https://www.idrc.ca/en/research-in-action/research-quality-plus) assessment instrument if you have questions.* @@ -634,7 +811,7 @@ def upgrade(): 6 - 10 : Demonstrates variable research quality with some high-quality research 11 - 15 : Significant experience producing high quality research """, - 'fr': """Veuillez examiner les exemples fournis par le candidat à partir du portefeuille de recherche de l’organisme. Sur la base de ce qu’ils ont écrit et fourni, dans quelle mesure l’organisme démontre-t-il sa capacité à produire des recherches politiques de haute qualité sur ces questions? + "fr": """Veuillez examiner les exemples fournis par le candidat à partir du portefeuille de recherche de l’organisme. Sur la base de ce qu’ils ont écrit et fourni, dans quelle mesure l’organisme démontre-t-il sa capacité à produire des recherches politiques de haute qualité sur ces questions? \[Total des points possibles = 15\] Une recherche de haute qualité est pertinente, fait preuve d’intégrité, de légitimité, d’importance (originalité) et est bien placée pour être utilisée. Veuillez-vous référer à l’instrument d’évaluation [Qualité de la recherche plus](https://idl-bnc-idrc.dspacedirect.org/bitstream/handle/10625/56600/IDL-56600.pdf?sequence=2&isAllowed=y) si vous avez des questions. @@ -643,21 +820,28 @@ def upgrade(): 0 - 5 : Expérience limitée dans la production de recherches de haute qualité 6 - 10 : Démontre une qualité de recherche variable avec quelques recherches de haute qualité 11 - 15 : Une expérience significative dans la production de recherches de haute qualité -""" - }, min_score=0, max_score=15) - - add_comment_question(form.id, descriptions={ - 'en': "Comments", - 'fr': "Des commentaires?" - }, headlines=None) - - print('Finished configuring Section I') - - add_divider(form.id, { - 'en': 'Section II: Policy Research', - 'fr': 'Section II: Recherche sur les politiques' - }, { - 'en': """Please review and evaluate the applicant’s answers on their research, objectives and methodologies. +""", + }, + min_score=0, + max_score=15, + ) + + add_comment_question( + form.id, + descriptions={"en": "Comments", "fr": "Des commentaires?"}, + headlines=None, + ) + + print("Finished configuring Section I") + + add_divider( + form.id, + { + "en": "Section II: Policy Research", + "fr": "Section II: Recherche sur les politiques", + }, + { + "en": """Please review and evaluate the applicant’s answers on their research, objectives and methodologies. \[Total possible points = 135\] Evaluation criteria, proposals must demonstrate: @@ -672,7 +856,7 @@ def upgrade(): - Clear articulation of results and the metrics to gauge them. - Clear and appropriate project management plan that is tied to budget. """, - 'fr': """Veuillez examiner et évaluer les réponses des candidats sur leurs recherches, leurs objectifs et leurs méthodologies. + "fr": """Veuillez examiner et évaluer les réponses des candidats sur leurs recherches, leurs objectifs et leurs méthodologies. \[Total des points possibles = 135\] Critères d’évaluation: Les propositions doivent démontrer : @@ -686,12 +870,20 @@ def upgrade(): - Une stratégie de communication claire pour étendre l’impact de la recherche et des résultats. - Une articulation claire des résultats et des mesures permettant de les évaluer. - Un plan de gestion de projet clair et approprié qui est lié au budget. -""" - }) - - add_information_question(form.id, form.application_form_id, '1. Research background, problem statement and justification') - add_evaluation_question(form.id, headlines=None, descriptions={ - 'en': """To what extent does the applicant clearly articulate why these priorities are significant for the AI4D research agenda of the organization and the contexts in which they work? +""", + }, + ) + + add_information_question( + form.id, + form.application_form_id, + "1. Research background, problem statement and justification", + ) + add_evaluation_question( + form.id, + headlines=None, + descriptions={ + "en": """To what extent does the applicant clearly articulate why these priorities are significant for the AI4D research agenda of the organization and the contexts in which they work? \[Total possible points = 10\] **Scale:** @@ -699,18 +891,24 @@ def upgrade(): 4 - 7 : Substantive clarity of research priorities 8 - 10 : Superior/significant clarity of research priorities """, - 'fr': """Dans quelle mesure le candidat explique-t-il clairement pourquoi ces priorités sont importantes pour l’agenda de recherche IAPD de l’organisation et les contextes dans lesquels elles s’inscrivent? + "fr": """Dans quelle mesure le candidat explique-t-il clairement pourquoi ces priorités sont importantes pour l’agenda de recherche IAPD de l’organisation et les contextes dans lesquels elles s’inscrivent? \[Total des points possibles = 10\] **Scale:** 0 - 3 : Clarté des priorités de recherche nulle ou limitée 4 - 7 : Clarté de fond des priorités de recherche 8 - 10 : Clarté supérieure des priorités de recherche -""" - }, min_score=0, max_score=10) - - add_evaluation_question(form.id, headlines=None, descriptions={ - 'en': """To what extent does the applicant demonstrate the ability to draw upon evidence to support the background, context and arguments for why these are the priorities for their research? +""", + }, + min_score=0, + max_score=10, + ) + + add_evaluation_question( + form.id, + headlines=None, + descriptions={ + "en": """To what extent does the applicant demonstrate the ability to draw upon evidence to support the background, context and arguments for why these are the priorities for their research? \[Total possible points = 10\] **Scale:** @@ -718,18 +916,24 @@ def upgrade(): 4 - 7 : Substantive demonstrated ability 8 - 10 : Superior/significant demonstrated ability """, - 'fr': """Dans quelle mesure le candidat démontre-t-il sa capacité à s’appuyer sur des preuves pour étayer le contexte et les arguments qui expliquent pourquoi il s’agit là des priorités de sa recherche? + "fr": """Dans quelle mesure le candidat démontre-t-il sa capacité à s’appuyer sur des preuves pour étayer le contexte et les arguments qui expliquent pourquoi il s’agit là des priorités de sa recherche? \[Total des points possibles = 10\] **Scale:** 0 - 3 : Pas ou peu de capacité démontrée 4 - 7 : Capacité démontrée 8 - 10 : Capacité démontrée supérieure -""" - }, min_score=0, max_score=10) - - add_evaluation_question(form.id, headlines=None, descriptions={ - 'en': """To what extent does the applicant demonstrate the ability to write on complex issues succinctly and intelligently? +""", + }, + min_score=0, + max_score=10, + ) + + add_evaluation_question( + form.id, + headlines=None, + descriptions={ + "en": """To what extent does the applicant demonstrate the ability to write on complex issues succinctly and intelligently? \[Total possible points = 5\] **Scale:** @@ -737,25 +941,34 @@ def upgrade(): 2 - 3 : Some ability to write on complex issues (some good ideas, structure may lack coherence) 4 - 5 : Superior/significant ability to write on complex issues (exceptionally clear and coherent) """, - 'fr': """Dans quelle mesure le candidat démontre-t-il sa capacité à écrire sur des questions complexes de manière succincte et intelligente? + "fr": """Dans quelle mesure le candidat démontre-t-il sa capacité à écrire sur des questions complexes de manière succincte et intelligente? \[Total des points possibles = 5\] Scale: 0 - 1 : Capacité limitée à écrire sur des sujets complexes (confus, manque de logique et de cohérence) 2 - 3 : Une certaine capacité à écrire sur des questions complexes (quelques bonnes idées, la structure peut manquer de cohérence) 4 - 5 : Capacité supérieure à rédiger sur des questions complexes (exceptionnellement clair et cohérent) -""" - }, min_score=0, max_score=5) - - add_comment_question(form.id, descriptions={ - 'en': "Comments", - 'fr': "Des commentaires?" - }, headlines=None) - - add_information_question(form.id, form.application_form_id, '2. General and specific objectives') - - add_evaluation_question(form.id, headlines=None, descriptions={ - 'en': """To what extent are the general and specific objectives appropriate and logically connected to problem statement and justification? Are they feasible? +""", + }, + min_score=0, + max_score=5, + ) + + add_comment_question( + form.id, + descriptions={"en": "Comments", "fr": "Des commentaires?"}, + headlines=None, + ) + + add_information_question( + form.id, form.application_form_id, "2. General and specific objectives" + ) + + add_evaluation_question( + form.id, + headlines=None, + descriptions={ + "en": """To what extent are the general and specific objectives appropriate and logically connected to problem statement and justification? Are they feasible? \[Total possible points = 10\] **Scale:** @@ -763,25 +976,34 @@ def upgrade(): 4 - 7 : Objectives need improvement, but are relatively appropriate and connected to problem statement 8 - 10 : Objectives are coherent, flow logically, and are appropriate and feasible """, - 'fr': """Dans quelle mesure les objectifs généraux et spécifiques sont-ils appropriés et logiquement liés à l’énoncé et à la justification du problème? Sont-ils réalisables? + "fr": """Dans quelle mesure les objectifs généraux et spécifiques sont-ils appropriés et logiquement liés à l’énoncé et à la justification du problème? Sont-ils réalisables? \[Total des points possibles = 10\] **Scale:** 0 - 3 : Mal articulé et sans lien avec l’énoncé du problème, objectifs inappropriés et/ou irréalisables 4 - 7 : Les objectifs doivent être améliorés, mais ils sont relativement appropriés et liés à l’énoncé du problème 8 - 10 : Les objectifs sont cohérents et logiques, et sont appropriés et réalisables -""" - }, min_score=0, max_score=10) - - add_comment_question(form.id, descriptions={ - 'en': "Comments", - 'fr': "Des commentaires?" - }, headlines=None) - - add_information_question(form.id, form.application_form_id, '3. Methodology and approach') - add_information_question(form.id, form.application_form_id, 'Approach') - add_evaluation_question(form.id, headlines=None, descriptions={ - 'en': """To what extent does the organization demonstrate a coherent approach to achieving their research objectives? +""", + }, + min_score=0, + max_score=10, + ) + + add_comment_question( + form.id, + descriptions={"en": "Comments", "fr": "Des commentaires?"}, + headlines=None, + ) + + add_information_question( + form.id, form.application_form_id, "3. Methodology and approach" + ) + add_information_question(form.id, form.application_form_id, "Approach") + add_evaluation_question( + form.id, + headlines=None, + descriptions={ + "en": """To what extent does the organization demonstrate a coherent approach to achieving their research objectives? \[Total possible points = 10\] **Scale:** @@ -789,25 +1011,32 @@ def upgrade(): 4 - 7 : Some demonstrated coherence 8 - 10 : Significant demonstrated coherence """, - 'fr': """Dans quelle mesure l’organisme fait-il preuve d’une approche cohérente pour atteindre ses objectifs de recherche? + "fr": """Dans quelle mesure l’organisme fait-il preuve d’une approche cohérente pour atteindre ses objectifs de recherche? \[Total des points possibles = 10\] **Scale:** 0 - 3 : Peu de cohérence démontrée 4 - 7 : Certains aspects font preuve de cohérence 8 - 10 : Démontre une cohérence significative -""" - }, min_score=0, max_score=10) - - add_comment_question(form.id, descriptions={ - 'en': "Comments", - 'fr': "Des commentaires?" - }, headlines=None) - - add_information_question(form.id, form.application_form_id, 'Policy research') - - add_evaluation_question(form.id, headlines=None, descriptions={ - 'en': """To what extent is the conceptual and theoretical framework appropriate to the research question at hand? How well is the framework linked to the proposed research design? +""", + }, + min_score=0, + max_score=10, + ) + + add_comment_question( + form.id, + descriptions={"en": "Comments", "fr": "Des commentaires?"}, + headlines=None, + ) + + add_information_question(form.id, form.application_form_id, "Policy research") + + add_evaluation_question( + form.id, + headlines=None, + descriptions={ + "en": """To what extent is the conceptual and theoretical framework appropriate to the research question at hand? How well is the framework linked to the proposed research design? \[Total possible points = 10\] **Scale:** @@ -815,23 +1044,30 @@ def upgrade(): 4 - 7 : Some demonstrated strategy in linking framework to research design 8 - 10 : Significant demonstrated strategy in linking framework to research design """, - 'fr': """Dans quelle mesure le cadre conceptuel et théorique est-il adapté à la question de recherche en question? Dans quelle mesure le cadre est-il lié à la conception de la recherche proposée? + "fr": """Dans quelle mesure le cadre conceptuel et théorique est-il adapté à la question de recherche en question? Dans quelle mesure le cadre est-il lié à la conception de la recherche proposée? \[Total des points possibles = 10\] **Scale:** 0 - 3 : Stratégie peu démontrée pour relier le cadre à la conception de la recherche 4 - 7 : Quelques stratégies démontrées pour relier le cadre à la conception de la recherche 8 - 10 : Stratégie importante démontrée pour relier le cadre à la conception de la recherche -""" - }, min_score=0, max_score=10) - - add_comment_question(form.id, descriptions={ - 'en': "Comments", - 'fr': "Des commentaires?" - }, headlines=None) - - add_evaluation_question(form.id, headlines=None, descriptions={ - 'en': """To what extent does the organization demonstrate a rigorous approach to answering their research questions? +""", + }, + min_score=0, + max_score=10, + ) + + add_comment_question( + form.id, + descriptions={"en": "Comments", "fr": "Des commentaires?"}, + headlines=None, + ) + + add_evaluation_question( + form.id, + headlines=None, + descriptions={ + "en": """To what extent does the organization demonstrate a rigorous approach to answering their research questions? \[Total possible points = 10\] **Scale:** @@ -839,35 +1075,52 @@ def upgrade(): 4 - 7 : Some demonstrated rigor 8 - 10 : Significant/superior demonstrated rigor """, - 'fr': """Dans quelle mesure l’organisation fait-elle preuve de rigueur dans ses réponses aux questions de recherche? + "fr": """Dans quelle mesure l’organisation fait-elle preuve de rigueur dans ses réponses aux questions de recherche? \[Total des points possibles = 10\] **Scale:** 0 - 3 : Démontre peu de rigueur 4 - 7 : Certains aspects font preuve de rigueur 8 - 10 : Démontre une rigueur supérieure -""" - }, min_score=0, max_score=10) - - add_comment_question(form.id, descriptions={ - 'en': "Comments", - 'fr': "Des commentaires?" - }, headlines=None) - - add_information_question(form.id, form.application_form_id, 'Gender and inclusion', description_override={ - 'en': """How has your organization addressed gender-equality and other kinds of inclusion and diversity dimensions socioeconomic, political, cultural, ethnic in its policy research? +""", + }, + min_score=0, + max_score=10, + ) + + add_comment_question( + form.id, + descriptions={"en": "Comments", "fr": "Des commentaires?"}, + headlines=None, + ) + + add_information_question( + form.id, + form.application_form_id, + "Gender and inclusion", + description_override={ + "en": """How has your organization addressed gender-equality and other kinds of inclusion and diversity dimensions socioeconomic, political, cultural, ethnic in its policy research? How do you anticipate this will improve with this AI4D support? Please give a brief explanation of your strategy with gender, inclusion and diversity related issues as it relates to AI/advanced technologies and offer examples of the research work. Please provide examples PDF or links to material that demonstrates your strategy and research on gender, inclusion and diversity. Maximum 750 words""", - 'fr': "" - }) - add_information_question(form.id, form.application_form_id, 'Gender and inclusion: Supporting documents', type='multi-file') - - add_evaluation_question(form.id, headlines=None, descriptions={ - 'en': """To what extent does the organization demonstrate in-depth knowledge of gender-related dimensions of AI4D? + "fr": "", + }, + ) + add_information_question( + form.id, + form.application_form_id, + "Gender and inclusion: Supporting documents", + type="multi-file", + ) + + add_evaluation_question( + form.id, + headlines=None, + descriptions={ + "en": """To what extent does the organization demonstrate in-depth knowledge of gender-related dimensions of AI4D? \[Total possible points = 10\] **Scale:** @@ -875,18 +1128,24 @@ def upgrade(): 4 - 7 : Substantive demonstrated ability with gender related issues 8 - 10 : Superior/significant demonstrated ability with gender related issues """, - 'fr': """Dans quelle mesure l’organisation fait-elle preuve d’une connaissance approfondie des dimensions de genre d’IAPD? + "fr": """Dans quelle mesure l’organisation fait-elle preuve d’une connaissance approfondie des dimensions de genre d’IAPD? \[Total des points possibles = 10\] **Scale:** 0 - 3 : Capacité limitée ou démontrée à traiter des questions liées au genre 4 - 7 : Capacité démontrée à traiter des questions liées au genre 8 - 10 : Capacité supérieure démontrée en matière de questions liées au genre -""" - }, min_score=0, max_score=10) - - add_evaluation_question(form.id, headlines=None, descriptions={ - 'en': """To what extent does the organization demonstrate in-depth knowledge of other inclusion-related dimensions of AI4D, such as socioeconomics, cultural/ethnic and broader social inclusion issues? +""", + }, + min_score=0, + max_score=10, + ) + + add_evaluation_question( + form.id, + headlines=None, + descriptions={ + "en": """To what extent does the organization demonstrate in-depth knowledge of other inclusion-related dimensions of AI4D, such as socioeconomics, cultural/ethnic and broader social inclusion issues? \[Total possible points = 10\] **Scale:** @@ -894,18 +1153,24 @@ def upgrade(): 4 - 7 : Substantive demonstrated understanding of inclusion-related challenges 8 - 10 : Significant demonstrated understanding of inclusion-related challenges """, - 'fr': """Dans quelle mesure l’organisation démontre-t-elle une connaissance approfondie d’autres dimensions de l’IAPD liées à l’inclusion, telles que la socio-économie, la culture/ethnie et les questions plus larges d’inclusion sociale? + "fr": """Dans quelle mesure l’organisation démontre-t-elle une connaissance approfondie d’autres dimensions de l’IAPD liées à l’inclusion, telles que la socio-économie, la culture/ethnie et les questions plus larges d’inclusion sociale? \[Total des points possibles = 10\] **Scale:** 0 - 3 : Compréhension limitée ou démontrée des défis liés à l’inclusion 4 - 7 : Compréhension démontrée des défis liés à l’nclusion 8 - 10 : Une compréhension significative et démontrée des défis liés à l’inclusion -""" - }, min_score=0, max_score=10) - - add_evaluation_question(form.id, headlines=None, descriptions={ - 'en': """Considering the answer on gender and inclusion and the examples provided by the applicant, to what extent does the organization demonstrate in-depth knowledge of gender and inclusion-related issues/challenges in AI and advanced technologies? +""", + }, + min_score=0, + max_score=10, + ) + + add_evaluation_question( + form.id, + headlines=None, + descriptions={ + "en": """Considering the answer on gender and inclusion and the examples provided by the applicant, to what extent does the organization demonstrate in-depth knowledge of gender and inclusion-related issues/challenges in AI and advanced technologies? \[Total possible points = 5\] **Scale:** @@ -913,25 +1178,34 @@ def upgrade(): 2 - 3 : Some knowledge of gender and inclusion-related issues 4 - 5 : Superior/significant knowledge of gender and inclusion-related issues """, - 'fr': """Compte tenu de la réponse sur le genre et l’inclusion et des exemples fournis par le candidat, dans quelle mesure l’organisation démontre-t-elle une connaissance approfondie des questions et des défis liés au genre et à l’inclusion dans le domaine de l’IA et des technologies avancées? + "fr": """Compte tenu de la réponse sur le genre et l’inclusion et des exemples fournis par le candidat, dans quelle mesure l’organisation démontre-t-elle une connaissance approfondie des questions et des défis liés au genre et à l’inclusion dans le domaine de l’IA et des technologies avancées? \[Total des points possibles = 5\] **Scale:** 0 - 1 : Aucune connaissance ou une connaissance limitée des questions liées au genre et à l’inclusion 2 - 3 : Une certaine connaissance des questions liées à l’égalité des sexes et à l’inclusion 4 - 5 : Connaissance supérieure des questions liées au genre et à l’inclusion -""" - }, min_score=0, max_score=5) - - add_comment_question(form.id, descriptions={ - 'en': "Comments", - 'fr': "Des commentaires?" - }, headlines=None) - - add_information_question(form.id, form.application_form_id, 'Ethical considerations') - - add_evaluation_question(form.id, headlines=None, descriptions={ - 'en': """Please assess the applicant’s ethics review process for their research. Does the applicant outline a clear and independent process for reviewing and ensuring the privacy, dignity, and integrity of individuals who are the subjects of their research? +""", + }, + min_score=0, + max_score=5, + ) + + add_comment_question( + form.id, + descriptions={"en": "Comments", "fr": "Des commentaires?"}, + headlines=None, + ) + + add_information_question( + form.id, form.application_form_id, "Ethical considerations" + ) + + add_evaluation_question( + form.id, + headlines=None, + descriptions={ + "en": """Please assess the applicant’s ethics review process for their research. Does the applicant outline a clear and independent process for reviewing and ensuring the privacy, dignity, and integrity of individuals who are the subjects of their research? \[Total possible points = 5\] **Scale:** @@ -939,25 +1213,34 @@ def upgrade(): 2 - 3 : Some research ethics protocols 4 - 5 : Significant research ethics protocols """, - 'fr': """Veuillez évaluer le processus d’évaluation éthique du candidat pour sa recherche. Le candidat décrit-il un processus clair et indépendant pour évaluer et garantir la vie privée, la dignité et l’ntégrité des personnes qui font l’objet de ses recherches? + "fr": """Veuillez évaluer le processus d’évaluation éthique du candidat pour sa recherche. Le candidat décrit-il un processus clair et indépendant pour évaluer et garantir la vie privée, la dignité et l’ntégrité des personnes qui font l’objet de ses recherches? \[Total des points possibles = 5\] **Scale:** 0 - 1 : Protocoles d’éthique de la recherche limités 2 - 3 : Quelques protocoles d’éthique de la recherche 4 - 5 : Protocoles importants en matière d’éthique de la recherche -""" - }, min_score=0, max_score=5) - - add_comment_question(form.id, descriptions={ - 'en': "Comments", - 'fr': "Des commentaires?" - }, headlines=None) - - add_information_question(form.id, form.application_form_id, '4. Capacity development') - - add_evaluation_question(form.id, headlines=None, descriptions={ - 'en': """To what extent does the applicant clearly identify where they need the most capacity building to improve work and outcomes to advance responsible AI in their country/region - and offer a coherent, reasonable strategy for addressing the gap? +""", + }, + min_score=0, + max_score=5, + ) + + add_comment_question( + form.id, + descriptions={"en": "Comments", "fr": "Des commentaires?"}, + headlines=None, + ) + + add_information_question( + form.id, form.application_form_id, "4. Capacity development" + ) + + add_evaluation_question( + form.id, + headlines=None, + descriptions={ + "en": """To what extent does the applicant clearly identify where they need the most capacity building to improve work and outcomes to advance responsible AI in their country/region - and offer a coherent, reasonable strategy for addressing the gap? \[Total possible points = 10\] **Scale:** @@ -965,25 +1248,34 @@ def upgrade(): 4 - 7 : Some ability to identify capacity development needs 8 - 10 : Significant ability to identify capacity development needs """, - 'fr': """Dans quelle mesure le candidat identifie-t-il clairement les domaines dans lesquels il a le plus besoin de renforcer ses capacités pour améliorer le travail et les résultats afin de faire progresser l’IA responsable dans son pays/région - et propose-t-il une stratégie cohérente et raisonnable pour combler les lacunes? + "fr": """Dans quelle mesure le candidat identifie-t-il clairement les domaines dans lesquels il a le plus besoin de renforcer ses capacités pour améliorer le travail et les résultats afin de faire progresser l’IA responsable dans son pays/région - et propose-t-il une stratégie cohérente et raisonnable pour combler les lacunes? \[Total des points possibles = 10\] **Scale:** 0 - 3 : Capacité limitée à identifier les besoins en matière de renforcement des capacités 4 - 7 : Une certaine capacité à identifier les besoins en matière de développement des capacités 8 - 10 : Capacité importante à identifier les besoins en matière de renforcement des capacités -""" - }, min_score=0, max_score=10) - - add_comment_question(form.id, descriptions={ - 'en': "Comments", - 'fr': "Des commentaires?" - }, headlines=None) - - add_information_question(form.id, form.application_form_id, '5. Communicating for influence') - - add_evaluation_question(form.id, headlines=None, descriptions={ - 'en': """Please review the applicant's description of their research communications strategy. To what extent does the organization demonstrate an ability or approach to communicate their research well to influence policy? +""", + }, + min_score=0, + max_score=10, + ) + + add_comment_question( + form.id, + descriptions={"en": "Comments", "fr": "Des commentaires?"}, + headlines=None, + ) + + add_information_question( + form.id, form.application_form_id, "5. Communicating for influence" + ) + + add_evaluation_question( + form.id, + headlines=None, + descriptions={ + "en": """Please review the applicant's description of their research communications strategy. To what extent does the organization demonstrate an ability or approach to communicate their research well to influence policy? \[Total possible points = 10\] **Scale:** @@ -991,25 +1283,32 @@ def upgrade(): 4 - 7 : Some demonstrated ability to communicate research 8 - 10 : Significant/superior demonstrated ability to communicate research """, - 'fr': """Veuillez examiner la description par le candidat de sa stratégie de communication sur la recherche. Dans quelle mesure l’organisme démontre-t-il une capacité ou une approche pour bien communiquer sur sa recherche afin d’influencer les politiques? + "fr": """Veuillez examiner la description par le candidat de sa stratégie de communication sur la recherche. Dans quelle mesure l’organisme démontre-t-il une capacité ou une approche pour bien communiquer sur sa recherche afin d’influencer les politiques? \[Total des points possibles = 10\] **Scale:** 0 - 3 : Démontre peu d’aptitude à communiquer sur la recherche 4 - 7 : Démontre une capacité modérée à communiquer sur la recherche 8 - 10 : Démontre une capacité supérieure à communiquer sur la recherche -""" - }, min_score=0, max_score=10) - - add_comment_question(form.id, descriptions={ - 'en': "Comments", - 'fr': "Des commentaires?" - }, headlines=None) - - add_information_question(form.id, form.application_form_id, '6. Results') - - add_evaluation_question(form.id, headlines=None, descriptions={ - 'en': """To what extent are the proposed markers for measuring success for the organization in the context of this project specific, appropriate, achievable, timely and measurable? +""", + }, + min_score=0, + max_score=10, + ) + + add_comment_question( + form.id, + descriptions={"en": "Comments", "fr": "Des commentaires?"}, + headlines=None, + ) + + add_information_question(form.id, form.application_form_id, "6. Results") + + add_evaluation_question( + form.id, + headlines=None, + descriptions={ + "en": """To what extent are the proposed markers for measuring success for the organization in the context of this project specific, appropriate, achievable, timely and measurable? \[Total possible points = 15\] **Scale:** @@ -1017,25 +1316,32 @@ def upgrade(): 6 - 10 : Some articulation of success, markers may lack clarity, specificity, appropriateness, feasibility, or measurability 11- 15 : Clear vision of success matched with strong, measurable markers """, - 'fr': """Dans quelle mesure les marqueurs proposés pour mesurer le succès de l’organisation dans le cadre de ce projet sont-ils spécifiques, appropriés, réalisables, opportuns et mesurables? + "fr": """Dans quelle mesure les marqueurs proposés pour mesurer le succès de l’organisation dans le cadre de ce projet sont-ils spécifiques, appropriés, réalisables, opportuns et mesurables? \[Total des points possibles = 15\] **Scale:** 0 - 5 : Clarté limitée pour une vision de la réussite, les marqueurs sont peu clairs ou mal définis 6 - 10 : Une certaine articulation de la réussite, les marqueurs peuvent manquer de clarté, de spécificité, de pertinence, de faisabilité ou de mesurabilité 11- 15 : Une vision claire de la réussite assortie de marqueurs forts et mesurables -""" - }, min_score=0, max_score=15) - - add_comment_question(form.id, descriptions={ - 'en': "Comments", - 'fr': "Des commentaires?" - }, headlines=None) - - add_information_question(form.id, form.application_form_id, '7. Project schedule') - - add_evaluation_question(form.id, headlines=None, descriptions={ - 'en': """To what extent does the applicant demonstrate a clear and realistic project schedule for achieving their objectives with outputs and milestones as they relate to the budget? +""", + }, + min_score=0, + max_score=15, + ) + + add_comment_question( + form.id, + descriptions={"en": "Comments", "fr": "Des commentaires?"}, + headlines=None, + ) + + add_information_question(form.id, form.application_form_id, "7. Project schedule") + + add_evaluation_question( + form.id, + headlines=None, + descriptions={ + "en": """To what extent does the applicant demonstrate a clear and realistic project schedule for achieving their objectives with outputs and milestones as they relate to the budget? \[Total possible points = 5\] **Scale:** @@ -1043,59 +1349,77 @@ def upgrade(): 2 - 3 : Some demonstration of how to achieve project objectives 4 - 5 : Superior/significant demonstration of how to achieve project objectives """, - 'fr': """Dans quelle mesure le demandeur démontre-t-il un calendrier de projet clair et réaliste pour atteindre ses objectifs, avec des résultats et des étapes en rapport avec le budget? + "fr": """Dans quelle mesure le demandeur démontre-t-il un calendrier de projet clair et réaliste pour atteindre ses objectifs, avec des résultats et des étapes en rapport avec le budget? \[Total des points possibles = 5\] Scale: 0 - 1 : Démonstration limitée de la manière d’atteindre les objectifs du projet 2 - 3 : Quelques démonstrations de la manière d’atteindre les objectifs du projet 4 - 5 : Démonstration supérieure/significative de la manière d’atteindre les objectifs du projet -""" - }, min_score=0, max_score=5) - - add_comment_question(form.id, descriptions={ - 'en': "Comments", - 'fr': "Des commentaires?" - }, headlines=None) - - add_information_question(form.id, form.application_form_id, '8. Supplemental documentation (optional).') - - add_comment_question(form.id, headlines=None, descriptions={ - 'en': """Please review the supplemental documentation and provide your assessment of these documents. This question is not weighted, but your assessment will be taken into consideration should there be a tie, or a close score that requires an assessment of the applicant’s overall work.""", - 'fr': """Veuillez examiner les documents complémentaires et donner votre évaluation de ces documents. Cette question n’est pas pondérée, mais votre évaluation sera prise en considération en cas d’égalité ou de score serré nécessitant une évaluation de l’ensemble du travail du candidat.""" - }) - - add_comment_question(form.id, descriptions={ - 'en': "Comments", - 'fr': "Des commentaires?" - }, headlines=None) - - print('Finished configuring Section II') - - add_divider(form.id, headlines={ - 'en': 'Section III: Policy Engagement Experience', - 'fr': 'Section III : Expérience de l’engagement politique' - }, descriptions={ - 'en': """Please review and evaluate the applicant’s answers on their policy engagement experience. +""", + }, + min_score=0, + max_score=5, + ) + + add_comment_question( + form.id, + descriptions={"en": "Comments", "fr": "Des commentaires?"}, + headlines=None, + ) + + add_information_question( + form.id, form.application_form_id, "8. Supplemental documentation (optional)." + ) + + add_comment_question( + form.id, + headlines=None, + descriptions={ + "en": """Please review the supplemental documentation and provide your assessment of these documents. This question is not weighted, but your assessment will be taken into consideration should there be a tie, or a close score that requires an assessment of the applicant’s overall work.""", + "fr": """Veuillez examiner les documents complémentaires et donner votre évaluation de ces documents. Cette question n’est pas pondérée, mais votre évaluation sera prise en considération en cas d’égalité ou de score serré nécessitant une évaluation de l’ensemble du travail du candidat.""", + }, + ) + + add_comment_question( + form.id, + descriptions={"en": "Comments", "fr": "Des commentaires?"}, + headlines=None, + ) + + print("Finished configuring Section II") + + add_divider( + form.id, + headlines={ + "en": "Section III: Policy Engagement Experience", + "fr": "Section III : Expérience de l’engagement politique", + }, + descriptions={ + "en": """Please review and evaluate the applicant’s answers on their policy engagement experience. \[Total possible points: 75\] Evaluation criteria: - Evidence of national and regional policy engagement and influence as well as regional and global experience. Applicant must be able to show regular policy engagement with target audiences, including policymakers, media, and civil society organizations. - Neutrality and ability to convene a broad spectrum of views, with a strong commitment to expand your research portfolio, engage broader perspectives on AI policy issues, and improve organizational performance. """, - 'fr': """Veuillez examiner et évaluer les réponses du candidat en fonction de son expérience en matière d’engagement politique. + "fr": """Veuillez examiner et évaluer les réponses du candidat en fonction de son expérience en matière d’engagement politique. \[Total des points possibles = 75\] Critères d’évaluation : - Preuve de l’engagement et de l’influence de la politique nationale et régionale ainsi que de l’expérience régionale et mondiale. Le candidat doit être en mesure de démontrer un engagement politique régulier auprès des publics cibles, notamment les décideurs politiques, les médias et les organisations de la société civile. - Neutralité et capacité à réunir un large éventail de points de vue, avec un engagement fort à élargir votre portefeuille de recherche, à engager des perspectives plus larges sur les questions de politique d’IA et à améliorer la performance organisationnelle. -""" - }) +""", + }, + ) - add_information_question(form.id, form.application_form_id, '1. Policy engagement') + add_information_question(form.id, form.application_form_id, "1. Policy engagement") - add_evaluation_question(form.id, headlines=None, descriptions={ - 'en': """To what extent does the organization demonstrate quality engagement with national governing institutions (e.g. government departments, bodies and agencies, as well as regulators, and other public-sector institutions) on advanced technology issues? \[Total possible points = 15\] + add_evaluation_question( + form.id, + headlines=None, + descriptions={ + "en": """To what extent does the organization demonstrate quality engagement with national governing institutions (e.g. government departments, bodies and agencies, as well as regulators, and other public-sector institutions) on advanced technology issues? \[Total possible points = 15\] *Note: For the purposes of this initiative, having national policy influence is critical.* Applicant receives and responds to requests for input from governments/other regulatory institutions. @@ -1105,7 +1429,7 @@ def upgrade(): 6 - 10 : Some quality engagement 11- 15 : Significant quality engagement """, - 'fr': """Dans quelle mesure l’organisation fait-elle preuve d’un engagement de qualité avec les institutions nationales de gouvernance (par exemple, les ministères, les organismes et les agences gouvernementales, ainsi que les régulateurs et autres institutions du secteur public) sur les questions de technologie avancée? + "fr": """Dans quelle mesure l’organisation fait-elle preuve d’un engagement de qualité avec les institutions nationales de gouvernance (par exemple, les ministères, les organismes et les agences gouvernementales, ainsi que les régulateurs et autres institutions du secteur public) sur les questions de technologie avancée? \[Total des points possibles = 15\] *Note: Pour les besoins de cette initiative, il est essentiel d'avoir une influence sur les politiques nationales.* @@ -1114,18 +1438,24 @@ def upgrade(): 0 - 5 : Engagement de qualité limité 6 - 10 : Engagement de qualité partiel 11- 15 : Engagement de qualité important -""" - }, min_score=0, max_score=15) +""", + }, + min_score=0, + max_score=15, + ) - add_evaluation_question(form.id, headlines=None, descriptions={ - 'en': """Applicant proactively engages with policy makers (such as on steering committees, or on emerging policy issues). + add_evaluation_question( + form.id, + headlines=None, + descriptions={ + "en": """Applicant proactively engages with policy makers (such as on steering committees, or on emerging policy issues). **Scale:** 0 - 5 : Limited quality engagement 6 - 10 : Some quality engagement 11- 15 : Significant quality engagement """, - 'fr': """Dans quelle mesure l’organisation fait-elle preuve d’un engagement de qualité avec les institutions nationales de gouvernance (par exemple, les ministères, les organismes et les agences gouvernementales, ainsi que les régulateurs et autres institutions du secteur public) sur les questions de technologie avancée? + "fr": """Dans quelle mesure l’organisation fait-elle preuve d’un engagement de qualité avec les institutions nationales de gouvernance (par exemple, les ministères, les organismes et les agences gouvernementales, ainsi que les régulateurs et autres institutions du secteur public) sur les questions de technologie avancée? \[Total des points possibles = 15\] *Note: Pour les besoins de cette initiative, il est essentiel d'avoir une influence sur les politiques nationales.* @@ -1134,11 +1464,17 @@ def upgrade(): 0 - 5 : Engagement de qualité limité 6 - 10 : Engagement de qualité partiel 11- 15 : Engagement de qualité important -""" - }, min_score=0, max_score=15) - - add_evaluation_question(form.id, headlines=None, descriptions={ - 'en': """Please review the three examples of policy engagement. To what extent does the applicant demonstrate an ability to engage with policy makers on substantive/technical policy issues? +""", + }, + min_score=0, + max_score=15, + ) + + add_evaluation_question( + form.id, + headlines=None, + descriptions={ + "en": """Please review the three examples of policy engagement. To what extent does the applicant demonstrate an ability to engage with policy makers on substantive/technical policy issues? \[Total possible points = 15\] **Scale:** @@ -1146,25 +1482,34 @@ def upgrade(): 6 - 10 : Some demonstrated ability to engage with policy makers on substantive/technical policy issues 11- 15 : Significant/superior demonstrated ability to engage with policy makers on substantive/technical policy issues """, - 'fr': """Veuillez examiner les trois exemples d’engagement politique. Dans quelle mesure le candidat démontre-t-il une capacité à s’engager auprès des décideurs politiques sur des questions politiques de fond/techniques? + "fr": """Veuillez examiner les trois exemples d’engagement politique. Dans quelle mesure le candidat démontre-t-il une capacité à s’engager auprès des décideurs politiques sur des questions politiques de fond/techniques? \[Total des points possibles = 15\] **Scale:** 0 - 5 : Capacité limitée et avérée à dialoguer avec les décideurs politiques sur des questions politiques de fond/techniques 6 - 10 : Certains aspects font preuve de leur capacité à dialoguer avec les décideurs politiques sur des questions politiques de fond/techniques 11- 15 : Capacité supérieurement démontrée à s’engager avec les décideurs politiques sur des questions de fond/techniques -""" - }, min_score=0, max_score=15) - - add_comment_question(form.id, descriptions={ - 'en': "Comments", - 'fr': "Des commentaires?" - }, headlines=None) - - add_information_question(form.id, form.application_form_id, '2. Informing debates and policies') - - add_evaluation_question(form.id, headlines=None, descriptions={ - 'en': """To what extent does the applicant demonstrate an ability to inform public debate and policy processes nationally or regionally? +""", + }, + min_score=0, + max_score=15, + ) + + add_comment_question( + form.id, + descriptions={"en": "Comments", "fr": "Des commentaires?"}, + headlines=None, + ) + + add_information_question( + form.id, form.application_form_id, "2. Informing debates and policies" + ) + + add_evaluation_question( + form.id, + headlines=None, + descriptions={ + "en": """To what extent does the applicant demonstrate an ability to inform public debate and policy processes nationally or regionally? \[Total possible points = 15\] **Scale:** @@ -1172,21 +1517,31 @@ def upgrade(): 6 - 10 : Some demonstrated ability 11- 15 : Significant demonstrated ability """, - 'fr': """Dans quelle mesure le candidat démontre-t-il une capacité à informer le débat public et les processus politiques au niveau national ou régional? + "fr": """Dans quelle mesure le candidat démontre-t-il une capacité à informer le débat public et les processus politiques au niveau national ou régional? \[Total des points possibles = 15\] **Scale:** 0 - 5 : Capacité limitée 6 - 10 : Certains aspects font preuve de capacités 11- 15 : Capacité significative -""" - }, min_score=0, max_score=15) - - add_information_question(form.id, form.application_form_id, '3. Regional engagement') - add_information_question(form.id, form.application_form_id, 'Regional engagement', heading_override=True) - - add_evaluation_question(form.id, headlines=None, descriptions={ - 'en': """To what extent does the organization demonstrate clear regional/international engagement on AI/advanced technology policy issues with other governments, public sector institutions, bi-lateral, multilateral or international institutions/organizations or multi stakeholder fora? +""", + }, + min_score=0, + max_score=15, + ) + + add_information_question( + form.id, form.application_form_id, "3. Regional engagement" + ) + add_information_question( + form.id, form.application_form_id, "Regional engagement", heading_override=True + ) + + add_evaluation_question( + form.id, + headlines=None, + descriptions={ + "en": """To what extent does the organization demonstrate clear regional/international engagement on AI/advanced technology policy issues with other governments, public sector institutions, bi-lateral, multilateral or international institutions/organizations or multi stakeholder fora? \[Total possible points = 15\] Scale: @@ -1194,28 +1549,32 @@ def upgrade(): 6 - 10 : Substantive regional/international engagement 11- 15 : Significant regional/international engagement """, - 'fr': """Dans quelle mesure l’organisation fait-elle preuve d’un engagement régional/international clair sur les questions de politique technologique avancée et d’IA avec d’autres gouvernements, des institutions du secteur public, des institutions/organisations bilatérales, multilatérales ou internationales ou des forums multipartites? + "fr": """Dans quelle mesure l’organisation fait-elle preuve d’un engagement régional/international clair sur les questions de politique technologique avancée et d’IA avec d’autres gouvernements, des institutions du secteur public, des institutions/organisations bilatérales, multilatérales ou internationales ou des forums multipartites? \[Total des points possibles = 15\] **Scale:** 0 - 5 : Engagement régional/international limité 6 - 10 : Engagement régional/international substantiel 11- 15 : Un engagement régional/international important -""" - }, min_score=0, max_score=15) - - add_comment_question(form.id, descriptions={ - 'en': "Comments", - 'fr': "Des commentaires?" - }, headlines=None) - - print('Finished configuring Section III') - - add_divider(form.id, headlines={ - 'en': "Section IV: Budget", - 'fr': "Section IV: Budget" - }, descriptions={ - 'en': """Please review and evaluate the applicant’s budget. +""", + }, + min_score=0, + max_score=15, + ) + + add_comment_question( + form.id, + descriptions={"en": "Comments", "fr": "Des commentaires?"}, + headlines=None, + ) + + print("Finished configuring Section III") + + add_divider( + form.id, + headlines={"en": "Section IV: Budget", "fr": "Section IV: Budget"}, + descriptions={ + "en": """Please review and evaluate the applicant’s budget. \[Total possible points = 30\] Evaluation criteria: @@ -1223,20 +1582,24 @@ def upgrade(): - Appropriate for the proposed activities - Aligned with management plan """, - 'fr': """Veuillez examiner et évaluer le budget du candidat. + "fr": """Veuillez examiner et évaluer le budget du candidat. \[Total des points possibles = 30\] Critères d’évaluation : - Clair et cohérent - Approprié pour les activités proposées - Aligné sur le plan de gestion -""" - }) +""", + }, + ) - add_information_question(form.id, form.application_form_id, 'Budget', 'file') + add_information_question(form.id, form.application_form_id, "Budget", "file") - add_evaluation_question(form.id, headlines=None, descriptions={ - 'en': """Please review the applicant’s budget and consider the coherence and clarity, the appropriateness of budget lines for proposed activities and the budget’s alignment with the research plan. + add_evaluation_question( + form.id, + headlines=None, + descriptions={ + "en": """Please review the applicant’s budget and consider the coherence and clarity, the appropriateness of budget lines for proposed activities and the budget’s alignment with the research plan. In terms of overall coherence, appropriateness to research activities and fiscal management, please rate the applicant’s budget. \[Total possible points = 30\] @@ -1245,7 +1608,7 @@ def upgrade(): 11 - 20 : Budget substantively demonstrates coherence and appropriateness for research and policy impact 21 - 30 : Budget demonstrates superior coherence and appropriateness for research and policy impact """, - 'fr': """Veuillez examiner le budget du candidat et considérer la cohérence et la clarté, la pertinence des lignes budgétaires pour les activités proposées et l’alignement du budget sur le plan de recherche. + "fr": """Veuillez examiner le budget du candidat et considérer la cohérence et la clarté, la pertinence des lignes budgétaires pour les activités proposées et l’alignement du budget sur le plan de recherche. En termes de cohérence globale, d’adéquation aux activités de recherche et de gestion budgétaire, veuillez évaluer le budget du candidat. \[Total des points possibles = 30\] @@ -1253,28 +1616,42 @@ def upgrade(): 0 - 10 : Le budget démontre quelque peu la cohérence et l’adéquation à la recherche et à l’impact politique 11 - 20 : Le budget démontre de manière substantielle la cohérence et l’adéquation à la recherche et à l’impact politique 21 - 30 : Le budget démontre une cohérence et une adéquation supérieures pour la recherche et l’impact politique -""" - }, min_score=0, max_score=30) - - add_comment_question(form.id, headlines=None, descriptions={ - 'en': """IDRC’s grants administration team will be reviewing all of the budgets independently, and their assessment will be discussed during the final selection process. However, given the significance of budgets in terms of institutional strength, research planning and strategy, and risk mitigation, please provide feedback on the applicant’s proposed budget, and offer any input or insights you may have regarding the soundness of the applicant’s budget in relation to their research objectives and overall plan.""", - 'fr': """L’équipe d’administration des subventions du CRDI examinera tous les budgets de manière indépendante et leur évaluation sera discutée lors du processus de sélection final. Toutefois, étant donné l’importance des budgets en termes de force institutionnelle, de planification et de stratégie de recherche et d’atténuation des risques, veuillez donner votre avis sur le budget proposé par le candidat et faire part de vos commentaires ou de vos idées concernant""" - }) - - print('Finished configuring Section IV') - - add_divider(form.id, headlines={ - 'en': "Section V: Overall quality of application", - 'fr': "Section V : Qualité générale de la demande" - }, descriptions={ - 'en': """Taking the proposal as a whole, please answer the following questions. +""", + }, + min_score=0, + max_score=30, + ) + + add_comment_question( + form.id, + headlines=None, + descriptions={ + "en": """IDRC’s grants administration team will be reviewing all of the budgets independently, and their assessment will be discussed during the final selection process. However, given the significance of budgets in terms of institutional strength, research planning and strategy, and risk mitigation, please provide feedback on the applicant’s proposed budget, and offer any input or insights you may have regarding the soundness of the applicant’s budget in relation to their research objectives and overall plan.""", + "fr": """L’équipe d’administration des subventions du CRDI examinera tous les budgets de manière indépendante et leur évaluation sera discutée lors du processus de sélection final. Toutefois, étant donné l’importance des budgets en termes de force institutionnelle, de planification et de stratégie de recherche et d’atténuation des risques, veuillez donner votre avis sur le budget proposé par le candidat et faire part de vos commentaires ou de vos idées concernant""", + }, + ) + + print("Finished configuring Section IV") + + add_divider( + form.id, + headlines={ + "en": "Section V: Overall quality of application", + "fr": "Section V : Qualité générale de la demande", + }, + descriptions={ + "en": """Taking the proposal as a whole, please answer the following questions. \[Total possible points = 25\]""", - 'fr': """En prenant la proposition dans son ensemble, veuillez répondre aux questions suivantes. -\[Total des points possibles = 25\]""" - }) - - add_evaluation_question(form.id, headlines=None, descriptions={ - 'en': """Is the overall application well-articulated and does it demonstrate clarity of purpose and potential impact for AI4D? + "fr": """En prenant la proposition dans son ensemble, veuillez répondre aux questions suivantes. +\[Total des points possibles = 25\]""", + }, + ) + + add_evaluation_question( + form.id, + headlines=None, + descriptions={ + "en": """Is the overall application well-articulated and does it demonstrate clarity of purpose and potential impact for AI4D? \[Total possible points = 10\] **Scale:** @@ -1282,18 +1659,24 @@ def upgrade(): 4 - 7 : Somewhat well-articulated and clear 8 - 10 : Very well-articulated and clear """, - 'fr': """La demande globale est-elle bien articulée et démontre-t-elle la clarté de l’objectif et l’impact potentiel pour IAPD? + "fr": """La demande globale est-elle bien articulée et démontre-t-elle la clarté de l’objectif et l’impact potentiel pour IAPD? \[Total des points possibles = 10\] **Scale:** 0 - 3 : Articulation et clarté limitées 4 - 7 : Assez bien articulé et clair 8 - 10 : Très bien articulé et clair -""" - }, min_score=0, max_score=10) - - add_evaluation_question(form.id, headlines=None, descriptions={ - 'en': """Does the proposal demonstrate in-depth, well-rounded, and multi-disciplinary knowledge of AI and advanced technology policy themes and issues? +""", + }, + min_score=0, + max_score=10, + ) + + add_evaluation_question( + form.id, + headlines=None, + descriptions={ + "en": """Does the proposal demonstrate in-depth, well-rounded, and multi-disciplinary knowledge of AI and advanced technology policy themes and issues? \[Total possible points = 10\] **Scale:** @@ -1301,18 +1684,24 @@ def upgrade(): 4 - 7 : Some multi-disciplinary knowledge 8 - 10 : Significant multi-disciplinary knowledge """, - 'fr': """La proposition démontre-t-elle une connaissance approfondie, bien équilibrée et pluridisciplinaire des thèmes et des questions de politique en matière d’IA et de technologies avancées? + "fr": """La proposition démontre-t-elle une connaissance approfondie, bien équilibrée et pluridisciplinaire des thèmes et des questions de politique en matière d’IA et de technologies avancées? \[Total des points possibles = 10\] **Scale:** 0 - 3 : Peu de connaissances pluridisciplinaires 4 - 7 : Quelques connaissances pluridisciplinaires 8 - 10 : Connaissances pluridisciplinaires importantes -""" - }, min_score=0, max_score=10) - - add_evaluation_question(form.id, headlines=None, descriptions={ - 'en': """Does the proposal demonstrate a clear strategy of how the organization will benefit from the support offered by this initiative and add to debates on the AI and advanced technology policy themes in the region? +""", + }, + min_score=0, + max_score=10, + ) + + add_evaluation_question( + form.id, + headlines=None, + descriptions={ + "en": """Does the proposal demonstrate a clear strategy of how the organization will benefit from the support offered by this initiative and add to debates on the AI and advanced technology policy themes in the region? \[Total possible points = 5\] **Scale:** @@ -1320,24 +1709,30 @@ def upgrade(): 2 - 3 : Some 4 - 5 : Significant """, - 'fr': """La proposition présente-t-elle une stratégie claire quant à la manière dont l’organisation bénéficiera du soutien offert par cette initiative et contribue-t-elle aux débats sur les thèmes de l’IA et de la politique en matière de technologies avancées dans la région? + "fr": """La proposition présente-t-elle une stratégie claire quant à la manière dont l’organisation bénéficiera du soutien offert par cette initiative et contribue-t-elle aux débats sur les thèmes de l’IA et de la politique en matière de technologies avancées dans la région? \[Total des points possibles = 5\] **Scale:** 0 - 1 : Peu 2 - 3 : Quelques 4 - 5 : Important -""" - }, min_score=0, max_score=5) +""", + }, + min_score=0, + max_score=5, + ) - add_comment_question(form.id, headlines=None, descriptions={ - 'en': """Please provide any final comments on the overall application. These will feed into discussions""", - 'fr': """Veuillez fournir tout commentaire final sur l’ensemble de la demande. Ces commentaires seront pris en compte dans les discussions.""" - }) + add_comment_question( + form.id, + headlines=None, + descriptions={ + "en": """Please provide any final comments on the overall application. These will feed into discussions""", + "fr": """Veuillez fournir tout commentaire final sur l’ensemble de la demande. Ces commentaires seront pris en compte dans les discussions.""", + }, + ) - print('DONE') + print("DONE") def downgrade(): pass - diff --git a/api/migrations/versions/6d34d1ab6864_eeml_review_form.py b/api/migrations/versions/6d34d1ab6864_eeml_review_form.py index 9f14ff52d..b59878894 100644 --- a/api/migrations/versions/6d34d1ab6864_eeml_review_form.py +++ b/api/migrations/versions/6d34d1ab6864_eeml_review_form.py @@ -7,28 +7,30 @@ """ # revision identifiers, used by Alembic. -revision = '6d34d1ab6864' -down_revision = 'a4662031beca' +revision = "6d34d1ab6864" +down_revision = "a4662031beca" + +import datetime +from enum import Enum -from alembic import op import sqlalchemy as sa +from alembic import op from sqlalchemy import orm from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy import orm + from app import db -import datetime -from enum import Enum Base = declarative_base() + class EventType(Enum): - EVENT = 'event' - AWARD = 'award' + EVENT = "event" + AWARD = "award" class Event(Base): - __tablename__ = 'event' - __table_args__ = {'extend_existing': True} + __tablename__ = "event" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(50), nullable=False) @@ -36,8 +38,9 @@ class Event(Base): start_date = db.Column(db.DateTime(), nullable=False) end_date = db.Column(db.DateTime(), nullable=False) key = db.Column(db.String(255), nullable=False, unique=True) - organisation_id = db.Column(db.Integer(), db.ForeignKey( - 'organisation.id'), nullable=False) + organisation_id = db.Column( + db.Integer(), db.ForeignKey("organisation.id"), nullable=False + ) email_from = db.Column(db.String(255), nullable=False) url = db.Column(db.String(255), nullable=False) @@ -55,21 +58,23 @@ class Event(Base): class ApplicationForm(Base): - __tablename__ = 'application_form' - __table_args__ = {'extend_existing': True} + __tablename__ = "application_form" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - event_id = db.Column(db.Integer(), db.ForeignKey('event.id'), nullable=False) + event_id = db.Column(db.Integer(), db.ForeignKey("event.id"), nullable=False) is_open = db.Column(db.Boolean(), nullable=False) class Question(Base): - __tablename__ = 'question' - __table_args__ = {'extend_existing': True} + __tablename__ = "question" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) - section_id = db.Column(db.Integer(), db.ForeignKey('section.id'), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) + section_id = db.Column(db.Integer(), db.ForeignKey("section.id"), nullable=False) type = db.Column(db.String(), nullable=False) description = db.Column(db.String(), nullable=True) headline = db.Column(db.String(), nullable=False) @@ -79,17 +84,21 @@ class Question(Base): order = db.Column(db.Integer(), nullable=False) options = db.Column(db.JSON(), nullable=True) is_required = db.Column(db.Boolean(), nullable=False) - depends_on_question_id = db.Column(db.Integer(), db.ForeignKey('question.id'), nullable=True) + depends_on_question_id = db.Column( + db.Integer(), db.ForeignKey("question.id"), nullable=True + ) show_for_values = db.Column(db.JSON(), nullable=True) key = db.Column(db.String(255), nullable=True) class ReviewForm(Base): - __tablename__ = 'review_form' - __table_args__ = {'extend_existing': True} + __tablename__ = "review_form" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) is_open = db.Column(db.Boolean(), nullable=False) deadline = db.Column(db.DateTime(), nullable=False) @@ -103,12 +112,14 @@ def close(self): class ReviewQuestion(Base): - __tablename__ = 'review_question' - __table_args__ = {'extend_existing': True} + __tablename__ = "review_question" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer, primary_key=True) - review_form_id = db.Column(db.Integer(), db.ForeignKey('review_form.id'), nullable=False) - question_id = db.Column(db.Integer(), db.ForeignKey('question.id'), nullable=True) + review_form_id = db.Column( + db.Integer(), db.ForeignKey("review_form.id"), nullable=False + ) + question_id = db.Column(db.Integer(), db.ForeignKey("question.id"), nullable=True) description = db.Column(db.String(), nullable=True) headline = db.Column(db.String(), nullable=True) type = db.Column(db.String(), nullable=False) @@ -120,19 +131,21 @@ class ReviewQuestion(Base): validation_text = db.Column(db.String(), nullable=True) weight = db.Column(db.Float(), nullable=False) - def __init__(self, - review_form_id, - type, - is_required, - order, - question_id=None, - description=None, - headline=None, - placeholder=None, - options=None, - validation_regex=None, - validation_text=None, - weight=0): + def __init__( + self, + review_form_id, + type, + is_required, + order, + question_id=None, + description=None, + headline=None, + placeholder=None, + options=None, + validation_regex=None, + validation_text=None, + weight=0, + ): self.review_form_id = review_form_id self.question_id = question_id self.description = description @@ -148,17 +161,28 @@ def __init__(self, class ReviewConfiguration(Base): - __tablename__ = 'review_configuration' - __table_args__ = {'extend_existing': True} + __tablename__ = "review_configuration" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - review_form_id = db.Column(db.Integer(), db.ForeignKey('review_form.id'), nullable=False) + review_form_id = db.Column( + db.Integer(), db.ForeignKey("review_form.id"), nullable=False + ) num_reviews_required = db.Column(db.Integer(), nullable=False) num_optional_reviews = db.Column(db.Integer(), nullable=False) - drop_optional_question_id = db.Column(db.Integer(), db.ForeignKey('review_question.id'), nullable=True) + drop_optional_question_id = db.Column( + db.Integer(), db.ForeignKey("review_question.id"), nullable=True + ) drop_optional_agreement_values = db.Column(db.String(), nullable=True) - def __init__(self, review_form_id, num_reviews_required, num_optional_reviews=0, drop_optional_question_id=None, drop_optional_agreement_values=None): + def __init__( + self, + review_form_id, + num_reviews_required, + num_optional_reviews=0, + drop_optional_question_id=None, + drop_optional_agreement_values=None, + ): self.review_form_id = review_form_id self.num_reviews_required = num_reviews_required self.num_optional_reviews = num_optional_reviews @@ -173,76 +197,133 @@ def find_question(session, headline): def upgrade(): Base.metadata.bind = op.get_bind() session = orm.Session(bind=Base.metadata.bind) - - op.get_bind().execute("""SELECT setval('review_form_id_seq', (SELECT max(id) FROM review_form));""") - op.get_bind().execute("""SELECT setval('review_question_id_seq', (SELECT max(id) FROM review_question));""") - event = session.query(Event).filter_by(key='eeml2020').first() - application_form = session.query(ApplicationForm).filter_by(event_id=event.id).first() + op.get_bind().execute( + """SELECT setval('review_form_id_seq', (SELECT max(id) FROM review_form));""" + ) + op.get_bind().execute( + """SELECT setval('review_question_id_seq', (SELECT max(id) FROM review_question));""" + ) + + event = session.query(Event).filter_by(key="eeml2020").first() + application_form = ( + session.query(ApplicationForm).filter_by(event_id=event.id).first() + ) form = ReviewForm(application_form.id, datetime.datetime(2020, 5, 20)) session.add(form) session.commit() - affiliation_question = find_question(session, 'Affiliation') - affiliation_info = ReviewQuestion(form.id, 'information', False, 1, question_id=affiliation_question.id, headline='Affiliation') - - research_interests_question = find_question(session, 'Research Interests') - research_interests_info = ReviewQuestion(form.id, 'information', False, 2, question_id=research_interests_question.id, headline='Research Interests') - - extended_abstract_type = find_question(session, 'Extended abstract type') - extended_abstract_type_info = ReviewQuestion(form.id, 'information', False, 3, question_id=extended_abstract_type.id) - - extended_abstract_question = find_question(session, 'Extended abstract') - extended_abstract_info = ReviewQuestion(form.id, 'information', False, 4, question_id=extended_abstract_question.id) - - cv_question = find_question(session, 'CV') - cv_info = ReviewQuestion(form.id, 'information', False, 5, question_id=cv_question.id, headline='CV') - - financial_support_question = find_question(session, 'Motivation') - financial_support_info = ReviewQuestion(form.id, 'information', False, 6, question_id=financial_support_question.id, headline='Financial Support Motivation') + affiliation_question = find_question(session, "Affiliation") + affiliation_info = ReviewQuestion( + form.id, + "information", + False, + 1, + question_id=affiliation_question.id, + headline="Affiliation", + ) + + research_interests_question = find_question(session, "Research Interests") + research_interests_info = ReviewQuestion( + form.id, + "information", + False, + 2, + question_id=research_interests_question.id, + headline="Research Interests", + ) + + extended_abstract_type = find_question(session, "Extended abstract type") + extended_abstract_type_info = ReviewQuestion( + form.id, "information", False, 3, question_id=extended_abstract_type.id + ) + + extended_abstract_question = find_question(session, "Extended abstract") + extended_abstract_info = ReviewQuestion( + form.id, "information", False, 4, question_id=extended_abstract_question.id + ) + + cv_question = find_question(session, "CV") + cv_info = ReviewQuestion( + form.id, "information", False, 5, question_id=cv_question.id, headline="CV" + ) + + financial_support_question = find_question(session, "Motivation") + financial_support_info = ReviewQuestion( + form.id, + "information", + False, + 6, + question_id=financial_support_question.id, + headline="Financial Support Motivation", + ) ten_scale = [ - {'value': 1, 'label': '1'}, - {'value': 2, 'label': '2'}, - {'value': 3, 'label': '3'}, - {'value': 4, 'label': '4'}, - {'value': 5, 'label': '5'}, - {'value': 6, 'label': '6'}, - {'value': 7, 'label': '7'}, - {'value': 8, 'label': '8'}, - {'value': 9, 'label': '9'}, - {'value': 10, 'label': '10'}, + {"value": 1, "label": "1"}, + {"value": 2, "label": "2"}, + {"value": 3, "label": "3"}, + {"value": 4, "label": "4"}, + {"value": 5, "label": "5"}, + {"value": 6, "label": "6"}, + {"value": 7, "label": "7"}, + {"value": 8, "label": "8"}, + {"value": 9, "label": "9"}, + {"value": 10, "label": "10"}, ] - benefit = ReviewQuestion(form.id, 'multi-choice', True, 7, description=""" + benefit = ReviewQuestion( + form.id, + "multi-choice", + True, + 7, + description=""" 1 - 4: the person would not benefit as they lack basic knowledge OR the application is incomplete (e.g. abstract has one short paragraph) 5: borderline, the person has some minimal knowledge and seem keen to learn more 6-7: the person has potential, they are enthusiastic about the field and/or the school, but they are not very strong (e.g. common for a highschool student or beginner) OR the person is good, but they could easily attend a similar event someplace else OR the person is good but have not put a lot of effort in the application 8-10: the person is technically very good and they are very enthusiastic (e.g. put effort in the application), OR the person is good and they would not be able to easily attend summer schools/conferences in the West (eg if they are affiliated to a smaller EE university) - """, headline='Would this person benefit from attending the school?', - options=ten_scale) - - school = ReviewQuestion(form.id, 'multi-choice', True, 8, description=""" + """, + headline="Would this person benefit from attending the school?", + options=ten_scale, + ) + + school = ReviewQuestion( + form.id, + "multi-choice", + True, + 8, + description=""" 1-4: they have poor technical skills 5-7: they have some technical skills, but nothing extraordinary 8-10: they have strong technical skills, they are enthusiastic, good communicators, exposure to different ML topics so could discuss on a variety of topics -""", headline='Would the school benefit from this person''s participation?', options=ten_scale) - - assessment = ReviewQuestion(form.id, 'long-text', True, 9, description=""" +""", + headline="Would the school benefit from this person" "s participation?", + options=ten_scale, + ) + + assessment = ReviewQuestion( + form.id, + "long-text", + True, + 9, + description=""" Write 2-3 sentences that explain the scores given above; also indicate the topic the person is working on -- this will help us obtain a good coverage of topics. -""", headline='Overall assessment') - - session.add_all([ - affiliation_info, - research_interests_info, - extended_abstract_type_info, - extended_abstract_info, - cv_info, - financial_support_info, - benefit, - school, - assessment - ]) +""", + headline="Overall assessment", + ) + + session.add_all( + [ + affiliation_info, + research_interests_info, + extended_abstract_type_info, + extended_abstract_info, + cv_info, + financial_support_info, + benefit, + school, + assessment, + ] + ) session.commit() config = ReviewConfiguration(form.id, 2) @@ -253,18 +334,32 @@ def upgrade(): def downgrade(): Base.metadata.bind = op.get_bind() session = orm.Session(bind=Base.metadata.bind) - - event = session.query(Event).filter_by(key='eeml2020').first() - application_form = session.query(ApplicationForm).filter_by(event_id=event.id).first() - form = session.query(ReviewForm).filter_by(application_form_id=application_form.id).first() + + event = session.query(Event).filter_by(key="eeml2020").first() + application_form = ( + session.query(ApplicationForm).filter_by(event_id=event.id).first() + ) + form = ( + session.query(ReviewForm) + .filter_by(application_form_id=application_form.id) + .first() + ) if form: session.query(ReviewQuestion).filter_by(review_form_id=form.id).delete() session.query(ReviewConfiguration).filter_by(review_form_id=form.id).delete() - - form = session.query(ReviewForm).filter_by(application_form_id=application_form.id).delete() - + + form = ( + session.query(ReviewForm) + .filter_by(application_form_id=application_form.id) + .delete() + ) + session.commit() session.flush() - op.get_bind().execute("""SELECT setval('review_form_id_seq', (SELECT max(id) FROM review_form));""") - op.get_bind().execute("""SELECT setval('review_question_id_seq', (SELECT max(id) FROM review_question));""") \ No newline at end of file + op.get_bind().execute( + """SELECT setval('review_form_id_seq', (SELECT max(id) FROM review_form));""" + ) + op.get_bind().execute( + """SELECT setval('review_question_id_seq', (SELECT max(id) FROM review_question));""" + ) diff --git a/api/migrations/versions/6fa47605c615_.py b/api/migrations/versions/6fa47605c615_.py index e441df90c..026c50b3e 100644 --- a/api/migrations/versions/6fa47605c615_.py +++ b/api/migrations/versions/6fa47605c615_.py @@ -7,43 +7,46 @@ """ # revision identifiers, used by Alembic. -revision = '6fa47605c615' -down_revision = 'b3f7a73c98f3' +revision = "6fa47605c615" +down_revision = "b3f7a73c98f3" -from alembic import op import sqlalchemy as sa +from alembic import op from sqlalchemy.dialects import postgresql + def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.create_table('response', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('application_form_id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=False), - sa.Column('is_submitted', sa.Boolean(), nullable=False), - sa.Column('submitted_timestamp', sa.DateTime(), nullable=True), - sa.Column('is_withdrawn', sa.Boolean(), nullable=False), - sa.Column('withdrawn_timestamp', sa.DateTime(), nullable=True), - sa.Column('started_timestamp', sa.DateTime(), nullable=True), - sa.ForeignKeyConstraint(['user_id'], ['app_user.id'] ), - sa.ForeignKeyConstraint(['application_form_id'], ['application_form.id'] ), - sa.PrimaryKeyConstraint('id') + op.create_table( + "response", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("application_form_id", sa.Integer(), nullable=False), + sa.Column("user_id", sa.Integer(), nullable=False), + sa.Column("is_submitted", sa.Boolean(), nullable=False), + sa.Column("submitted_timestamp", sa.DateTime(), nullable=True), + sa.Column("is_withdrawn", sa.Boolean(), nullable=False), + sa.Column("withdrawn_timestamp", sa.DateTime(), nullable=True), + sa.Column("started_timestamp", sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(["user_id"], ["app_user.id"]), + sa.ForeignKeyConstraint(["application_form_id"], ["application_form.id"]), + sa.PrimaryKeyConstraint("id"), ) - op.create_table('answer', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('response_id', sa.Integer(), nullable=False), - sa.Column('question_id', sa.Integer(), nullable=False), - sa.Column('value', sa.String(), nullable=False), - sa.ForeignKeyConstraint(['response_id'], ['response.id'] ), - sa.ForeignKeyConstraint(['question_id'], ['question.id'] ), - sa.PrimaryKeyConstraint('id') + op.create_table( + "answer", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("response_id", sa.Integer(), nullable=False), + sa.Column("question_id", sa.Integer(), nullable=False), + sa.Column("value", sa.String(), nullable=False), + sa.ForeignKeyConstraint(["response_id"], ["response.id"]), + sa.ForeignKeyConstraint(["question_id"], ["question.id"]), + sa.PrimaryKeyConstraint("id"), ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('answer') - op.drop_table('response') + op.drop_table("answer") + op.drop_table("response") # ### end Alembic commands ### diff --git a/api/migrations/versions/72548fc390d4_.py b/api/migrations/versions/72548fc390d4_.py index 302fe98c6..f0f6e13de 100644 --- a/api/migrations/versions/72548fc390d4_.py +++ b/api/migrations/versions/72548fc390d4_.py @@ -7,20 +7,22 @@ """ # revision identifiers, used by Alembic. -revision = '72548fc390d4' -down_revision = '0023aeda53b0' +revision = "72548fc390d4" +down_revision = "0023aeda53b0" -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.add_column('app_user', sa.Column('policy_agreed_datetime', sa.DateTime(), nullable=True)) + op.add_column( + "app_user", sa.Column("policy_agreed_datetime", sa.DateTime(), nullable=True) + ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('app_user', 'policy_agreed_datetime') + op.drop_column("app_user", "policy_agreed_datetime") # ### end Alembic commands ### diff --git a/api/migrations/versions/742a2a556eb9_.py b/api/migrations/versions/742a2a556eb9_.py index 9dc9a5eef..52fd5a9d9 100644 --- a/api/migrations/versions/742a2a556eb9_.py +++ b/api/migrations/versions/742a2a556eb9_.py @@ -7,22 +7,24 @@ """ # revision identifiers, used by Alembic. -revision = '742a2a556eb9' -down_revision = 'de12a6f382c1' +revision = "742a2a556eb9" +down_revision = "de12a6f382c1" -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.add_column('organisation', sa.Column('languages', sa.JSON(), nullable=True)) - op.execute("""UPDATE organisation SET languages = '[{"code": "en", "description": "English"}]'""") - op.alter_column('organisation', 'languages', nullable=False) + op.add_column("organisation", sa.Column("languages", sa.JSON(), nullable=True)) + op.execute( + """UPDATE organisation SET languages = '[{"code": "en", "description": "English"}]'""" + ) + op.alter_column("organisation", "languages", nullable=False) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('organisation', 'languages') + op.drop_column("organisation", "languages") # ### end Alembic commands ### diff --git a/api/migrations/versions/74f1f1476873_update_eeml_form.py b/api/migrations/versions/74f1f1476873_update_eeml_form.py index 74d4ad848..0c2263e7f 100644 --- a/api/migrations/versions/74f1f1476873_update_eeml_form.py +++ b/api/migrations/versions/74f1f1476873_update_eeml_form.py @@ -7,28 +7,52 @@ """ # revision identifiers, used by Alembic. -revision = '74f1f1476873' -down_revision = '87213d612eaf' +revision = "74f1f1476873" +down_revision = "87213d612eaf" -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): - op.execute(r"""UPDATE question SET validation_regex='^\W*(\w+(\W+|$)){150,500}$', validation_text='You must enter between 150 and 500 words' WHERE headline='Research Interests' and application_form_id=2""") - op.execute(r"""UPDATE question SET validation_regex='^\W*(\w+(\W+|$)){0,150}$', validation_text='You must enter fewer than 150 words' WHERE headline='Motivation' and application_form_id=2""") - op.execute(r"""UPDATE question SET description='Upload your extended abstract (PDF, max size 10MB)' where headline='Extended abstract' and application_form_id=2""") - op.execute(r"""UPDATE question SET description='Upload your CV (PDF, max size 10MB)' where headline='CV' and application_form_id=2""") - op.execute(r"""UPDATE organisation SET system_url='https://test-baobab.deeplearningindaba.com' where id=1""") - op.execute(r"""UPDATE organisation SET system_url='https://test-portal.eeml.eu' where id=2""") + op.execute( + r"""UPDATE question SET validation_regex='^\W*(\w+(\W+|$)){150,500}$', validation_text='You must enter between 150 and 500 words' WHERE headline='Research Interests' and application_form_id=2""" + ) + op.execute( + r"""UPDATE question SET validation_regex='^\W*(\w+(\W+|$)){0,150}$', validation_text='You must enter fewer than 150 words' WHERE headline='Motivation' and application_form_id=2""" + ) + op.execute( + r"""UPDATE question SET description='Upload your extended abstract (PDF, max size 10MB)' where headline='Extended abstract' and application_form_id=2""" + ) + op.execute( + r"""UPDATE question SET description='Upload your CV (PDF, max size 10MB)' where headline='CV' and application_form_id=2""" + ) + op.execute( + r"""UPDATE organisation SET system_url='https://test-baobab.deeplearningindaba.com' where id=1""" + ) + op.execute( + r"""UPDATE organisation SET system_url='https://test-portal.eeml.eu' where id=2""" + ) op.execute(r"""UPDATE organisation SET system_url='http://localhost' where id=3""") def downgrade(): - op.execute(r"""UPDATE question SET validation_regex='^.{500,2000}$', validation_text='You must enter between 500 and 2000 characters' WHERE headline='Research Interests' and application_form_id=2""") - op.execute(r"""UPDATE question SET validation_regex='^.{0,500}$', validation_text='Maximum 500 characters' WHERE headline='Motivation' and application_form_id=2""") - op.execute(r"""UPDATE question SET description='' where headline='Extended abstract' and application_form_id=2""") - op.execute(r"""UPDATE question SET description='' where headline='CV' and application_form_id=2""") - op.execute(r"""UPDATE organisation SET system_url='test-baobab.deeplearningindaba.com' where id=1""") - op.execute(r"""UPDATE organisation SET system_url='test-portal.eeml.eu' where id=2""") + op.execute( + r"""UPDATE question SET validation_regex='^.{500,2000}$', validation_text='You must enter between 500 and 2000 characters' WHERE headline='Research Interests' and application_form_id=2""" + ) + op.execute( + r"""UPDATE question SET validation_regex='^.{0,500}$', validation_text='Maximum 500 characters' WHERE headline='Motivation' and application_form_id=2""" + ) + op.execute( + r"""UPDATE question SET description='' where headline='Extended abstract' and application_form_id=2""" + ) + op.execute( + r"""UPDATE question SET description='' where headline='CV' and application_form_id=2""" + ) + op.execute( + r"""UPDATE organisation SET system_url='test-baobab.deeplearningindaba.com' where id=1""" + ) + op.execute( + r"""UPDATE organisation SET system_url='test-portal.eeml.eu' where id=2""" + ) op.execute(r"""UPDATE organisation SET system_url='localhost' where id=3""") diff --git a/api/migrations/versions/76c9226545bb_update_review_assigned_template.py b/api/migrations/versions/76c9226545bb_update_review_assigned_template.py index d932ac4b7..afcb7e074 100644 --- a/api/migrations/versions/76c9226545bb_update_review_assigned_template.py +++ b/api/migrations/versions/76c9226545bb_update_review_assigned_template.py @@ -8,25 +8,25 @@ """ # revision identifiers, used by Alembic. -revision = '76c9226545bb' -down_revision = '303140b3cefb' +revision = "76c9226545bb" +down_revision = "303140b3cefb" + +import datetime -from alembic import op import sqlalchemy as sa +from alembic import op from sqlalchemy import orm from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy import orm + from app import db -import datetime Base = declarative_base() - class EmailTemplate(Base): - __tablename__ = 'email_template' - __table_args__ = {'extend_existing': True} + __tablename__ = "email_template" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) key = db.Column(db.String(50), nullable=False) @@ -44,7 +44,11 @@ def __init__(self, key, event_id, subject, template, language): def upgrade(): - en = db.session.query(EmailTemplate).filter_by(language='en', key='reviews-assigned').first() + en = ( + db.session.query(EmailTemplate) + .filter_by(language="en", key="reviews-assigned") + .first() + ) en.template = """Dear {title} {firstname} {lastname}, You have been assigned {num_reviews} reviews on {system_name}. Please visit {baobab_host}/{event_key}/reviewlist to begin. @@ -54,7 +58,11 @@ def upgrade(): Kind Regards, The {event_name} Organisers""" - fr = db.session.query(EmailTemplate).filter_by(language='fr', key='reviews-assigned').first() + fr = ( + db.session.query(EmailTemplate) + .filter_by(language="fr", key="reviews-assigned") + .first() + ) fr.template = """Madame / Monsieur {lastname}, {num_reviews} tâches de révision vous ont été attribuées dans {system_name}. Veuillez consulter {baobab_host}/{event_key}/reviewlist pour commencer. @@ -69,7 +77,9 @@ def upgrade(): def downgrade(): - en = db.session.query(EmailTemplate).filter_by(language='en', key='reviews-assigned') + en = db.session.query(EmailTemplate).filter_by( + language="en", key="reviews-assigned" + ) en.template = """Dear {title} {firstname} {lastname}, You have been assigned {num_reviews} reviews on {system_name}. Please visit {baobab_host}/{event_key}/review to begin. @@ -80,7 +90,9 @@ def downgrade(): Kind Regards, The {event_name} Organisers""" - fr = db.session.query(EmailTemplate).filter_by(language='fr', key='reviews-assigned') + fr = db.session.query(EmailTemplate).filter_by( + language="fr", key="reviews-assigned" + ) fr.template = """Madame / Monsieur {lastname}, {num_reviews} tâches de révision vous ont été attribuées dans {system_name}. Veuillez consulter {baobab_host}/{event_key}/review pour commencer. diff --git a/api/migrations/versions/78f4d11203c5_ai4d_more_changes.py b/api/migrations/versions/78f4d11203c5_ai4d_more_changes.py index 16b8d0cbc..fa2d1cdff 100644 --- a/api/migrations/versions/78f4d11203c5_ai4d_more_changes.py +++ b/api/migrations/versions/78f4d11203c5_ai4d_more_changes.py @@ -9,26 +9,27 @@ """ # revision identifiers, used by Alembic. -revision = '78f4d11203c5' -down_revision = 'ea987b257ee0' +revision = "78f4d11203c5" +down_revision = "ea987b257ee0" + +import datetime +from enum import Enum -from alembic import op import sqlalchemy as sa -from sqlalchemy import orm +from alembic import op +from sqlalchemy import orm, select from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy import orm -from sqlalchemy import select from sqlalchemy.orm import column_property + from app import db -import datetime -from enum import Enum Base = declarative_base() + class Organisation(Base): __tablename__ = "organisation" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(50), nullable=False) @@ -40,7 +41,17 @@ class Organisation(Base): email_from = db.Column(db.String(100), nullable=True) system_url = db.Column(db.String(100), nullable=False) - def __init__(self, name, system_name, small_logo, large_logo, domain, url, email_from, system_url): + def __init__( + self, + name, + system_name, + small_logo, + large_logo, + domain, + url, + email_from, + system_url, + ): self.name = name self.small_logo = small_logo self.large_logo = large_logo @@ -50,9 +61,10 @@ def __init__(self, name, system_name, small_logo, large_logo, domain, url, email self.email_from = email_from self.system_url = system_url + class Country(Base): __tablename__ = "country" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(100), nullable=False) @@ -60,22 +72,25 @@ class Country(Base): def __init__(self, name): self.name = name + class EventType(Enum): - EVENT = 'event' - AWARD = 'award' - CALL = 'call' + EVENT = "event" + AWARD = "award" + CALL = "call" class Event(Base): __tablename__ = "event" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) start_date = db.Column(db.DateTime(), nullable=False) end_date = db.Column(db.DateTime(), nullable=False) key = db.Column(db.String(255), nullable=False, unique=True) - organisation_id = db.Column(db.Integer(), db.ForeignKey('organisation.id'), nullable=False) + organisation_id = db.Column( + db.Integer(), db.ForeignKey("organisation.id"), nullable=False + ) email_from = db.Column(db.String(255), nullable=False) url = db.Column(db.String(255), nullable=False) application_open = db.Column(db.DateTime(), nullable=False) @@ -92,7 +107,7 @@ class Event(Base): travel_grant = db.Column(db.Boolean(), nullable=False) miniconf_url = db.Column(db.String(100), nullable=True) - event_translations = db.relationship('EventTranslation', lazy='dynamic') + event_translations = db.relationship("EventTranslation", lazy="dynamic") def __init__( self, @@ -116,7 +131,7 @@ def __init__( registration_close, event_type, travel_grant, - miniconf_url=None + miniconf_url=None, ): self.start_date = start_date self.end_date = end_date @@ -148,10 +163,11 @@ def add_event_translations(self, names, descriptions): event_translation = EventTranslation(name, description, language) self.event_translations.append(event_translation) + class EventTranslation(Base): __tablename__ = "event_translation" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) event_id = db.Column(db.Integer(), db.ForeignKey("event.id"), nullable=False) @@ -159,19 +175,20 @@ class EventTranslation(Base): description = db.Column(db.String(255), nullable=False) language = db.Column(db.String(2)) - event = db.relationship('Event', foreign_keys=[event_id]) + event = db.relationship("Event", foreign_keys=[event_id]) def __init__(self, name, description, language): self.name = name self.description = description self.language = language + class ApplicationForm(Base): - __tablename__ = 'application_form' - __table_args__ = {'extend_existing': True} + __tablename__ = "application_form" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - event_id = db.Column(db.Integer(), db.ForeignKey('event.id'), nullable=False) + event_id = db.Column(db.Integer(), db.ForeignKey("event.id"), nullable=False) is_open = db.Column(db.Boolean(), nullable=False) nominations = db.Column(db.Boolean(), nullable=False) @@ -180,22 +197,29 @@ def __init__(self, event_id, is_open, nominations): self.is_open = is_open self.nominations = nominations + class Question(Base): - __tablename__ = 'question' - __table_args__ = {'extend_existing': True} + __tablename__ = "question" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) - section_id = db.Column(db.Integer(), db.ForeignKey('section.id'), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) + section_id = db.Column(db.Integer(), db.ForeignKey("section.id"), nullable=False) type = db.Column(db.String(), nullable=False) order = db.Column(db.Integer(), nullable=False) is_required = db.Column(db.Boolean(), nullable=False) - depends_on_question_id = db.Column(db.Integer(), db.ForeignKey('question.id'), nullable=True) + depends_on_question_id = db.Column( + db.Integer(), db.ForeignKey("question.id"), nullable=True + ) key = db.Column(db.String(255), nullable=True) - question_translations = db.relationship('QuestionTranslation', lazy='dynamic') + question_translations = db.relationship("QuestionTranslation", lazy="dynamic") - def __init__(self, application_form_id, section_id, order, questionType, is_required=True): + def __init__( + self, application_form_id, section_id, order, questionType, is_required=True + ): self.application_form_id = application_form_id self.section_id = section_id self.order = order @@ -203,45 +227,55 @@ def __init__(self, application_form_id, section_id, order, questionType, is_requ self.is_required = is_required def get_translation(self, language): - question_translation = self.question_translations.filter_by(language=language).first() + question_translation = self.question_translations.filter_by( + language=language + ).first() return question_translation class Section(Base): - __tablename__ = 'section' - __table_args__ = {'extend_existing': True} + __tablename__ = "section" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) order = db.Column(db.Integer(), nullable=False) - depends_on_question_id = db.Column(db.Integer(), db.ForeignKey('question.id', use_alter=True), nullable=True) + depends_on_question_id = db.Column( + db.Integer(), db.ForeignKey("question.id", use_alter=True), nullable=True + ) key = db.Column(db.String(255), nullable=True) - section_translations = db.relationship('SectionTranslation', lazy='dynamic') + section_translations = db.relationship("SectionTranslation", lazy="dynamic") - def __init__(self, application_form_id, order, depends_on_question_id=None, key=None): + def __init__( + self, application_form_id, order, depends_on_question_id=None, key=None + ): self.application_form_id = application_form_id self.order = order self.depends_on_question_id = depends_on_question_id self.key = key def get_translation(self, language): - section_translation = self.section_translations.filter_by(language=language).first() + section_translation = self.section_translations.filter_by( + language=language + ).first() return section_translation class SectionTranslation(Base): - __tablename__ = 'section_translation' - __table_args__ = {'extend_existing': True} + __tablename__ = "section_translation" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - section_id = db.Column(db.Integer(), db.ForeignKey('section.id'), nullable=False) + section_id = db.Column(db.Integer(), db.ForeignKey("section.id"), nullable=False) language = db.Column(db.String(2), nullable=False) name = db.Column(db.String(255), nullable=False) description = db.Column(db.String(), nullable=False) show_for_values = db.Column(db.JSON(), nullable=True) - section = db.relationship('Section', foreign_keys=[section_id]) + section = db.relationship("Section", foreign_keys=[section_id]) def __init__(self, section_id, language, name, description, show_for_values=None): self.section_id = section_id @@ -252,11 +286,11 @@ def __init__(self, section_id, language, name, description, show_for_values=None class QuestionTranslation(Base): - __tablename__ = 'question_translation' - __table_args__ = {'extend_existing': True} + __tablename__ = "question_translation" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - question_id = db.Column(db.Integer(), db.ForeignKey('question.id'), nullable=False) + question_id = db.Column(db.Integer(), db.ForeignKey("question.id"), nullable=False) language = db.Column(db.String(2), nullable=False) headline = db.Column(db.String(), nullable=False) description = db.Column(db.String(), nullable=True) @@ -266,7 +300,7 @@ class QuestionTranslation(Base): options = db.Column(db.JSON(), nullable=True) show_for_values = db.Column(db.JSON(), nullable=True) - question = db.relationship('Question', foreign_keys=[question_id]) + question = db.relationship("Question", foreign_keys=[question_id]) def __init__( self, @@ -278,7 +312,7 @@ def __init__( validation_regex=None, validation_text=None, options=None, - show_for_values=None + show_for_values=None, ): self.question_id = question_id self.language = language @@ -293,8 +327,8 @@ def __init__( class EmailTemplate(Base): - __tablename__ = 'email_template' - __table_args__ = {'extend_existing': True} + __tablename__ = "email_template" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) key = db.Column(db.String(50), nullable=False) @@ -315,29 +349,37 @@ def upgrade(): Base.metadata.bind = op.get_bind() session = orm.Session(bind=Base.metadata.bind) - event = session.query(Event).filter_by(key='prc').first() + event = session.query(Event).filter_by(key="prc").first() form = session.query(ApplicationForm).filter_by(event_id=event.id).first() def get_question_by_en_headline(en_headline): - en = (session.query(QuestionTranslation) - .filter_by(headline=en_headline, language='en') - .join(Question) - .filter_by(application_form_id=form.id) - .first()) + en = ( + session.query(QuestionTranslation) + .filter_by(headline=en_headline, language="en") + .join(Question) + .filter_by(application_form_id=form.id) + .first() + ) question = en.question - fr = question.get_translation('fr') + fr = question.get_translation("fr") return question, en, fr - section_en = session.query(SectionTranslation).filter_by( - name='Section IV: Budget', language='en').first() + section_en = ( + session.query(SectionTranslation) + .filter_by(name="Section IV: Budget", language="en") + .first() + ) section_en.description = """Please submit a full three year budget with notes using the Excel template available on the IDRC public website: [https://www.idrc.ca/en/resources/guides-and-forms](https://www.idrc.ca/en/resources/guides-and-forms) (Select the ‘Proposal budget’ and download.) Applications that do not submit a complete budget in this template will not be considered. """ - section_fr = session.query(SectionTranslation).filter_by( - name='Section IV: Budget', language='fr').first() + section_fr = ( + session.query(SectionTranslation) + .filter_by(name="Section IV: Budget", language="fr") + .first() + ) section_en.description = """Veuillez soumettre un budget triennal complet comprenant des notes en utilisant la feuille Excel qui est disponible sur le site Web public du CRDI, à la page suivante: [https://www.idrc.ca/fr/ressources/guides-et-formulaires](https://www.idrc.ca/en/resources/guides-and-forms) Cliquez sur ‘proposition de projet’, puis téléchargez la feuille Excel. Les demandes qui ne comprennent pas un budget complet préparé au moyen de ce modèle ne seront pas prises en considération. @@ -345,16 +387,18 @@ def get_question_by_en_headline(en_headline): Insérez ce budget en tant que pièce jointe à votre demande. Dans le cas contraire, elle sera rejetée. """ - question, en, fr = get_question_by_en_headline('How many people in total work in your organization?') - en.validation_regex = r'\d*' - en.validation_text = 'You must enter a number' - fr.validation_regex = r'\d*' - fr.validation_text = 'Vous devez entrer un nombre' + question, en, fr = get_question_by_en_headline( + "How many people in total work in your organization?" + ) + en.validation_regex = r"\d*" + en.validation_text = "You must enter a number" + fr.validation_regex = r"\d*" + fr.validation_text = "Vous devez entrer un nombre" call_email_template_en = EmailTemplate( - 'confirmation-response-call', + "confirmation-response-call", event.id, - 'Your response to the {event_name}', + "Your response to the {event_name}", """Dear {title} {firstname} {lastname}, Thank you for responding to the {event_name}. Your application is being reviewed by our committee and we will get back to you as soon as possible. Included below is a copy of your responses. @@ -362,13 +406,15 @@ def get_question_by_en_headline(en_headline): {question_answer_summary} Kind Regards, -The {event_name} Team""", 'en') +The {event_name} Team""", + "en", + ) session.add(call_email_template_en) call_email_template_fr = EmailTemplate( - 'confirmation-response-call', + "confirmation-response-call", event.id, - 'Votre demande pour {event_name}', + "Votre demande pour {event_name}", """Madame / Monsieur {lastname}, Merci d’avoir soumis votre demande de participation à {event_name}. Votre demande sera examinée par notre comité et nous vous répondrons dès que possible. Vous trouverez ci-dessous une copie de vos réponses. @@ -376,7 +422,9 @@ def get_question_by_en_headline(en_headline): {question_answer_summary} Cordialement, -L’équipe de {event_name}""", 'fr') +L’équipe de {event_name}""", + "fr", + ) session.add(call_email_template_fr) session.commit() @@ -385,5 +433,5 @@ def get_question_by_en_headline(en_headline): def downgrade(): Base.metadata.bind = op.get_bind() session = orm.Session(bind=Base.metadata.bind) - session.query(EmailTemplate).filter_by(key='confirmation-response-call').delete() + session.query(EmailTemplate).filter_by(key="confirmation-response-call").delete() session.commit() diff --git a/api/migrations/versions/794506c08e10_.py b/api/migrations/versions/794506c08e10_.py index 495ca5d97..c9fb0f9f1 100644 --- a/api/migrations/versions/794506c08e10_.py +++ b/api/migrations/versions/794506c08e10_.py @@ -7,44 +7,51 @@ """ # revision identifiers, used by Alembic. -revision = '794506c08e10' -down_revision = '1afd8dfc8d05' +revision = "794506c08e10" +down_revision = "1afd8dfc8d05" -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.add_column('review_score', sa.Column('is_active', sa.Boolean, nullable=True)) - op.add_column('guest_registration_answer', sa.Column('is_active', sa.Boolean(), nullable=True)) - op.add_column('answer', sa.Column('is_active', sa.Boolean(), nullable=True)) - - op.add_column('review_score', sa.Column('created_on', sa.TIMESTAMP, nullable=True)) - op.add_column('guest_registration_answer', sa.Column('created_on', sa.TIMESTAMP, nullable=True)) - op.add_column('answer', sa.Column('created_on', sa.TIMESTAMP, nullable=True)) + op.add_column("review_score", sa.Column("is_active", sa.Boolean, nullable=True)) + op.add_column( + "guest_registration_answer", sa.Column("is_active", sa.Boolean(), nullable=True) + ) + op.add_column("answer", sa.Column("is_active", sa.Boolean(), nullable=True)) + + op.add_column("review_score", sa.Column("created_on", sa.TIMESTAMP, nullable=True)) + op.add_column( + "guest_registration_answer", + sa.Column("created_on", sa.TIMESTAMP, nullable=True), + ) + op.add_column("answer", sa.Column("created_on", sa.TIMESTAMP, nullable=True)) op.execute("UPDATE review_score SET is_active = true, created_on=NOW()") - op.execute("UPDATE guest_registration_answer SET is_active = true, created_on=NOW()") + op.execute( + "UPDATE guest_registration_answer SET is_active = true, created_on=NOW()" + ) op.execute("UPDATE answer SET is_active = true, created_on=NOW()") - op.alter_column('review_score', 'is_active', nullable=False) - op.alter_column('guest_registration_answer', 'is_active', nullable=False) - op.alter_column('answer', 'is_active', nullable=False) + op.alter_column("review_score", "is_active", nullable=False) + op.alter_column("guest_registration_answer", "is_active", nullable=False) + op.alter_column("answer", "is_active", nullable=False) - op.alter_column('review_score', 'created_on', nullable=False) - op.alter_column('guest_registration_answer', 'created_on', nullable=False) - op.alter_column('answer', 'created_on', nullable=False) + op.alter_column("review_score", "created_on", nullable=False) + op.alter_column("guest_registration_answer", "created_on", nullable=False) + op.alter_column("answer", "created_on", nullable=False) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('answer', 'is_active') - op.drop_column('guest_registration_answer', 'is_active') - op.drop_column('review_score', 'is_active') + op.drop_column("answer", "is_active") + op.drop_column("guest_registration_answer", "is_active") + op.drop_column("review_score", "is_active") - op.drop_column('answer', 'created_on') - op.drop_column('guest_registration_answer', 'created_on') - op.drop_column('review_score', 'created_on') + op.drop_column("answer", "created_on") + op.drop_column("guest_registration_answer", "created_on") + op.drop_column("review_score", "created_on") # ### end Alembic commands ### diff --git a/api/migrations/versions/79c61673a487_.py b/api/migrations/versions/79c61673a487_.py index eccad06be..46719d443 100644 --- a/api/migrations/versions/79c61673a487_.py +++ b/api/migrations/versions/79c61673a487_.py @@ -7,22 +7,26 @@ """ # revision identifiers, used by Alembic. -revision = '79c61673a487' -down_revision = '539d0567cd6c' +revision = "79c61673a487" +down_revision = "539d0567cd6c" -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.add_column('offer', sa.Column('accepted_accommodation_award', sa.Boolean(), nullable=True)) - op.add_column('offer', sa.Column('accepted_travel_award', sa.Boolean(), nullable=True)) + op.add_column( + "offer", sa.Column("accepted_accommodation_award", sa.Boolean(), nullable=True) + ) + op.add_column( + "offer", sa.Column("accepted_travel_award", sa.Boolean(), nullable=True) + ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('offer', 'accepted_travel_award') - op.drop_column('offer', 'accepted_accommodation_award') + op.drop_column("offer", "accepted_travel_award") + op.drop_column("offer", "accepted_accommodation_award") # ### end Alembic commands ### diff --git a/api/migrations/versions/7e8bffa88454_.py b/api/migrations/versions/7e8bffa88454_.py index ab9a83ce6..23ce297ed 100644 --- a/api/migrations/versions/7e8bffa88454_.py +++ b/api/migrations/versions/7e8bffa88454_.py @@ -7,24 +7,25 @@ """ # revision identifiers, used by Alembic. -revision = '7e8bffa88454' -down_revision = 'c06c236ad4c5' +revision = "7e8bffa88454" +down_revision = "c06c236ad4c5" -from alembic import op import sqlalchemy as sa +from alembic import op from sqlalchemy.dialects import postgresql + def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.alter_column('offer', 'responded_at', - existing_type=postgresql.TIMESTAMP(), - nullable=True) + op.alter_column( + "offer", "responded_at", existing_type=postgresql.TIMESTAMP(), nullable=True + ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.alter_column('offer', 'responded_at', - existing_type=postgresql.TIMESTAMP(), - nullable=False) + op.alter_column( + "offer", "responded_at", existing_type=postgresql.TIMESTAMP(), nullable=False + ) # ### end Alembic commands ### diff --git a/api/migrations/versions/7f37bf248c81_add_org_icon.py b/api/migrations/versions/7f37bf248c81_add_org_icon.py index c18715807..e15c50498 100644 --- a/api/migrations/versions/7f37bf248c81_add_org_icon.py +++ b/api/migrations/versions/7f37bf248c81_add_org_icon.py @@ -7,22 +7,34 @@ """ # revision identifiers, used by Alembic. -revision = '7f37bf248c81' -down_revision = '2279e1fa2e49' +revision = "7f37bf248c81" +down_revision = "2279e1fa2e49" -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): - op.add_column('organisation', sa.Column('icon_logo', sa.String(length=100), nullable=True)) + op.add_column( + "organisation", sa.Column("icon_logo", sa.String(length=100), nullable=True) + ) op.execute("""UPDATE organisation SET icon_logo=small_logo""") - op.execute("""UPDATE organisation SET small_logo='indaba_logo_cufflink_small.png' WHERE system_name IN ('Baobab', 'Baobab Dev', 'integration_test')""") - op.execute("""UPDATE organisation SET small_logo='eeml_logo_black_small.png' WHERE system_name IN ('EEML Portal')""") - op.alter_column('organisation', 'icon_logo', existing_type=sa.String(), nullable=False) + op.execute( + """UPDATE organisation SET small_logo='indaba_logo_cufflink_small.png' WHERE system_name IN ('Baobab', 'Baobab Dev', 'integration_test')""" + ) + op.execute( + """UPDATE organisation SET small_logo='eeml_logo_black_small.png' WHERE system_name IN ('EEML Portal')""" + ) + op.alter_column( + "organisation", "icon_logo", existing_type=sa.String(), nullable=False + ) def downgrade(): - op.drop_column('organisation', 'icon_logo') - op.execute("""UPDATE organisation SET small_logo='logo-32x32-white.png' WHERE system_name IN ('Baobab', 'Baobab Dev', 'integration_test')""") - op.execute("""UPDATE organisation SET small_logo='logo_eeml_white.png' WHERE system_name IN ('EEML Portal')""") + op.drop_column("organisation", "icon_logo") + op.execute( + """UPDATE organisation SET small_logo='logo-32x32-white.png' WHERE system_name IN ('Baobab', 'Baobab Dev', 'integration_test')""" + ) + op.execute( + """UPDATE organisation SET small_logo='logo_eeml_white.png' WHERE system_name IN ('EEML Portal')""" + ) diff --git a/api/migrations/versions/7fced3d22209_.py b/api/migrations/versions/7fced3d22209_.py index adfea9198..be5f39fcc 100644 --- a/api/migrations/versions/7fced3d22209_.py +++ b/api/migrations/versions/7fced3d22209_.py @@ -7,60 +7,66 @@ """ # revision identifiers, used by Alembic. -revision = '7fced3d22209' -down_revision = '32c1ae6dd23a' +revision = "7fced3d22209" +down_revision = "32c1ae6dd23a" -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.alter_column('app_user', 'affiliation', - existing_type=sa.VARCHAR(length=255), - nullable=True) - op.alter_column('app_user', 'department', - existing_type=sa.VARCHAR(length=255), - nullable=True) - op.alter_column('app_user', 'nationality_country_id', - existing_type=sa.INTEGER(), - nullable=True) - op.alter_column('app_user', 'residence_country_id', - existing_type=sa.INTEGER(), - nullable=True) - op.alter_column('app_user', 'user_category_id', - existing_type=sa.INTEGER(), - nullable=True) - op.alter_column('app_user', 'user_disability', - existing_type=sa.VARCHAR(length=255), - nullable=True) - op.alter_column('app_user', 'user_gender', - existing_type=sa.VARCHAR(length=20), - nullable=True) + op.alter_column( + "app_user", "affiliation", existing_type=sa.VARCHAR(length=255), nullable=True + ) + op.alter_column( + "app_user", "department", existing_type=sa.VARCHAR(length=255), nullable=True + ) + op.alter_column( + "app_user", "nationality_country_id", existing_type=sa.INTEGER(), nullable=True + ) + op.alter_column( + "app_user", "residence_country_id", existing_type=sa.INTEGER(), nullable=True + ) + op.alter_column( + "app_user", "user_category_id", existing_type=sa.INTEGER(), nullable=True + ) + op.alter_column( + "app_user", + "user_disability", + existing_type=sa.VARCHAR(length=255), + nullable=True, + ) + op.alter_column( + "app_user", "user_gender", existing_type=sa.VARCHAR(length=20), nullable=True + ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.alter_column('app_user', 'user_gender', - existing_type=sa.VARCHAR(length=20), - nullable=False) - op.alter_column('app_user', 'user_disability', - existing_type=sa.VARCHAR(length=255), - nullable=False) - op.alter_column('app_user', 'user_category_id', - existing_type=sa.INTEGER(), - nullable=False) - op.alter_column('app_user', 'residence_country_id', - existing_type=sa.INTEGER(), - nullable=False) - op.alter_column('app_user', 'nationality_country_id', - existing_type=sa.INTEGER(), - nullable=False) - op.alter_column('app_user', 'department', - existing_type=sa.VARCHAR(length=255), - nullable=False) - op.alter_column('app_user', 'affiliation', - existing_type=sa.VARCHAR(length=255), - nullable=False) + op.alter_column( + "app_user", "user_gender", existing_type=sa.VARCHAR(length=20), nullable=False + ) + op.alter_column( + "app_user", + "user_disability", + existing_type=sa.VARCHAR(length=255), + nullable=False, + ) + op.alter_column( + "app_user", "user_category_id", existing_type=sa.INTEGER(), nullable=False + ) + op.alter_column( + "app_user", "residence_country_id", existing_type=sa.INTEGER(), nullable=False + ) + op.alter_column( + "app_user", "nationality_country_id", existing_type=sa.INTEGER(), nullable=False + ) + op.alter_column( + "app_user", "department", existing_type=sa.VARCHAR(length=255), nullable=False + ) + op.alter_column( + "app_user", "affiliation", existing_type=sa.VARCHAR(length=255), nullable=False + ) # ### end Alembic commands ### diff --git a/api/migrations/versions/8339a70d7345_small_ai4d_update.py b/api/migrations/versions/8339a70d7345_small_ai4d_update.py index 2b388e686..f2738d93a 100644 --- a/api/migrations/versions/8339a70d7345_small_ai4d_update.py +++ b/api/migrations/versions/8339a70d7345_small_ai4d_update.py @@ -9,19 +9,28 @@ """ # revision identifiers, used by Alembic. -revision = '8339a70d7345' -down_revision = 'cb503c2afd08' +revision = "8339a70d7345" +down_revision = "cb503c2afd08" -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): - op.execute("""update question_translation set headline='Website (Optional)' where headline='Website'""") - op.execute("""update question_translation set headline='Social media handles (Optional)' where headline='Social media handles'""") - op.execute("""update question_translation set headline='Pseudonymes sur les médias sociaux (facultatif)' where headline='Pseudonymes sur les médias sociaux'""") - op.execute("""update question_translation set headline='Site Web (facultatif)' where headline='Site Web'""") - + op.execute( + """update question_translation set headline='Website (Optional)' where headline='Website'""" + ) + op.execute( + """update question_translation set headline='Social media handles (Optional)' where headline='Social media handles'""" + ) + op.execute( + """update question_translation set headline='Pseudonymes sur les médias sociaux (facultatif)' where headline='Pseudonymes sur les médias sociaux'""" + ) + op.execute( + """update question_translation set headline='Site Web (facultatif)' where headline='Site Web'""" + ) + + def downgrade(): # ### commands auto generated by Alembic - please adjust! ### pass diff --git a/api/migrations/versions/838a2bd9a618_add_integration_test_organization.py b/api/migrations/versions/838a2bd9a618_add_integration_test_organization.py index a1f35934d..8f415b32b 100644 --- a/api/migrations/versions/838a2bd9a618_add_integration_test_organization.py +++ b/api/migrations/versions/838a2bd9a618_add_integration_test_organization.py @@ -7,58 +7,56 @@ """ -# revision identifiers, used by Alembic. -from sqlalchemy.sql import table, column import sqlalchemy as sa from alembic import op -revision = '838a2bd9a618' -down_revision = 'c3c5ef958ca1' +# revision identifiers, used by Alembic. +from sqlalchemy.sql import column, table -def upgrade(): - organisation = table('organisation', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('name', sa.String( - length=50), nullable=False), - sa.Column('small_logo', sa.String( - length=100), nullable=False), - sa.Column('large_logo', sa.String( - length=100), nullable=False), - sa.Column('domain', sa.String( - length=100), nullable=False), - sa.Column('url', sa.String(100), nullable=False), - sa.Column('email_from', sa.String( - 100), nullable=True), - sa.Column('system_url', sa.String( - 100), nullable=False), - sa.Column('privacy_policy', sa.String( - 100), nullable=False), - sa.Column('system_name', sa.String( - 100), nullable=False) - ) +revision = "838a2bd9a618" +down_revision = "c3c5ef958ca1" - op.bulk_insert(organisation, - [ - { - 'id': 4, - 'name': 'Cypress Integration Test', - 'small_logo': 'logo-32x32-white.png', - 'large_logo': 'indaba-logo-dark.png', - 'domain': 'webappintegration', - 'url': 'www.deeplearningindaba.com', - 'email_from': 'Integration Test', - 'system_url': 'Integration Test', - 'privacy_policy': 'PrivacyPolicy.pdf', - 'system_name': 'integration_test' - } - ] - ) + +def upgrade(): + organisation = table( + "organisation", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("name", sa.String(length=50), nullable=False), + sa.Column("small_logo", sa.String(length=100), nullable=False), + sa.Column("large_logo", sa.String(length=100), nullable=False), + sa.Column("domain", sa.String(length=100), nullable=False), + sa.Column("url", sa.String(100), nullable=False), + sa.Column("email_from", sa.String(100), nullable=True), + sa.Column("system_url", sa.String(100), nullable=False), + sa.Column("privacy_policy", sa.String(100), nullable=False), + sa.Column("system_name", sa.String(100), nullable=False), + ) + + op.bulk_insert( + organisation, + [ + { + "id": 4, + "name": "Cypress Integration Test", + "small_logo": "logo-32x32-white.png", + "large_logo": "indaba-logo-dark.png", + "domain": "webappintegration", + "url": "www.deeplearningindaba.com", + "email_from": "Integration Test", + "system_url": "Integration Test", + "privacy_policy": "PrivacyPolicy.pdf", + "system_name": "integration_test", + } + ], + ) op.get_bind().execute( - """SELECT setval('organisation_id_seq', (SELECT max(id) FROM organisation));""") + """SELECT setval('organisation_id_seq', (SELECT max(id) FROM organisation));""" + ) def downgrade(): - op.get_bind().execute('DELETE FROM organisation where id = 4') + op.get_bind().execute("DELETE FROM organisation where id = 4") op.get_bind().execute( - """SELECT setval('organisation_id_seq', (SELECT max(id) FROM organisation));""") + """SELECT setval('organisation_id_seq', (SELECT max(id) FROM organisation));""" + ) diff --git a/api/migrations/versions/85ce7069fb96_.py b/api/migrations/versions/85ce7069fb96_.py index 3e16c9c96..7be57e93d 100644 --- a/api/migrations/versions/85ce7069fb96_.py +++ b/api/migrations/versions/85ce7069fb96_.py @@ -7,12 +7,12 @@ """ # revision identifiers, used by Alembic. -revision = '85ce7069fb96' -down_revision = '33e175de61ca' +revision = "85ce7069fb96" +down_revision = "33e175de61ca" -from alembic import op import sqlalchemy as sa -from sqlalchemy.sql import table, column +from alembic import op +from sqlalchemy.sql import column, table from app.email_template.models import EmailTemplate @@ -20,33 +20,33 @@ def upgrade(): # ### commands auto generated by Alembic - please adjust! ### email_template = table( - 'email_template', - column('id', sa.Integer()), - column('key', sa.String(50)), - column('event_id', sa.Integer()), - column('template', sa.String()) + "email_template", + column("id", sa.Integer()), + column("key", sa.String(50)), + column("event_id", sa.Integer()), + column("template", sa.String()), ) op.bulk_insert( email_template, [ { - 'id': 1, - 'key': 'application-not-submitted', - 'event_id': None, - 'template': """ + "id": 1, + "key": "application-not-submitted", + "event_id": None, + "template": """ Dear {title} {firstname} {lastname}, We noticed that you started applying to attend the {event} but have not completed and submitted your application. This is a final reminder that you have until {deadline} to submit your application in order to be considered. Please complete and submit your application if you would still like to attend this event. We have noticed that some people were confused about the "check your answers" page and may not have clicked on the Submit button on this page. Please be aware that you must click on the Submit button to confirm your application. If you do not receive an email from us with a copy of your answers, your application will not be considered. Kind Regards, The {organisation_name} Team -""" +""", }, { - 'id': 2, - 'key': 'application-not-started', - 'event_id': None, - 'template': """ + "id": 2, + "key": "application-not-started", + "event_id": None, + "template": """ Dear {title} {firstname} {lastname}, WE HAVE NOT RECEIVED YOUR APPLICATION TO ATTEND {event} We noticed that you have created a {system_name} account, but have not yet started an application to attend {event}. @@ -55,24 +55,24 @@ def upgrade(): Please ensure you have submitted your application before this date if you would still like to attend this event. Kind Regards, The {organisation_name} Team -""" +""", }, { - 'id': 3, - 'key': 'withdrawal', - 'event_id': None, - 'template': """Dear {title} {firstname} {lastname}, + "id": 3, + "key": "withdrawal", + "event_id": None, + "template": """Dear {title} {firstname} {lastname}, This email serves to confirm that you have withdrawn your application to attend the Deep Learning Indaba 2019. If this was a mistake, you may resubmit an application before the application deadline. If the deadline has past, please get in touch with us. Kind Regards, The {organisation_name} Team -""" +""", }, { - 'id': 4, - 'key':'confirmation-response', - 'event_id': None, - 'template': """Dear {title} {firstname} {lastname}, + "id": 4, + "key": "confirmation-response", + "event_id": None, + "template": """Dear {title} {firstname} {lastname}, Thank you for applying to attend {event_description}. Your application is being reviewed by our committee and we will get back to you as soon as possible. Included below is a copy of your responses. @@ -81,14 +81,14 @@ def upgrade(): Kind Regards, -The {event_name} Team""" - } - ] +The {event_name} Team""", + }, + ], ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.execute('DELETE FROM email_template where id in (1,2,3,4)') + op.execute("DELETE FROM email_template where id in (1,2,3,4)") # ### end Alembic commands ### diff --git a/api/migrations/versions/85d13062a5e7_.py b/api/migrations/versions/85d13062a5e7_.py index 990a1cdad..c6c6182d4 100644 --- a/api/migrations/versions/85d13062a5e7_.py +++ b/api/migrations/versions/85d13062a5e7_.py @@ -7,26 +7,36 @@ """ # revision identifiers, used by Alembic. -revision = '85d13062a5e7' -down_revision = 'd80784e674a8' +revision = "85d13062a5e7" +down_revision = "d80784e674a8" -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.add_column('response', sa.Column('nomination_email', sa.String(length=255), nullable=True)) - op.add_column('response', sa.Column('nomination_firstname', sa.String(length=100), nullable=True)) - op.add_column('response', sa.Column('nomination_lastname', sa.String(length=100), nullable=True)) - op.add_column('response', sa.Column('nomination_title', sa.String(length=20), nullable=True)) + op.add_column( + "response", sa.Column("nomination_email", sa.String(length=255), nullable=True) + ) + op.add_column( + "response", + sa.Column("nomination_firstname", sa.String(length=100), nullable=True), + ) + op.add_column( + "response", + sa.Column("nomination_lastname", sa.String(length=100), nullable=True), + ) + op.add_column( + "response", sa.Column("nomination_title", sa.String(length=20), nullable=True) + ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('response', 'nomination_title') - op.drop_column('response', 'nomination_lastname') - op.drop_column('response', 'nomination_firstname') - op.drop_column('response', 'nomination_email') + op.drop_column("response", "nomination_title") + op.drop_column("response", "nomination_lastname") + op.drop_column("response", "nomination_firstname") + op.drop_column("response", "nomination_email") # ### end Alembic commands ### diff --git a/api/migrations/versions/865b2e2d02ff_.py b/api/migrations/versions/865b2e2d02ff_.py index 84f23916f..f80bb13a6 100644 --- a/api/migrations/versions/865b2e2d02ff_.py +++ b/api/migrations/versions/865b2e2d02ff_.py @@ -7,11 +7,11 @@ """ # revision identifiers, used by Alembic. -revision = '865b2e2d02ff' -down_revision = ('866611669529', 'cb391b878586') +revision = "865b2e2d02ff" +down_revision = ("866611669529", "cb391b878586") -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): diff --git a/api/migrations/versions/866611669529_.py b/api/migrations/versions/866611669529_.py index 9dd0a8f61..fe7948808 100644 --- a/api/migrations/versions/866611669529_.py +++ b/api/migrations/versions/866611669529_.py @@ -7,11 +7,11 @@ """ # revision identifiers, used by Alembic. -revision = '866611669529' -down_revision = ('65f15544a656', '88199ece9b26') +revision = "866611669529" +down_revision = ("65f15544a656", "88199ece9b26") -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): diff --git a/api/migrations/versions/87213d612eaf_eeml_application_form.py b/api/migrations/versions/87213d612eaf_eeml_application_form.py index 65ce22ca4..10b655dd4 100644 --- a/api/migrations/versions/87213d612eaf_eeml_application_form.py +++ b/api/migrations/versions/87213d612eaf_eeml_application_form.py @@ -7,23 +7,25 @@ """ # revision identifiers, used by Alembic. -revision = '87213d612eaf' -down_revision = 'b2d1fc607635' +revision = "87213d612eaf" +down_revision = "b2d1fc607635" + +import datetime -from alembic import op import sqlalchemy as sa +from alembic import op from sqlalchemy import orm from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy import orm + from app import db -import datetime Base = declarative_base() + class Organisation(Base): __tablename__ = "organisation" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(50), nullable=False) @@ -35,7 +37,17 @@ class Organisation(Base): email_from = db.Column(db.String(100), nullable=True) system_url = db.Column(db.String(100), nullable=False) - def __init__(self, name, system_name, small_logo, large_logo, domain, url, email_from, system_url): + def __init__( + self, + name, + system_name, + small_logo, + large_logo, + domain, + url, + email_from, + system_url, + ): self.name = name self.small_logo = small_logo self.large_logo = large_logo @@ -45,9 +57,10 @@ def __init__(self, name, system_name, small_logo, large_logo, domain, url, email self.email_from = email_from self.system_url = system_url + class Country(Base): __tablename__ = "country" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(100), nullable=False) @@ -55,10 +68,11 @@ class Country(Base): def __init__(self, name): self.name = name + class Event(Base): __tablename__ = "event" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(50), nullable=False) @@ -66,8 +80,9 @@ class Event(Base): start_date = db.Column(db.DateTime(), nullable=False) end_date = db.Column(db.DateTime(), nullable=False) key = db.Column(db.String(255), nullable=False, unique=True) - organisation_id = db.Column(db.Integer(), db.ForeignKey( - 'organisation.id'), nullable=False) + organisation_id = db.Column( + db.Integer(), db.ForeignKey("organisation.id"), nullable=False + ) email_from = db.Column(db.String(255), nullable=False) url = db.Column(db.String(255), nullable=False) @@ -82,26 +97,27 @@ class Event(Base): registration_open = db.Column(db.DateTime(), nullable=False) registration_close = db.Column(db.DateTime(), nullable=False) - def __init__(self, - name, - description, - start_date, - end_date, - key, - organisation_id, - email_from, - url, - application_open, - application_close, - review_open, - review_close, - selection_open, - selection_close, - offer_open, - offer_close, - registration_open, - registration_close - ): + def __init__( + self, + name, + description, + start_date, + end_date, + key, + organisation_id, + email_from, + url, + application_open, + application_close, + review_open, + review_close, + selection_open, + selection_close, + offer_open, + offer_close, + registration_open, + registration_close, + ): self.name = name self.description = description @@ -122,14 +138,15 @@ def __init__(self, self.registration_open = registration_open self.registration_close = registration_close + class ApplicationForm(Base): - __tablename__ = 'application_form' - __table_args__ = {'extend_existing': True} + __tablename__ = "application_form" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - event_id = db.Column(db.Integer(), db.ForeignKey('event.id'), nullable=False) + event_id = db.Column(db.Integer(), db.ForeignKey("event.id"), nullable=False) is_open = db.Column(db.Boolean(), nullable=False) - deadline = db.Column(db.DateTime(), nullable=False) + deadline = db.Column(db.DateTime(), nullable=False) def __init__(self, event_id, is_open, deadline): self.event_id = event_id @@ -138,11 +155,13 @@ def __init__(self, event_id, is_open, deadline): class Section(Base): - __tablename__ = 'section' - __table_args__ = {'extend_existing': True} + __tablename__ = "section" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) name = db.Column(db.String(255), nullable=False) description = db.Column(db.String(255), nullable=False) order = db.Column(db.Integer(), nullable=False) @@ -153,13 +172,16 @@ def __init__(self, application_form_id, name, description, order): self.description = description self.order = order + class Question(Base): - __tablename__ = 'question' - __table_args__ = {'extend_existing': True} + __tablename__ = "question" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) - section_id = db.Column(db.Integer(), db.ForeignKey('section.id'), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) + section_id = db.Column(db.Integer(), db.ForeignKey("section.id"), nullable=False) type = db.Column(db.String(), nullable=False) description = db.Column(db.String(), nullable=True) headline = db.Column(db.String(), nullable=False) @@ -170,7 +192,20 @@ class Question(Base): options = db.Column(db.JSON(), nullable=True) is_required = db.Column(db.Boolean(), nullable=False) - def __init__(self, application_form_id, section_id, headline, placeholder, order, questionType, validation_regex, validation_text=None, is_required = True, description = None, options = None): + def __init__( + self, + application_form_id, + section_id, + headline, + placeholder, + order, + questionType, + validation_regex, + validation_text=None, + is_required=True, + description=None, + options=None, + ): self.application_form_id = application_form_id self.section_id = section_id self.headline = headline @@ -189,17 +224,40 @@ def upgrade(): session = orm.Session(bind=Base.metadata.bind) # Reset auto-increment ids - op.get_bind().execute("""SELECT setval('event_id_seq', (SELECT max(id) FROM event));""") - op.get_bind().execute("""SELECT setval('application_form_id_seq', (SELECT max(id) FROM application_form));""") - op.get_bind().execute("""SELECT setval('section_id_seq', (SELECT max(id) FROM section));""") - op.get_bind().execute("""SELECT setval('question_id_seq', (SELECT max(id) FROM question));""") + op.get_bind().execute( + """SELECT setval('event_id_seq', (SELECT max(id) FROM event));""" + ) + op.get_bind().execute( + """SELECT setval('application_form_id_seq', (SELECT max(id) FROM application_form));""" + ) + op.get_bind().execute( + """SELECT setval('section_id_seq', (SELECT max(id) FROM section));""" + ) + op.get_bind().execute( + """SELECT setval('question_id_seq', (SELECT max(id) FROM question));""" + ) # Add event - eeml2020 = Event('EEML 2020', 'Eastern European Machine Learning Summer School 2020, 6-11 July 2020, Krakow, Poland', - datetime.date(2020, 7, 6), datetime.date(2020, 7, 11), 'eeml2020', 2, 'contact@eeml.eu', - 'http://eeml.eu', datetime.date(2020, 2, 2), datetime.date(2020, 3, 20), datetime.date(2020, 3, 20), - datetime.date(2020, 4, 20), datetime.date(2020, 4, 20), datetime.date(2020, 5, 20), datetime.date(2020, 5, 20), - datetime.date(2020, 6, 20), datetime.date(2020, 6, 20), datetime.date(2020, 6, 30)) + eeml2020 = Event( + "EEML 2020", + "Eastern European Machine Learning Summer School 2020, 6-11 July 2020, Krakow, Poland", + datetime.date(2020, 7, 6), + datetime.date(2020, 7, 11), + "eeml2020", + 2, + "contact@eeml.eu", + "http://eeml.eu", + datetime.date(2020, 2, 2), + datetime.date(2020, 3, 20), + datetime.date(2020, 3, 20), + datetime.date(2020, 4, 20), + datetime.date(2020, 4, 20), + datetime.date(2020, 5, 20), + datetime.date(2020, 5, 20), + datetime.date(2020, 6, 20), + datetime.date(2020, 6, 20), + datetime.date(2020, 6, 30), + ) session.add(eeml2020) session.commit() @@ -208,120 +266,296 @@ def upgrade(): session.add(app_form) session.commit() - main_section = Section(app_form.id, 'EEML 2020 Application Form', """ + main_section = Section( + app_form.id, + "EEML 2020 Application Form", + """ This is the official application form to apply for participation in EEML 2020, 6-11 July, Krakow, Poland. Do not forget to press Submit once you fill in the application form. It is possible (and recommended!) to submit partially completed forms early on to get familiar with the form, and come back at a later date before the application deadline (20 March 2020, 23:59 Eastern European Time) to edit/complete your responses. - """, 1) + """, + 1, + ) session.add(main_section) session.commit() - personal_info = Section(app_form.id, 'Personal Information', '', 2) + personal_info = Section(app_form.id, "Personal Information", "", 2) session.add(personal_info) session.commit() - personal_q1 = Question(app_form.id, personal_info.id, 'I Am:', 'Select an Option...', 1, 'multi-choice', None, - options=[ - {'label': 'An undergraduate student', 'value': 'undergrad'}, - {'label': 'A masters student', 'value': 'masters'}, - {'label': 'A PhD student', 'value': 'phd'}, - {'label': 'A Post-doc', 'value': 'postdoc'}, - {'label': 'Faculty', 'value': 'faculty'}, - {'label': 'Industry', 'value': 'industry'}, - {'label': 'Other (free-lancer etc)', 'value': 'other'} - ]) - personal_q2 = Question(app_form.id, personal_info.id, 'Other', 'Other', 2, 'short-text', None, None, description='If you answered "Other", please specify', is_required=False) - personal_q3 = Question(app_form.id, personal_info.id, 'Affiliation', 'Affiliation', 3, 'short-text', None, None, is_required=False) - personal_q4 = Question(app_form.id, personal_info.id, 'Country', 'Select a Country...', 4, 'multi-choice', None, None, options=get_country_list(session)) - personal_q5 = Question(app_form.id, personal_info.id, 'Institution or Company', 'Institution or Company', 5, 'short-text', None, None, is_required=False, description='(if applicable)') - personal_q6 = Question(app_form.id, personal_info.id, 'Research Group / Laboratory', 'Research Group / Laboratory', 6, 'short-text', None, None, is_required=False, description='(if applicable)') - personal_q7 = Question(app_form.id, personal_info.id, 'Supervisor', 'Supervisor', 7, 'short-text', None, None, is_required=False, description='(if applicable)') - personal_q8 = Question(app_form.id, personal_info.id, 'Previous attendance', 'Select an option...', 8, 'multi-choice', None, None, + personal_q1 = Question( + app_form.id, + personal_info.id, + "I Am:", + "Select an Option...", + 1, + "multi-choice", + None, options=[ - {'label': 'Yes', 'value': 'yes'}, - {'label': 'No', 'value': 'no'} + {"label": "An undergraduate student", "value": "undergrad"}, + {"label": "A masters student", "value": "masters"}, + {"label": "A PhD student", "value": "phd"}, + {"label": "A Post-doc", "value": "postdoc"}, + {"label": "Faculty", "value": "faculty"}, + {"label": "Industry", "value": "industry"}, + {"label": "Other (free-lancer etc)", "value": "other"}, ], - description='Have you attended the previous edition of this school - TMLSS2018 or EEML2019?') - session.add_all([personal_q1, personal_q2, personal_q3, personal_q4, personal_q5, personal_q6, personal_q7, personal_q8]) - - research_interests = Section(app_form.id, 'Research Interests', '', 3) + ) + personal_q2 = Question( + app_form.id, + personal_info.id, + "Other", + "Other", + 2, + "short-text", + None, + None, + description='If you answered "Other", please specify', + is_required=False, + ) + personal_q3 = Question( + app_form.id, + personal_info.id, + "Affiliation", + "Affiliation", + 3, + "short-text", + None, + None, + is_required=False, + ) + personal_q4 = Question( + app_form.id, + personal_info.id, + "Country", + "Select a Country...", + 4, + "multi-choice", + None, + None, + options=get_country_list(session), + ) + personal_q5 = Question( + app_form.id, + personal_info.id, + "Institution or Company", + "Institution or Company", + 5, + "short-text", + None, + None, + is_required=False, + description="(if applicable)", + ) + personal_q6 = Question( + app_form.id, + personal_info.id, + "Research Group / Laboratory", + "Research Group / Laboratory", + 6, + "short-text", + None, + None, + is_required=False, + description="(if applicable)", + ) + personal_q7 = Question( + app_form.id, + personal_info.id, + "Supervisor", + "Supervisor", + 7, + "short-text", + None, + None, + is_required=False, + description="(if applicable)", + ) + personal_q8 = Question( + app_form.id, + personal_info.id, + "Previous attendance", + "Select an option...", + 8, + "multi-choice", + None, + None, + options=[{"label": "Yes", "value": "yes"}, {"label": "No", "value": "no"}], + description="Have you attended the previous edition of this school - TMLSS2018 or EEML2019?", + ) + session.add_all( + [ + personal_q1, + personal_q2, + personal_q3, + personal_q4, + personal_q5, + personal_q6, + personal_q7, + personal_q8, + ] + ) + + research_interests = Section(app_form.id, "Research Interests", "", 3) session.add(research_interests) session.commit() - research_q1 = Question(app_form.id, research_interests.id, 'Research Interests', 'Enter 500 - 2000 characters', 1, 'long-text', - validation_regex='^.{500,2000}$', validation_text='You must enter between 500 and 2000 characters', - description='Describe your research interests, your projects related to ML, and your motivation for attending the summer school') - research_q2 = Question(app_form.id, research_interests.id, 'Extended abstract type', 'Select an option...', 2, 'multi-choice', None, None, + research_q1 = Question( + app_form.id, + research_interests.id, + "Research Interests", + "Enter 500 - 2000 characters", + 1, + "long-text", + validation_regex="^.{500,2000}$", + validation_text="You must enter between 500 and 2000 characters", + description="Describe your research interests, your projects related to ML, and your motivation for attending the summer school", + ) + research_q2 = Question( + app_form.id, + research_interests.id, + "Extended abstract type", + "Select an option...", + 2, + "multi-choice", + None, + None, options=[ - {'label': 'Research', 'value': 'research'}, - {'label': 'Reproduction', 'value': 'reproduction'}, - {'label': 'Review', 'value': 'review'} - ], - description='Check https://www.eeml.eu/application for clarification', - is_required=False) - research_q3 = Question(app_form.id, research_interests.id, 'Extended abstract', 'Upload your extended abstract (pdf, max size 10MB)', 3, 'file', None, is_required=False) - research_q4 = Question(app_form.id, research_interests.id, 'CV', 'Upload your CV (pdf, max size 10MB)', 4, 'file', None, is_required=True) + {"label": "Research", "value": "research"}, + {"label": "Reproduction", "value": "reproduction"}, + {"label": "Review", "value": "review"}, + ], + description="Check https://www.eeml.eu/application for clarification", + is_required=False, + ) + research_q3 = Question( + app_form.id, + research_interests.id, + "Extended abstract", + "Upload your extended abstract (pdf, max size 10MB)", + 3, + "file", + None, + is_required=False, + ) + research_q4 = Question( + app_form.id, + research_interests.id, + "CV", + "Upload your CV (pdf, max size 10MB)", + 4, + "file", + None, + is_required=True, + ) session.add_all([research_q1, research_q2, research_q3, research_q4]) - diversity = Section(app_form.id, 'Diversity', '', 4) + diversity = Section(app_form.id, "Diversity", "", 4) session.add(diversity) session.commit() - diversity_q1 = Question(app_form.id, diversity.id, 'Gender Identity', 'Select an option...', 1, 'multi-choice', None, + diversity_q1 = Question( + app_form.id, + diversity.id, + "Gender Identity", + "Select an option...", + 1, + "multi-choice", + None, options=[ - {'label': 'Female', 'value': 'female'}, - {'label': 'Male', 'value': 'male'}, - {'label': 'Prefer not to say', 'value': 'prefer-not-to-say'} - ]) - - diversity_q2 = Question(app_form.id, diversity.id, 'Ethnicity', 'Ethnicity', 2, 'short-text', None, is_required=False, description='Leave blank if you prefer not to answer') - diversity_q3 = Question(app_form.id, diversity.id, 'Nationality', 'Nationality', 3, 'short-text', None, is_required=False, description='Leave blank if you prefer not to answer') + {"label": "Female", "value": "female"}, + {"label": "Male", "value": "male"}, + {"label": "Prefer not to say", "value": "prefer-not-to-say"}, + ], + ) + + diversity_q2 = Question( + app_form.id, + diversity.id, + "Ethnicity", + "Ethnicity", + 2, + "short-text", + None, + is_required=False, + description="Leave blank if you prefer not to answer", + ) + diversity_q3 = Question( + app_form.id, + diversity.id, + "Nationality", + "Nationality", + 3, + "short-text", + None, + is_required=False, + description="Leave blank if you prefer not to answer", + ) session.add_all([diversity_q1, diversity_q2, diversity_q3]) - financial_support = Section(app_form.id, 'Financial support for attending the school', """ + financial_support = Section( + app_form.id, + "Financial support for attending the school", + """ The financial support will vary on a case-by-case basis and will cover fully or partially the costs of attending the school (registration, accommodation, travel). Please provide any information that you consider relevant to support your application (max 500 characters). If you need help covering travelling, please provide also an estimated travel cost (in EUR). Note that financial support is offered on financial considerations, not on merit. This information will be hidden for reviewers, so it will not influence the selection decision. -""", 5) +""", + 5, + ) session.add(financial_support) session.commit() - - financial_q1 = Question(app_form.id, financial_support.id, 'Motivation', 'Enter up to 500 characters', 1, 'long-text', - validation_regex='^.{0,500}$', validation_text='Maximum 500 characters', - is_required=False, description='Max 500 characters') + + financial_q1 = Question( + app_form.id, + financial_support.id, + "Motivation", + "Enter up to 500 characters", + 1, + "long-text", + validation_regex="^.{0,500}$", + validation_text="Maximum 500 characters", + is_required=False, + description="Max 500 characters", + ) session.add(financial_q1) session.commit() + def get_country_list(session): countries = session.query(Country).all() country_list = [] for country in countries: - country_list.append({ - 'label': country.name, - 'value': country.name - }) + country_list.append({"label": country.name, "value": country.name}) return country_list + def downgrade(): Base.metadata.bind = op.get_bind() session = orm.Session(bind=Base.metadata.bind) - event = session.query(Event).filter_by(key='eeml2020').first() + event = session.query(Event).filter_by(key="eeml2020").first() app_form = session.query(ApplicationForm).filter_by(event_id=event.id).first() session.query(Question).filter_by(application_form_id=app_form.id).delete() session.query(Section).filter_by(application_form_id=app_form.id).delete() session.query(ApplicationForm).filter_by(event_id=event.id).delete() - session.query(Event).filter_by(key='eeml2020').delete() + session.query(Event).filter_by(key="eeml2020").delete() session.commit() - op.get_bind().execute("""SELECT setval('event_id_seq', (SELECT max(id) FROM event));""") - op.get_bind().execute("""SELECT setval('application_form_id_seq', (SELECT max(id) FROM application_form));""") - op.get_bind().execute("""SELECT setval('section_id_seq', (SELECT max(id) FROM section));""") - op.get_bind().execute("""SELECT setval('question_id_seq', (SELECT max(id) FROM question));""") - - + op.get_bind().execute( + """SELECT setval('event_id_seq', (SELECT max(id) FROM event));""" + ) + op.get_bind().execute( + """SELECT setval('application_form_id_seq', (SELECT max(id) FROM application_form));""" + ) + op.get_bind().execute( + """SELECT setval('section_id_seq', (SELECT max(id) FROM section));""" + ) + op.get_bind().execute( + """SELECT setval('question_id_seq', (SELECT max(id) FROM question));""" + ) diff --git a/api/migrations/versions/878f67285c72_.py b/api/migrations/versions/878f67285c72_.py index 8c9592eab..4a6f94400 100644 --- a/api/migrations/versions/878f67285c72_.py +++ b/api/migrations/versions/878f67285c72_.py @@ -7,43 +7,47 @@ """ # revision identifiers, used by Alembic. -revision = '878f67285c72' -down_revision = '122dd6a5c035' +revision = "878f67285c72" +down_revision = "122dd6a5c035" -from alembic import op import sqlalchemy as sa +from alembic import op from sqlalchemy.dialects import postgresql def upgrade(): # ###commandsautogeneratedbyAlembic-pleaseadjust!### - op.create_table('registration', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('offer_id', sa.Integer(), nullable=False), - sa.Column('registration_form_id', sa.Integer(), nullable=False), - sa.Column('confirmed', sa.Boolean(), nullable=False), - sa.Column('created_at', sa.DateTime(), nullable=False), - sa.Column('confirmation_email_sent_at', sa.DateTime(), nullable=False), - sa.ForeignKeyConstraint(['offer_id'], ['offer.id']), - sa.ForeignKeyConstraint(['registration_form_id'], ['registration_form.id']), - sa.PrimaryKeyConstraint('id') - ) - - op.create_table('registration_answer', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('registration_id', sa.Integer(), nullable=False), - sa.Column('registration_question_id', sa.Integer(), nullable=False), - sa.Column('value', sa.String(), nullable=False), - sa.ForeignKeyConstraint(['registration_id'], ['registration.id']), - sa.ForeignKeyConstraint(['registration_question_id'], ['registration_question.id']), - sa.PrimaryKeyConstraint('id') - ) + op.create_table( + "registration", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("offer_id", sa.Integer(), nullable=False), + sa.Column("registration_form_id", sa.Integer(), nullable=False), + sa.Column("confirmed", sa.Boolean(), nullable=False), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.Column("confirmation_email_sent_at", sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(["offer_id"], ["offer.id"]), + sa.ForeignKeyConstraint(["registration_form_id"], ["registration_form.id"]), + sa.PrimaryKeyConstraint("id"), + ) + + op.create_table( + "registration_answer", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("registration_id", sa.Integer(), nullable=False), + sa.Column("registration_question_id", sa.Integer(), nullable=False), + sa.Column("value", sa.String(), nullable=False), + sa.ForeignKeyConstraint(["registration_id"], ["registration.id"]), + sa.ForeignKeyConstraint( + ["registration_question_id"], ["registration_question.id"] + ), + sa.PrimaryKeyConstraint("id"), + ) # ###endAlembiccommands### def downgrade(): # ### commands auto generated by Alembic-please adjust!### - op.drop_table('registration_answer') - op.drop_table('registration') + op.drop_table("registration_answer") + op.drop_table("registration") # ###endAlembiccommands### diff --git a/api/migrations/versions/88199ece9b26_.py b/api/migrations/versions/88199ece9b26_.py index d8413b9f9..7abb7e089 100644 --- a/api/migrations/versions/88199ece9b26_.py +++ b/api/migrations/versions/88199ece9b26_.py @@ -7,11 +7,11 @@ """ # revision identifiers, used by Alembic. -revision = '88199ece9b26' -down_revision = ('9630ab6b0437', '438ca9e1c2e8') +revision = "88199ece9b26" +down_revision = ("9630ab6b0437", "438ca9e1c2e8") -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): diff --git a/api/migrations/versions/88cc4397bdf5_ai4d_org.py b/api/migrations/versions/88cc4397bdf5_ai4d_org.py index 3012e74da..feb0edaa8 100644 --- a/api/migrations/versions/88cc4397bdf5_ai4d_org.py +++ b/api/migrations/versions/88cc4397bdf5_ai4d_org.py @@ -7,23 +7,25 @@ """ # revision identifiers, used by Alembic. -revision = '88cc4397bdf5' -down_revision = '742a2a556eb9' +revision = "88cc4397bdf5" +down_revision = "742a2a556eb9" + +import datetime -from alembic import op import sqlalchemy as sa +from alembic import op from sqlalchemy import orm from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy import orm + from app import db -import datetime Base = declarative_base() + class Organisation(Base): __tablename__ = "organisation" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(50), nullable=False) @@ -38,7 +40,20 @@ class Organisation(Base): privacy_policy = db.Column(db.String(100), nullable=False) languages = db.Column(db.JSON(), nullable=False) - def __init__(self, name, system_name, small_logo, large_logo, icon_logo, domain, url, email_from, system_url, privacy_policy, languages): + def __init__( + self, + name, + system_name, + small_logo, + large_logo, + icon_logo, + domain, + url, + email_from, + system_url, + privacy_policy, + languages, + ): self.name = name self.small_logo = small_logo self.large_logo = large_logo @@ -56,10 +71,22 @@ def upgrade(): Base.metadata.bind = op.get_bind() session = orm.Session(bind=Base.metadata.bind) - ai4d = Organisation('AI4D Africa', 'Baobab', small_logo='ai4d_square_logo.png', large_logo='ai4d_logo.png', - icon_logo='ai4d_white.png', domain='ai4d', url='http://www.ai4d.ai', email_from='calls@ai4d.ai', - system_url='https://baobab.ai4d.ai', privacy_policy='AI4D_privacy_policy_en_fr.pdf', - languages=[{"code": "en", "description": "English"}, {"code": "fr", "description": "French"}]) + ai4d = Organisation( + "AI4D Africa", + "Baobab", + small_logo="ai4d_square_logo.png", + large_logo="ai4d_logo.png", + icon_logo="ai4d_white.png", + domain="ai4d", + url="http://www.ai4d.ai", + email_from="calls@ai4d.ai", + system_url="https://baobab.ai4d.ai", + privacy_policy="AI4D_privacy_policy_en_fr.pdf", + languages=[ + {"code": "en", "description": "English"}, + {"code": "fr", "description": "French"}, + ], + ) session.add(ai4d) session.commit() @@ -69,5 +96,5 @@ def downgrade(): Base.metadata.bind = op.get_bind() session = orm.Session(bind=Base.metadata.bind) - session.query(Organisation).filter_by(name='AI4D Africa').delete() + session.query(Organisation).filter_by(name="AI4D Africa").delete() session.commit() diff --git a/api/migrations/versions/8a1468838847_.py b/api/migrations/versions/8a1468838847_.py index bd6b3a603..34aac8013 100644 --- a/api/migrations/versions/8a1468838847_.py +++ b/api/migrations/versions/8a1468838847_.py @@ -7,26 +7,49 @@ """ # revision identifiers, used by Alembic. -revision = '8a1468838847' -down_revision = '99f650dc0924' +revision = "8a1468838847" +down_revision = "99f650dc0924" -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.add_column('response_reviewer', sa.Column('reviewer_user_id', sa.Integer(), nullable=False)) - op.drop_constraint('response_reviewer_user_id_fkey', 'response_reviewer', type_='foreignkey') - op.create_foreign_key('response_reviewer_reviewer_user_id_fkey', 'response_reviewer', 'app_user', ['reviewer_user_id'], ['id']) - op.drop_column('response_reviewer', 'user_id') + op.add_column( + "response_reviewer", sa.Column("reviewer_user_id", sa.Integer(), nullable=False) + ) + op.drop_constraint( + "response_reviewer_user_id_fkey", "response_reviewer", type_="foreignkey" + ) + op.create_foreign_key( + "response_reviewer_reviewer_user_id_fkey", + "response_reviewer", + "app_user", + ["reviewer_user_id"], + ["id"], + ) + op.drop_column("response_reviewer", "user_id") # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.add_column('response_reviewer', sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=False)) - op.drop_constraint('response_reviewer_reviewer_user_id_fkey', 'response_reviewer', type_='foreignkey') - op.create_foreign_key('response_reviewer_user_id_fkey', 'response_reviewer', 'app_user', ['user_id'], ['id']) - op.drop_column('response_reviewer', 'reviewer_user_id') + op.add_column( + "response_reviewer", + sa.Column("user_id", sa.INTEGER(), autoincrement=False, nullable=False), + ) + op.drop_constraint( + "response_reviewer_reviewer_user_id_fkey", + "response_reviewer", + type_="foreignkey", + ) + op.create_foreign_key( + "response_reviewer_user_id_fkey", + "response_reviewer", + "app_user", + ["user_id"], + ["id"], + ) + op.drop_column("response_reviewer", "reviewer_user_id") # ### end Alembic commands ### diff --git a/api/migrations/versions/8ba06d0a54ee_.py b/api/migrations/versions/8ba06d0a54ee_.py index a7a03c9a0..b0c0f22fb 100644 --- a/api/migrations/versions/8ba06d0a54ee_.py +++ b/api/migrations/versions/8ba06d0a54ee_.py @@ -7,11 +7,11 @@ """ # revision identifiers, used by Alembic. -revision = '8ba06d0a54ee' -down_revision = '0023aeda53b0' +revision = "8ba06d0a54ee" +down_revision = "0023aeda53b0" -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): diff --git a/api/migrations/versions/910e74b87f06_.py b/api/migrations/versions/910e74b87f06_.py index ba8f37d2a..5df3dfc51 100644 --- a/api/migrations/versions/910e74b87f06_.py +++ b/api/migrations/versions/910e74b87f06_.py @@ -7,28 +7,33 @@ """ # revision identifiers, used by Alembic. -revision = '910e74b87f06' -down_revision = '742a2a556eb9' +revision = "910e74b87f06" +down_revision = "742a2a556eb9" -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): op.create_table( - 'event_translation', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('event_id', sa.Integer(), nullable=False), - sa.Column('name', sa.String(length=50), nullable=False), - sa.Column('description', sa.String(length=255), nullable=False), - sa.Column('language', sa.String(length=2), nullable=True), - sa.ForeignKeyConstraint(['event_id'], ['event.id'], ), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('event_id', 'language', name='uq_event_id_language') + "event_translation", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("event_id", sa.Integer(), nullable=False), + sa.Column("name", sa.String(length=50), nullable=False), + sa.Column("description", sa.String(length=255), nullable=False), + sa.Column("language", sa.String(length=2), nullable=True), + sa.ForeignKeyConstraint( + ["event_id"], + ["event.id"], + ), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("event_id", "language", name="uq_event_id_language"), ) - op.execute("INSERT INTO event_translation (event_id, name, description, language) SELECT id, name, description, 'en' FROM event") + op.execute( + "INSERT INTO event_translation (event_id, name, description, language) SELECT id, name, description, 'en' FROM event" + ) def downgrade(): - op.drop_table('event_translation') + op.drop_table("event_translation") diff --git a/api/migrations/versions/91468f804113_.py b/api/migrations/versions/91468f804113_.py index e8b086b6e..84891dcb9 100644 --- a/api/migrations/versions/91468f804113_.py +++ b/api/migrations/versions/91468f804113_.py @@ -7,20 +7,25 @@ """ # revision identifiers, used by Alembic. -revision = '91468f804113' -down_revision = '0ab2d6bca67d' +revision = "91468f804113" +down_revision = "0ab2d6bca67d" -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('app_user', 'user_ethnicity') + op.drop_column("app_user", "user_ethnicity") # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.add_column('app_user', sa.Column('user_ethnicity', sa.VARCHAR(length=50), autoincrement=False, nullable=False)) + op.add_column( + "app_user", + sa.Column( + "user_ethnicity", sa.VARCHAR(length=50), autoincrement=False, nullable=False + ), + ) # ### end Alembic commands ### diff --git a/api/migrations/versions/91a7bc19a5ec_.py b/api/migrations/versions/91a7bc19a5ec_.py index a600c79a6..6feeba302 100644 --- a/api/migrations/versions/91a7bc19a5ec_.py +++ b/api/migrations/versions/91a7bc19a5ec_.py @@ -7,11 +7,11 @@ """ # revision identifiers, used by Alembic. -revision = '91a7bc19a5ec' -down_revision = ('4605cce59919', 'e8d1ee2746e0') +revision = "91a7bc19a5ec" +down_revision = ("4605cce59919", "e8d1ee2746e0") -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): diff --git a/api/migrations/versions/9630ab6b0437_alter_rejection_reason.py b/api/migrations/versions/9630ab6b0437_alter_rejection_reason.py index 7ba9927e3..10f6469fd 100644 --- a/api/migrations/versions/9630ab6b0437_alter_rejection_reason.py +++ b/api/migrations/versions/9630ab6b0437_alter_rejection_reason.py @@ -7,16 +7,16 @@ """ # revision identifiers, used by Alembic. -revision = '9630ab6b0437' -down_revision = '57e55dc645c1' +revision = "9630ab6b0437" +down_revision = "57e55dc645c1" -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): - op.alter_column('offer', 'rejected_reason', type_=sa.String(5000)) + op.alter_column("offer", "rejected_reason", type_=sa.String(5000)) def downgrade(): - op.alter_column('offer', 'rejected_reason', type_=sa.String(50)) + op.alter_column("offer", "rejected_reason", type_=sa.String(50)) diff --git a/api/migrations/versions/964ead196cb9_add_miniconf_url.py b/api/migrations/versions/964ead196cb9_add_miniconf_url.py index e0fff77bf..3fdbb6cf1 100644 --- a/api/migrations/versions/964ead196cb9_add_miniconf_url.py +++ b/api/migrations/versions/964ead196cb9_add_miniconf_url.py @@ -7,25 +7,33 @@ """ # revision identifiers, used by Alembic. -revision = '964ead196cb9' -down_revision = '1e05a293f402' +revision = "964ead196cb9" +down_revision = "1e05a293f402" -from alembic import op import sqlalchemy as sa -from app.events.models import Event -from sqlalchemy.ext.declarative import declarative_base +from alembic import op from sqlalchemy import orm +from sqlalchemy.ext.declarative import declarative_base + +from app.events.models import Event Base = declarative_base() + def upgrade(): Base.metadata.bind = op.get_bind() session = orm.Session(bind=Base.metadata.bind) - op.add_column('event', sa.Column('miniconf_url', sa.String(length=100), nullable=True)) - event = session.query(Event).filter_by(key='eeml2020').first() - op.execute("""UPDATE event SET miniconf_url = 'miniconf.eeml.eu' WHERE id = {}""".format(event.id)) + op.add_column( + "event", sa.Column("miniconf_url", sa.String(length=100), nullable=True) + ) + event = session.query(Event).filter_by(key="eeml2020").first() + op.execute( + """UPDATE event SET miniconf_url = 'miniconf.eeml.eu' WHERE id = {}""".format( + event.id + ) + ) def downgrade(): - op.drop_column('event', 'miniconf_url') + op.drop_column("event", "miniconf_url") diff --git a/api/migrations/versions/984cb348ec98_add_response_language.py b/api/migrations/versions/984cb348ec98_add_response_language.py index a5df8880a..776703c02 100644 --- a/api/migrations/versions/984cb348ec98_add_response_language.py +++ b/api/migrations/versions/984cb348ec98_add_response_language.py @@ -7,22 +7,22 @@ """ # revision identifiers, used by Alembic. -revision = '984cb348ec98' -down_revision = '297276d57414' +revision = "984cb348ec98" +down_revision = "297276d57414" -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.add_column('response', sa.Column('language', sa.String(length=2), nullable=True)) + op.add_column("response", sa.Column("language", sa.String(length=2), nullable=True)) op.execute("""UPDATE response SET language='en'""") - op.alter_column('response', 'language', nullable=False) + op.alter_column("response", "language", nullable=False) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('response', 'language') + op.drop_column("response", "language") # ### end Alembic commands ### diff --git a/api/migrations/versions/99cfdad9869c_.py b/api/migrations/versions/99cfdad9869c_.py index 85ec41553..b05949b12 100644 --- a/api/migrations/versions/99cfdad9869c_.py +++ b/api/migrations/versions/99cfdad9869c_.py @@ -7,28 +7,36 @@ """ # revision identifiers, used by Alembic. -revision = '99cfdad9869c' -down_revision = '7fced3d22209' +revision = "99cfdad9869c" +down_revision = "7fced3d22209" -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.create_unique_constraint('org_email_unique', 'app_user', ['email', 'organisation_id']) - op.drop_constraint('app_user_email_key', 'app_user', type_='unique') - op.alter_column('organisation', 'system_name', - existing_type=sa.VARCHAR(length=50), - nullable=False) + op.create_unique_constraint( + "org_email_unique", "app_user", ["email", "organisation_id"] + ) + op.drop_constraint("app_user_email_key", "app_user", type_="unique") + op.alter_column( + "organisation", + "system_name", + existing_type=sa.VARCHAR(length=50), + nullable=False, + ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.alter_column('organisation', 'system_name', - existing_type=sa.VARCHAR(length=50), - nullable=True) - op.create_unique_constraint('app_user_email_key', 'app_user', ['email']) - op.drop_constraint('org_email_unique', 'app_user', type_='unique') + op.alter_column( + "organisation", + "system_name", + existing_type=sa.VARCHAR(length=50), + nullable=True, + ) + op.create_unique_constraint("app_user_email_key", "app_user", ["email"]) + op.drop_constraint("org_email_unique", "app_user", type_="unique") # ### end Alembic commands ### diff --git a/api/migrations/versions/99f650dc0924_.py b/api/migrations/versions/99f650dc0924_.py index 0d01fd8bc..03f666c64 100644 --- a/api/migrations/versions/99f650dc0924_.py +++ b/api/migrations/versions/99f650dc0924_.py @@ -7,77 +7,112 @@ """ # revision identifiers, used by Alembic. -revision = '99f650dc0924' -down_revision = 'b4c1dfa70233' +revision = "99f650dc0924" +down_revision = "b4c1dfa70233" -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.create_table('review_form', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('application_form_id', sa.Integer(), nullable=False), - sa.Column('is_open', sa.Boolean(), nullable=False), - sa.Column('deadline', sa.DateTime(), nullable=False), - sa.ForeignKeyConstraint(['application_form_id'], ['application_form.id'], ), - sa.PrimaryKeyConstraint('id') + op.create_table( + "review_form", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("application_form_id", sa.Integer(), nullable=False), + sa.Column("is_open", sa.Boolean(), nullable=False), + sa.Column("deadline", sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint( + ["application_form_id"], + ["application_form.id"], + ), + sa.PrimaryKeyConstraint("id"), ) - op.create_table('response_reviewer', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('response_id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['response_id'], ['response.id'], ), - sa.ForeignKeyConstraint(['user_id'], ['app_user.id'], ), - sa.PrimaryKeyConstraint('id') + op.create_table( + "response_reviewer", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("response_id", sa.Integer(), nullable=False), + sa.Column("user_id", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint( + ["response_id"], + ["response.id"], + ), + sa.ForeignKeyConstraint( + ["user_id"], + ["app_user.id"], + ), + sa.PrimaryKeyConstraint("id"), ) - op.create_table('review_response', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('review_form_id', sa.Integer(), nullable=False), - sa.Column('reviewer_user_id', sa.Integer(), nullable=False), - sa.Column('response_id', sa.Integer(), nullable=False), - sa.Column('submitted_timestamp', sa.DateTime(), nullable=False), - sa.ForeignKeyConstraint(['response_id'], ['response.id'], ), - sa.ForeignKeyConstraint(['review_form_id'], ['review_form.id'], ), - sa.ForeignKeyConstraint(['reviewer_user_id'], ['app_user.id'], ), - sa.PrimaryKeyConstraint('id') + op.create_table( + "review_response", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("review_form_id", sa.Integer(), nullable=False), + sa.Column("reviewer_user_id", sa.Integer(), nullable=False), + sa.Column("response_id", sa.Integer(), nullable=False), + sa.Column("submitted_timestamp", sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint( + ["response_id"], + ["response.id"], + ), + sa.ForeignKeyConstraint( + ["review_form_id"], + ["review_form.id"], + ), + sa.ForeignKeyConstraint( + ["reviewer_user_id"], + ["app_user.id"], + ), + sa.PrimaryKeyConstraint("id"), ) - op.create_table('review_question', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('review_form_id', sa.Integer(), nullable=False), - sa.Column('question_id', sa.Integer(), nullable=True), - sa.Column('description', sa.String(), nullable=True), - sa.Column('headline', sa.String(), nullable=True), - sa.Column('type', sa.String(), nullable=False), - sa.Column('placeholder', sa.String(), nullable=True), - sa.Column('options', sa.JSON(), nullable=True), - sa.Column('is_required', sa.Boolean(), nullable=False), - sa.Column('order', sa.Integer(), nullable=False), - sa.Column('validation_regex', sa.String(), nullable=True), - sa.Column('validation_text', sa.String(), nullable=True), - sa.Column('weight', sa.Float(), nullable=False), - sa.ForeignKeyConstraint(['question_id'], ['question.id'], ), - sa.ForeignKeyConstraint(['review_form_id'], ['review_form.id'], ), - sa.PrimaryKeyConstraint('id') + op.create_table( + "review_question", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("review_form_id", sa.Integer(), nullable=False), + sa.Column("question_id", sa.Integer(), nullable=True), + sa.Column("description", sa.String(), nullable=True), + sa.Column("headline", sa.String(), nullable=True), + sa.Column("type", sa.String(), nullable=False), + sa.Column("placeholder", sa.String(), nullable=True), + sa.Column("options", sa.JSON(), nullable=True), + sa.Column("is_required", sa.Boolean(), nullable=False), + sa.Column("order", sa.Integer(), nullable=False), + sa.Column("validation_regex", sa.String(), nullable=True), + sa.Column("validation_text", sa.String(), nullable=True), + sa.Column("weight", sa.Float(), nullable=False), + sa.ForeignKeyConstraint( + ["question_id"], + ["question.id"], + ), + sa.ForeignKeyConstraint( + ["review_form_id"], + ["review_form.id"], + ), + sa.PrimaryKeyConstraint("id"), ) - op.create_table('review_score', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('review_response_id', sa.Integer(), nullable=False), - sa.Column('review_question_id', sa.Integer(), nullable=False), - sa.Column('value', sa.String(), nullable=False), - sa.ForeignKeyConstraint(['review_question_id'], ['review_question.id'], ), - sa.ForeignKeyConstraint(['review_response_id'], ['review_response.id'], ), - sa.PrimaryKeyConstraint('id') + op.create_table( + "review_score", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("review_response_id", sa.Integer(), nullable=False), + sa.Column("review_question_id", sa.Integer(), nullable=False), + sa.Column("value", sa.String(), nullable=False), + sa.ForeignKeyConstraint( + ["review_question_id"], + ["review_question.id"], + ), + sa.ForeignKeyConstraint( + ["review_response_id"], + ["review_response.id"], + ), + sa.PrimaryKeyConstraint("id"), ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('review_score') - op.drop_table('review_question') - op.drop_table('review_response') - op.drop_table('response_reviewer') - op.drop_table('review_form') + op.drop_table("review_score") + op.drop_table("review_question") + op.drop_table("review_response") + op.drop_table("response_reviewer") + op.drop_table("review_form") # ### end Alembic commands ### diff --git a/api/migrations/versions/9b181cdd3fa1_merge.py b/api/migrations/versions/9b181cdd3fa1_merge.py index 13dac8dae..56bd6d830 100644 --- a/api/migrations/versions/9b181cdd3fa1_merge.py +++ b/api/migrations/versions/9b181cdd3fa1_merge.py @@ -7,11 +7,11 @@ """ # revision identifiers, used by Alembic. -revision = '9b181cdd3fa1' -down_revision = ('3fb92272089d', '984cb348ec98') +revision = "9b181cdd3fa1" +down_revision = ("3fb92272089d", "984cb348ec98") -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): diff --git a/api/migrations/versions/a4662031beca_add_outcome_model.py b/api/migrations/versions/a4662031beca_add_outcome_model.py index 3731ba9f7..c2b0dde5f 100644 --- a/api/migrations/versions/a4662031beca_add_outcome_model.py +++ b/api/migrations/versions/a4662031beca_add_outcome_model.py @@ -7,36 +7,52 @@ """ # revision identifiers, used by Alembic. -revision = 'a4662031beca' -down_revision = '492546998537' +revision = "a4662031beca" +down_revision = "492546998537" -from alembic import op import sqlalchemy as sa +from alembic import op from sqlalchemy.dialects import postgresql -outcome_status = postgresql.ENUM('ACCEPTED', 'REJECTED', 'WAITLIST', name='outcome_status') +outcome_status = postgresql.ENUM( + "ACCEPTED", "REJECTED", "WAITLIST", name="outcome_status" +) def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.create_table('outcome', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('event_id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=False), - sa.Column('status', sa.Enum('ACCEPTED', 'REJECTED', 'WAITLIST', name='outcome_status'), nullable=False), - sa.Column('timestamp', sa.DateTime(), nullable=False), - sa.Column('latest', sa.Boolean(), nullable=False), - sa.Column('updated_by_user_id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['event_id'], ['event.id'], ), - sa.ForeignKeyConstraint(['updated_by_user_id'], ['app_user.id'], ), - sa.ForeignKeyConstraint(['user_id'], ['app_user.id'], ), - sa.PrimaryKeyConstraint('id') + op.create_table( + "outcome", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("event_id", sa.Integer(), nullable=False), + sa.Column("user_id", sa.Integer(), nullable=False), + sa.Column( + "status", + sa.Enum("ACCEPTED", "REJECTED", "WAITLIST", name="outcome_status"), + nullable=False, + ), + sa.Column("timestamp", sa.DateTime(), nullable=False), + sa.Column("latest", sa.Boolean(), nullable=False), + sa.Column("updated_by_user_id", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint( + ["event_id"], + ["event.id"], + ), + sa.ForeignKeyConstraint( + ["updated_by_user_id"], + ["app_user.id"], + ), + sa.ForeignKeyConstraint( + ["user_id"], + ["app_user.id"], + ), + sa.PrimaryKeyConstraint("id"), ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('outcome') + op.drop_table("outcome") outcome_status.drop(op.get_bind()) # ### end Alembic commands ### diff --git a/api/migrations/versions/a5480ca6646b_indaba2019_reg_form.py b/api/migrations/versions/a5480ca6646b_indaba2019_reg_form.py index 0fd11b0d7..55653e3fb 100644 --- a/api/migrations/versions/a5480ca6646b_indaba2019_reg_form.py +++ b/api/migrations/versions/a5480ca6646b_indaba2019_reg_form.py @@ -7,26 +7,40 @@ """ # revision identifiers, used by Alembic. -revision = 'a5480ca6646b' -down_revision = '472bf293e0a1' +revision = "a5480ca6646b" +down_revision = "472bf293e0a1" -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.add_column('registration_question', sa.Column('depends_on_question_id', sa.Integer(), nullable=True)) - op.add_column('registration_question', sa.Column('hide_for_dependent_value', sa.String(), nullable=True)) - op.add_column('registration_question', sa.Column('required_value', sa.String(), nullable=True)) - op.create_foreign_key(None, 'registration_question', 'registration_question', ['depends_on_question_id'], ['id']) + op.add_column( + "registration_question", + sa.Column("depends_on_question_id", sa.Integer(), nullable=True), + ) + op.add_column( + "registration_question", + sa.Column("hide_for_dependent_value", sa.String(), nullable=True), + ) + op.add_column( + "registration_question", sa.Column("required_value", sa.String(), nullable=True) + ) + op.create_foreign_key( + None, + "registration_question", + "registration_question", + ["depends_on_question_id"], + ["id"], + ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_constraint(None, 'registration_question', type_='foreignkey') - op.drop_column('registration_question', 'required_value') - op.drop_column('registration_question', 'hide_for_dependent_value') - op.drop_column('registration_question', 'depends_on_question_id') + op.drop_constraint(None, "registration_question", type_="foreignkey") + op.drop_column("registration_question", "required_value") + op.drop_column("registration_question", "hide_for_dependent_value") + op.drop_column("registration_question", "depends_on_question_id") # ### end Alembic commands ### diff --git a/api/migrations/versions/a5876b93baee_.py b/api/migrations/versions/a5876b93baee_.py index bbb83e8f8..d65d06c6f 100644 --- a/api/migrations/versions/a5876b93baee_.py +++ b/api/migrations/versions/a5876b93baee_.py @@ -7,24 +7,25 @@ """ # revision identifiers, used by Alembic. -revision = 'a5876b93baee' -down_revision = 'aa890a74c772' +revision = "a5876b93baee" +down_revision = "aa890a74c772" -from alembic import op import sqlalchemy as sa +from alembic import op from sqlalchemy.dialects import postgresql + def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.alter_column('registration_question', 'validation_regex', nullable=True) - op.alter_column('registration_question', 'validation_text', nullable=True) - op.alter_column('registration_question', 'options', nullable=True) + op.alter_column("registration_question", "validation_regex", nullable=True) + op.alter_column("registration_question", "validation_text", nullable=True) + op.alter_column("registration_question", "options", nullable=True) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.alter_column('registration_question', 'validation_regex', nullable=False) - op.alter_column('registration_question', 'validation_text', nullable=False) - op.alter_column('registration_question', 'options', nullable=False) + op.alter_column("registration_question", "validation_regex", nullable=False) + op.alter_column("registration_question", "validation_text", nullable=False) + op.alter_column("registration_question", "options", nullable=False) # ### end Alembic commands ### diff --git a/api/migrations/versions/a714c69d1a6c_.py b/api/migrations/versions/a714c69d1a6c_.py index ce2dea199..a98770ede 100644 --- a/api/migrations/versions/a714c69d1a6c_.py +++ b/api/migrations/versions/a714c69d1a6c_.py @@ -7,20 +7,22 @@ """ # revision identifiers, used by Alembic. -revision = 'a714c69d1a6c' -down_revision = 'b7006cbdd923' +revision = "a714c69d1a6c" +down_revision = "b7006cbdd923" -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.add_column('organisation', sa.Column('email_from', sa.String(length=100), nullable=True)) + op.add_column( + "organisation", sa.Column("email_from", sa.String(length=100), nullable=True) + ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('organisation', 'email_from') + op.drop_column("organisation", "email_from") # ### end Alembic commands ### diff --git a/api/migrations/versions/aa890a74c772_.py b/api/migrations/versions/aa890a74c772_.py index 2a53a07fb..ad61379ef 100644 --- a/api/migrations/versions/aa890a74c772_.py +++ b/api/migrations/versions/aa890a74c772_.py @@ -7,60 +7,63 @@ """ # revision identifiers, used by Alembic. -revision = 'aa890a74c772' -down_revision = '3745cf17152b' +revision = "aa890a74c772" +down_revision = "3745cf17152b" -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.create_table('registration_form', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('event_id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['event_id'], ['event.id']), - sa.PrimaryKeyConstraint('id') - ) + op.create_table( + "registration_form", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("event_id", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(["event_id"], ["event.id"]), + sa.PrimaryKeyConstraint("id"), + ) - op.create_table('registration_section', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('registration_form_id', sa.Integer(), nullable=False), - sa.Column('name', sa.String(), nullable=False), - sa.Column('description', sa.String(), nullable=False), - sa.Column('order', sa.Integer(), nullable=False), - sa.Column('show_for_travel_award', sa.Boolean(), nullable=False), - sa.Column('show_for_accommodation_award', sa.Boolean(), nullable=False), - sa.Column('show_for_payment_required', sa.Boolean(), nullable=False), - sa.ForeignKeyConstraint(['registration_form_id'], ['registration_form.id']), - sa.PrimaryKeyConstraint('id') - ) + op.create_table( + "registration_section", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("registration_form_id", sa.Integer(), nullable=False), + sa.Column("name", sa.String(), nullable=False), + sa.Column("description", sa.String(), nullable=False), + sa.Column("order", sa.Integer(), nullable=False), + sa.Column("show_for_travel_award", sa.Boolean(), nullable=False), + sa.Column("show_for_accommodation_award", sa.Boolean(), nullable=False), + sa.Column("show_for_payment_required", sa.Boolean(), nullable=False), + sa.ForeignKeyConstraint(["registration_form_id"], ["registration_form.id"]), + sa.PrimaryKeyConstraint("id"), + ) - op.create_table('registration_question', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('registration_form_id', sa.Integer(), nullable=False), - sa.Column('section_id', sa.Integer(), nullable=False), - sa.Column('type', sa.String(), nullable=False), - sa.Column('description', sa.String(), nullable=False), - sa.Column('headline', sa.String(), nullable=False), - sa.Column('placeholder', sa.String(), nullable=False), - sa.Column('validation_regex', sa.String(), nullable=False), - sa.Column('validation_text', sa.String(), nullable=False), - sa.Column('order', sa.Integer(), nullable=False), - sa.Column('options', sa.JSON(), nullable=False), - sa.Column('is_required', sa.Boolean(), nullable=False), - sa.ForeignKeyConstraint(['registration_form_id'], ['registration_form.id']), - sa.ForeignKeyConstraint(['section_id'], ['registration_section.id']), - sa.PrimaryKeyConstraint('id') - ) + op.create_table( + "registration_question", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("registration_form_id", sa.Integer(), nullable=False), + sa.Column("section_id", sa.Integer(), nullable=False), + sa.Column("type", sa.String(), nullable=False), + sa.Column("description", sa.String(), nullable=False), + sa.Column("headline", sa.String(), nullable=False), + sa.Column("placeholder", sa.String(), nullable=False), + sa.Column("validation_regex", sa.String(), nullable=False), + sa.Column("validation_text", sa.String(), nullable=False), + sa.Column("order", sa.Integer(), nullable=False), + sa.Column("options", sa.JSON(), nullable=False), + sa.Column("is_required", sa.Boolean(), nullable=False), + sa.ForeignKeyConstraint(["registration_form_id"], ["registration_form.id"]), + sa.ForeignKeyConstraint(["section_id"], ["registration_section.id"]), + sa.PrimaryKeyConstraint("id"), + ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('registration_question') - op.drop_table('registration_section') - op.drop_table('registration_form') + op.drop_table("registration_question") + op.drop_table("registration_section") + op.drop_table("registration_form") # ### end Alembic commands ### diff --git a/api/migrations/versions/ab8b7ff61eee_.py b/api/migrations/versions/ab8b7ff61eee_.py index c8812349b..fca04fd9c 100644 --- a/api/migrations/versions/ab8b7ff61eee_.py +++ b/api/migrations/versions/ab8b7ff61eee_.py @@ -7,44 +7,46 @@ """ # revision identifiers, used by Alembic. -revision = 'ab8b7ff61eee' -down_revision = '472bf293e0a1' +revision = "ab8b7ff61eee" +down_revision = "472bf293e0a1" -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): - op.create_table('invitation_template', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('event_id', sa.Integer(), nullable=False), - sa.Column('template_path', sa.String(), nullable=False), - sa.Column('send_for_travel_award_only', sa.Boolean(), nullable=False), - sa.Column('send_for_accommodation_award_only', sa.Boolean(), nullable=False), - sa.Column('send_for_both_travel_accommodation', sa.Boolean(), nullable=False), - sa.ForeignKeyConstraint(['event_id'], ['event.id']), - sa.PrimaryKeyConstraint('id') - ) - - op.create_table('invitation_letter_request', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('registration_id', sa.Integer(), nullable=False), - sa.Column('event_id', sa.Integer(), nullable=False), - sa.Column('work_address', sa.String(), nullable=False), - sa.Column('addressed_to', sa.String(), nullable=False), - sa.Column('residential_address', sa.String(), nullable=False), - sa.Column('passport_name', sa.String(), nullable=False), - sa.Column('passport_no', sa.String(), nullable=False), - sa.Column('passport_issued_by', sa.String(), nullable=False), - sa.Column('invitation_letter_sent_at', sa.String(), nullable=False), - sa.Column('to_date', sa.DateTime(), nullable=False), - sa.Column('from_date', sa.DateTime(), nullable=False), - sa.ForeignKeyConstraint(['registration_id'], ['registration.id']), - sa.ForeignKeyConstraint(['event_id'], ['event.id']), - sa.PrimaryKeyConstraint('id') - ) + op.create_table( + "invitation_template", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("event_id", sa.Integer(), nullable=False), + sa.Column("template_path", sa.String(), nullable=False), + sa.Column("send_for_travel_award_only", sa.Boolean(), nullable=False), + sa.Column("send_for_accommodation_award_only", sa.Boolean(), nullable=False), + sa.Column("send_for_both_travel_accommodation", sa.Boolean(), nullable=False), + sa.ForeignKeyConstraint(["event_id"], ["event.id"]), + sa.PrimaryKeyConstraint("id"), + ) + + op.create_table( + "invitation_letter_request", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("registration_id", sa.Integer(), nullable=False), + sa.Column("event_id", sa.Integer(), nullable=False), + sa.Column("work_address", sa.String(), nullable=False), + sa.Column("addressed_to", sa.String(), nullable=False), + sa.Column("residential_address", sa.String(), nullable=False), + sa.Column("passport_name", sa.String(), nullable=False), + sa.Column("passport_no", sa.String(), nullable=False), + sa.Column("passport_issued_by", sa.String(), nullable=False), + sa.Column("invitation_letter_sent_at", sa.String(), nullable=False), + sa.Column("to_date", sa.DateTime(), nullable=False), + sa.Column("from_date", sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(["registration_id"], ["registration.id"]), + sa.ForeignKeyConstraint(["event_id"], ["event.id"]), + sa.PrimaryKeyConstraint("id"), + ) def downgrade(): - op.drop_table('invitation_template') - op.drop_table('invitation_letter_request') + op.drop_table("invitation_template") + op.drop_table("invitation_letter_request") diff --git a/api/migrations/versions/ae94d067a229_.py b/api/migrations/versions/ae94d067a229_.py index 29f7a8e4c..63413a530 100644 --- a/api/migrations/versions/ae94d067a229_.py +++ b/api/migrations/versions/ae94d067a229_.py @@ -1,5 +1,6 @@ import sqlalchemy as sa from alembic import op + """empty message Revision ID: ae94d067a229 @@ -9,34 +10,56 @@ """ # revision identifiers, used by Alembic. -revision = 'ae94d067a229' -down_revision = 'f2cefa124236' +revision = "ae94d067a229" +down_revision = "f2cefa124236" def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.add_column('invitation_letter_request', sa.Column( - 'guest_registration_id', sa.Integer(), nullable=True)) - op.alter_column('invitation_letter_request', 'registration_id', - existing_type=sa.INTEGER(), - nullable=True) - op.alter_column('invitation_letter_request', 'work_address', - existing_type=sa.VARCHAR(), - nullable=True) - op.create_foreign_key(None, 'invitation_letter_request', 'guest_registration', [ - 'guest_registration_id'], ['id']) + op.add_column( + "invitation_letter_request", + sa.Column("guest_registration_id", sa.Integer(), nullable=True), + ) + op.alter_column( + "invitation_letter_request", + "registration_id", + existing_type=sa.INTEGER(), + nullable=True, + ) + op.alter_column( + "invitation_letter_request", + "work_address", + existing_type=sa.VARCHAR(), + nullable=True, + ) + op.create_foreign_key( + None, + "invitation_letter_request", + "guest_registration", + ["guest_registration_id"], + ["id"], + ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_constraint('invitation_letter_request_registration_id_fkey', - 'invitation_letter_request', type_='foreignkey') - op.alter_column('invitation_letter_request', 'work_address', - existing_type=sa.VARCHAR(), - nullable=False) - op.alter_column('invitation_letter_request', 'registration_id', - existing_type=sa.INTEGER(), - nullable=False) - op.drop_column('invitation_letter_request', 'guest_registration_id') + op.drop_constraint( + "invitation_letter_request_registration_id_fkey", + "invitation_letter_request", + type_="foreignkey", + ) + op.alter_column( + "invitation_letter_request", + "work_address", + existing_type=sa.VARCHAR(), + nullable=False, + ) + op.alter_column( + "invitation_letter_request", + "registration_id", + existing_type=sa.INTEGER(), + nullable=False, + ) + op.drop_column("invitation_letter_request", "guest_registration_id") # ### end Alembic commands ### diff --git a/api/migrations/versions/af9c317d2c92_.py b/api/migrations/versions/af9c317d2c92_.py index c45927492..407a55233 100644 --- a/api/migrations/versions/af9c317d2c92_.py +++ b/api/migrations/versions/af9c317d2c92_.py @@ -7,23 +7,26 @@ """ # revision identifiers, used by Alembic. -revision = 'af9c317d2c92' -down_revision = '245d12695c69' +revision = "af9c317d2c92" +down_revision = "245d12695c69" + +import datetime +from enum import Enum -from alembic import op import sqlalchemy as sa +from alembic import op from sqlalchemy import orm -from app import db -from enum import Enum -import datetime from sqlalchemy.ext.declarative import declarative_base +from app import db + Base = declarative_base() + class Organisation(Base): __tablename__ = "organisation" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(50), nullable=False) @@ -36,7 +39,18 @@ class Organisation(Base): system_url = db.Column(db.String(100), nullable=False) privacy_policy = db.Column(db.String(100), nullable=False) - def __init__(self, name, system_name, small_logo, large_logo, domain, url, email_from, system_url, privacy_policy): + def __init__( + self, + name, + system_name, + small_logo, + large_logo, + domain, + url, + email_from, + system_url, + privacy_policy, + ): self.name = name self.small_logo = small_logo self.large_logo = large_logo @@ -47,10 +61,11 @@ def __init__(self, name, system_name, small_logo, large_logo, domain, url, email self.system_url = system_url self.privacy_policy = privacy_policy + class Country(Base): __tablename__ = "country" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(100), nullable=False) @@ -58,14 +73,16 @@ class Country(Base): def __init__(self, name): self.name = name + class EventType(Enum): - EVENT = 'event' - AWARD = 'award' + EVENT = "event" + AWARD = "award" + class Event(Base): __tablename__ = "event" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(50), nullable=False) @@ -73,8 +90,9 @@ class Event(Base): start_date = db.Column(db.DateTime(), nullable=False) end_date = db.Column(db.DateTime(), nullable=False) key = db.Column(db.String(255), nullable=False, unique=True) - organisation_id = db.Column(db.Integer(), db.ForeignKey( - 'organisation.id'), nullable=False) + organisation_id = db.Column( + db.Integer(), db.ForeignKey("organisation.id"), nullable=False + ) email_from = db.Column(db.String(255), nullable=False) url = db.Column(db.String(255), nullable=False) @@ -90,27 +108,28 @@ class Event(Base): registration_close = db.Column(db.DateTime(), nullable=False) event_type = db.Column(db.Enum(EventType), nullable=False) - def __init__(self, - name, - description, - start_date, - end_date, - key, - organisation_id, - email_from, - url, - application_open, - application_close, - review_open, - review_close, - selection_open, - selection_close, - offer_open, - offer_close, - registration_open, - registration_close, - event_type - ): + def __init__( + self, + name, + description, + start_date, + end_date, + key, + organisation_id, + email_from, + url, + application_open, + application_close, + review_open, + review_close, + selection_open, + selection_close, + offer_open, + offer_close, + registration_open, + registration_close, + event_type, + ): self.name = name self.description = description @@ -133,15 +152,16 @@ def __init__(self, self.event_roles = [] self.event_type = event_type + class ApplicationForm(Base): - __tablename__ = 'application_form' - __table_args__ = {'extend_existing': True} + __tablename__ = "application_form" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - event_id = db.Column(db.Integer(), db.ForeignKey('event.id'), nullable=False) + event_id = db.Column(db.Integer(), db.ForeignKey("event.id"), nullable=False) is_open = db.Column(db.Boolean(), nullable=False) - event = db.relationship('Event', foreign_keys=[event_id]) + event = db.relationship("Event", foreign_keys=[event_id]) nominations = db.Column(db.Boolean(), nullable=False) def __init__(self, event_id, is_open, nominations): @@ -149,13 +169,16 @@ def __init__(self, event_id, is_open, nominations): self.is_open = is_open self.nominations = nominations + class Question(Base): - __tablename__ = 'question' - __table_args__ = {'extend_existing': True} + __tablename__ = "question" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) - section_id = db.Column(db.Integer(), db.ForeignKey('section.id'), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) + section_id = db.Column(db.Integer(), db.ForeignKey("section.id"), nullable=False) type = db.Column(db.String(), nullable=False) description = db.Column(db.String(), nullable=True) headline = db.Column(db.String(), nullable=False) @@ -165,10 +188,25 @@ class Question(Base): order = db.Column(db.Integer(), nullable=False) options = db.Column(db.JSON(), nullable=True) is_required = db.Column(db.Boolean(), nullable=False) - depends_on_question_id = db.Column(db.Integer(), db.ForeignKey('question.id'), nullable=True) + depends_on_question_id = db.Column( + db.Integer(), db.ForeignKey("question.id"), nullable=True + ) show_for_values = db.Column(db.JSON(), nullable=True) - def __init__(self, application_form_id, section_id, headline, placeholder, order, questionType, validation_regex, validation_text=None, is_required = True, description = None, options = None): + def __init__( + self, + application_form_id, + section_id, + headline, + placeholder, + order, + questionType, + validation_regex, + validation_text=None, + is_required=True, + description=None, + options=None, + ): self.application_form_id = application_form_id self.section_id = section_id self.headline = headline @@ -183,15 +221,19 @@ def __init__(self, application_form_id, section_id, headline, placeholder, order class Section(Base): - __tablename__ = 'section' - __table_args__ = {'extend_existing': True} + __tablename__ = "section" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) name = db.Column(db.String(255), nullable=False) description = db.Column(db.String(255), nullable=False) order = db.Column(db.Integer(), nullable=False) - depends_on_question_id = db.Column(db.Integer(), db.ForeignKey('question.id', use_alter=True), nullable=True) + depends_on_question_id = db.Column( + db.Integer(), db.ForeignKey("question.id", use_alter=True), nullable=True + ) show_for_values = db.Column(db.JSON(), nullable=True) def __init__(self, application_form_id, name, description, order): @@ -200,14 +242,12 @@ def __init__(self, application_form_id, name, description, order): self.description = description self.order = order + def get_country_list(session): countries = session.query(Country).all() country_list = [] for country in countries: - country_list.append({ - 'label': country.name, - 'value': country.name - }) + country_list.append({"label": country.name, "value": country.name}) return country_list @@ -219,14 +259,27 @@ def upgrade(): Base.metadata.bind = op.get_bind() session = orm.Session(bind=Base.metadata.bind) - maathaiimpact2020 = Event('Wangari Maathai Impact Award 2020', - 'Wangari Maathai Impact Award 2020', - datetime.date(2020, 8, 23), datetime.date(2020, 8, 28), 'maathai2020', - 1, 'baobab@deeplearningindaba.com', 'http://www.deeplearningindaba.com', - datetime.date(2020,3,1), datetime.date(2020,4,17), datetime.date(2020,4,25), - datetime.date(2020,5,15),datetime.date(2020,1,1), datetime.date(2020,1,1), - datetime.date(2020,1,1),datetime.date(2020,1,1), datetime.date(2020,1,1), - datetime.date(2020,1,1), EventType.AWARD) + maathaiimpact2020 = Event( + "Wangari Maathai Impact Award 2020", + "Wangari Maathai Impact Award 2020", + datetime.date(2020, 8, 23), + datetime.date(2020, 8, 28), + "maathai2020", + 1, + "baobab@deeplearningindaba.com", + "http://www.deeplearningindaba.com", + datetime.date(2020, 3, 1), + datetime.date(2020, 4, 17), + datetime.date(2020, 4, 25), + datetime.date(2020, 5, 15), + datetime.date(2020, 1, 1), + datetime.date(2020, 1, 1), + datetime.date(2020, 1, 1), + datetime.date(2020, 1, 1), + datetime.date(2020, 1, 1), + datetime.date(2020, 1, 1), + EventType.AWARD, + ) session.add(maathaiimpact2020) session.commit() @@ -238,7 +291,10 @@ def upgrade(): app_form_id = application_form.id - main_section = Section(app_form_id, 'Wangari Maathai Impact Award 2020', """ + main_section = Section( + app_form_id, + "Wangari Maathai Impact Award 2020", + """ This is the official application form for the Wangari Maathai Impact Award 2020, an award to encourage and recognise work by African innovators that shows impactful application of machine learning and artificial intelligence. This award will be made at the Deep Learning Indaba in Tunis, Tunisia in August 2020. This application will require: @@ -249,250 +305,278 @@ def upgrade(): For eligibility criteria for the Maathai Award, please see www.deeplearningindaba.com/maathai-2020 For any queries, please email awards@deeplearningindaba.com. - """, 1) + """, + 1, + ) session.add(main_section) session.commit() q1_nomination_capacity = Question( application_form_id=app_form_id, section_id=main_section.id, - headline='Nominating Capacity', - placeholder='', + headline="Nominating Capacity", + placeholder="", order=1, - questionType='multi-choice', + questionType="multi-choice", validation_regex=None, is_required=True, options=[ - {'label':'Self-nomination', 'value':'self'}, - {'label':'Nomination on behalf of a candidate','value':'other'} - ] + {"label": "Self-nomination", "value": "self"}, + {"label": "Nomination on behalf of a candidate", "value": "other"}, + ], ) session.add(q1_nomination_capacity) session.commit() - nominator_information = Section(app_form_id, 'Nominator Information',""" + nominator_information = Section( + app_form_id, + "Nominator Information", + """ Details of the person nominating an individual, team or organisation - """,2) + """, + 2, + ) nominator_information.depends_on_question_id = q1_nomination_capacity.id - nominator_information.show_for_values = ['other'] + nominator_information.show_for_values = ["other"] session.add(nominator_information) session.commit() nominator_q1 = Question( application_form_id=app_form_id, section_id=nominator_information.id, - headline='Affiliation', - placeholder='Affiliation', + headline="Affiliation", + placeholder="Affiliation", order=1, - questionType='short-text', + questionType="short-text", validation_regex=None, is_required=True, - description='(university, institute, company, etc)' + description="(university, institute, company, etc)", ) nominator_q2 = Question( application_form_id=app_form_id, section_id=nominator_information.id, - headline='Department', - placeholder='Department', + headline="Department", + placeholder="Department", order=2, - questionType='short-text', + questionType="short-text", validation_regex=None, - is_required=True + is_required=True, ) nominator_q3 = Question( application_form_id=app_form_id, section_id=nominator_information.id, - headline='Describe your relationship to the nominee', - placeholder='', + headline="Describe your relationship to the nominee", + placeholder="", order=3, - questionType='long-text', + questionType="long-text", validation_regex=None, - is_required=True + is_required=True, ) session.add_all([nominator_q1, nominator_q2, nominator_q3]) session.commit() - nominee_information = Section(app_form_id, 'Nominee Information',""" + nominee_information = Section( + app_form_id, + "Nominee Information", + """ Details of the nominated individual, team or organisation to be considered for the award. For any teams/organisations, details of the principal contact should be entered below. - """,3) + """, + 3, + ) session.add(nominee_information) session.commit() nominee_q1 = Question( application_form_id=app_form_id, section_id=nominee_information.id, - headline='Title', - placeholder='Title', + headline="Title", + placeholder="Title", order=1, - questionType='short-text', + questionType="short-text", validation_regex=None, - is_required=True + is_required=True, ) nominee_q1.depends_on_question_id = q1_nomination_capacity.id - nominee_q1.show_for_values = ['other'] + nominee_q1.show_for_values = ["other"] nominee_q2 = Question( application_form_id=app_form_id, section_id=nominee_information.id, - headline='Firstname', - placeholder='Firstname', + headline="Firstname", + placeholder="Firstname", order=2, - questionType='short-text', + questionType="short-text", validation_regex=None, - is_required=True + is_required=True, ) nominee_q2.depends_on_question_id = q1_nomination_capacity.id - nominee_q2.show_for_values = ['other'] + nominee_q2.show_for_values = ["other"] nominee_q3 = Question( application_form_id=app_form_id, section_id=nominee_information.id, - headline='Lastname', - placeholder='Lastname', + headline="Lastname", + placeholder="Lastname", order=3, - questionType='short-text', + questionType="short-text", validation_regex=None, - is_required=True + is_required=True, ) nominee_q3.depends_on_question_id = q1_nomination_capacity.id - nominee_q3.show_for_values = ['other'] + nominee_q3.show_for_values = ["other"] nominee_q4 = Question( application_form_id=app_form_id, section_id=nominee_information.id, - headline='Email Address', - placeholder='Email Address', + headline="Email Address", + placeholder="Email Address", order=4, - questionType='short-text', + questionType="short-text", validation_regex=None, - is_required=True + is_required=True, ) nominee_q4.depends_on_question_id = q1_nomination_capacity.id - nominee_q4.show_for_values = ['other'] + nominee_q4.show_for_values = ["other"] nominee_q5 = Question( application_form_id=app_form_id, section_id=nominee_information.id, - headline='Affiliation', - placeholder='Affiliation', + headline="Affiliation", + placeholder="Affiliation", order=5, - questionType='short-text', + questionType="short-text", validation_regex=None, is_required=True, - description='(university, institute, company, etc)' + description="(university, institute, company, etc)", ) nominee_q6 = Question( application_form_id=app_form_id, section_id=nominee_information.id, - headline='If a team/organisation, names of team members', - placeholder='Names of team members', + headline="If a team/organisation, names of team members", + placeholder="Names of team members", order=6, - questionType='short-text', + questionType="short-text", validation_regex=None, - is_required=False + is_required=False, ) nominee_q7 = Question( application_form_id=app_form_id, section_id=nominee_information.id, - headline='Country of Residence', - placeholder='Choose an option', + headline="Country of Residence", + placeholder="Choose an option", order=7, - questionType='multi-choice', + questionType="multi-choice", validation_regex=None, is_required=True, - options=get_country_list(session) + options=get_country_list(session), ) nominee_q8 = Question( application_form_id=app_form_id, section_id=nominee_information.id, - headline='Nationality', - placeholder='Choose an option', + headline="Nationality", + placeholder="Choose an option", order=8, - questionType='multi-choice', + questionType="multi-choice", validation_regex=None, is_required=True, - options=get_country_list(session) + options=get_country_list(session), ) nominee_q9 = Question( application_form_id=app_form_id, section_id=nominee_information.id, - headline='Website (or other online presence)', - placeholder='Enter a URL', + headline="Website (or other online presence)", + placeholder="Enter a URL", order=9, - questionType='short-text', + questionType="short-text", validation_regex=None, - is_required=False + is_required=False, + ) + session.add_all( + [ + nominee_q1, + nominee_q2, + nominee_q3, + nominee_q4, + nominee_q5, + nominee_q6, + nominee_q7, + nominee_q8, + nominee_q9, + ] ) - session.add_all([nominee_q1,nominee_q2,nominee_q3,nominee_q4,nominee_q5, - nominee_q6,nominee_q7,nominee_q8,nominee_q9]) session.commit() - impact_info = Section(app_form_id, 'Information about impactful work','',3) + impact_info = Section(app_form_id, "Information about impactful work", "", 3) session.add(impact_info) session.commit() impact_q1 = Question( application_form_id=app_form_id, section_id=impact_info.id, - headline='What impactful work or project is the team/individual doing?', - placeholder='Enter 300-500 words', + headline="What impactful work or project is the team/individual doing?", + placeholder="Enter 300-500 words", order=1, - questionType='long-text', - validation_regex=r'^\s*(\S+(\s+|$)){300,500}$', + questionType="long-text", + validation_regex=r"^\s*(\S+(\s+|$)){300,500}$", is_required=True, - description='Describe the work/project. In particular, describe the role of machine learning and/or artificial intelligence (300-500 words)' + description="Describe the work/project. In particular, describe the role of machine learning and/or artificial intelligence (300-500 words)", ) impact_q2 = Question( application_form_id=app_form_id, section_id=impact_info.id, - headline='Who does this work impact? Say how.', - placeholder='Enter 150-200 words', + headline="Who does this work impact? Say how.", + placeholder="Enter 150-200 words", order=2, - questionType='long-text', - validation_regex=r'^\s*(\S+(\s+|$)){150,200}$', + questionType="long-text", + validation_regex=r"^\s*(\S+(\s+|$)){150,200}$", is_required=True, - description='Describe who is benefitting from this work (location, how many people etc). Describe how this work is positively affecting this group (150-200 words)' + description="Describe who is benefitting from this work (location, how many people etc). Describe how this work is positively affecting this group (150-200 words)", ) impact_q3 = Question( application_form_id=app_form_id, section_id=impact_info.id, - headline='Why is this work innovative?', - placeholder='Enter 150-200 words', + headline="Why is this work innovative?", + placeholder="Enter 150-200 words", order=3, - questionType='long-text', - validation_regex=r'^\s*(\S+(\s+|$)){150,200}$', + questionType="long-text", + validation_regex=r"^\s*(\S+(\s+|$)){150,200}$", is_required=True, - description='Describe the novel parts of the work, what difference it is making, or how it is moving Africa forwards (150-200 words)' + description="Describe the novel parts of the work, what difference it is making, or how it is moving Africa forwards (150-200 words)", ) - session.add_all([impact_q1,impact_q2,impact_q3]) + session.add_all([impact_q1, impact_q2, impact_q3]) session.commit() - supporting_docs = Section(app_form_id, 'Supporting Documentation', """ + supporting_docs = Section( + app_form_id, + "Supporting Documentation", + """ If this is a self-nomination, two supporting letters are required, otherwise one supporting letter is sufficient. The supporting letters should describe the nature of the impactful work, why it is considered to be impactful, and in what way the candidate strengthens African machine learning, and any other relevant information. Letter writers can be from anyone familiar with the impactful work. Supporting letters should be 600 words at most, written in English, and submitted electronically in PDF by the closing date through Baobab - """,4) + """, + 4, + ) session.add(supporting_docs) session.commit() supporting_docs_q1 = Question( application_form_id=app_form_id, section_id=supporting_docs.id, - headline='Add the details of the 1 or 2 people who will provide supporting letters.', - placeholder='', + headline="Add the details of the 1 or 2 people who will provide supporting letters.", + placeholder="", order=1, - questionType='reference', + questionType="reference", validation_regex=None, is_required=True, - description='Add at least two people if this is a self nomination and at least one if you are nominating someone else.', - options={'min_num_referral': 1, 'max_num_referral': 3} + description="Add at least two people if this is a self nomination and at least one if you are nominating someone else.", + options={"min_num_referral": 1, "max_num_referral": 3}, ) supporting_docs_q2 = Question( application_form_id=app_form_id, section_id=supporting_docs.id, - headline='Additional comments', - placeholder='', + headline="Additional comments", + placeholder="", order=2, - questionType='long-text', + questionType="long-text", validation_regex=None, is_required=False, - description='Use this space to provide any additional details which you feel are relevant to this nomination and have not been captured by this form.' + description="Use this space to provide any additional details which you feel are relevant to this nomination and have not been captured by this form.", ) session.add_all([supporting_docs_q1, supporting_docs_q2]) session.commit() @@ -505,16 +589,16 @@ def downgrade(): Base.metadata.bind = op.get_bind() session = orm.Session(bind=Base.metadata.bind) - event = session.query(Event).filter_by(key='maathai2020').first() + event = session.query(Event).filter_by(key="maathai2020").first() app_form = session.query(ApplicationForm).filter_by(event_id=event.id).first() - nominator = session.query(Section).filter_by(name='Nominator Information').first() + nominator = session.query(Section).filter_by(name="Nominator Information").first() nominator.depends_on_question_id = None session.query(Question).filter_by(application_form_id=app_form.id).delete() session.query(Section).filter_by(application_form_id=app_form.id).delete() session.query(ApplicationForm).filter_by(event_id=event.id).delete() - session.query(Event).filter_by(key='maathai2020').delete() + session.query(Event).filter_by(key="maathai2020").delete() - session.commit() \ No newline at end of file + session.commit() diff --git a/api/migrations/versions/b2d1fc607635_add_organisation_system_url.py b/api/migrations/versions/b2d1fc607635_add_organisation_system_url.py index 8953af328..dad21276e 100644 --- a/api/migrations/versions/b2d1fc607635_add_organisation_system_url.py +++ b/api/migrations/versions/b2d1fc607635_add_organisation_system_url.py @@ -7,22 +7,28 @@ """ # revision identifiers, used by Alembic. -revision = 'b2d1fc607635' -down_revision = '29dd5b8bc663' +revision = "b2d1fc607635" +down_revision = "29dd5b8bc663" -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): - op.add_column('organisation', sa.Column('system_url', sa.String(length=100), nullable=True)) - - op.execute("""UPDATE organisation SET system_url = 'test-baobab.deeplearningindaba.com' WHERE id = 1""") - op.execute("""UPDATE organisation SET system_url = 'test-portal.eeml.eu' WHERE id = 2""") + op.add_column( + "organisation", sa.Column("system_url", sa.String(length=100), nullable=True) + ) + + op.execute( + """UPDATE organisation SET system_url = 'test-baobab.deeplearningindaba.com' WHERE id = 1""" + ) + op.execute( + """UPDATE organisation SET system_url = 'test-portal.eeml.eu' WHERE id = 2""" + ) op.execute("""UPDATE organisation SET system_url = 'localhost' WHERE id = 3""") - op.alter_column('organisation', 'system_url', nullable=False) + op.alter_column("organisation", "system_url", nullable=False) def downgrade(): - op.drop_column('organisation', 'system_url') + op.drop_column("organisation", "system_url") diff --git a/api/migrations/versions/b3f7a73c98f3_.py b/api/migrations/versions/b3f7a73c98f3_.py index 5f554ecfc..bebd84bd0 100644 --- a/api/migrations/versions/b3f7a73c98f3_.py +++ b/api/migrations/versions/b3f7a73c98f3_.py @@ -7,358 +7,358 @@ """ # revision identifiers, used by Alembic. -revision = 'b3f7a73c98f3' -down_revision = '38dde1052c5e' +revision = "b3f7a73c98f3" +down_revision = "38dde1052c5e" -from alembic import op import sqlalchemy as sa +from alembic import op from sqlalchemy.dialects import postgresql + def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - application_form = op.create_table('application_form', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('event_id', sa.Integer(), nullable=False), - sa.Column('is_open', sa.Boolean(), nullable=False), - sa.Column('deadline', sa.DateTime(), nullable=False), - sa.PrimaryKeyConstraint('id'), - sa.ForeignKeyConstraint(['event_id'], ['event.id']) + application_form = op.create_table( + "application_form", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("event_id", sa.Integer(), nullable=False), + sa.Column("is_open", sa.Boolean(), nullable=False), + sa.Column("deadline", sa.DateTime(), nullable=False), + sa.PrimaryKeyConstraint("id"), + sa.ForeignKeyConstraint(["event_id"], ["event.id"]), ) - section = op.create_table('section', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('application_form_id', sa.Integer(), nullable=False), - sa.Column('name', sa.String(), nullable=False), - sa.Column('description', sa.String(), nullable=False), - sa.Column('order', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['application_form_id'], ['application_form.id']), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('name') + section = op.create_table( + "section", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("application_form_id", sa.Integer(), nullable=False), + sa.Column("name", sa.String(), nullable=False), + sa.Column("description", sa.String(), nullable=False), + sa.Column("order", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(["application_form_id"], ["application_form.id"]), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("name"), ) - question = op.create_table('question', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('application_form_id', sa.Integer(), nullable=False), - sa.Column('section_id', sa.Integer(), nullable=False), - sa.Column('type', sa.String(length=25), nullable=False), - sa.Column('description', sa.String(), nullable = True), - sa.Column('order', sa.Integer(), nullable=False), - sa.Column('headline', sa.String(), nullable=False), - sa.Column('options', sa.JSON(), nullable = True), - sa.Column('is_required', sa.Boolean(), nullable=False), - sa.ForeignKeyConstraint(['application_form_id'], ['application_form.id']), - sa.ForeignKeyConstraint(['section_id'], ['section.id'] ), - sa.PrimaryKeyConstraint('id'), + question = op.create_table( + "question", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("application_form_id", sa.Integer(), nullable=False), + sa.Column("section_id", sa.Integer(), nullable=False), + sa.Column("type", sa.String(length=25), nullable=False), + sa.Column("description", sa.String(), nullable=True), + sa.Column("order", sa.Integer(), nullable=False), + sa.Column("headline", sa.String(), nullable=False), + sa.Column("options", sa.JSON(), nullable=True), + sa.Column("is_required", sa.Boolean(), nullable=False), + sa.ForeignKeyConstraint(["application_form_id"], ["application_form.id"]), + sa.ForeignKeyConstraint(["section_id"], ["section.id"]), + sa.PrimaryKeyConstraint("id"), ) seed_application_form_data(application_form) seed_section_data(section) seed_question_data(question) - + # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('question') - op.drop_table('section') - op.drop_table('application_form') + op.drop_table("question") + op.drop_table("section") + op.drop_table("application_form") # ### end Alembic commands ### + def seed_application_form_data(application_form): - op.bulk_insert(application_form, - [ - { - 'id': 1, - 'event_id': 1, - 'is_open': True, - 'deadline': '30 April 2019' - } - ]) + op.bulk_insert( + application_form, + [{"id": 1, "event_id": 1, "is_open": True, "deadline": "30 April 2019"}], + ) -def seed_section_data(section): - op.bulk_insert(section, - [ - { - 'id': 1, - 'application_form_id': 1, - 'name': 'Indaba 2019 Application Form', - 'description': 'This is the official application form to apply for participation in the Deep Learning Indaba to be held 25-31 August 2019 in Nairobi, Kenya. Students can also use this application form to apply for travel and accommodation awards. \n \n \n Closing date: 30 April 2019', - 'order': 1 - }, - { - 'id': 2, - 'application_form_id': 1, - 'name': 'Tell Us a Bit About You', - 'description': 'Please use this section to tell us a bit more about yourself and your intentions as a future Deep Learning Indaba ambassador.', - 'order': 2 - }, - { - 'id': 3, - 'application_form_id': 1, - 'name': 'Apply For Travel Support ', - 'description': 'We may be able to sponsor the cost of travel and accommodation for some attendees. These travel awards are limited and highly competitive, but are assessed independently of Indaba attendance: applying for a travel award neither enhances nor undermines your chances of being accepted. To help as many people attend as possible, before applying for travel support, please check if your supervisor, department or university is able to support you in any way. ', - 'order': 3 - }, - { - 'id': 4, - 'application_form_id': 1, - 'name': 'Demographic Information', - 'description': 'We regard the promotion of diversity, inclusivity and transformation as a high priority. The information below helps us track our progress in this regard.', - 'order': 4 - }, - { - 'id': 5, - 'application_form_id': 1, - 'name': 'Previous Attendance', - 'description': 'Help us quantify our progress by giving us some info on your previous Indaba experiences.', - 'order': 5 - }, - { - 'id': 6, - 'application_form_id': 1, - 'name': 'Additional Information', - 'description': 'Anything else you may want us to know', - 'order': 6 - } - ]) -def seed_question_data(question): - op.bulk_insert(question, - [ - { - 'id': 1, - 'application_form_id': 1, - 'section_id': 2, - 'type': 'long_text', - 'headline': 'Why is attending the Deep Learning Indaba 2019 important to you?', - 'description': 'Provide a paragraph of 50-150 words.', - 'order': 1, - 'is_required': True, - 'options': None - }, - { - 'id': 2, - 'application_form_id': 1, - 'section_id': 2, - 'type': 'long_text', - 'headline': 'How will you share what you have learnt after the Indaba?', - 'description': 'Provide a paragraph of 50-150 words.', - 'order': 2, - 'is_required': True, - 'options': None - }, - { - 'id': 3, - 'application_form_id': 1, - 'section_id': 2, - 'type': 'long_text', - 'headline': 'Share with us a favourite machine learning resource you use: a paper, blog post, algorithm, result, or finding. Tell us why.', - 'description': 'Maximum 80 words.', - 'order': 3, - 'is_required': True, - 'options': None - }, - { - 'id': 4, - 'application_form_id': 1, - 'section_id': 2, - 'type': 'long_text', - 'headline': 'Are you or have you been a tutor for any relevant course, or part of any machine learning or data science society or meetup? If yes, give details.', - 'description': 'Maximum 80 words.', - 'order': 4, - 'is_required': True, - 'options': None - }, - { - 'id': 5, - 'application_form_id': 1, - 'section_id': 2, - 'type': 'long_text', - 'headline': 'Have you worked on a project that uses machine learning? If yes, give a short description.', - 'description': 'Maximum 150 words.', - 'order': 5, - 'is_required': False, - 'options': None - }, - { - 'id': 6, - 'application_form_id': 1, - 'section_id': 2, - 'type': 'file', - 'headline': 'Please upload your CV (pdf, max size 10MB)', - 'description': 'We generally do not look at CVs, but ask for this so that we can make a more informed selection, if needed.', - 'order': 6, - 'is_required': True, - 'options': None - }, - { - 'id': 7, - 'application_form_id': 1, - 'section_id': 2, - 'type': 'multi-choice', - 'headline': 'May we add your CV and email address to a database for sharing with our sponsors?', - 'description': 'If you are accepted, we would like to add your CV and email address to a database. We will share this database with our sponsors (only) so they may get in touch with you about events, internships, job openings, grants and scholarships, etc. We think you will find this useful in exploring future career options. Click yes if you would like us to add your CV and email address to this database (no other information from your application will be included).' , - 'order': 7, - 'is_required': True, - 'options': [ - {'selection': 'Yes'}, - {'selection': 'No'} - ] - }, - { - 'id': 8, - 'application_form_id': 1, - 'section_id': 3, - 'type': 'multi-choice', - 'headline': 'Would you like to be considered for a travel award?', - 'description': 'Travel awards will be used to cover the costs of return flights to the host city, as well as accommodation in shared dorm rooms close to the venue.', - 'order': 1, - 'is_required': True, - 'options': [ - {'selection': 'Yes, transport and accomodation'}, - {'selection': 'Yes, transport only'}, - {'selection': 'Yes, accommodation only'}, - {'selection': 'No'} - ] - }, - { - 'id': 9, - 'application_form_id': 1, - 'section_id': 3, - 'type': 'long-text', - 'headline': 'If you selected yes, please provide a short motivation for why you should be considered for a travel award.', - 'description': 'Maximum 150 words.', - 'order': 2, - 'is_required': False, - 'options': None - }, - { - 'id': 10, - 'application_form_id': 1, - 'section_id': 3, - 'type': 'short-text', - 'headline': 'If you selected yes to an award including transport, please state the intended airport of departure.', - 'order': 3, - 'description': None, - 'is_required': False, - 'options': None - }, - { - 'id': 11, - 'application_form_id': 1, - 'section_id': 3, - 'type': 'multi-choice', - 'headline': 'If you do not receive a travel award from us, will you still be able to attend?', - 'order': 4, - 'description': None, - 'is_required': False, - 'options': [ - {'selection': 'Yes'}, - {'selection': 'No'} - ] - }, - { - 'id': 12, - 'application_form_id': 1, - 'section_id': 4, - 'type': 'multi-choice', - 'headline': 'Gender', - 'order': 1, - 'description': None, - 'is_required': True, - 'options': [ - {'selection': 'Female'}, - {'selection': 'Male'}, - {'selection': 'Prefer not to say'} - ] - }, - { - 'id': 13, - 'application_form_id': 1, - 'section_id': 4, - 'type': 'multi-choice', - 'headline': 'Ethnicity', - 'description': 'We use South African demographic groupings for reporting.', - 'order': 2, - 'is_required': True, - 'options': [ - {'selection': 'Black'}, - {'selection': 'Coloured / Mixed Descent'}, - {'selection': 'White'}, - {'selection': 'Indian Descent'}, - {'selection': 'Other'} - ] - }, - { - 'id': 14, - 'application_form_id': 1, - 'section_id': 4, - 'type': 'multi-choice', - 'headline': 'Do you have any disability?', - 'description': 'We use the Washington Group difficulty schemes, and it helps us to ensure that our facilities can accommodate the needs of all attendees.', - 'order': 3, - 'is_required': True, - 'options': [ - {'selection': 'No disabilities'}, - {'selection': 'Sight disability'}, - {'selection': 'Hearing disability'}, - {'selection': 'Communication disability'}, - {'selection': 'Physical disability (e.g. difficulty in walking)'}, - {'selection': 'Mental disability (e.g. difficulty in remembering or concentrating)'}, - {'selection': 'Difficulty in self-care'}, - {'selection': 'Other'} - ] - }, - { - 'id': 15, - 'application_form_id': 1, - 'section_id': 5, - 'type': 'multi-choice', - 'headline': 'Did you attend the 2017 or 2018 Indaba', - 'order': 1, - 'description': None, - 'is_required': True, - 'options': [ - {'selection': 'Yes, I attended the 2017 Indaba'}, - {'selection': 'Yes, I attended the 2018 Indaba'}, - {'selection': 'Yes, I attended both Indabas'}, - {'selection': 'No'} - ] - }, - { - 'id': 16, - 'application_form_id': 1, - 'section_id': 5, - 'type': 'long-text', - 'headline': 'If you attended a previous Indaba, tell us how it has helped you grow or how you have used what you have learnt.', - 'order': 2, - 'description': None, - 'is_required': False, - 'options': None - }, - { - 'id': 17, - 'application_form_id': 1, - 'section_id': 6, - 'type': 'long-text', - 'headline': 'Any additional comments or remarks for the selection committee?', - 'order': 1, - 'description': None, - 'is_required': False, - 'options': None - }, - { - 'id': 18, - 'application_form_id': 1, - 'section_id': 6, - 'type': 'long-text', - 'headline': 'Anything else you think relevant, for example links to personal webpage, papers, GitHub/code repositories, community and outreach activities. ', - 'description': 'Maximum 150 words.', - 'order': 2, - 'is_required': False, - 'options': None - } - ]) +def seed_section_data(section): + op.bulk_insert( + section, + [ + { + "id": 1, + "application_form_id": 1, + "name": "Indaba 2019 Application Form", + "description": "This is the official application form to apply for participation in the Deep Learning Indaba to be held 25-31 August 2019 in Nairobi, Kenya. Students can also use this application form to apply for travel and accommodation awards. \n \n \n Closing date: 30 April 2019", + "order": 1, + }, + { + "id": 2, + "application_form_id": 1, + "name": "Tell Us a Bit About You", + "description": "Please use this section to tell us a bit more about yourself and your intentions as a future Deep Learning Indaba ambassador.", + "order": 2, + }, + { + "id": 3, + "application_form_id": 1, + "name": "Apply For Travel Support ", + "description": "We may be able to sponsor the cost of travel and accommodation for some attendees. These travel awards are limited and highly competitive, but are assessed independently of Indaba attendance: applying for a travel award neither enhances nor undermines your chances of being accepted. To help as many people attend as possible, before applying for travel support, please check if your supervisor, department or university is able to support you in any way. ", + "order": 3, + }, + { + "id": 4, + "application_form_id": 1, + "name": "Demographic Information", + "description": "We regard the promotion of diversity, inclusivity and transformation as a high priority. The information below helps us track our progress in this regard.", + "order": 4, + }, + { + "id": 5, + "application_form_id": 1, + "name": "Previous Attendance", + "description": "Help us quantify our progress by giving us some info on your previous Indaba experiences.", + "order": 5, + }, + { + "id": 6, + "application_form_id": 1, + "name": "Additional Information", + "description": "Anything else you may want us to know", + "order": 6, + }, + ], + ) +def seed_question_data(question): + op.bulk_insert( + question, + [ + { + "id": 1, + "application_form_id": 1, + "section_id": 2, + "type": "long_text", + "headline": "Why is attending the Deep Learning Indaba 2019 important to you?", + "description": "Provide a paragraph of 50-150 words.", + "order": 1, + "is_required": True, + "options": None, + }, + { + "id": 2, + "application_form_id": 1, + "section_id": 2, + "type": "long_text", + "headline": "How will you share what you have learnt after the Indaba?", + "description": "Provide a paragraph of 50-150 words.", + "order": 2, + "is_required": True, + "options": None, + }, + { + "id": 3, + "application_form_id": 1, + "section_id": 2, + "type": "long_text", + "headline": "Share with us a favourite machine learning resource you use: a paper, blog post, algorithm, result, or finding. Tell us why.", + "description": "Maximum 80 words.", + "order": 3, + "is_required": True, + "options": None, + }, + { + "id": 4, + "application_form_id": 1, + "section_id": 2, + "type": "long_text", + "headline": "Are you or have you been a tutor for any relevant course, or part of any machine learning or data science society or meetup? If yes, give details.", + "description": "Maximum 80 words.", + "order": 4, + "is_required": True, + "options": None, + }, + { + "id": 5, + "application_form_id": 1, + "section_id": 2, + "type": "long_text", + "headline": "Have you worked on a project that uses machine learning? If yes, give a short description.", + "description": "Maximum 150 words.", + "order": 5, + "is_required": False, + "options": None, + }, + { + "id": 6, + "application_form_id": 1, + "section_id": 2, + "type": "file", + "headline": "Please upload your CV (pdf, max size 10MB)", + "description": "We generally do not look at CVs, but ask for this so that we can make a more informed selection, if needed.", + "order": 6, + "is_required": True, + "options": None, + }, + { + "id": 7, + "application_form_id": 1, + "section_id": 2, + "type": "multi-choice", + "headline": "May we add your CV and email address to a database for sharing with our sponsors?", + "description": "If you are accepted, we would like to add your CV and email address to a database. We will share this database with our sponsors (only) so they may get in touch with you about events, internships, job openings, grants and scholarships, etc. We think you will find this useful in exploring future career options. Click yes if you would like us to add your CV and email address to this database (no other information from your application will be included).", + "order": 7, + "is_required": True, + "options": [{"selection": "Yes"}, {"selection": "No"}], + }, + { + "id": 8, + "application_form_id": 1, + "section_id": 3, + "type": "multi-choice", + "headline": "Would you like to be considered for a travel award?", + "description": "Travel awards will be used to cover the costs of return flights to the host city, as well as accommodation in shared dorm rooms close to the venue.", + "order": 1, + "is_required": True, + "options": [ + {"selection": "Yes, transport and accomodation"}, + {"selection": "Yes, transport only"}, + {"selection": "Yes, accommodation only"}, + {"selection": "No"}, + ], + }, + { + "id": 9, + "application_form_id": 1, + "section_id": 3, + "type": "long-text", + "headline": "If you selected yes, please provide a short motivation for why you should be considered for a travel award.", + "description": "Maximum 150 words.", + "order": 2, + "is_required": False, + "options": None, + }, + { + "id": 10, + "application_form_id": 1, + "section_id": 3, + "type": "short-text", + "headline": "If you selected yes to an award including transport, please state the intended airport of departure.", + "order": 3, + "description": None, + "is_required": False, + "options": None, + }, + { + "id": 11, + "application_form_id": 1, + "section_id": 3, + "type": "multi-choice", + "headline": "If you do not receive a travel award from us, will you still be able to attend?", + "order": 4, + "description": None, + "is_required": False, + "options": [{"selection": "Yes"}, {"selection": "No"}], + }, + { + "id": 12, + "application_form_id": 1, + "section_id": 4, + "type": "multi-choice", + "headline": "Gender", + "order": 1, + "description": None, + "is_required": True, + "options": [ + {"selection": "Female"}, + {"selection": "Male"}, + {"selection": "Prefer not to say"}, + ], + }, + { + "id": 13, + "application_form_id": 1, + "section_id": 4, + "type": "multi-choice", + "headline": "Ethnicity", + "description": "We use South African demographic groupings for reporting.", + "order": 2, + "is_required": True, + "options": [ + {"selection": "Black"}, + {"selection": "Coloured / Mixed Descent"}, + {"selection": "White"}, + {"selection": "Indian Descent"}, + {"selection": "Other"}, + ], + }, + { + "id": 14, + "application_form_id": 1, + "section_id": 4, + "type": "multi-choice", + "headline": "Do you have any disability?", + "description": "We use the Washington Group difficulty schemes, and it helps us to ensure that our facilities can accommodate the needs of all attendees.", + "order": 3, + "is_required": True, + "options": [ + {"selection": "No disabilities"}, + {"selection": "Sight disability"}, + {"selection": "Hearing disability"}, + {"selection": "Communication disability"}, + {"selection": "Physical disability (e.g. difficulty in walking)"}, + { + "selection": "Mental disability (e.g. difficulty in remembering or concentrating)" + }, + {"selection": "Difficulty in self-care"}, + {"selection": "Other"}, + ], + }, + { + "id": 15, + "application_form_id": 1, + "section_id": 5, + "type": "multi-choice", + "headline": "Did you attend the 2017 or 2018 Indaba", + "order": 1, + "description": None, + "is_required": True, + "options": [ + {"selection": "Yes, I attended the 2017 Indaba"}, + {"selection": "Yes, I attended the 2018 Indaba"}, + {"selection": "Yes, I attended both Indabas"}, + {"selection": "No"}, + ], + }, + { + "id": 16, + "application_form_id": 1, + "section_id": 5, + "type": "long-text", + "headline": "If you attended a previous Indaba, tell us how it has helped you grow or how you have used what you have learnt.", + "order": 2, + "description": None, + "is_required": False, + "options": None, + }, + { + "id": 17, + "application_form_id": 1, + "section_id": 6, + "type": "long-text", + "headline": "Any additional comments or remarks for the selection committee?", + "order": 1, + "description": None, + "is_required": False, + "options": None, + }, + { + "id": 18, + "application_form_id": 1, + "section_id": 6, + "type": "long-text", + "headline": "Anything else you think relevant, for example links to personal webpage, papers, GitHub/code repositories, community and outreach activities. ", + "description": "Maximum 150 words.", + "order": 2, + "is_required": False, + "options": None, + }, + ], + ) diff --git a/api/migrations/versions/b4c1dfa70233_add_ethnicity_question.py b/api/migrations/versions/b4c1dfa70233_add_ethnicity_question.py index 5bcafb8c9..3536ad30b 100644 --- a/api/migrations/versions/b4c1dfa70233_add_ethnicity_question.py +++ b/api/migrations/versions/b4c1dfa70233_add_ethnicity_question.py @@ -7,25 +7,29 @@ """ # revision identifiers, used by Alembic. -revision = 'b4c1dfa70233' -down_revision = '91468f804113' +revision = "b4c1dfa70233" +down_revision = "91468f804113" -from alembic import op import sqlalchemy as sa -from sqlalchemy.ext.declarative import declarative_base +from alembic import op from sqlalchemy import orm +from sqlalchemy.ext.declarative import declarative_base + from app import db # Freeze the question model Base = declarative_base() + class Question(Base): - __tablename__ = 'question' - __table_args__ = {'extend_existing': True} + __tablename__ = "question" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) - section_id = db.Column(db.Integer(), db.ForeignKey('section.id'), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) + section_id = db.Column(db.Integer(), db.ForeignKey("section.id"), nullable=False) type = db.Column(db.String(), nullable=False) description = db.Column(db.String(), nullable=True) headline = db.Column(db.String(), nullable=False) @@ -36,7 +40,20 @@ class Question(Base): options = db.Column(db.JSON(), nullable=True) is_required = db.Column(db.Boolean(), nullable=False) - def __init__(self, application_form_id, section_id, headline, placeholder, order, questionType, validation_regex, validation_text=None, is_required = True, description = None, options = None): + def __init__( + self, + application_form_id, + section_id, + headline, + placeholder, + order, + questionType, + validation_regex, + validation_text=None, + is_required=True, + description=None, + options=None, + ): self.application_form_id = application_form_id self.section_id = section_id self.headline = headline @@ -51,10 +68,10 @@ def __init__(self, application_form_id, section_id, headline, placeholder, order class ApplicationForm(Base): - __tablename__ = 'application_form' - __table_args__ = {'extend_existing': True} + __tablename__ = "application_form" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - event_id = db.Column(db.Integer(), db.ForeignKey('event.id'), nullable=False) + event_id = db.Column(db.Integer(), db.ForeignKey("event.id"), nullable=False) is_open = db.Column(db.Boolean(), nullable=False) deadline = db.Column(db.DateTime(), nullable=False) @@ -65,10 +82,12 @@ def __init__(self, event_id, is_open, deadline): class Section(Base): - __tablename__ = 'section' - __table_args__ = {'extend_existing': True} + __tablename__ = "section" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) name = db.Column(db.String(255), unique=True, nullable=False) description = db.Column(db.String(255), nullable=False) order = db.Column(db.Integer(), unique=True, nullable=False) @@ -85,23 +104,30 @@ def upgrade(): session = orm.Session(bind=Base.metadata.bind) # Add ethnicity question to apply for travel support section - section = session.query(Section).filter(Section.name == 'Apply For Travel Support ').first() - new_question = Question(section.application_form_id, section.id, - 'You Ethnicity (South African applicants only)', - 'Select an option...', + section = ( + session.query(Section) + .filter(Section.name == "Apply For Travel Support ") + .first() + ) + new_question = Question( + section.application_form_id, + section.id, + "You Ethnicity (South African applicants only)", + "Select an option...", 5, - 'multi-choice', + "multi-choice", None, None, - False, - 'Please fill this in if you are applying for travel support from South Africa, leave blank otherwise. We may be required to provide this information to some of our South African sponsors for reporting purposes.', + False, + "Please fill this in if you are applying for travel support from South Africa, leave blank otherwise. We may be required to provide this information to some of our South African sponsors for reporting purposes.", [ - {"label": "Black", "value": 'black'}, - {"label": "Coloured / Mixed Descent", "value": 'coloured/mixed'}, - {"label": "White", "value": 'white'}, - {"label": "Indian Descent", "value": 'indian'}, - {"label": "Other", "value": "other"} - ]) + {"label": "Black", "value": "black"}, + {"label": "Coloured / Mixed Descent", "value": "coloured/mixed"}, + {"label": "White", "value": "white"}, + {"label": "Indian Descent", "value": "indian"}, + {"label": "Other", "value": "other"}, + ], + ) # Reset the auto-increment ID sequence for the table op.execute("""SELECT setval('question_id_seq', (SELECT max(id) FROM question));""") @@ -110,14 +136,28 @@ def upgrade(): session.flush() # Add word limit to additional information sections - question = session.query(Question).filter(Question.headline == 'Any additional comments or remarks for the selection committee?').first() - question.validation_regex = r'^\W*(\w+(\W+|$)){0,150}$' - question.validation_text = 'Enter a maximum of 150 words' - question.description = 'Maximum 150 words.' + question = ( + session.query(Question) + .filter( + Question.headline + == "Any additional comments or remarks for the selection committee?" + ) + .first() + ) + question.validation_regex = r"^\W*(\w+(\W+|$)){0,150}$" + question.validation_text = "Enter a maximum of 150 words" + question.description = "Maximum 150 words." # Update validation text on anything relevant question - question = session.query(Question).filter(Question.headline == 'Anything else you think relevant, for example links to personal webpage, papers, GitHub/code repositories, community and outreach activities. ').first() - question.validation_text = 'Maximum 150 words.' + question = ( + session.query(Question) + .filter( + Question.headline + == "Anything else you think relevant, for example links to personal webpage, papers, GitHub/code repositories, community and outreach activities. " + ) + .first() + ) + question.validation_text = "Maximum 150 words." session.commit() session.flush() @@ -131,13 +171,27 @@ def downgrade(): # question = session.query(Question).filter(Question.headline == 'You Ethnicity (South African applicants only)').delete() # Undo changes to additional comments question - question = session.query(Question).filter(Question.headline == 'Any additional comments or remarks for the selection committee?').first() + question = ( + session.query(Question) + .filter( + Question.headline + == "Any additional comments or remarks for the selection committee?" + ) + .first() + ) question.validation_regex = None question.validation_text = None question.description = None # Undo changes to anything relevant question - question = session.query(Question).filter(Question.headline == 'Anything else you think relevant, for example links to personal webpage, papers, GitHub/code repositories, community and outreach activities. ').first() + question = ( + session.query(Question) + .filter( + Question.headline + == "Anything else you think relevant, for example links to personal webpage, papers, GitHub/code repositories, community and outreach activities. " + ) + .first() + ) question.validation_text = None session.commit() diff --git a/api/migrations/versions/b6a2a69fbb13_.py b/api/migrations/versions/b6a2a69fbb13_.py index ccc25279e..617587ad4 100644 --- a/api/migrations/versions/b6a2a69fbb13_.py +++ b/api/migrations/versions/b6a2a69fbb13_.py @@ -7,26 +7,37 @@ """ # revision identifiers, used by Alembic. -revision = 'b6a2a69fbb13' -down_revision = 'd8d17ed47c53' +revision = "b6a2a69fbb13" +down_revision = "d8d17ed47c53" -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.alter_column('reference', 'uploaded_document', - existing_type=sa.VARCHAR(length=128), - nullable=False) - op.drop_column('reference_request', 'reference_submitted') + op.alter_column( + "reference", + "uploaded_document", + existing_type=sa.VARCHAR(length=128), + nullable=False, + ) + op.drop_column("reference_request", "reference_submitted") # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.add_column('reference_request', sa.Column('reference_submitted', sa.BOOLEAN(), autoincrement=False, nullable=False)) - op.alter_column('reference', 'uploaded_document', - existing_type=sa.VARCHAR(length=128), - nullable=True) + op.add_column( + "reference_request", + sa.Column( + "reference_submitted", sa.BOOLEAN(), autoincrement=False, nullable=False + ), + ) + op.alter_column( + "reference", + "uploaded_document", + existing_type=sa.VARCHAR(length=128), + nullable=True, + ) # ### end Alembic commands ### diff --git a/api/migrations/versions/b7006cbdd923_.py b/api/migrations/versions/b7006cbdd923_.py index a33cff76b..d84d27633 100644 --- a/api/migrations/versions/b7006cbdd923_.py +++ b/api/migrations/versions/b7006cbdd923_.py @@ -7,27 +7,30 @@ """ # revision identifiers, used by Alembic. -revision = 'b7006cbdd923' -down_revision = '3bc5355f159c' +revision = "b7006cbdd923" +down_revision = "3bc5355f159c" -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.add_column('event', sa.Column('application_close', sa.DateTime(), nullable=True)) - op.add_column('event', sa.Column('application_open', sa.DateTime(), nullable=True)) - op.add_column('event', sa.Column('offer_close', sa.DateTime(), nullable=True)) - op.add_column('event', sa.Column('offer_open', sa.DateTime(), nullable=True)) - op.add_column('event', sa.Column('registration_close', sa.DateTime(), nullable=True)) - op.add_column('event', sa.Column('registration_open', sa.DateTime(), nullable=True)) - op.add_column('event', sa.Column('review_close', sa.DateTime(), nullable=True)) - op.add_column('event', sa.Column('review_open', sa.DateTime(), nullable=True)) - op.add_column('event', sa.Column('selection_close', sa.DateTime(), nullable=True)) - op.add_column('event', sa.Column('selection_open', sa.DateTime(), nullable=True)) + op.add_column("event", sa.Column("application_close", sa.DateTime(), nullable=True)) + op.add_column("event", sa.Column("application_open", sa.DateTime(), nullable=True)) + op.add_column("event", sa.Column("offer_close", sa.DateTime(), nullable=True)) + op.add_column("event", sa.Column("offer_open", sa.DateTime(), nullable=True)) + op.add_column( + "event", sa.Column("registration_close", sa.DateTime(), nullable=True) + ) + op.add_column("event", sa.Column("registration_open", sa.DateTime(), nullable=True)) + op.add_column("event", sa.Column("review_close", sa.DateTime(), nullable=True)) + op.add_column("event", sa.Column("review_open", sa.DateTime(), nullable=True)) + op.add_column("event", sa.Column("selection_close", sa.DateTime(), nullable=True)) + op.add_column("event", sa.Column("selection_open", sa.DateTime(), nullable=True)) - op.execute(""" + op.execute( + """ UPDATE event SET application_open = '2019-03-01', @@ -40,31 +43,32 @@ def upgrade(): offer_close = '2019-07-14', registration_open = '2019-07-15', registration_close = '2019-07-31' - WHERE id = 1""") + WHERE id = 1""" + ) - op.alter_column('event', 'application_close', nullable=False) - op.alter_column('event', 'application_open', nullable=False) - op.alter_column('event', 'offer_close', nullable=False) - op.alter_column('event', 'offer_open', nullable=False) - op.alter_column('event', 'registration_close', nullable=False) - op.alter_column('event', 'registration_open', nullable=False) - op.alter_column('event', 'review_close', nullable=False) - op.alter_column('event', 'review_open', nullable=False) - op.alter_column('event', 'selection_close', nullable=False) - op.alter_column('event', 'selection_open', nullable=False) + op.alter_column("event", "application_close", nullable=False) + op.alter_column("event", "application_open", nullable=False) + op.alter_column("event", "offer_close", nullable=False) + op.alter_column("event", "offer_open", nullable=False) + op.alter_column("event", "registration_close", nullable=False) + op.alter_column("event", "registration_open", nullable=False) + op.alter_column("event", "review_close", nullable=False) + op.alter_column("event", "review_open", nullable=False) + op.alter_column("event", "selection_close", nullable=False) + op.alter_column("event", "selection_open", nullable=False) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('event', 'selection_open') - op.drop_column('event', 'selection_close') - op.drop_column('event', 'review_open') - op.drop_column('event', 'review_close') - op.drop_column('event', 'registration_open') - op.drop_column('event', 'registration_close') - op.drop_column('event', 'offer_open') - op.drop_column('event', 'offer_close') - op.drop_column('event', 'application_open') - op.drop_column('event', 'application_close') + op.drop_column("event", "selection_open") + op.drop_column("event", "selection_close") + op.drop_column("event", "review_open") + op.drop_column("event", "review_close") + op.drop_column("event", "registration_open") + op.drop_column("event", "registration_close") + op.drop_column("event", "offer_open") + op.drop_column("event", "offer_close") + op.drop_column("event", "application_open") + op.drop_column("event", "application_close") # ### end Alembic commands ### diff --git a/api/migrations/versions/c06c236ad4c5_.py b/api/migrations/versions/c06c236ad4c5_.py index 26aff9638..9d616951f 100644 --- a/api/migrations/versions/c06c236ad4c5_.py +++ b/api/migrations/versions/c06c236ad4c5_.py @@ -7,28 +7,41 @@ """ # revision identifiers, used by Alembic. -revision = 'c06c236ad4c5' -down_revision = '539d0567cd6c' +revision = "c06c236ad4c5" +down_revision = "539d0567cd6c" -from alembic import op import sqlalchemy as sa +from alembic import op from sqlalchemy.dialects import postgresql + def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.add_column('offer', sa.Column('candidate_response', sa.Boolean(), nullable=True)) - op.add_column('offer', sa.Column('responded_at', sa.DateTime(), nullable=False)) - op.drop_column('offer', 'accepted') - op.drop_column('offer', 'updated_at') - op.drop_column('offer', 'rejected') + op.add_column("offer", sa.Column("candidate_response", sa.Boolean(), nullable=True)) + op.add_column("offer", sa.Column("responded_at", sa.DateTime(), nullable=False)) + op.drop_column("offer", "accepted") + op.drop_column("offer", "updated_at") + op.drop_column("offer", "rejected") # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.add_column('offer', sa.Column('rejected', sa.VARCHAR(length=50), autoincrement=False, nullable=True)) - op.add_column('offer', sa.Column('updated_at', postgresql.TIMESTAMP(), autoincrement=False, nullable=False)) - op.add_column('offer', sa.Column('accepted', sa.BOOLEAN(), autoincrement=False, nullable=True)) - op.drop_column('offer', 'responded_at') - op.drop_column('offer', 'candidate_response') + op.add_column( + "offer", + sa.Column( + "rejected", sa.VARCHAR(length=50), autoincrement=False, nullable=True + ), + ) + op.add_column( + "offer", + sa.Column( + "updated_at", postgresql.TIMESTAMP(), autoincrement=False, nullable=False + ), + ) + op.add_column( + "offer", sa.Column("accepted", sa.BOOLEAN(), autoincrement=False, nullable=True) + ) + op.drop_column("offer", "responded_at") + op.drop_column("offer", "candidate_response") # ### end Alembic commands ### diff --git a/api/migrations/versions/c0885e531d29_.py b/api/migrations/versions/c0885e531d29_.py index 5027c05bd..611e5aa90 100644 --- a/api/migrations/versions/c0885e531d29_.py +++ b/api/migrations/versions/c0885e531d29_.py @@ -7,40 +7,76 @@ """ # revision identifiers, used by Alembic. -revision = 'c0885e531d29' -down_revision = '878f67285c72' +revision = "c0885e531d29" +down_revision = "878f67285c72" -from alembic import op import sqlalchemy as sa +from alembic import op from sqlalchemy.dialects import postgresql + def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.add_column('offer', sa.Column('accommodation_award', sa.Boolean(), nullable=True)) - op.add_column('offer', sa.Column('payment_required', sa.Boolean(), nullable=False)) - op.drop_column('offer', 'payement_required') - op.drop_column('offer', 'accomodation_award') - op.drop_column('offer', 'accepted') - op.alter_column('registration', 'confirmation_email_sent_at', - existing_type=postgresql.TIMESTAMP(), - nullable=True) - op.alter_column('registration', 'created_at', - existing_type=postgresql.TIMESTAMP(), - nullable=True) + op.add_column( + "offer", sa.Column("accommodation_award", sa.Boolean(), nullable=True) + ) + op.add_column("offer", sa.Column("payment_required", sa.Boolean(), nullable=False)) + op.drop_column("offer", "payement_required") + op.drop_column("offer", "accomodation_award") + op.drop_column("offer", "accepted") + op.alter_column( + "registration", + "confirmation_email_sent_at", + existing_type=postgresql.TIMESTAMP(), + nullable=True, + ) + op.alter_column( + "registration", + "created_at", + existing_type=postgresql.TIMESTAMP(), + nullable=True, + ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.alter_column('registration', 'created_at', - existing_type=postgresql.TIMESTAMP(), - nullable=False) - op.alter_column('registration', 'confirmation_email_sent_at', - existing_type=postgresql.TIMESTAMP(), - nullable=False) - op.add_column('offer', sa.Column('accepted', sa.VARCHAR(length=50), autoincrement=False, nullable=True)) - op.add_column('offer', sa.Column('accomodation_award', sa.VARCHAR(length=50), autoincrement=False, nullable=False)) - op.add_column('offer', sa.Column('payement_required', sa.VARCHAR(length=50), autoincrement=False, nullable=False)) - op.drop_column('offer', 'payment_required') - op.drop_column('offer', 'accommodation_award') + op.alter_column( + "registration", + "created_at", + existing_type=postgresql.TIMESTAMP(), + nullable=False, + ) + op.alter_column( + "registration", + "confirmation_email_sent_at", + existing_type=postgresql.TIMESTAMP(), + nullable=False, + ) + op.add_column( + "offer", + sa.Column( + "accepted", sa.VARCHAR(length=50), autoincrement=False, nullable=True + ), + ) + op.add_column( + "offer", + sa.Column( + "accomodation_award", + sa.VARCHAR(length=50), + autoincrement=False, + nullable=False, + ), + ) + op.add_column( + "offer", + sa.Column( + "payement_required", + sa.VARCHAR(length=50), + autoincrement=False, + nullable=False, + ), + ) + op.drop_column("offer", "payment_required") + op.drop_column("offer", "accommodation_award") # ### end Alembic commands ### diff --git a/api/migrations/versions/c3b4ca7ceef9_add_org_privacy_policy.py b/api/migrations/versions/c3b4ca7ceef9_add_org_privacy_policy.py index 89e22537b..92386e9c4 100644 --- a/api/migrations/versions/c3b4ca7ceef9_add_org_privacy_policy.py +++ b/api/migrations/versions/c3b4ca7ceef9_add_org_privacy_policy.py @@ -7,20 +7,26 @@ """ # revision identifiers, used by Alembic. -revision = 'c3b4ca7ceef9' -down_revision = '74f1f1476873' +revision = "c3b4ca7ceef9" +down_revision = "74f1f1476873" -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): - op.add_column('organisation', sa.Column('privacy_policy', sa.String(length=100), nullable=True)) - op.execute("""UPDATE organisation set privacy_policy='PrivacyPolicy.pdf' WHERE id in (1, 3)""") - op.execute("""UPDATE organisation set privacy_policy='EEML_PrivacyPolicy.pdf' WHERE id = 2""") - op.alter_column('organisation', 'privacy_policy', nullable=False) + op.add_column( + "organisation", + sa.Column("privacy_policy", sa.String(length=100), nullable=True), + ) + op.execute( + """UPDATE organisation set privacy_policy='PrivacyPolicy.pdf' WHERE id in (1, 3)""" + ) + op.execute( + """UPDATE organisation set privacy_policy='EEML_PrivacyPolicy.pdf' WHERE id = 2""" + ) + op.alter_column("organisation", "privacy_policy", nullable=False) def downgrade(): - op.drop_column('organisation', 'privacy_policy') - + op.drop_column("organisation", "privacy_policy") diff --git a/api/migrations/versions/c3c5ef958ca1_.py b/api/migrations/versions/c3c5ef958ca1_.py index e15f23af2..297e1de1e 100644 --- a/api/migrations/versions/c3c5ef958ca1_.py +++ b/api/migrations/versions/c3c5ef958ca1_.py @@ -7,23 +7,25 @@ """ # revision identifiers, used by Alembic. -revision = 'c3c5ef958ca1' -down_revision = 'c795c179c2b2' +revision = "c3c5ef958ca1" +down_revision = "c795c179c2b2" + +import datetime +from enum import Enum -from alembic import op import sqlalchemy as sa +from alembic import op from sqlalchemy import orm from sqlalchemy.ext.declarative import declarative_base + from app import db -import datetime -from enum import Enum Base = declarative_base() class Organisation(Base): __tablename__ = "organisation" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(50), nullable=False) @@ -36,7 +38,18 @@ class Organisation(Base): system_url = db.Column(db.String(100), nullable=False) privacy_policy = db.Column(db.String(100), nullable=False) - def __init__(self, name, system_name, small_logo, large_logo, domain, url, email_from, system_url, privacy_policy): + def __init__( + self, + name, + system_name, + small_logo, + large_logo, + domain, + url, + email_from, + system_url, + privacy_policy, + ): self.name = name self.small_logo = small_logo self.large_logo = large_logo @@ -50,7 +63,7 @@ def __init__(self, name, system_name, small_logo, large_logo, domain, url, email class Country(Base): __tablename__ = "country" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(100), nullable=False) @@ -60,13 +73,13 @@ def __init__(self, name): class EventType(Enum): - EVENT = 'event' - AWARD = 'award' + EVENT = "event" + AWARD = "award" class Event(Base): __tablename__ = "event" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(50), nullable=False) @@ -74,8 +87,9 @@ class Event(Base): start_date = db.Column(db.DateTime(), nullable=False) end_date = db.Column(db.DateTime(), nullable=False) key = db.Column(db.String(255), nullable=False, unique=True) - organisation_id = db.Column(db.Integer(), db.ForeignKey( - 'organisation.id'), nullable=False) + organisation_id = db.Column( + db.Integer(), db.ForeignKey("organisation.id"), nullable=False + ) email_from = db.Column(db.String(255), nullable=False) url = db.Column(db.String(255), nullable=False) @@ -91,27 +105,28 @@ class Event(Base): registration_close = db.Column(db.DateTime(), nullable=False) event_type = db.Column(db.Enum(EventType), nullable=False) - def __init__(self, - name, - description, - start_date, - end_date, - key, - organisation_id, - email_from, - url, - application_open, - application_close, - review_open, - review_close, - selection_open, - selection_close, - offer_open, - offer_close, - registration_open, - registration_close, - event_type - ): + def __init__( + self, + name, + description, + start_date, + end_date, + key, + organisation_id, + email_from, + url, + application_open, + application_close, + review_open, + review_close, + selection_open, + selection_close, + offer_open, + offer_close, + registration_open, + registration_close, + event_type, + ): self.name = name self.description = description self.start_date = start_date @@ -135,14 +150,14 @@ def __init__(self, class ApplicationForm(Base): - __tablename__ = 'application_form' - __table_args__ = {'extend_existing': True} + __tablename__ = "application_form" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - event_id = db.Column(db.Integer(), db.ForeignKey('event.id'), nullable=False) + event_id = db.Column(db.Integer(), db.ForeignKey("event.id"), nullable=False) is_open = db.Column(db.Boolean(), nullable=False) - event = db.relationship('Event', foreign_keys=[event_id]) + event = db.relationship("Event", foreign_keys=[event_id]) nominations = db.Column(db.Boolean(), nullable=False) def __init__(self, event_id, is_open, nominations): @@ -152,12 +167,14 @@ def __init__(self, event_id, is_open, nominations): class Question(Base): - __tablename__ = 'question' - __table_args__ = {'extend_existing': True} + __tablename__ = "question" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) - section_id = db.Column(db.Integer(), db.ForeignKey('section.id'), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) + section_id = db.Column(db.Integer(), db.ForeignKey("section.id"), nullable=False) type = db.Column(db.String(), nullable=False) description = db.Column(db.String(), nullable=True) headline = db.Column(db.String(), nullable=False) @@ -167,11 +184,25 @@ class Question(Base): order = db.Column(db.Integer(), nullable=False) options = db.Column(db.JSON(), nullable=True) is_required = db.Column(db.Boolean(), nullable=False) - depends_on_question_id = db.Column(db.Integer(), db.ForeignKey('question.id'), nullable=True) + depends_on_question_id = db.Column( + db.Integer(), db.ForeignKey("question.id"), nullable=True + ) show_for_values = db.Column(db.JSON(), nullable=True) - def __init__(self, application_form_id, section_id, headline, placeholder, order, questionType, validation_regex, - validation_text=None, is_required=True, description=None, options=None): + def __init__( + self, + application_form_id, + section_id, + headline, + placeholder, + order, + questionType, + validation_regex, + validation_text=None, + is_required=True, + description=None, + options=None, + ): self.application_form_id = application_form_id self.section_id = section_id self.headline = headline @@ -186,15 +217,19 @@ def __init__(self, application_form_id, section_id, headline, placeholder, order class Section(Base): - __tablename__ = 'section' - __table_args__ = {'extend_existing': True} + __tablename__ = "section" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) name = db.Column(db.String(255), nullable=False) description = db.Column(db.String(255), nullable=False) order = db.Column(db.Integer(), nullable=False) - depends_on_question_id = db.Column(db.Integer(), db.ForeignKey('question.id', use_alter=True), nullable=True) + depends_on_question_id = db.Column( + db.Integer(), db.ForeignKey("question.id", use_alter=True), nullable=True + ) show_for_values = db.Column(db.JSON(), nullable=True) def __init__(self, application_form_id, name, description, order): @@ -208,10 +243,7 @@ def get_country_list(session): countries = session.query(Country).all() country_list = [] for country in countries: - country_list.append({ - 'label': country.name, - 'value': country.name - }) + country_list.append({"label": country.name, "value": country.name}) return country_list @@ -220,14 +252,27 @@ def upgrade(): Base.metadata.bind = op.get_bind() session = orm.Session(bind=Base.metadata.bind) - kambulemasters2020 = Event('Thamsanqa Kambule Masters Dissertation Award 2020', - 'Thamsanqa Kambule Masters Dissertation Award 2020', - datetime.date(2020, 8, 23), datetime.date(2020, 8, 28), 'kambulemasters2020', - 1, 'baobab@deeplearningindaba.com', 'http://www.deeplearningindaba.com', - datetime.date(2020, 3, 1), datetime.date(2020, 4, 17), datetime.date(2020, 4, 25), - datetime.date(2020, 5, 15), datetime.date(2020, 1, 1), datetime.date(2020, 1, 1), - datetime.date(2020, 1, 1), datetime.date(2020, 1, 1), datetime.date(2020, 1, 1), - datetime.date(2020, 1, 1), EventType.AWARD) + kambulemasters2020 = Event( + "Thamsanqa Kambule Masters Dissertation Award 2020", + "Thamsanqa Kambule Masters Dissertation Award 2020", + datetime.date(2020, 8, 23), + datetime.date(2020, 8, 28), + "kambulemasters2020", + 1, + "baobab@deeplearningindaba.com", + "http://www.deeplearningindaba.com", + datetime.date(2020, 3, 1), + datetime.date(2020, 4, 17), + datetime.date(2020, 4, 25), + datetime.date(2020, 5, 15), + datetime.date(2020, 1, 1), + datetime.date(2020, 1, 1), + datetime.date(2020, 1, 1), + datetime.date(2020, 1, 1), + datetime.date(2020, 1, 1), + datetime.date(2020, 1, 1), + EventType.AWARD, + ) session.add(kambulemasters2020) session.commit() @@ -240,7 +285,10 @@ def upgrade(): app_form_id = application_form.id - main_section = Section(app_form_id, 'Thamsanqa Kambule Masters Dissertation Award 2020', """ + main_section = Section( + app_form_id, + "Thamsanqa Kambule Masters Dissertation Award 2020", + """ This is the official application form for the Thamsanqa Kambule Masters Dissertation Award 2020, an award to encourage and recognise excellence in research and writing by Masters candidates at African universities, in any area of computational and statistical science. This award will be made at the Deep Learning Indaba at Tunis, Tunisia, in August 2020. This application will require: @@ -251,7 +299,9 @@ def upgrade(): For eligibility criteria for the Kambule Masters Award, please see www.deeplearningindaba.com/awards For any queries, please email awards@deeplearningindaba.com. - """, 1) + """, + 1, + ) session.add(main_section) session.commit() @@ -259,252 +309,283 @@ def upgrade(): q1_nomination_capacity = Question( application_form_id=app_form_id, section_id=main_section.id, - headline='Nomination Capacity', - placeholder='', + headline="Nomination Capacity", + placeholder="", order=1, - questionType='multi-choice', + questionType="multi-choice", validation_regex=None, is_required=True, options=[ - {'label': 'Self-nomination', 'value': 'self'}, - {'label': 'Nomination on behalf of a candidate', 'value': 'other'} - ] + {"label": "Self-nomination", "value": "self"}, + {"label": "Nomination on behalf of a candidate", "value": "other"}, + ], ) session.add(q1_nomination_capacity) session.commit() - nominator_information = Section(app_form_id, 'Nominator Information', """ + nominator_information = Section( + app_form_id, + "Nominator Information", + """ Details of the person nominating the masters candidate. - """, 2) + """, + 2, + ) nominator_information.depends_on_question_id = q1_nomination_capacity.id - nominator_information.show_for_values = ['other'] + nominator_information.show_for_values = ["other"] session.add(nominator_information) session.commit() nominator_q1 = Question( application_form_id=app_form_id, section_id=nominator_information.id, - headline='University', - placeholder='University', + headline="University", + placeholder="University", order=1, - questionType='short-text', + questionType="short-text", validation_regex=None, is_required=True, - description='The university that you (the nominator) are based at.' + description="The university that you (the nominator) are based at.", ) nominator_q2 = Question( application_form_id=app_form_id, section_id=nominator_information.id, - headline='Department', - placeholder='Department', + headline="Department", + placeholder="Department", order=2, - questionType='short-text', + questionType="short-text", validation_regex=None, is_required=True, - description='The department that you (the nominator) are based at.' + description="The department that you (the nominator) are based at.", ) nominator_q3 = Question( application_form_id=app_form_id, section_id=nominator_information.id, - headline='Describe your relationship to the masters candidate', - placeholder='', + headline="Describe your relationship to the masters candidate", + placeholder="", order=3, - questionType='long-text', + questionType="long-text", validation_regex=None, - is_required=True + is_required=True, ) session.add_all([nominator_q1, nominator_q2, nominator_q3]) session.commit() - candidate_information = Section(app_form_id, 'Masters Candidate Information', - 'Details of the nominated masters candidate to be considered for the award.', 3) + candidate_information = Section( + app_form_id, + "Masters Candidate Information", + "Details of the nominated masters candidate to be considered for the award.", + 3, + ) session.add(candidate_information) session.commit() candidate_q1 = Question( application_form_id=app_form_id, section_id=candidate_information.id, - headline='Title', - placeholder='Title', + headline="Title", + placeholder="Title", order=1, - questionType='short-text', + questionType="short-text", validation_regex=None, - is_required=True + is_required=True, ) candidate_q1.depends_on_question_id = q1_nomination_capacity.id - candidate_q1.show_for_values = ['other'] + candidate_q1.show_for_values = ["other"] candidate_q2 = Question( application_form_id=app_form_id, section_id=candidate_information.id, - headline='Firstname', - placeholder='Firstname', + headline="Firstname", + placeholder="Firstname", order=2, - questionType='short-text', + questionType="short-text", validation_regex=None, - is_required=True + is_required=True, ) candidate_q2.depends_on_question_id = q1_nomination_capacity.id - candidate_q2.show_for_values = ['other'] + candidate_q2.show_for_values = ["other"] candidate_q3 = Question( application_form_id=app_form_id, section_id=candidate_information.id, - headline='Lastname', - placeholder='Lastname', + headline="Lastname", + placeholder="Lastname", order=3, - questionType='short-text', + questionType="short-text", validation_regex=None, - is_required=True + is_required=True, ) candidate_q3.depends_on_question_id = q1_nomination_capacity.id - candidate_q3.show_for_values = ['other'] + candidate_q3.show_for_values = ["other"] candidate_q4 = Question( application_form_id=app_form_id, section_id=candidate_information.id, - headline='Email Address', - placeholder='Email Address', + headline="Email Address", + placeholder="Email Address", order=4, - questionType='short-text', + questionType="short-text", validation_regex=None, - is_required=True + is_required=True, ) candidate_q4.depends_on_question_id = q1_nomination_capacity.id - candidate_q4.show_for_values = ['other'] + candidate_q4.show_for_values = ["other"] candidate_q5 = Question( application_form_id=app_form_id, section_id=candidate_information.id, - headline='University', - placeholder='University', + headline="University", + placeholder="University", order=5, - questionType='short-text', + questionType="short-text", validation_regex=None, - is_required=True + is_required=True, ) candidate_q6 = Question( application_form_id=app_form_id, section_id=candidate_information.id, - headline='Country of Residence', - placeholder='Choose an option', + headline="Country of Residence", + placeholder="Choose an option", order=6, - questionType='multi-choice', + questionType="multi-choice", validation_regex=None, is_required=True, - options=get_country_list(session) + options=get_country_list(session), ) candidate_q7 = Question( application_form_id=app_form_id, section_id=candidate_information.id, - headline='Nationality', - placeholder='Choose an option', + headline="Nationality", + placeholder="Choose an option", order=7, - questionType='multi-choice', + questionType="multi-choice", validation_regex=None, is_required=True, - options=get_country_list(session) + options=get_country_list(session), + ) + session.add_all( + [ + candidate_q1, + candidate_q2, + candidate_q3, + candidate_q4, + candidate_q5, + candidate_q6, + candidate_q7, + ] ) - session.add_all([candidate_q1, candidate_q2, candidate_q3, candidate_q4, candidate_q5, candidate_q6, candidate_q7]) session.commit() - dissertation_information = Section(app_form_id, 'Masters dissertation information', - 'Details of the Masters dissertation of the nominated candidate.', 4) + dissertation_information = Section( + app_form_id, + "Masters dissertation information", + "Details of the Masters dissertation of the nominated candidate.", + 4, + ) session.add(dissertation_information) session.commit() dissertation_q1 = Question( application_form_id=app_form_id, section_id=dissertation_information.id, - headline='Title', - placeholder='Title', + headline="Title", + placeholder="Title", order=1, - questionType='short-text', + questionType="short-text", validation_regex=None, - is_required=True + is_required=True, ) dissertation_q2 = Question( application_form_id=app_form_id, section_id=dissertation_information.id, - headline='Abstract', - placeholder='Enter up to 800 words', + headline="Abstract", + placeholder="Enter up to 800 words", order=2, - questionType='long-text', - validation_regex=r'^\s*(\S+(\s+|$)){0,800}$', + questionType="long-text", + validation_regex=r"^\s*(\S+(\s+|$)){0,800}$", is_required=True, - description='Abstract of dissertation (<= 800 words)' + description="Abstract of dissertation (<= 800 words)", ) dissertation_q3 = Question( application_form_id=app_form_id, section_id=dissertation_information.id, headline="What are the dissertation's primary contributions to its field of research?", - placeholder='Enter 400-500 words', + placeholder="Enter 400-500 words", order=3, - questionType='long-text', - validation_regex=r'^\s*(\S+(\s+|$)){400,500}$', + questionType="long-text", + validation_regex=r"^\s*(\S+(\s+|$)){400,500}$", is_required=True, - description='Succinctly describe the novel contributions of this work (400-500 words)' + description="Succinctly describe the novel contributions of this work (400-500 words)", ) dissertation_q4 = Question( application_form_id=app_form_id, section_id=dissertation_information.id, - headline='Name of Masters academic supervisor', - placeholder='Name of Supervisor', + headline="Name of Masters academic supervisor", + placeholder="Name of Supervisor", order=4, - questionType='short-text', + questionType="short-text", validation_regex=None, - is_required=True + is_required=True, ) dissertation_q5 = Question( application_form_id=app_form_id, section_id=dissertation_information.id, - headline='E-mail address of Masters academic supervisor', - placeholder='Supervisor Email Address', + headline="E-mail address of Masters academic supervisor", + placeholder="Supervisor Email Address", order=5, - questionType='short-text', + questionType="short-text", validation_regex=None, - is_required=True + is_required=True, ) dissertation_q6 = Question( application_form_id=app_form_id, section_id=dissertation_information.id, - headline='Date of submission/defence of dissertation', - placeholder='Date of submission', + headline="Date of submission/defence of dissertation", + placeholder="Date of submission", order=6, - questionType='date', + questionType="date", validation_regex=None, - is_required=True + is_required=True, ) dissertation_q7 = Question( application_form_id=app_form_id, section_id=dissertation_information.id, - headline='Name(s) of examiner/s', - placeholder='Name(s) of examiner/s', + headline="Name(s) of examiner/s", + placeholder="Name(s) of examiner/s", order=7, - questionType='short-text', + questionType="short-text", validation_regex=None, - is_required=False + is_required=False, ) session.add_all( - [dissertation_q1, dissertation_q2, dissertation_q3, dissertation_q4, dissertation_q5, dissertation_q6, - dissertation_q7]) + [ + dissertation_q1, + dissertation_q2, + dissertation_q3, + dissertation_q4, + dissertation_q5, + dissertation_q6, + dissertation_q7, + ] + ) session.commit() - supporting_information = Section(app_form_id, 'Supporting Documentation', '', 5) + supporting_information = Section(app_form_id, "Supporting Documentation", "", 5) session.add(supporting_information) session.commit() supporting_q1 = Question( application_form_id=app_form_id, section_id=supporting_information.id, - headline='Dissertation', - placeholder='Dissertation', + headline="Dissertation", + placeholder="Dissertation", order=1, - questionType='file', + questionType="file", validation_regex=None, is_required=True, - description='We require the electronic submission of the dissertation. We recommended that dissertations be written in English (the Awards Committee may require an English translation for full consideration of theses written in other languages).' + description="We require the electronic submission of the dissertation. We recommended that dissertations be written in English (the Awards Committee may require an English translation for full consideration of theses written in other languages).", ) supporting_q2 = Question( application_form_id=app_form_id, @@ -512,18 +593,18 @@ def upgrade(): headline="Examiners' reports", placeholder="Examiners' reports", order=2, - questionType='file', + questionType="file", validation_regex=None, is_required=True, - description="We require the examiners' report to be submitted. If this is not available, please contact awards@deeplearningindaba.com." + description="We require the examiners' report to be submitted. If this is not available, please contact awards@deeplearningindaba.com.", ) supporting_q3 = Question( application_form_id=app_form_id, section_id=supporting_information.id, - headline='Supporting Letter', - placeholder='', + headline="Supporting Letter", + placeholder="", order=3, - questionType='reference', + questionType="reference", validation_regex=None, is_required=True, description=""" @@ -531,18 +612,18 @@ def upgrade(): Supporting letters should be 600 words at most, written in English, and submitted electronically in PDF format by the closing date, using Baobab. """, - options={"min_num_referrals": 1, "max_num_referrals": 1} + options={"min_num_referrals": 1, "max_num_referrals": 1}, ) supporting_q4 = Question( application_form_id=app_form_id, section_id=supporting_information.id, - headline='Additional Comments', - placeholder='', + headline="Additional Comments", + placeholder="", order=4, - questionType='long-text', + questionType="long-text", validation_regex=None, is_required=False, - description='Use this space to provide any additional details which you feel are relevant to this nomination and have not been captured by this form.' + description="Use this space to provide any additional details which you feel are relevant to this nomination and have not been captured by this form.", ) session.add_all([supporting_q1, supporting_q2, supporting_q3, supporting_q4]) @@ -554,16 +635,20 @@ def downgrade(): Base.metadata.bind = op.get_bind() session = orm.Session(bind=Base.metadata.bind) - event = session.query(Event).filter_by(key='kambulemasters2020').first() + event = session.query(Event).filter_by(key="kambulemasters2020").first() app_form = session.query(ApplicationForm).filter_by(event_id=event.id).first() - nominator = session.query(Section).filter(Section.application_form_id == app_form.id, Section.order == 2).first() + nominator = ( + session.query(Section) + .filter(Section.application_form_id == app_form.id, Section.order == 2) + .first() + ) nominator.depends_on_question_id = None session.query(Question).filter_by(application_form_id=app_form.id).delete() session.query(Section).filter_by(application_form_id=app_form.id).delete() session.query(ApplicationForm).filter_by(event_id=event.id).delete() - session.query(Event).filter_by(key='kambulemasters2020').delete() + session.query(Event).filter_by(key="kambulemasters2020").delete() - session.commit() \ No newline at end of file + session.commit() diff --git a/api/migrations/versions/c795c179c2b2_.py b/api/migrations/versions/c795c179c2b2_.py index a7b1bdf5f..7ffe6a44b 100644 --- a/api/migrations/versions/c795c179c2b2_.py +++ b/api/migrations/versions/c795c179c2b2_.py @@ -7,23 +7,26 @@ """ # revision identifiers, used by Alembic. -revision = 'c795c179c2b2' -down_revision = 'af9c317d2c92' +revision = "c795c179c2b2" +down_revision = "af9c317d2c92" + +import datetime +from enum import Enum -from alembic import op import sqlalchemy as sa +from alembic import op from sqlalchemy import orm -from app import db -from enum import Enum -import datetime from sqlalchemy.ext.declarative import declarative_base +from app import db + Base = declarative_base() + class Organisation(Base): __tablename__ = "organisation" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(50), nullable=False) @@ -36,7 +39,18 @@ class Organisation(Base): system_url = db.Column(db.String(100), nullable=False) privacy_policy = db.Column(db.String(100), nullable=False) - def __init__(self, name, system_name, small_logo, large_logo, domain, url, email_from, system_url, privacy_policy): + def __init__( + self, + name, + system_name, + small_logo, + large_logo, + domain, + url, + email_from, + system_url, + privacy_policy, + ): self.name = name self.small_logo = small_logo self.large_logo = large_logo @@ -47,10 +61,11 @@ def __init__(self, name, system_name, small_logo, large_logo, domain, url, email self.system_url = system_url self.privacy_policy = privacy_policy + class Country(Base): __tablename__ = "country" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(100), nullable=False) @@ -58,14 +73,16 @@ class Country(Base): def __init__(self, name): self.name = name + class EventType(Enum): - EVENT = 'event' - AWARD = 'award' + EVENT = "event" + AWARD = "award" + class Event(Base): __tablename__ = "event" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(50), nullable=False) @@ -73,8 +90,9 @@ class Event(Base): start_date = db.Column(db.DateTime(), nullable=False) end_date = db.Column(db.DateTime(), nullable=False) key = db.Column(db.String(255), nullable=False, unique=True) - organisation_id = db.Column(db.Integer(), db.ForeignKey( - 'organisation.id'), nullable=False) + organisation_id = db.Column( + db.Integer(), db.ForeignKey("organisation.id"), nullable=False + ) email_from = db.Column(db.String(255), nullable=False) url = db.Column(db.String(255), nullable=False) @@ -90,27 +108,28 @@ class Event(Base): registration_close = db.Column(db.DateTime(), nullable=False) event_type = db.Column(db.Enum(EventType), nullable=False) - def __init__(self, - name, - description, - start_date, - end_date, - key, - organisation_id, - email_from, - url, - application_open, - application_close, - review_open, - review_close, - selection_open, - selection_close, - offer_open, - offer_close, - registration_open, - registration_close, - event_type - ): + def __init__( + self, + name, + description, + start_date, + end_date, + key, + organisation_id, + email_from, + url, + application_open, + application_close, + review_open, + review_close, + selection_open, + selection_close, + offer_open, + offer_close, + registration_open, + registration_close, + event_type, + ): self.name = name self.description = description @@ -133,15 +152,16 @@ def __init__(self, self.event_roles = [] self.event_type = event_type + class ApplicationForm(Base): - __tablename__ = 'application_form' - __table_args__ = {'extend_existing': True} + __tablename__ = "application_form" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - event_id = db.Column(db.Integer(), db.ForeignKey('event.id'), nullable=False) + event_id = db.Column(db.Integer(), db.ForeignKey("event.id"), nullable=False) is_open = db.Column(db.Boolean(), nullable=False) - event = db.relationship('Event', foreign_keys=[event_id]) + event = db.relationship("Event", foreign_keys=[event_id]) nominations = db.Column(db.Boolean(), nullable=False) def __init__(self, event_id, is_open, nominations): @@ -149,13 +169,16 @@ def __init__(self, event_id, is_open, nominations): self.is_open = is_open self.nominations = nominations + class Question(Base): - __tablename__ = 'question' - __table_args__ = {'extend_existing': True} + __tablename__ = "question" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) - section_id = db.Column(db.Integer(), db.ForeignKey('section.id'), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) + section_id = db.Column(db.Integer(), db.ForeignKey("section.id"), nullable=False) type = db.Column(db.String(), nullable=False) description = db.Column(db.String(), nullable=True) headline = db.Column(db.String(), nullable=False) @@ -165,10 +188,25 @@ class Question(Base): order = db.Column(db.Integer(), nullable=False) options = db.Column(db.JSON(), nullable=True) is_required = db.Column(db.Boolean(), nullable=False) - depends_on_question_id = db.Column(db.Integer(), db.ForeignKey('question.id'), nullable=True) + depends_on_question_id = db.Column( + db.Integer(), db.ForeignKey("question.id"), nullable=True + ) show_for_values = db.Column(db.JSON(), nullable=True) - def __init__(self, application_form_id, section_id, headline, placeholder, order, questionType, validation_regex, validation_text=None, is_required = True, description = None, options = None): + def __init__( + self, + application_form_id, + section_id, + headline, + placeholder, + order, + questionType, + validation_regex, + validation_text=None, + is_required=True, + description=None, + options=None, + ): self.application_form_id = application_form_id self.section_id = section_id self.headline = headline @@ -183,15 +221,19 @@ def __init__(self, application_form_id, section_id, headline, placeholder, order class Section(Base): - __tablename__ = 'section' - __table_args__ = {'extend_existing': True} + __tablename__ = "section" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) name = db.Column(db.String(255), nullable=False) description = db.Column(db.String(255), nullable=False) order = db.Column(db.Integer(), nullable=False) - depends_on_question_id = db.Column(db.Integer(), db.ForeignKey('question.id', use_alter=True), nullable=True) + depends_on_question_id = db.Column( + db.Integer(), db.ForeignKey("question.id", use_alter=True), nullable=True + ) show_for_values = db.Column(db.JSON(), nullable=True) def __init__(self, application_form_id, name, description, order): @@ -200,16 +242,15 @@ def __init__(self, application_form_id, name, description, order): self.description = description self.order = order + def get_country_list(session): countries = session.query(Country).all() country_list = [] for country in countries: - country_list.append({ - 'label': country.name, - 'value': country.name - }) + country_list.append({"label": country.name, "value": country.name}) return country_list + def upgrade(): # ### commands auto generated by Alembic - please adjust! ### # pass @@ -218,92 +259,176 @@ def upgrade(): session = orm.Session(bind=Base.metadata.bind) # Update section title for Kambule Doctoral Dissertation award - event = session.query(Event).filter_by(key='kambuledoctoral2020').first() + event = session.query(Event).filter_by(key="kambuledoctoral2020").first() app_form = session.query(ApplicationForm).filter_by(event_id=event.id).first() # Updating main section - section = session.query(Section).filter_by(name='Thamsanqa Kambule Doctoral Dissertation Award 2020').first() - question = session.query(Question).filter(Question.section_id == section.id, - Question.headline == 'Nomination Capacity').first() + section = ( + session.query(Section) + .filter_by(name="Thamsanqa Kambule Doctoral Dissertation Award 2020") + .first() + ) + question = ( + session.query(Question) + .filter( + Question.section_id == section.id, + Question.headline == "Nomination Capacity", + ) + .first() + ) question.validation_regex = None # Updating nominator section - section = session.query(Section).filter(Section.application_form_id == app_form.id, Section.order == 2).first() - section.name = 'Nominator Information' - question = session.query(Question).filter(Question.section_id == section.id, - Question.order == 1).first() + section = ( + session.query(Section) + .filter(Section.application_form_id == app_form.id, Section.order == 2) + .first() + ) + section.name = "Nominator Information" + question = ( + session.query(Question) + .filter(Question.section_id == section.id, Question.order == 1) + .first() + ) question.validation_regex = None - question = session.query(Question).filter(Question.section_id == section.id, - Question.order == 2).first() + question = ( + session.query(Question) + .filter(Question.section_id == section.id, Question.order == 2) + .first() + ) question.validation_regex = None - question = session.query(Question).filter(Question.section_id == section.id, - Question.order == 3).first() + question = ( + session.query(Question) + .filter(Question.section_id == section.id, Question.order == 3) + .first() + ) question.validation_regex = None # Updating candidate section - section = session.query(Section).filter_by(name='Doctoral Candidate Information').first() - question = session.query(Question).filter(Question.section_id == section.id, - Question.order == 1).first() + section = ( + session.query(Section).filter_by(name="Doctoral Candidate Information").first() + ) + question = ( + session.query(Question) + .filter(Question.section_id == section.id, Question.order == 1) + .first() + ) question.validation_regex = None - question = session.query(Question).filter(Question.section_id == section.id, - Question.order == 2).first() + question = ( + session.query(Question) + .filter(Question.section_id == section.id, Question.order == 2) + .first() + ) question.validation_regex = None - question = session.query(Question).filter(Question.section_id == section.id, - Question.order == 3).first() + question = ( + session.query(Question) + .filter(Question.section_id == section.id, Question.order == 3) + .first() + ) question.validation_regex = None - question = session.query(Question).filter(Question.section_id == section.id, - Question.order == 4).first() + question = ( + session.query(Question) + .filter(Question.section_id == section.id, Question.order == 4) + .first() + ) question.validation_regex = None - question = session.query(Question).filter(Question.section_id == section.id, - Question.order == 5).first() + question = ( + session.query(Question) + .filter(Question.section_id == section.id, Question.order == 5) + .first() + ) question.validation_regex = None - question = session.query(Question).filter(Question.section_id == section.id, - Question.order == 6).first() + question = ( + session.query(Question) + .filter(Question.section_id == section.id, Question.order == 6) + .first() + ) question.validation_regex = None - question = session.query(Question).filter(Question.section_id == section.id, - Question.order == 7).first() + question = ( + session.query(Question) + .filter(Question.section_id == section.id, Question.order == 7) + .first() + ) question.validation_regex = None # Updating dissertation section - section = session.query(Section).filter_by(name='Doctoral dissertation information').first() - question = session.query(Question).filter(Question.section_id == section.id, - Question.order == 1).first() + section = ( + session.query(Section) + .filter_by(name="Doctoral dissertation information") + .first() + ) + question = ( + session.query(Question) + .filter(Question.section_id == section.id, Question.order == 1) + .first() + ) question.validation_regex = None - question = session.query(Question).filter(Question.section_id == section.id, - Question.order == 2).first() - question.validation_regex = r'^\s*(\S+(\s+|$)){0,800}$' - question = session.query(Question).filter(Question.section_id == section.id, - Question.order == 3).first() - question.validation_regex = r'^\s*(\S+(\s+|$)){400,500}$' - question = session.query(Question).filter(Question.section_id == section.id, - Question.order == 4).first() + question = ( + session.query(Question) + .filter(Question.section_id == section.id, Question.order == 2) + .first() + ) + question.validation_regex = r"^\s*(\S+(\s+|$)){0,800}$" + question = ( + session.query(Question) + .filter(Question.section_id == section.id, Question.order == 3) + .first() + ) + question.validation_regex = r"^\s*(\S+(\s+|$)){400,500}$" + question = ( + session.query(Question) + .filter(Question.section_id == section.id, Question.order == 4) + .first() + ) question.validation_regex = None - question = session.query(Question).filter(Question.section_id == section.id, - Question.order == 5).first() + question = ( + session.query(Question) + .filter(Question.section_id == section.id, Question.order == 5) + .first() + ) question.validation_regex = None - question = session.query(Question).filter(Question.section_id == section.id, - Question.order == 6).first() + question = ( + session.query(Question) + .filter(Question.section_id == section.id, Question.order == 6) + .first() + ) question.validation_regex = None - question = session.query(Question).filter(Question.section_id == section.id, - Question.order == 7).first() + question = ( + session.query(Question) + .filter(Question.section_id == section.id, Question.order == 7) + .first() + ) question.validation_regex = None # Updating dissertation section - section = session.query(Section).filter_by(name='Supporting Documentation').first() - question = session.query(Question).filter(Question.section_id == section.id, - Question.order == 1).first() + section = session.query(Section).filter_by(name="Supporting Documentation").first() + question = ( + session.query(Question) + .filter(Question.section_id == section.id, Question.order == 1) + .first() + ) question.validation_regex = None - question = session.query(Question).filter(Question.section_id == section.id, - Question.order == 2).first() + question = ( + session.query(Question) + .filter(Question.section_id == section.id, Question.order == 2) + .first() + ) question.validation_regex = None - question = session.query(Question).filter(Question.section_id == section.id, - Question.order == 3).first() + question = ( + session.query(Question) + .filter(Question.section_id == section.id, Question.order == 3) + .first() + ) question.validation_regex = None question.options = {"min_num_referrals": 1, "max_num_referrals": 1} - question = session.query(Question).filter(Question.section_id == section.id, - Question.order == 4).first() + question = ( + session.query(Question) + .filter(Question.section_id == section.id, Question.order == 4) + .first() + ) question.validation_regex = None + def downgrade(): # ### commands auto generated by Alembic - please adjust! ### pass diff --git a/api/migrations/versions/c8d762cf6784_.py b/api/migrations/versions/c8d762cf6784_.py index fef3bae1f..a5d44369b 100644 --- a/api/migrations/versions/c8d762cf6784_.py +++ b/api/migrations/versions/c8d762cf6784_.py @@ -7,55 +7,95 @@ """ # revision identifiers, used by Alembic. -revision = 'c8d762cf6784' -down_revision = '351c26992f23' +revision = "c8d762cf6784" +down_revision = "351c26992f23" -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - country = op.create_table('country', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('name', sa.String(length=100), nullable=False), - sa.PrimaryKeyConstraint('id') + country = op.create_table( + "country", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("name", sa.String(length=100), nullable=False), + sa.PrimaryKeyConstraint("id"), + ) + user_category = op.create_table( + "user_category", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("name", sa.String(length=100), nullable=False), + sa.Column("description", sa.String(length=500), nullable=True), + sa.Column("group", sa.String(length=100), nullable=True), + sa.PrimaryKeyConstraint("id"), + ) + op.add_column( + "app_user", sa.Column("affiliation", sa.String(length=255), nullable=False) + ) + op.add_column( + "app_user", sa.Column("deleted_datetime_utc", sa.DateTime(), nullable=True) + ) + op.add_column( + "app_user", sa.Column("department", sa.String(length=255), nullable=False) + ) + op.add_column( + "app_user", sa.Column("firstname", sa.String(length=100), nullable=False) + ) + op.add_column("app_user", sa.Column("is_deleted", sa.Boolean(), nullable=False)) + op.add_column( + "app_user", sa.Column("lastname", sa.String(length=100), nullable=False) + ) + op.add_column( + "app_user", sa.Column("nationality_country_id", sa.Integer(), nullable=False) + ) + op.add_column( + "app_user", sa.Column("residence_country_id", sa.Integer(), nullable=False) + ) + op.add_column( + "app_user", sa.Column("user_category_id", sa.Integer(), nullable=False) + ) + op.add_column( + "app_user", sa.Column("user_disability", sa.String(length=255), nullable=False) + ) + op.add_column( + "app_user", sa.Column("user_ethnicity", sa.String(length=50), nullable=False) ) - user_category = op.create_table('user_category', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('name', sa.String(length=100), nullable=False), - sa.Column('description', sa.String(length=500), nullable=True), - sa.Column('group', sa.String(length=100), nullable=True), - sa.PrimaryKeyConstraint('id') + op.add_column( + "app_user", sa.Column("user_gender", sa.String(length=20), nullable=False) + ) + op.add_column( + "app_user", sa.Column("user_title", sa.String(length=20), nullable=False) + ) + op.alter_column("app_user", "active", existing_type=sa.BOOLEAN(), nullable=False) + op.alter_column( + "app_user", "email", existing_type=sa.VARCHAR(length=255), nullable=False + ) + op.alter_column("app_user", "is_admin", existing_type=sa.BOOLEAN(), nullable=False) + op.alter_column( + "app_user", "password", existing_type=sa.VARCHAR(length=255), nullable=False + ) + op.create_foreign_key( + "app_user_residence_country_id", + "app_user", + "country", + ["residence_country_id"], + ["id"], + ) + op.create_foreign_key( + "app_user_nationality_country_id", + "app_user", + "country", + ["nationality_country_id"], + ["id"], + ) + op.create_foreign_key( + "app_user_user_category_id", + "app_user", + "user_category", + ["user_category_id"], + ["id"], ) - op.add_column('app_user', sa.Column('affiliation', sa.String(length=255), nullable=False)) - op.add_column('app_user', sa.Column('deleted_datetime_utc', sa.DateTime(), nullable=True)) - op.add_column('app_user', sa.Column('department', sa.String(length=255), nullable=False)) - op.add_column('app_user', sa.Column('firstname', sa.String(length=100), nullable=False)) - op.add_column('app_user', sa.Column('is_deleted', sa.Boolean(), nullable=False)) - op.add_column('app_user', sa.Column('lastname', sa.String(length=100), nullable=False)) - op.add_column('app_user', sa.Column('nationality_country_id', sa.Integer(), nullable=False)) - op.add_column('app_user', sa.Column('residence_country_id', sa.Integer(), nullable=False)) - op.add_column('app_user', sa.Column('user_category_id', sa.Integer(), nullable=False)) - op.add_column('app_user', sa.Column('user_disability', sa.String(length=255), nullable=False)) - op.add_column('app_user', sa.Column('user_ethnicity', sa.String(length=50), nullable=False)) - op.add_column('app_user', sa.Column('user_gender', sa.String(length=20), nullable=False)) - op.add_column('app_user', sa.Column('user_title', sa.String(length=20), nullable=False)) - op.alter_column('app_user', 'active', - existing_type=sa.BOOLEAN(), - nullable=False) - op.alter_column('app_user', 'email', - existing_type=sa.VARCHAR(length=255), - nullable=False) - op.alter_column('app_user', 'is_admin', - existing_type=sa.BOOLEAN(), - nullable=False) - op.alter_column('app_user', 'password', - existing_type=sa.VARCHAR(length=255), - nullable=False) - op.create_foreign_key('app_user_residence_country_id', 'app_user', 'country', ['residence_country_id'], ['id']) - op.create_foreign_key('app_user_nationality_country_id', 'app_user', 'country', ['nationality_country_id'], ['id']) - op.create_foreign_key('app_user_user_category_id', 'app_user', 'user_category', ['user_category_id'], ['id']) # ### end Alembic commands ### seed_countries(op, country) seed_user_categories(op, user_category) @@ -63,309 +103,315 @@ def upgrade(): def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_constraint('app_user_residence_country_id', 'app_user', type_='foreignkey') - op.drop_constraint('app_user_nationality_country_id', 'app_user', type_='foreignkey') - op.drop_constraint('app_user_user_category_id', 'app_user', type_='foreignkey') - op.alter_column('app_user', 'password', - existing_type=sa.VARCHAR(length=255), - nullable=True) - op.alter_column('app_user', 'is_admin', - existing_type=sa.BOOLEAN(), - nullable=True) - op.alter_column('app_user', 'email', - existing_type=sa.VARCHAR(length=255), - nullable=True) - op.alter_column('app_user', 'active', - existing_type=sa.BOOLEAN(), - nullable=True) - op.drop_column('app_user', 'user_title') - op.drop_column('app_user', 'user_gender') - op.drop_column('app_user', 'user_ethnicity') - op.drop_column('app_user', 'user_disability') - op.drop_column('app_user', 'user_category_id') - op.drop_column('app_user', 'residence_country_id') - op.drop_column('app_user', 'nationality_country_id') - op.drop_column('app_user', 'lastname') - op.drop_column('app_user', 'is_deleted') - op.drop_column('app_user', 'firstname') - op.drop_column('app_user', 'department') - op.drop_column('app_user', 'deleted_datetime_utc') - op.drop_column('app_user', 'affiliation') - op.drop_table('user_category') - op.drop_table('country') + op.drop_constraint("app_user_residence_country_id", "app_user", type_="foreignkey") + op.drop_constraint( + "app_user_nationality_country_id", "app_user", type_="foreignkey" + ) + op.drop_constraint("app_user_user_category_id", "app_user", type_="foreignkey") + op.alter_column( + "app_user", "password", existing_type=sa.VARCHAR(length=255), nullable=True + ) + op.alter_column("app_user", "is_admin", existing_type=sa.BOOLEAN(), nullable=True) + op.alter_column( + "app_user", "email", existing_type=sa.VARCHAR(length=255), nullable=True + ) + op.alter_column("app_user", "active", existing_type=sa.BOOLEAN(), nullable=True) + op.drop_column("app_user", "user_title") + op.drop_column("app_user", "user_gender") + op.drop_column("app_user", "user_ethnicity") + op.drop_column("app_user", "user_disability") + op.drop_column("app_user", "user_category_id") + op.drop_column("app_user", "residence_country_id") + op.drop_column("app_user", "nationality_country_id") + op.drop_column("app_user", "lastname") + op.drop_column("app_user", "is_deleted") + op.drop_column("app_user", "firstname") + op.drop_column("app_user", "department") + op.drop_column("app_user", "deleted_datetime_utc") + op.drop_column("app_user", "affiliation") + op.drop_table("user_category") + op.drop_table("country") # ### end Alembic commands ### + def seed_user_categories(op, user_category): - op.bulk_insert(user_category, [ - {'name': '4th year/honours student'}, - {'name': 'Student at a coding academy/institute'}, - {'name': 'MSc student'}, - {'name': 'PhD student'}, - {'name': 'Postdoc'}, - {'name': 'Academic'}, - {'name': 'Industry professional'}, - {'name': 'Mentor Attendee (By Invitation)'} - ]) + op.bulk_insert( + user_category, + [ + {"name": "4th year/honours student"}, + {"name": "Student at a coding academy/institute"}, + {"name": "MSc student"}, + {"name": "PhD student"}, + {"name": "Postdoc"}, + {"name": "Academic"}, + {"name": "Industry professional"}, + {"name": "Mentor Attendee (By Invitation)"}, + ], + ) + def seed_countries(op, country): - op.bulk_insert(country, [ - {'name': 'Afghanistan'}, - {'name': 'Akrotiri'}, - {'name': 'Albania'}, - {'name': 'Algeria'}, - {'name': 'American Samoa'}, - {'name': 'Andorra'}, - {'name': 'Angola'}, - {'name': 'Anguilla'}, - {'name': 'Antarctica'}, - {'name': 'Antigua and Barbuda'}, - {'name': 'Argentina'}, - {'name': 'Armenia'}, - {'name': 'Aruba'}, - {'name': 'Ashmore and Cartier Islands'}, - {'name': 'Australia'}, - {'name': 'Austria'}, - {'name': 'Azerbaijan'}, - {'name': 'Bahamas, The'}, - {'name': 'Bahrain'}, - {'name': 'Bangladesh'}, - {'name': 'Barbados'}, - {'name': 'Bassas da India'}, - {'name': 'Belarus'}, - {'name': 'Belgium'}, - {'name': 'Belize'}, - {'name': 'Benin'}, - {'name': 'Bermuda'}, - {'name': 'Bhutan'}, - {'name': 'Bolivia'}, - {'name': 'Bosnia and Herzegovina'}, - {'name': 'Botswana'}, - {'name': 'Bouvet Island'}, - {'name': 'Brazil'}, - {'name': 'British Indian Ocean Territory'}, - {'name': 'British Virgin Islands'}, - {'name': 'Brunei'}, - {'name': 'Bulgaria'}, - {'name': 'Burkina Faso'}, - {'name': 'Burma'}, - {'name': 'Burundi'}, - {'name': 'Cambodia'}, - {'name': 'Cameroon'}, - {'name': 'Canada'}, - {'name': 'Cape Verde'}, - {'name': 'Cayman Islands'}, - {'name': 'Central African Republic'}, - {'name': 'Chad'}, - {'name': 'Chile'}, - {'name': 'China'}, - {'name': 'Christmas Island'}, - {'name': 'Clipperton Island'}, - {'name': 'Cocos (Keeling) Islands'}, - {'name': 'Colombia'}, - {'name': 'Comoros'}, - {'name': 'Congo, Democratic Republic of the'}, - {'name': 'Congo, Republic of the'}, - {'name': 'Cook Islands'}, - {'name': 'Coral Sea Islands'}, - {'name': 'Costa Rica'}, - {'name': 'Cote d\'Ivoire'}, - {'name': 'Croatia'}, - {'name': 'Cuba'}, - {'name': 'Cyprus'}, - {'name': 'Czech Republic'}, - {'name': 'Denmark'}, - {'name': 'Dhekelia'}, - {'name': 'Djibouti'}, - {'name': 'Dominica'}, - {'name': 'Dominican Republic'}, - {'name': 'Ecuador'}, - {'name': 'Egypt'}, - {'name': 'El Salvador'}, - {'name': 'Equatorial Guinea'}, - {'name': 'Eritrea'}, - {'name': 'Estonia'}, - {'name': 'Ethiopia'}, - {'name': 'Europa Island'}, - {'name': 'Falkland Islands (Islas Malvinas)'}, - {'name': 'Faroe Islands'}, - {'name': 'Fiji'}, - {'name': 'Finland'}, - {'name': 'France'}, - {'name': 'French Guiana'}, - {'name': 'French Polynesia'}, - {'name': 'French Southern and Antarctic Lands'}, - {'name': 'Gabon'}, - {'name': 'Gambia, The'}, - {'name': 'Gaza Strip'}, - {'name': 'Georgia'}, - {'name': 'Germany'}, - {'name': 'Ghana'}, - {'name': 'Gibraltar'}, - {'name': 'Glorioso Islands'}, - {'name': 'Greece'}, - {'name': 'Greenland'}, - {'name': 'Grenada'}, - {'name': 'Guadeloupe'}, - {'name': 'Guam'}, - {'name': 'Guatemala'}, - {'name': 'Guernsey'}, - {'name': 'Guinea'}, - {'name': 'Guinea-Bissau'}, - {'name': 'Guyana'}, - {'name': 'Haiti'}, - {'name': 'Heard Island and McDonald Islands'}, - {'name': 'Holy See (Vatican City)'}, - {'name': 'Honduras'}, - {'name': 'Hong Kong'}, - {'name': 'Hungary'}, - {'name': 'Iceland'}, - {'name': 'India'}, - {'name': 'Indonesia'}, - {'name': 'Iran'}, - {'name': 'Iraq'}, - {'name': 'Ireland'}, - {'name': 'Isle of Man'}, - {'name': 'Israel'}, - {'name': 'Italy'}, - {'name': 'Jamaica'}, - {'name': 'Jan Mayen'}, - {'name': 'Japan'}, - {'name': 'Jersey'}, - {'name': 'Jordan'}, - {'name': 'Juan de Nova Island'}, - {'name': 'Kazakhstan'}, - {'name': 'Kenya'}, - {'name': 'Kiribati'}, - {'name': 'Korea, North'}, - {'name': 'Korea, South'}, - {'name': 'Kuwait'}, - {'name': 'Kyrgyzstan'}, - {'name': 'Laos'}, - {'name': 'Latvia'}, - {'name': 'Lebanon'}, - {'name': 'Lesotho'}, - {'name': 'Liberia'}, - {'name': 'Libya'}, - {'name': 'Liechtenstein'}, - {'name': 'Lithuania'}, - {'name': 'Luxembourg'}, - {'name': 'Macau'}, - {'name': 'Macedonia'}, - {'name': 'Madagascar'}, - {'name': 'Malawi'}, - {'name': 'Malaysia'}, - {'name': 'Maldives'}, - {'name': 'Mali'}, - {'name': 'Malta'}, - {'name': 'Marshall Islands'}, - {'name': 'Martinique'}, - {'name': 'Mauritania'}, - {'name': 'Mauritius'}, - {'name': 'Mayotte'}, - {'name': 'Mexico'}, - {'name': 'Micronesia, Federated States of'}, - {'name': 'Moldova'}, - {'name': 'Monaco'}, - {'name': 'Mongolia'}, - {'name': 'Montenegro'}, - {'name': 'Montserrat'}, - {'name': 'Morocco'}, - {'name': 'Mozambique'}, - {'name': 'Namibia'}, - {'name': 'Nauru'}, - {'name': 'Navassa Island'}, - {'name': 'Nepal'}, - {'name': 'Netherlands'}, - {'name': 'Netherlands Antilles'}, - {'name': 'New Caledonia'}, - {'name': 'New Zealand'}, - {'name': 'Nicaragua'}, - {'name': 'Niger'}, - {'name': 'Nigeria'}, - {'name': 'Niue'}, - {'name': 'Norfolk Island'}, - {'name': 'Northern Mariana Islands'}, - {'name': 'Norway'}, - {'name': 'Oman'}, - {'name': 'Pakistan'}, - {'name': 'Palau'}, - {'name': 'Panama'}, - {'name': 'Papua New Guinea'}, - {'name': 'Paracel Islands'}, - {'name': 'Paraguay'}, - {'name': 'Peru'}, - {'name': 'Philippines'}, - {'name': 'Pitcairn Islands'}, - {'name': 'Poland'}, - {'name': 'Portugal'}, - {'name': 'Puerto Rico'}, - {'name': 'Qatar'}, - {'name': 'Reunion'}, - {'name': 'Romania'}, - {'name': 'Russia'}, - {'name': 'Rwanda'}, - {'name': 'Saint Helena'}, - {'name': 'Saint Kitts and Nevis'}, - {'name': 'Saint Lucia'}, - {'name': 'Saint Pierre and Miquelon'}, - {'name': 'Saint Vincent and the Grenadines'}, - {'name': 'Samoa'}, - {'name': 'San Marino'}, - {'name': 'Sao Tome and Principe'}, - {'name': 'Saudi Arabia'}, - {'name': 'Senegal'}, - {'name': 'Serbia'}, - {'name': 'Seychelles'}, - {'name': 'Sierra Leone'}, - {'name': 'Singapore'}, - {'name': 'Slovakia'}, - {'name': 'Slovenia'}, - {'name': 'Solomon Islands'}, - {'name': 'Somalia'}, - {'name': 'South Africa'}, - {'name': 'South Georgia and the South Sandwich Islands'}, - {'name': 'South Sudan'}, - {'name': 'Spain'}, - {'name': 'Spratly Islands'}, - {'name': 'Sri Lanka'}, - {'name': 'Sudan'}, - {'name': 'Suriname'}, - {'name': 'Svalbard'}, - {'name': 'Swaziland'}, - {'name': 'Sweden'}, - {'name': 'Switzerland'}, - {'name': 'Syria'}, - {'name': 'Taiwan'}, - {'name': 'Tajikistan'}, - {'name': 'Tanzania'}, - {'name': 'Thailand'}, - {'name': 'Timor-Leste'}, - {'name': 'Togo'}, - {'name': 'Tokelau'}, - {'name': 'Tonga'}, - {'name': 'Trinidad and Tobago'}, - {'name': 'Tromelin Island'}, - {'name': 'Tunisia'}, - {'name': 'Turkey'}, - {'name': 'Turkmenistan'}, - {'name': 'Turks and Caicos Islands'}, - {'name': 'Tuvalu'}, - {'name': 'Uganda'}, - {'name': 'Ukraine'}, - {'name': 'United Arab Emirates'}, - {'name': 'United Kingdom'}, - {'name': 'United States'}, - {'name': 'Uruguay'}, - {'name': 'Uzbekistan'}, - {'name': 'Vanuatu'}, - {'name': 'Venezuela'}, - {'name': 'Vietnam'}, - {'name': 'Virgin Islands'}, - {'name': 'Wake Island'}, - {'name': 'Wallis and Futuna'}, - {'name': 'West Bank'}, - {'name': 'Western Sahara'}, - {'name': 'Yemen'}, - {'name': 'Zambia'}, - {'name': 'Zimbabwe'} - ]) \ No newline at end of file + op.bulk_insert( + country, + [ + {"name": "Afghanistan"}, + {"name": "Akrotiri"}, + {"name": "Albania"}, + {"name": "Algeria"}, + {"name": "American Samoa"}, + {"name": "Andorra"}, + {"name": "Angola"}, + {"name": "Anguilla"}, + {"name": "Antarctica"}, + {"name": "Antigua and Barbuda"}, + {"name": "Argentina"}, + {"name": "Armenia"}, + {"name": "Aruba"}, + {"name": "Ashmore and Cartier Islands"}, + {"name": "Australia"}, + {"name": "Austria"}, + {"name": "Azerbaijan"}, + {"name": "Bahamas, The"}, + {"name": "Bahrain"}, + {"name": "Bangladesh"}, + {"name": "Barbados"}, + {"name": "Bassas da India"}, + {"name": "Belarus"}, + {"name": "Belgium"}, + {"name": "Belize"}, + {"name": "Benin"}, + {"name": "Bermuda"}, + {"name": "Bhutan"}, + {"name": "Bolivia"}, + {"name": "Bosnia and Herzegovina"}, + {"name": "Botswana"}, + {"name": "Bouvet Island"}, + {"name": "Brazil"}, + {"name": "British Indian Ocean Territory"}, + {"name": "British Virgin Islands"}, + {"name": "Brunei"}, + {"name": "Bulgaria"}, + {"name": "Burkina Faso"}, + {"name": "Burma"}, + {"name": "Burundi"}, + {"name": "Cambodia"}, + {"name": "Cameroon"}, + {"name": "Canada"}, + {"name": "Cape Verde"}, + {"name": "Cayman Islands"}, + {"name": "Central African Republic"}, + {"name": "Chad"}, + {"name": "Chile"}, + {"name": "China"}, + {"name": "Christmas Island"}, + {"name": "Clipperton Island"}, + {"name": "Cocos (Keeling) Islands"}, + {"name": "Colombia"}, + {"name": "Comoros"}, + {"name": "Congo, Democratic Republic of the"}, + {"name": "Congo, Republic of the"}, + {"name": "Cook Islands"}, + {"name": "Coral Sea Islands"}, + {"name": "Costa Rica"}, + {"name": "Cote d'Ivoire"}, + {"name": "Croatia"}, + {"name": "Cuba"}, + {"name": "Cyprus"}, + {"name": "Czech Republic"}, + {"name": "Denmark"}, + {"name": "Dhekelia"}, + {"name": "Djibouti"}, + {"name": "Dominica"}, + {"name": "Dominican Republic"}, + {"name": "Ecuador"}, + {"name": "Egypt"}, + {"name": "El Salvador"}, + {"name": "Equatorial Guinea"}, + {"name": "Eritrea"}, + {"name": "Estonia"}, + {"name": "Ethiopia"}, + {"name": "Europa Island"}, + {"name": "Falkland Islands (Islas Malvinas)"}, + {"name": "Faroe Islands"}, + {"name": "Fiji"}, + {"name": "Finland"}, + {"name": "France"}, + {"name": "French Guiana"}, + {"name": "French Polynesia"}, + {"name": "French Southern and Antarctic Lands"}, + {"name": "Gabon"}, + {"name": "Gambia, The"}, + {"name": "Gaza Strip"}, + {"name": "Georgia"}, + {"name": "Germany"}, + {"name": "Ghana"}, + {"name": "Gibraltar"}, + {"name": "Glorioso Islands"}, + {"name": "Greece"}, + {"name": "Greenland"}, + {"name": "Grenada"}, + {"name": "Guadeloupe"}, + {"name": "Guam"}, + {"name": "Guatemala"}, + {"name": "Guernsey"}, + {"name": "Guinea"}, + {"name": "Guinea-Bissau"}, + {"name": "Guyana"}, + {"name": "Haiti"}, + {"name": "Heard Island and McDonald Islands"}, + {"name": "Holy See (Vatican City)"}, + {"name": "Honduras"}, + {"name": "Hong Kong"}, + {"name": "Hungary"}, + {"name": "Iceland"}, + {"name": "India"}, + {"name": "Indonesia"}, + {"name": "Iran"}, + {"name": "Iraq"}, + {"name": "Ireland"}, + {"name": "Isle of Man"}, + {"name": "Israel"}, + {"name": "Italy"}, + {"name": "Jamaica"}, + {"name": "Jan Mayen"}, + {"name": "Japan"}, + {"name": "Jersey"}, + {"name": "Jordan"}, + {"name": "Juan de Nova Island"}, + {"name": "Kazakhstan"}, + {"name": "Kenya"}, + {"name": "Kiribati"}, + {"name": "Korea, North"}, + {"name": "Korea, South"}, + {"name": "Kuwait"}, + {"name": "Kyrgyzstan"}, + {"name": "Laos"}, + {"name": "Latvia"}, + {"name": "Lebanon"}, + {"name": "Lesotho"}, + {"name": "Liberia"}, + {"name": "Libya"}, + {"name": "Liechtenstein"}, + {"name": "Lithuania"}, + {"name": "Luxembourg"}, + {"name": "Macau"}, + {"name": "Macedonia"}, + {"name": "Madagascar"}, + {"name": "Malawi"}, + {"name": "Malaysia"}, + {"name": "Maldives"}, + {"name": "Mali"}, + {"name": "Malta"}, + {"name": "Marshall Islands"}, + {"name": "Martinique"}, + {"name": "Mauritania"}, + {"name": "Mauritius"}, + {"name": "Mayotte"}, + {"name": "Mexico"}, + {"name": "Micronesia, Federated States of"}, + {"name": "Moldova"}, + {"name": "Monaco"}, + {"name": "Mongolia"}, + {"name": "Montenegro"}, + {"name": "Montserrat"}, + {"name": "Morocco"}, + {"name": "Mozambique"}, + {"name": "Namibia"}, + {"name": "Nauru"}, + {"name": "Navassa Island"}, + {"name": "Nepal"}, + {"name": "Netherlands"}, + {"name": "Netherlands Antilles"}, + {"name": "New Caledonia"}, + {"name": "New Zealand"}, + {"name": "Nicaragua"}, + {"name": "Niger"}, + {"name": "Nigeria"}, + {"name": "Niue"}, + {"name": "Norfolk Island"}, + {"name": "Northern Mariana Islands"}, + {"name": "Norway"}, + {"name": "Oman"}, + {"name": "Pakistan"}, + {"name": "Palau"}, + {"name": "Panama"}, + {"name": "Papua New Guinea"}, + {"name": "Paracel Islands"}, + {"name": "Paraguay"}, + {"name": "Peru"}, + {"name": "Philippines"}, + {"name": "Pitcairn Islands"}, + {"name": "Poland"}, + {"name": "Portugal"}, + {"name": "Puerto Rico"}, + {"name": "Qatar"}, + {"name": "Reunion"}, + {"name": "Romania"}, + {"name": "Russia"}, + {"name": "Rwanda"}, + {"name": "Saint Helena"}, + {"name": "Saint Kitts and Nevis"}, + {"name": "Saint Lucia"}, + {"name": "Saint Pierre and Miquelon"}, + {"name": "Saint Vincent and the Grenadines"}, + {"name": "Samoa"}, + {"name": "San Marino"}, + {"name": "Sao Tome and Principe"}, + {"name": "Saudi Arabia"}, + {"name": "Senegal"}, + {"name": "Serbia"}, + {"name": "Seychelles"}, + {"name": "Sierra Leone"}, + {"name": "Singapore"}, + {"name": "Slovakia"}, + {"name": "Slovenia"}, + {"name": "Solomon Islands"}, + {"name": "Somalia"}, + {"name": "South Africa"}, + {"name": "South Georgia and the South Sandwich Islands"}, + {"name": "South Sudan"}, + {"name": "Spain"}, + {"name": "Spratly Islands"}, + {"name": "Sri Lanka"}, + {"name": "Sudan"}, + {"name": "Suriname"}, + {"name": "Svalbard"}, + {"name": "Swaziland"}, + {"name": "Sweden"}, + {"name": "Switzerland"}, + {"name": "Syria"}, + {"name": "Taiwan"}, + {"name": "Tajikistan"}, + {"name": "Tanzania"}, + {"name": "Thailand"}, + {"name": "Timor-Leste"}, + {"name": "Togo"}, + {"name": "Tokelau"}, + {"name": "Tonga"}, + {"name": "Trinidad and Tobago"}, + {"name": "Tromelin Island"}, + {"name": "Tunisia"}, + {"name": "Turkey"}, + {"name": "Turkmenistan"}, + {"name": "Turks and Caicos Islands"}, + {"name": "Tuvalu"}, + {"name": "Uganda"}, + {"name": "Ukraine"}, + {"name": "United Arab Emirates"}, + {"name": "United Kingdom"}, + {"name": "United States"}, + {"name": "Uruguay"}, + {"name": "Uzbekistan"}, + {"name": "Vanuatu"}, + {"name": "Venezuela"}, + {"name": "Vietnam"}, + {"name": "Virgin Islands"}, + {"name": "Wake Island"}, + {"name": "Wallis and Futuna"}, + {"name": "West Bank"}, + {"name": "Western Sahara"}, + {"name": "Yemen"}, + {"name": "Zambia"}, + {"name": "Zimbabwe"}, + ], + ) diff --git a/api/migrations/versions/ca538998737f_add_ai4d_first_call.py b/api/migrations/versions/ca538998737f_add_ai4d_first_call.py index 139979999..fcd441451 100644 --- a/api/migrations/versions/ca538998737f_add_ai4d_first_call.py +++ b/api/migrations/versions/ca538998737f_add_ai4d_first_call.py @@ -9,139 +9,140 @@ """ # revision identifiers, used by Alembic. -revision = 'ca538998737f' -down_revision = '4ec21502ae82' +revision = "ca538998737f" +down_revision = "4ec21502ae82" + +import datetime +from enum import Enum -from alembic import op import sqlalchemy as sa +from alembic import op from sqlalchemy import orm from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy import orm + from app import db -import datetime -from enum import Enum Base = declarative_base() en_countries = [ - 'Algeria', - 'Angola', - 'Benin', - 'Botswana', - 'Burkina Faso', - 'Burundi', - 'Cameroon', - 'Cape Verde', - 'Central African Republic', - 'Chad', - 'Comoros', - 'Democratic Republic of the Congo', - 'Republic of the Congo', - 'Côte d’Ivoire', - 'Djibouti', - 'Egypt', - 'Equatorial Guinea', - 'Eritrea', - 'Eswatini', - 'Ethiopia', - 'Gabon', - 'The Gambia', - 'Ghana', - 'Guinea', - 'Guinea-Bissau', - 'Kenya', - 'Lesotho', - 'Liberia', - 'Libya', - 'Madagascar', - 'Malawi', - 'Mali', - 'Mauritania', - 'Mauritius', - 'Morocco', - 'Mozambique', - 'Namibia', - 'Niger', - 'Nigeria', - 'Rwanda', - 'Sao Tome and Principe', - 'Senegal', - 'Seychelles', - 'Sierra Leone', - 'Somalia', - 'South Africa', - 'South Sudan', - 'Sudan', - 'Tanzania', - 'Togo', - 'Tunisia', - 'Uganda', - 'Zambia', - 'Zimbabwe' + "Algeria", + "Angola", + "Benin", + "Botswana", + "Burkina Faso", + "Burundi", + "Cameroon", + "Cape Verde", + "Central African Republic", + "Chad", + "Comoros", + "Democratic Republic of the Congo", + "Republic of the Congo", + "Côte d’Ivoire", + "Djibouti", + "Egypt", + "Equatorial Guinea", + "Eritrea", + "Eswatini", + "Ethiopia", + "Gabon", + "The Gambia", + "Ghana", + "Guinea", + "Guinea-Bissau", + "Kenya", + "Lesotho", + "Liberia", + "Libya", + "Madagascar", + "Malawi", + "Mali", + "Mauritania", + "Mauritius", + "Morocco", + "Mozambique", + "Namibia", + "Niger", + "Nigeria", + "Rwanda", + "Sao Tome and Principe", + "Senegal", + "Seychelles", + "Sierra Leone", + "Somalia", + "South Africa", + "South Sudan", + "Sudan", + "Tanzania", + "Togo", + "Tunisia", + "Uganda", + "Zambia", + "Zimbabwe", ] fr_countries = [ - 'Algérie', - 'Angola', - 'Bénin', - 'Botswana', - 'Burkina Faso', - 'Burundi', - 'Cameroun', - 'Cap-Vert', - 'République centrafricaine', - 'Tchad', - 'Comores', - 'République démocratique du Congo', - 'République du Congo', - 'Côte d’Ivoire', - 'Djibouti', - 'Égypte', - 'Guinée équatoriale', - 'Érythrée', - 'Eswatini', - 'Éthiopie', - 'Gabon', - 'Gambie', - 'Ghana', - 'Guinée', - 'Guinée-Bissau', - 'Kenya', - 'Lesotho', - 'Libéria', - 'Libye', - 'Madagascar', - 'Malawi', - 'Mali', - 'Maroc', - 'Mauritanie', - 'île Maurice', - 'Mozambique', - 'Namibie', - 'Niger', - 'Nigeria', - 'Rwanda', - 'Sao Tomé-et-Principe', - 'Sénégal', - 'Seychelles', - 'Sierra Leone', - 'Somalie', - 'Afrique du Sud', - 'Soudan du Sud', - 'Soudan', - 'Tanzanie', - 'Togo', - 'Tunisie', - 'Ouganda', - 'Zambie', - 'Zimbabwe' + "Algérie", + "Angola", + "Bénin", + "Botswana", + "Burkina Faso", + "Burundi", + "Cameroun", + "Cap-Vert", + "République centrafricaine", + "Tchad", + "Comores", + "République démocratique du Congo", + "République du Congo", + "Côte d’Ivoire", + "Djibouti", + "Égypte", + "Guinée équatoriale", + "Érythrée", + "Eswatini", + "Éthiopie", + "Gabon", + "Gambie", + "Ghana", + "Guinée", + "Guinée-Bissau", + "Kenya", + "Lesotho", + "Libéria", + "Libye", + "Madagascar", + "Malawi", + "Mali", + "Maroc", + "Mauritanie", + "île Maurice", + "Mozambique", + "Namibie", + "Niger", + "Nigeria", + "Rwanda", + "Sao Tomé-et-Principe", + "Sénégal", + "Seychelles", + "Sierra Leone", + "Somalie", + "Afrique du Sud", + "Soudan du Sud", + "Soudan", + "Tanzanie", + "Togo", + "Tunisie", + "Ouganda", + "Zambie", + "Zimbabwe", ] class Organisation(Base): __tablename__ = "organisation" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(50), nullable=False) @@ -153,7 +154,17 @@ class Organisation(Base): email_from = db.Column(db.String(100), nullable=True) system_url = db.Column(db.String(100), nullable=False) - def __init__(self, name, system_name, small_logo, large_logo, domain, url, email_from, system_url): + def __init__( + self, + name, + system_name, + small_logo, + large_logo, + domain, + url, + email_from, + system_url, + ): self.name = name self.small_logo = small_logo self.large_logo = large_logo @@ -163,9 +174,10 @@ def __init__(self, name, system_name, small_logo, large_logo, domain, url, email self.email_from = email_from self.system_url = system_url + class Country(Base): __tablename__ = "country" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(100), nullable=False) @@ -173,22 +185,25 @@ class Country(Base): def __init__(self, name): self.name = name + class EventType(Enum): - EVENT = 'event' - AWARD = 'award' - CALL = 'call' + EVENT = "event" + AWARD = "award" + CALL = "call" class Event(Base): __tablename__ = "event" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) start_date = db.Column(db.DateTime(), nullable=False) end_date = db.Column(db.DateTime(), nullable=False) key = db.Column(db.String(255), nullable=False, unique=True) - organisation_id = db.Column(db.Integer(), db.ForeignKey('organisation.id'), nullable=False) + organisation_id = db.Column( + db.Integer(), db.ForeignKey("organisation.id"), nullable=False + ) email_from = db.Column(db.String(255), nullable=False) url = db.Column(db.String(255), nullable=False) application_open = db.Column(db.DateTime(), nullable=False) @@ -205,7 +220,7 @@ class Event(Base): travel_grant = db.Column(db.Boolean(), nullable=False) miniconf_url = db.Column(db.String(100), nullable=True) - event_translations = db.relationship('EventTranslation', lazy='dynamic') + event_translations = db.relationship("EventTranslation", lazy="dynamic") def __init__( self, @@ -229,7 +244,7 @@ def __init__( registration_close, event_type, travel_grant, - miniconf_url=None + miniconf_url=None, ): self.start_date = start_date self.end_date = end_date @@ -261,10 +276,11 @@ def add_event_translations(self, names, descriptions): event_translation = EventTranslation(name, description, language) self.event_translations.append(event_translation) + class EventTranslation(Base): __tablename__ = "event_translation" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) event_id = db.Column(db.Integer(), db.ForeignKey("event.id"), nullable=False) @@ -272,19 +288,20 @@ class EventTranslation(Base): description = db.Column(db.String(255), nullable=False) language = db.Column(db.String(2)) - event = db.relationship('Event', foreign_keys=[event_id]) + event = db.relationship("Event", foreign_keys=[event_id]) def __init__(self, name, description, language): self.name = name self.description = description self.language = language + class ApplicationForm(Base): - __tablename__ = 'application_form' - __table_args__ = {'extend_existing': True} + __tablename__ = "application_form" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - event_id = db.Column(db.Integer(), db.ForeignKey('event.id'), nullable=False) + event_id = db.Column(db.Integer(), db.ForeignKey("event.id"), nullable=False) is_open = db.Column(db.Boolean(), nullable=False) nominations = db.Column(db.Boolean(), nullable=False) @@ -293,22 +310,29 @@ def __init__(self, event_id, is_open, nominations): self.is_open = is_open self.nominations = nominations + class Question(Base): - __tablename__ = 'question' - __table_args__ = {'extend_existing': True} + __tablename__ = "question" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) - section_id = db.Column(db.Integer(), db.ForeignKey('section.id'), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) + section_id = db.Column(db.Integer(), db.ForeignKey("section.id"), nullable=False) type = db.Column(db.String(), nullable=False) order = db.Column(db.Integer(), nullable=False) is_required = db.Column(db.Boolean(), nullable=False) - depends_on_question_id = db.Column(db.Integer(), db.ForeignKey('question.id'), nullable=True) + depends_on_question_id = db.Column( + db.Integer(), db.ForeignKey("question.id"), nullable=True + ) key = db.Column(db.String(255), nullable=True) - question_translations = db.relationship('QuestionTranslation', lazy='dynamic') + question_translations = db.relationship("QuestionTranslation", lazy="dynamic") - def __init__(self, application_form_id, section_id, order, questionType, is_required=True): + def __init__( + self, application_form_id, section_id, order, questionType, is_required=True + ): self.application_form_id = application_form_id self.section_id = section_id self.order = order @@ -317,18 +341,24 @@ def __init__(self, application_form_id, section_id, order, questionType, is_requ class Section(Base): - __tablename__ = 'section' - __table_args__ = {'extend_existing': True} + __tablename__ = "section" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) order = db.Column(db.Integer(), nullable=False) - depends_on_question_id = db.Column(db.Integer(), db.ForeignKey('question.id', use_alter=True), nullable=True) + depends_on_question_id = db.Column( + db.Integer(), db.ForeignKey("question.id", use_alter=True), nullable=True + ) key = db.Column(db.String(255), nullable=True) - section_translations = db.relationship('SectionTranslation', lazy='dynamic') + section_translations = db.relationship("SectionTranslation", lazy="dynamic") - def __init__(self, application_form_id, order, depends_on_question_id=None, key=None): + def __init__( + self, application_form_id, order, depends_on_question_id=None, key=None + ): self.application_form_id = application_form_id self.order = order self.depends_on_question_id = depends_on_question_id @@ -336,11 +366,11 @@ def __init__(self, application_form_id, order, depends_on_question_id=None, key= class SectionTranslation(Base): - __tablename__ = 'section_translation' - __table_args__ = {'extend_existing': True} + __tablename__ = "section_translation" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - section_id = db.Column(db.Integer(), db.ForeignKey('section.id'), nullable=False) + section_id = db.Column(db.Integer(), db.ForeignKey("section.id"), nullable=False) language = db.Column(db.String(2), nullable=False) name = db.Column(db.String(255), nullable=False) description = db.Column(db.String(), nullable=False) @@ -355,11 +385,11 @@ def __init__(self, section_id, language, name, description, show_for_values=None class QuestionTranslation(Base): - __tablename__ = 'question_translation' - __table_args__ = {'extend_existing': True} + __tablename__ = "question_translation" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - question_id = db.Column(db.Integer(), db.ForeignKey('question.id'), nullable=False) + question_id = db.Column(db.Integer(), db.ForeignKey("question.id"), nullable=False) language = db.Column(db.String(2), nullable=False) headline = db.Column(db.String(), nullable=False) description = db.Column(db.String(), nullable=True) @@ -379,7 +409,7 @@ def __init__( validation_regex=None, validation_text=None, options=None, - show_for_values=None + show_for_values=None, ): self.question_id = question_id self.language = language @@ -396,40 +426,45 @@ def upgrade(): Base.metadata.bind = op.get_bind() session = orm.Session(bind=Base.metadata.bind) - op.alter_column('event_translation', 'name', - existing_type=sa.String(length=50), - type_=sa.String(length=100)) + op.alter_column( + "event_translation", + "name", + existing_type=sa.String(length=50), + type_=sa.String(length=100), + ) - ai4d = session.query(Organisation).filter_by(name='AI4D Africa').first() + ai4d = session.query(Organisation).filter_by(name="AI4D Africa").first() # Add Event event = Event( { - 'en': 'Call for Proposals for Policy Research Centres', - 'fr': 'Appel à Propositions pour les Centres de Recherche Politique' + "en": "Call for Proposals for Policy Research Centres", + "fr": "Appel à Propositions pour les Centres de Recherche Politique", }, { - 'en': 'Policy Research Centres on Artificial Intelligence for Development in Africa: Getting to responsible AI through research and policy development', - 'fr': 'Centres de Recherche sur les Politiques concernant l’intelligence artificielle (IA) pour le développement en Afrique: Vers une IA responsable grâce à la recherche et à l’élaboration de politiques' + "en": "Policy Research Centres on Artificial Intelligence for Development in Africa: Getting to responsible AI through research and policy development", + "fr": "Centres de Recherche sur les Politiques concernant l’intelligence artificielle (IA) pour le développement en Afrique: Vers une IA responsable grâce à la recherche et à l’élaboration de politiques", }, start_date=datetime.date(2020, 11, 6), end_date=datetime.date(2020, 11, 6), - key='prc', + key="prc", organisation_id=ai4d.id, - email_from='calls@ai4d.ai', - url='ai4d.ai', - application_open=datetime.date(2020,9,1), - application_close=datetime.datetime(2020,10,17,5,0,0), # Equivalent to midnight EST - review_open=datetime.date(2020,10,17), - review_close=datetime.date(2020,10,30), - selection_open=datetime.date(2020,11,1), - selection_close=datetime.date(2020,11,6), - offer_open=datetime.date(2020,12,31), - offer_close=datetime.date(2020,12,31), - registration_open=datetime.date(2020,12,31), - registration_close=datetime.date(2020,12,31), + email_from="calls@ai4d.ai", + url="ai4d.ai", + application_open=datetime.date(2020, 9, 1), + application_close=datetime.datetime( + 2020, 10, 17, 5, 0, 0 + ), # Equivalent to midnight EST + review_open=datetime.date(2020, 10, 17), + review_close=datetime.date(2020, 10, 30), + selection_open=datetime.date(2020, 11, 1), + selection_close=datetime.date(2020, 11, 6), + offer_open=datetime.date(2020, 12, 31), + offer_close=datetime.date(2020, 12, 31), + registration_open=datetime.date(2020, 12, 31), + registration_close=datetime.date(2020, 12, 31), event_type=EventType.CALL, - travel_grant=False + travel_grant=False, ) session.add(event) @@ -439,24 +474,40 @@ def upgrade(): session.add(form) session.commit() - def add_section(names, descriptions, order, depends_on_question_id=None, key=None, show_for_values=None): + def add_section( + names, + descriptions, + order, + depends_on_question_id=None, + key=None, + show_for_values=None, + ): section = Section(form.id, order) session.add(section) session.commit() translations = [] for language in names: - translations.append(SectionTranslation( - section.id, - language, - names[language], - '' if descriptions is None else descriptions[language], - show_for_values=None if show_for_values is None else show_for_values[language])) + translations.append( + SectionTranslation( + section.id, + language, + names[language], + "" if descriptions is None else descriptions[language], + show_for_values=None + if show_for_values is None + else show_for_values[language], + ) + ) session.add_all(translations) session.commit() return section - def add_question(section, order, questionType, headlines, + def add_question( + section, + order, + questionType, + headlines, descriptions=None, placeholders=None, validation_regexs=None, @@ -464,8 +515,11 @@ def add_question(section, order, questionType, headlines, options=None, show_for_values=None, is_required=True, - depends_on_question_id=None): - question = Question(form.id, section.id, order, questionType, is_required=is_required) + depends_on_question_id=None, + ): + question = Question( + form.id, section.id, order, questionType, is_required=is_required + ) if depends_on_question_id is not None: question.depends_on_question_id = depends_on_question_id @@ -475,13 +529,26 @@ def add_question(section, order, questionType, headlines, translations = [] for language in headlines: - translations.append(QuestionTranslation(question.id, language, - headline=headlines[language], - description=None if descriptions is None else descriptions[language], - placeholder=None if placeholders is None else placeholders[language], - validation_regex=None if validation_regexs is None else validation_regexs[language], - options=None if options is None else options[language], - show_for_values=None if show_for_values is None else show_for_values[language])) + translations.append( + QuestionTranslation( + question.id, + language, + headline=headlines[language], + description=None + if descriptions is None + else descriptions[language], + placeholder=None + if placeholders is None + else placeholders[language], + validation_regex=None + if validation_regexs is None + else validation_regexs[language], + options=None if options is None else options[language], + show_for_values=None + if show_for_values is None + else show_for_values[language], + ) + ) session.add_all(translations) session.commit() return question @@ -506,470 +573,608 @@ def add_question(section, order, questionType, headlines, Pour toute question, veuillez envoyer un e-mail à calls@ai4d.ai""" # (Description Page) - add_section({ - 'en': 'Call for Proposals for Policy Research Centres', - 'fr': 'Appel à Propositions pour les Centres de Recherche Politique' - }, - {'en': en_description, 'fr': fr_description}, - 1) + add_section( + { + "en": "Call for Proposals for Policy Research Centres", + "fr": "Appel à Propositions pour les Centres de Recherche Politique", + }, + {"en": en_description, "fr": fr_description}, + 1, + ) # Organisation and lead contact details - org_section = add_section({ - 'en': 'Organisation and Lead Applicant Contact details', - 'fr': "Coordonnées de l'organisation et de la personne-ressource pour cette demande" - }, None, 2) - - add_question(org_section, 1, 'information', { - 'en': 'Organization Information', - 'fr': 'Renseignements sur l’organisation' - }, is_required=False) - - add_question(org_section, 2, 'short-text', { - 'en': 'Name of Organisation', - 'fr': 'Nom de l’organisation' - }, is_required=True) - - add_question(org_section, 3, 'short-text', { - 'en': 'Director/CEO', - 'fr': 'Directeur ou premier dirigeant' - }, is_required=True) - - add_question(org_section, 4, 'short-text', { - 'en': 'Mailing address', - 'fr': 'Adresse postale' - }, is_required=True) - - add_question(org_section, 5, 'short-text', { - 'en': 'City', - 'fr': 'Ville' - }, is_required=True) + org_section = add_section( + { + "en": "Organisation and Lead Applicant Contact details", + "fr": "Coordonnées de l'organisation et de la personne-ressource pour cette demande", + }, + None, + 2, + ) - en_options = [ - {'label': c, 'value': c} for c in en_countries - ] - fr_options = [ - {'label': f, 'value': e} for e, f in zip(en_countries, fr_countries) - ] + add_question( + org_section, + 1, + "information", + {"en": "Organization Information", "fr": "Renseignements sur l’organisation"}, + is_required=False, + ) + + add_question( + org_section, + 2, + "short-text", + {"en": "Name of Organisation", "fr": "Nom de l’organisation"}, + is_required=True, + ) + + add_question( + org_section, + 3, + "short-text", + {"en": "Director/CEO", "fr": "Directeur ou premier dirigeant"}, + is_required=True, + ) + + add_question( + org_section, + 4, + "short-text", + {"en": "Mailing address", "fr": "Adresse postale"}, + is_required=True, + ) + + add_question( + org_section, 5, "short-text", {"en": "City", "fr": "Ville"}, is_required=True + ) + + en_options = [{"label": c, "value": c} for c in en_countries] + fr_options = [{"label": f, "value": e} for e, f in zip(en_countries, fr_countries)] + + add_question( + org_section, + 6, + "multi-choice", + {"en": "Country", "fr": "Pays"}, + descriptions={ + "en": "Eligible countries in sub-Saharan and North Africa", + "fr": "Pays admissibles de l’Afrique subsaharienne et de l’Afrique du Nord", + }, + placeholders={"en": "Select a Country", "fr": "Choisissez un Pays"}, + options={"en": en_options, "fr": fr_options}, + is_required=True, + ) + + add_question( + org_section, + 7, + "short-text", + {"en": "Telephone", "fr": "Numéro de téléphone"}, + is_required=True, + ) + + add_question( + org_section, + 8, + "short-text", + {"en": "Mobile (Optional)", "fr": "Numéro de téléphone mobile (facultatif)"}, + is_required=False, + ) + + add_question( + org_section, + 9, + "short-text", + {"en": "Email Address", "fr": "Adresse de courriel"}, + is_required=True, + ) + + add_question( + org_section, + 10, + "short-text", + {"en": "Website", "fr": "Site Web"}, + is_required=True, + ) + + add_question( + org_section, + 11, + "short-text", + {"en": "Social media handles", "fr": "Pseudonymes sur les médias sociaux"}, + is_required=True, + ) + + add_question( + org_section, + 12, + "information", + { + "en": "Name and contact details of principal contact for the application", + "fr": "Nom et coordonnées de la personne-ressource pour cette demande", + }, + is_required=False, + ) - add_question(org_section, 6, 'multi-choice', { - 'en': 'Country', - 'fr': 'Pays' - }, - descriptions={ - 'en': 'Eligible countries in sub-Saharan and North Africa', - 'fr': 'Pays admissibles de l’Afrique subsaharienne et de l’Afrique du Nord' - }, - placeholders={ - 'en': 'Select a Country', - 'fr': 'Choisissez un Pays' - }, - options={ - 'en': en_options, - 'fr': fr_options - }, - is_required=True) - - add_question(org_section, 7, 'short-text', { - 'en': 'Telephone', - 'fr': 'Numéro de téléphone' - }, is_required=True) - - add_question(org_section, 8, 'short-text', { - 'en': 'Mobile (Optional)', - 'fr': 'Numéro de téléphone mobile (facultatif)' - }, is_required=False) - - add_question(org_section, 9, 'short-text', { - 'en': 'Email Address', - 'fr': 'Adresse de courriel' - }, is_required=True) - - add_question(org_section, 10, 'short-text', { - 'en': 'Website', - 'fr': 'Site Web' - }, is_required=True) - - add_question(org_section, 11, 'short-text', { - 'en': 'Social media handles', - 'fr': 'Pseudonymes sur les médias sociaux' - }, is_required=True) - - add_question(org_section, 12, 'information', { - 'en': 'Name and contact details of principal contact for the application', - 'fr': 'Nom et coordonnées de la personne-ressource pour cette demande' - }, is_required=False) - - add_question(org_section, 13, 'short-text', { - 'en': 'Name of principal contact', - 'fr': 'Nom du contact principal' - }, is_required=True) - - add_question(org_section, 14, 'short-text', { - 'en': 'Email Address of principal contact', - 'fr': 'Adresse de courriel du contact principal' - }, is_required=True) - - add_question(org_section, 15, 'short-text', { - 'en': 'Telephone/Mobile of principal contact', - 'fr': 'Numéro de téléphone ou de téléphone mobile du contact principal' - }, is_required=True) + add_question( + org_section, + 13, + "short-text", + {"en": "Name of principal contact", "fr": "Nom du contact principal"}, + is_required=True, + ) + + add_question( + org_section, + 14, + "short-text", + { + "en": "Email Address of principal contact", + "fr": "Adresse de courriel du contact principal", + }, + is_required=True, + ) + + add_question( + org_section, + 15, + "short-text", + { + "en": "Telephone/Mobile of principal contact", + "fr": "Numéro de téléphone ou de téléphone mobile du contact principal", + }, + is_required=True, + ) # Section 1 - section1 = add_section({ - 'en': 'Section 1: Organisation Experience and Capacity', - 'fr': 'Section 1: Expérience et capacités de l’organisation' - }, None, 3) - - add_question(section1, 1, 'short-text', { - 'en': '1. How many people in total work in your organization?', - 'fr': '1. Combien d’employés compte votre organisation en tout?' - }, descriptions={ - 'en': 'Total number of employees', - 'fr': 'Nombre total d’employés' - }, - is_required=True) - - add_question(section1, 2, 'short-text', { - 'en': '1a. Of these, how many are full time policy researchers?', - 'fr': '1a. Combien d’entre eux occupent un poste de chercheur en matière de politiques à temps plein?' - }, is_required=True) - - add_question(section1, 3, 'multi-file', { - 'en': "1b. Please provide the names and CVs of your organization's principal researchers (up to five)", - 'fr': '1b. Veuillez fournir les noms et les CV des principaux chercheurs de votre organisation (maximum de cinq)' - }, descriptions={ - 'en': 'Upload the CVs of relevant researchers in the organization here.', - 'fr': 'Téléchargez ici les CV des chercheurs concernés de votre organisation' - }, is_required=True) - - add_question(section1, 4, 'long-text', { - 'en': '2. Mission for Responsible AI', - 'fr': '2. Mission pour une IA responsable' - }, descriptions={ - 'en': 'Responsible AI is an approach to artificial intelligence that is inclusive, sustainable, ethical and which supports human rights. Please describe the mission of your organization as it relates to the principles of responsible AI.', - 'fr': "L’intelligence artificielle (IA) responsable est une approche de l’IA à la fois inclusive, durable, éthique et fondée sur les droits de la personne. Veuillez décrire la mission de votre organisation par rapport aux principes de l’IA responsable." - }, placeholders={ - 'en': 'Enter up to 250 words', - 'fr': "Entrez jusqu'à 250 mots" - }, validation_regexs={ - 'en': r'^\s*(\S+(\s+|$)){0,250}$', - 'fr': r'^\s*(\S+(\s+|$)){0,250}$', - }, validation_texts={ - 'en': 'Maximum 250 words', - 'fr': 'Maximum 250 mots' - }, is_required=True) + section1 = add_section( + { + "en": "Section 1: Organisation Experience and Capacity", + "fr": "Section 1: Expérience et capacités de l’organisation", + }, + None, + 3, + ) + + add_question( + section1, + 1, + "short-text", + { + "en": "1. How many people in total work in your organization?", + "fr": "1. Combien d’employés compte votre organisation en tout?", + }, + descriptions={ + "en": "Total number of employees", + "fr": "Nombre total d’employés", + }, + is_required=True, + ) + + add_question( + section1, + 2, + "short-text", + { + "en": "1a. Of these, how many are full time policy researchers?", + "fr": "1a. Combien d’entre eux occupent un poste de chercheur en matière de politiques à temps plein?", + }, + is_required=True, + ) + + add_question( + section1, + 3, + "multi-file", + { + "en": "1b. Please provide the names and CVs of your organization's principal researchers (up to five)", + "fr": "1b. Veuillez fournir les noms et les CV des principaux chercheurs de votre organisation (maximum de cinq)", + }, + descriptions={ + "en": "Upload the CVs of relevant researchers in the organization here.", + "fr": "Téléchargez ici les CV des chercheurs concernés de votre organisation", + }, + is_required=True, + ) + + add_question( + section1, + 4, + "long-text", + { + "en": "2. Mission for Responsible AI", + "fr": "2. Mission pour une IA responsable", + }, + descriptions={ + "en": "Responsible AI is an approach to artificial intelligence that is inclusive, sustainable, ethical and which supports human rights. Please describe the mission of your organization as it relates to the principles of responsible AI.", + "fr": "L’intelligence artificielle (IA) responsable est une approche de l’IA à la fois inclusive, durable, éthique et fondée sur les droits de la personne. Veuillez décrire la mission de votre organisation par rapport aux principes de l’IA responsable.", + }, + placeholders={"en": "Enter up to 250 words", "fr": "Entrez jusqu'à 250 mots"}, + validation_regexs={ + "en": r"^\s*(\S+(\s+|$)){0,250}$", + "fr": r"^\s*(\S+(\s+|$)){0,250}$", + }, + validation_texts={"en": "Maximum 250 words", "fr": "Maximum 250 mots"}, + is_required=True, + ) en_options = [ - 'Enabling beneficial AI research and development', - 'Economic impacts, labor shifts, inequality, and technological unemployment', - 'Accountability, transparency, and explainability', - 'Surveillance, privacy, and civil liberties', - 'Fairness, ethics, and human rights', - 'Diversity and gender equality', - 'Data capacity, analytics and governance', - 'Political manipulation and computational propaganda', - 'Human dignity, autonomy, and psychological impact', - 'AI safety' + "Enabling beneficial AI research and development", + "Economic impacts, labor shifts, inequality, and technological unemployment", + "Accountability, transparency, and explainability", + "Surveillance, privacy, and civil liberties", + "Fairness, ethics, and human rights", + "Diversity and gender equality", + "Data capacity, analytics and governance", + "Political manipulation and computational propaganda", + "Human dignity, autonomy, and psychological impact", + "AI safety", ] fr_options = [ - 'Recherche-développement utile en lien avec l’IA', - 'Répercussions économiques, mouvements de main-d’oeuvre, inégalité et chômage technologique', - 'Responsabilité, transparence et explicabilité', - 'Surveillance, vie privée et libertés civiles', - 'Équité, éthique et droits de l’homme', - 'Diversité et égalité des genres', - 'Capacités en matière de données, d’analyses et de gouvernance', - 'Manipulation politique et propagande virtuelle', - 'Dignité humaine, autonomie et répercussions psychologiques', - 'Sécurité de l’IA' + "Recherche-développement utile en lien avec l’IA", + "Répercussions économiques, mouvements de main-d’oeuvre, inégalité et chômage technologique", + "Responsabilité, transparence et explicabilité", + "Surveillance, vie privée et libertés civiles", + "Équité, éthique et droits de l’homme", + "Diversité et égalité des genres", + "Capacités en matière de données, d’analyses et de gouvernance", + "Manipulation politique et propagande virtuelle", + "Dignité humaine, autonomie et répercussions psychologiques", + "Sécurité de l’IA", ] - fr_options = [ - {'label': f, 'value': e} for e, f in zip(en_options, fr_options) - ] - en_options = [ - {'label': o, 'value': o} for o in en_options - ] + fr_options = [{"label": f, "value": e} for e, f in zip(en_options, fr_options)] + en_options = [{"label": o, "value": o} for o in en_options] + + add_question( + section1, + 5, + "multi-checkbox", + {"en": "3. Demonstrated expertise.", "fr": "3. Une expertise reconnue."}, + descriptions={ + "en": "Please indicate the subject areas that your organization has experience in developing policy research and frameworks for, as they relate to artificial intelligence or any advanced technologies. Select all that apply.", + "fr": "Veuillez préciser les domaines dans lesquels votre organisation a acquis de l’expérience en matière de recherches et de cadres stratégiques liés à l’IA ou à d’autres technologies de pointe. Cochez toutes les réponses qui s’appliquent.", + }, + options={"en": en_options, "fr": fr_options}, + is_required=True, + ) - add_question(section1, 5, 'multi-checkbox', { - 'en': '3. Demonstrated expertise.', - 'fr': '3. Une expertise reconnue.' - }, descriptions={ - 'en': 'Please indicate the subject areas that your organization has experience in developing policy research and frameworks for, as they relate to artificial intelligence or any advanced technologies. Select all that apply.', - 'fr': 'Veuillez préciser les domaines dans lesquels votre organisation a acquis de l’expérience en matière de recherches et de cadres stratégiques liés à l’IA ou à d’autres technologies de pointe. Cochez toutes les réponses qui s’appliquent.' - }, options={ - 'en': en_options, - 'fr': fr_options - }, is_required=True) - - add_question(section1, 6, 'long-text', { - 'en': '3a. Examples.', - 'fr': '3a. Exemples.' - }, descriptions={ - 'en': 'Please provide three to five examples here that demonstrate your research and policy analysis conducted in these areas. You may provide links, or upload documents and/or PDFs at the end of the application.', - 'fr': 'Veuillez fournir trois à cinq exemples qui témoignent des recherches et des analyses des politiques que vous avez effectuées dans ces domaines. Vous pouvez fournir des liens ou télécharger des documents ou des documents PDF à la fin de la demande.' - }, is_required=True) + add_question( + section1, + 6, + "long-text", + {"en": "3a. Examples.", "fr": "3a. Exemples."}, + descriptions={ + "en": "Please provide three to five examples here that demonstrate your research and policy analysis conducted in these areas. You may provide links, or upload documents and/or PDFs at the end of the application.", + "fr": "Veuillez fournir trois à cinq exemples qui témoignent des recherches et des analyses des politiques que vous avez effectuées dans ces domaines. Vous pouvez fournir des liens ou télécharger des documents ou des documents PDF à la fin de la demande.", + }, + is_required=True, + ) # Section 2 - section2 = add_section({ - 'en': 'Section 2: Policy Research', - 'fr': 'Section 2: Recherche sur les politiques' - }, None, 4) - - add_question(section2, 1, 'long-text', { - 'en': '1. Research background, problem statement and justification.', - 'fr': '1. Contexte, énoncé du problème et justification' - }, descriptions={ - 'en': 'Please briefly outline what the top three priorities of your research agenda and what the key research questions will be as an AI Policy Center, and explain why you are selecting these priorities. Provide a background and review of the local context which describes these priorities within the country/region and how the focus of this project clearly links with the local needs of the country and region. It should also indicate whether others are addressing this problem and why these proposed priorities are justified, or whether the project derives some of its importance from involving research which is largely neglected.', - 'fr': "Veuillez décrire brièvement les trois principales priorités de votre programme de recherche et les principales questions de recherche que vous aborderez en tant que centre des politiques sur l’IA, puis expliquer pourquoi vous avez sélectionné ces priorités. Donnez une vue d’ensemble et effectuez un examen du contexte local décrivant ces priorités dans le pays ou la région et expliquez le lien établi entre l’axe thématique du projet et les besoins particuliers du pays ou de la région. Vous devez également préciser s’il existe d’autres parties concernées qui se penchent sur le problème énoncé, les raisons justifiant les priorités proposées, et si l’importance du projet tient au fait qu’il porte sur un sujet de recherche grandement négligé" - }, placeholders={ - 'en': 'Enter up to 1000 words', - 'fr': "Entrez jusqu'à 1000 mots" - }, validation_regexs={ - 'en': r'^\s*(\S+(\s+|$)){0,1000}$', - 'fr': r'^\s*(\S+(\s+|$)){0,1000}$', - }, validation_texts={ - 'en': 'Maximum 1000 words', - 'fr': 'Maximum 1000 mots' - }, is_required=True) - - add_question(section2, 2, 'long-text', { - 'en': '2. General and specific objectives.', - 'fr': '2. Objectifs généraux et précis.' - }, descriptions={ - 'en': 'The general objective should state the development goal being pursued by the research. The specific objectives should indicate the specific types of knowledge to be produced, the audiences to be reached, and forms of capacity to be reinforced. These are the objectives against which the success of the project will be judged. Use only active verbs (no passive). Typical objectives focus on 1) deepening understanding of a topic through research; 2) influencing policy and practice; and 3) building capacity.', - 'fr': "L’objectif général correspond à l’objectif de développement que l’on souhaite atteindre au moyen de la recherche. Les objectifs particuliers doivent signaler les types précis de connaissances recherchées, les destinataires et les capacités à renforcer. C’est en fonction de ces objectifs que sera jaugée la réussite du projet. Utilisez seulement des verbes d’action (évitez les verbes d’état). En règle générale, les objectifs permettent : 1) de comprendre un sujet en profondeur grâce à la recherche; 2) d’influencer les politiques et les pratiques; et 3) de favoriser le renforcement des capacités." - }, placeholders={ - 'en': 'Enter up to 250 words', - 'fr': "Entrez jusqu'à 250 mots" - }, validation_regexs={ - 'en': r'^\s*(\S+(\s+|$)){0,250}$', - 'fr': r'^\s*(\S+(\s+|$)){0,250}$', - }, validation_texts={ - 'en': 'Maximum 250 words', - 'fr': 'Maximum 250 mots' - }, is_required=True) - - add_question(section2, 3, 'information', { - 'en': '3. Methodology and approach.', - 'fr': '3. Méthodologie et approche.' - }, is_required=False) - - add_question(section2, 4, 'long-text', { - 'en': '3a. Approach.', - 'fr': '3a. Approche.' - }, descriptions={ - 'en': 'Briefly describe the overall approach/logic/pathway to achieve the above objectives.', - 'fr': "Décrivez brièvement l’approche, la logique ou le cheminement permettant d’atteindre les objectifs mentionnés précédemment." - }, placeholders={ - 'en': 'Enter up to 350 words', - 'fr': "Entrez jusqu'à 350 mots" - }, validation_regexs={ - 'en': r'^\s*(\S+(\s+|$)){0,350}$', - 'fr': r'^\s*(\S+(\s+|$)){0,350}$', - }, validation_texts={ - 'en': 'Maximum 350 words', - 'fr': 'Maximum 350 mots' - }, is_required=True) - - add_question(section2, 5, 'long-text', { - 'en': '3b. Policy research.', - 'fr': '3b. Recherche sur les politiques.' - }, descriptions={ - 'en': 'How will you answer the proposed research questions in the most rigorous way possible? This includes conceptual and theoretical frameworks, user participation, data collection and analysis.', - 'fr': "Comment comptez-vous répondre aux questions de recherche proposées de la façon la plus rigoureuse possible ? Veuillez notamment fournir des précisions à l’égard des cadres conceptuels et théoriques, de la participation des utilisateurs, et de la collecte et de l’analyse des données." - }, placeholders={ - 'en': 'Enter up to 750 words', - 'fr': "Entrez jusqu'à 750 mots" - }, validation_regexs={ - 'en': r'^\s*(\S+(\s+|$)){0,750}$', - 'fr': r'^\s*(\S+(\s+|$)){0,750}$', - }, validation_texts={ - 'en': 'Maximum 750 words', - 'fr': 'Maximum 750 mots' - }, is_required=True) - - add_question(section2, 6, 'long-text', { - 'en': '3c. Gender and inclusion.', - 'fr': '3c. Sexospécificité et inclusion.' - }, descriptions={ - 'en': """i. How has your organization addressed gender-equality and other kinds of inclusion and diversity dimensions [socioeconomic, political, cultural, ethnic] in its policy research? + section2 = add_section( + { + "en": "Section 2: Policy Research", + "fr": "Section 2: Recherche sur les politiques", + }, + None, + 4, + ) + + add_question( + section2, + 1, + "long-text", + { + "en": "1. Research background, problem statement and justification.", + "fr": "1. Contexte, énoncé du problème et justification", + }, + descriptions={ + "en": "Please briefly outline what the top three priorities of your research agenda and what the key research questions will be as an AI Policy Center, and explain why you are selecting these priorities. Provide a background and review of the local context which describes these priorities within the country/region and how the focus of this project clearly links with the local needs of the country and region. It should also indicate whether others are addressing this problem and why these proposed priorities are justified, or whether the project derives some of its importance from involving research which is largely neglected.", + "fr": "Veuillez décrire brièvement les trois principales priorités de votre programme de recherche et les principales questions de recherche que vous aborderez en tant que centre des politiques sur l’IA, puis expliquer pourquoi vous avez sélectionné ces priorités. Donnez une vue d’ensemble et effectuez un examen du contexte local décrivant ces priorités dans le pays ou la région et expliquez le lien établi entre l’axe thématique du projet et les besoins particuliers du pays ou de la région. Vous devez également préciser s’il existe d’autres parties concernées qui se penchent sur le problème énoncé, les raisons justifiant les priorités proposées, et si l’importance du projet tient au fait qu’il porte sur un sujet de recherche grandement négligé", + }, + placeholders={ + "en": "Enter up to 1000 words", + "fr": "Entrez jusqu'à 1000 mots", + }, + validation_regexs={ + "en": r"^\s*(\S+(\s+|$)){0,1000}$", + "fr": r"^\s*(\S+(\s+|$)){0,1000}$", + }, + validation_texts={"en": "Maximum 1000 words", "fr": "Maximum 1000 mots"}, + is_required=True, + ) + + add_question( + section2, + 2, + "long-text", + { + "en": "2. General and specific objectives.", + "fr": "2. Objectifs généraux et précis.", + }, + descriptions={ + "en": "The general objective should state the development goal being pursued by the research. The specific objectives should indicate the specific types of knowledge to be produced, the audiences to be reached, and forms of capacity to be reinforced. These are the objectives against which the success of the project will be judged. Use only active verbs (no passive). Typical objectives focus on 1) deepening understanding of a topic through research; 2) influencing policy and practice; and 3) building capacity.", + "fr": "L’objectif général correspond à l’objectif de développement que l’on souhaite atteindre au moyen de la recherche. Les objectifs particuliers doivent signaler les types précis de connaissances recherchées, les destinataires et les capacités à renforcer. C’est en fonction de ces objectifs que sera jaugée la réussite du projet. Utilisez seulement des verbes d’action (évitez les verbes d’état). En règle générale, les objectifs permettent : 1) de comprendre un sujet en profondeur grâce à la recherche; 2) d’influencer les politiques et les pratiques; et 3) de favoriser le renforcement des capacités.", + }, + placeholders={"en": "Enter up to 250 words", "fr": "Entrez jusqu'à 250 mots"}, + validation_regexs={ + "en": r"^\s*(\S+(\s+|$)){0,250}$", + "fr": r"^\s*(\S+(\s+|$)){0,250}$", + }, + validation_texts={"en": "Maximum 250 words", "fr": "Maximum 250 mots"}, + is_required=True, + ) + + add_question( + section2, + 3, + "information", + {"en": "3. Methodology and approach.", "fr": "3. Méthodologie et approche."}, + is_required=False, + ) + + add_question( + section2, + 4, + "long-text", + {"en": "3a. Approach.", "fr": "3a. Approche."}, + descriptions={ + "en": "Briefly describe the overall approach/logic/pathway to achieve the above objectives.", + "fr": "Décrivez brièvement l’approche, la logique ou le cheminement permettant d’atteindre les objectifs mentionnés précédemment.", + }, + placeholders={"en": "Enter up to 350 words", "fr": "Entrez jusqu'à 350 mots"}, + validation_regexs={ + "en": r"^\s*(\S+(\s+|$)){0,350}$", + "fr": r"^\s*(\S+(\s+|$)){0,350}$", + }, + validation_texts={"en": "Maximum 350 words", "fr": "Maximum 350 mots"}, + is_required=True, + ) + + add_question( + section2, + 5, + "long-text", + {"en": "3b. Policy research.", "fr": "3b. Recherche sur les politiques."}, + descriptions={ + "en": "How will you answer the proposed research questions in the most rigorous way possible? This includes conceptual and theoretical frameworks, user participation, data collection and analysis.", + "fr": "Comment comptez-vous répondre aux questions de recherche proposées de la façon la plus rigoureuse possible ? Veuillez notamment fournir des précisions à l’égard des cadres conceptuels et théoriques, de la participation des utilisateurs, et de la collecte et de l’analyse des données.", + }, + placeholders={"en": "Enter up to 750 words", "fr": "Entrez jusqu'à 750 mots"}, + validation_regexs={ + "en": r"^\s*(\S+(\s+|$)){0,750}$", + "fr": r"^\s*(\S+(\s+|$)){0,750}$", + }, + validation_texts={"en": "Maximum 750 words", "fr": "Maximum 750 mots"}, + is_required=True, + ) + + add_question( + section2, + 6, + "long-text", + { + "en": "3c. Gender and inclusion.", + "fr": "3c. Sexospécificité et inclusion.", + }, + descriptions={ + "en": """i. How has your organization addressed gender-equality and other kinds of inclusion and diversity dimensions [socioeconomic, political, cultural, ethnic] in its policy research? ii. How do you anticipate this will improve with this AI4D support? iii. Please give a brief explanation of your strategy with gender, inclusion and diversity related issues as it relates to AI/advanced technologies and offer examples of the research work. Please provide examples [PDF or links] to material that demonstrates your strategy and research on gender, inclusion and diversity. You may upload supporting documents at the end of the application. """, - 'fr': """i. Comment votre organisation a-t-elle tenu compte de l’égalité des genres et d’autres dimensions (p. ex. socio-économiques, politiques, culturelles, ethniques) liées à l’inclusion et à la diversité dans sa recherche sur les politiques? + "fr": """i. Comment votre organisation a-t-elle tenu compte de l’égalité des genres et d’autres dimensions (p. ex. socio-économiques, politiques, culturelles, ethniques) liées à l’inclusion et à la diversité dans sa recherche sur les politiques? ii. Dans quelle mesure pensez-vous que ces éléments s’amélioreront grâce au soutien de l’initiative Intelligence artificielle pour le développement (IAPD) ? iii. Veuillez expliquer brièvement votre stratégie en ce qui a trait aux questions liées à la sexospécificité, à l’inclusion et à la diversité, ainsi qu’à l’IA et aux technologies de pointe, et fournir des exemples de travaux de recherche. Veuillez fournir des exemples (c.-à-d. des documents en format PDF, ou des liens menant vers des fichiers) qui démontrent votre stratégie et vos recherches sur la sexospécificité, l’inclusion et la diversité. -""" - }, placeholders={ - 'en': 'Enter up to 750 words', - 'fr': "Entrez jusqu'à 750 mots" - }, validation_regexs={ - 'en': r'^\s*(\S+(\s+|$)){0,750}$', - 'fr': r'^\s*(\S+(\s+|$)){0,750}$', - }, validation_texts={ - 'en': 'Maximum 750 words', - 'fr': 'Maximum 750 mots' - }, is_required=True) - - add_question(section2, 7, 'long-text', { - 'en': '3d. Ethical considerations.', - 'fr': '3d. Considérations d’ordre éthique.' - }, descriptions={ - 'en': 'All projects that include human subjects must ensure that their privacy, dignity, and integrity are protected. An independent ethical review committee must approve the protocols. Please describe the key ethical considerations for the proposed research and the research ethics protocols your organization uses.', - 'fr': "Dans le cadre de tout projet de recherche portant sur des sujets humains, il faut garantir la protection de la vie privée, de la dignité et de l’intégrité de la personne. Un comité d’éthique indépendant doit autoriser les protocoles. Veuillez décrire les principales considérations d’ordre éthique pour la recherche proposée, ainsi que les protocoles d’éthique de la recherche utilisés par votre organisation." - }, placeholders={ - 'en': 'Enter up to 350 words', - 'fr': "Entrez jusqu'à 350 mots" - }, validation_regexs={ - 'en': r'^\s*(\S+(\s+|$)){0,350}$', - 'fr': r'^\s*(\S+(\s+|$)){0,350}$', - }, validation_texts={ - 'en': 'Maximum 350 words', - 'fr': 'Maximum 350 mots' - }, is_required=True) - - add_question(section2, 8, 'long-text', { - 'en': '4. Capacity development.', - 'fr': '4. Développement des capacités.' - }, descriptions={ - 'en': 'In terms of developing capacity, how would your organization use this grant to advance the capacity of your research team in any of the above subject areas? What would you need to do to build capacity to improve your work and outcomes to advance responsible artificial intelligence in your country/region?', - 'fr': "En matière de développement des capacités, comment votre organisation utiliserait-elle cette subvention pour renforcer les capacités de votre équipe de recherche dans l’un des domaines susmentionnés ? Que devriez- vous faire pour renforcer vos capacités, améliorer la qualité de votre travail, obtenir de meilleurs résultats, et faire progresser l’IA responsable dans votre pays ou votre région?" - }, placeholders={ - 'en': 'Enter up to 350 words', - 'fr': "Entrez jusqu'à 350 mots" - }, validation_regexs={ - 'en': r'^\s*(\S+(\s+|$)){0,350}$', - 'fr': r'^\s*(\S+(\s+|$)){0,350}$', - }, validation_texts={ - 'en': 'Maximum 350 words', - 'fr': 'Maximum 350 mots' - }, is_required=True) - - add_question(section2, 9, 'long-text', { - 'en': '5. Communicating for influence.', - 'fr': '5. Communication en vue d’exercer une influence.' - }, descriptions={ - 'en': 'Please provide a description of your intended research communications strategy to influence policy in the context of this AI4D Africa funding.', - 'fr': "Veuillez fournir une description de la stratégie de communication que vous comptez adopter dans le cadre de votre recherche pour exercer une influence sur les politiques, grâce à ce financement de l’initiative IAPD Afrique." - }, placeholders={ - 'en': 'Enter up to 350 words', - 'fr': "Entrez jusqu'à 350 mots" - }, validation_regexs={ - 'en': r'^\s*(\S+(\s+|$)){0,350}$', - 'fr': r'^\s*(\S+(\s+|$)){0,350}$', - }, validation_texts={ - 'en': 'Maximum 350 words', - 'fr': 'Maximum 350 mots' - }, is_required=True) - - add_question(section2, 10, 'long-text', { - 'en': '6. Results.', - 'fr': '6. Résultats.' - }, descriptions={ - 'en': 'Please describe what success will look like at the end of the three years. What markers will you use to gauge your success in AI4D policy and research? What are your expected research outputs and outcomes?', - 'fr': "Veuillez décrire ce à quoi ressemblera la réussite à la fin de la période de trois années. Quels marqueurs utiliserez-vous pour évaluer votre réussite pour les politiques et les recherches liées à l’initiative IAPD Afrique? Quels sont les résultats escomptés de votre recherche?" - }, placeholders={ - 'en': 'Enter up to 500 words', - 'fr': "Entrez jusqu'à 500 mots" - }, validation_regexs={ - 'en': r'^\s*(\S+(\s+|$)){0,500}$', - 'fr': r'^\s*(\S+(\s+|$)){0,500}$', - }, validation_texts={ - 'en': 'Maximum 500 words', - 'fr': 'Maximum 500 mots' - }, is_required=True) - - add_question(section2, 11, 'long-text', { - 'en': '7. Project schedule.', - 'fr': '7. Calendrier du projet.' - }, descriptions={ - 'en': 'Please provide a draft schedule for how you envision the activities will be organized according to the specific objectives of the project. The project schedule should include a list of key outputs and milestones (key events or deliverables). These need to be related to the items included in the project budget (Section 4).', - 'fr': "Veuillez fournir un calendrier provisoire pour expliquer la façon dont vous comptez organiser les activités, compte tenu des objectifs spécifiques du projet. Le calendrier du projet doit comprendre une liste des principaux extrants et jalons (événements ou produits livrables clés). Ils doivent être liés aux éléments qui figurent dans le budget du projet (section 4)." - }, placeholders={ - 'en': 'Enter up to 350 words', - 'fr': "Entrez jusqu'à 350 mots" - }, validation_regexs={ - 'en': r'^\s*(\S+(\s+|$)){0,350}$', - 'fr': r'^\s*(\S+(\s+|$)){0,350}$', - }, validation_texts={ - 'en': 'Maximum 350 words', - 'fr': 'Maximum 350 mots' - }, is_required=True) - - add_question(section2, 12, 'multi-file', { - 'en': '8. Supplemental documentation (optional).', - 'fr': '8. Documents supplémentaires (facultatifs).' - }, descriptions={ - 'en': 'Attach any supplemental documentation that will help clarify your application (e.g., Theory of Change, supporting graphics)', - 'fr': "Veuillez joindre toute documentation supplémentaire qui aidera à clarifier votre demande (p. ex. théorie du changement, graphiques à l’appui)." - }, is_required=False) +""", + }, + placeholders={"en": "Enter up to 750 words", "fr": "Entrez jusqu'à 750 mots"}, + validation_regexs={ + "en": r"^\s*(\S+(\s+|$)){0,750}$", + "fr": r"^\s*(\S+(\s+|$)){0,750}$", + }, + validation_texts={"en": "Maximum 750 words", "fr": "Maximum 750 mots"}, + is_required=True, + ) + + add_question( + section2, + 7, + "long-text", + { + "en": "3d. Ethical considerations.", + "fr": "3d. Considérations d’ordre éthique.", + }, + descriptions={ + "en": "All projects that include human subjects must ensure that their privacy, dignity, and integrity are protected. An independent ethical review committee must approve the protocols. Please describe the key ethical considerations for the proposed research and the research ethics protocols your organization uses.", + "fr": "Dans le cadre de tout projet de recherche portant sur des sujets humains, il faut garantir la protection de la vie privée, de la dignité et de l’intégrité de la personne. Un comité d’éthique indépendant doit autoriser les protocoles. Veuillez décrire les principales considérations d’ordre éthique pour la recherche proposée, ainsi que les protocoles d’éthique de la recherche utilisés par votre organisation.", + }, + placeholders={"en": "Enter up to 350 words", "fr": "Entrez jusqu'à 350 mots"}, + validation_regexs={ + "en": r"^\s*(\S+(\s+|$)){0,350}$", + "fr": r"^\s*(\S+(\s+|$)){0,350}$", + }, + validation_texts={"en": "Maximum 350 words", "fr": "Maximum 350 mots"}, + is_required=True, + ) + + add_question( + section2, + 8, + "long-text", + {"en": "4. Capacity development.", "fr": "4. Développement des capacités."}, + descriptions={ + "en": "In terms of developing capacity, how would your organization use this grant to advance the capacity of your research team in any of the above subject areas? What would you need to do to build capacity to improve your work and outcomes to advance responsible artificial intelligence in your country/region?", + "fr": "En matière de développement des capacités, comment votre organisation utiliserait-elle cette subvention pour renforcer les capacités de votre équipe de recherche dans l’un des domaines susmentionnés ? Que devriez- vous faire pour renforcer vos capacités, améliorer la qualité de votre travail, obtenir de meilleurs résultats, et faire progresser l’IA responsable dans votre pays ou votre région?", + }, + placeholders={"en": "Enter up to 350 words", "fr": "Entrez jusqu'à 350 mots"}, + validation_regexs={ + "en": r"^\s*(\S+(\s+|$)){0,350}$", + "fr": r"^\s*(\S+(\s+|$)){0,350}$", + }, + validation_texts={"en": "Maximum 350 words", "fr": "Maximum 350 mots"}, + is_required=True, + ) + + add_question( + section2, + 9, + "long-text", + { + "en": "5. Communicating for influence.", + "fr": "5. Communication en vue d’exercer une influence.", + }, + descriptions={ + "en": "Please provide a description of your intended research communications strategy to influence policy in the context of this AI4D Africa funding.", + "fr": "Veuillez fournir une description de la stratégie de communication que vous comptez adopter dans le cadre de votre recherche pour exercer une influence sur les politiques, grâce à ce financement de l’initiative IAPD Afrique.", + }, + placeholders={"en": "Enter up to 350 words", "fr": "Entrez jusqu'à 350 mots"}, + validation_regexs={ + "en": r"^\s*(\S+(\s+|$)){0,350}$", + "fr": r"^\s*(\S+(\s+|$)){0,350}$", + }, + validation_texts={"en": "Maximum 350 words", "fr": "Maximum 350 mots"}, + is_required=True, + ) + + add_question( + section2, + 10, + "long-text", + {"en": "6. Results.", "fr": "6. Résultats."}, + descriptions={ + "en": "Please describe what success will look like at the end of the three years. What markers will you use to gauge your success in AI4D policy and research? What are your expected research outputs and outcomes?", + "fr": "Veuillez décrire ce à quoi ressemblera la réussite à la fin de la période de trois années. Quels marqueurs utiliserez-vous pour évaluer votre réussite pour les politiques et les recherches liées à l’initiative IAPD Afrique? Quels sont les résultats escomptés de votre recherche?", + }, + placeholders={"en": "Enter up to 500 words", "fr": "Entrez jusqu'à 500 mots"}, + validation_regexs={ + "en": r"^\s*(\S+(\s+|$)){0,500}$", + "fr": r"^\s*(\S+(\s+|$)){0,500}$", + }, + validation_texts={"en": "Maximum 500 words", "fr": "Maximum 500 mots"}, + is_required=True, + ) + + add_question( + section2, + 11, + "long-text", + {"en": "7. Project schedule.", "fr": "7. Calendrier du projet."}, + descriptions={ + "en": "Please provide a draft schedule for how you envision the activities will be organized according to the specific objectives of the project. The project schedule should include a list of key outputs and milestones (key events or deliverables). These need to be related to the items included in the project budget (Section 4).", + "fr": "Veuillez fournir un calendrier provisoire pour expliquer la façon dont vous comptez organiser les activités, compte tenu des objectifs spécifiques du projet. Le calendrier du projet doit comprendre une liste des principaux extrants et jalons (événements ou produits livrables clés). Ils doivent être liés aux éléments qui figurent dans le budget du projet (section 4).", + }, + placeholders={"en": "Enter up to 350 words", "fr": "Entrez jusqu'à 350 mots"}, + validation_regexs={ + "en": r"^\s*(\S+(\s+|$)){0,350}$", + "fr": r"^\s*(\S+(\s+|$)){0,350}$", + }, + validation_texts={"en": "Maximum 350 words", "fr": "Maximum 350 mots"}, + is_required=True, + ) + + add_question( + section2, + 12, + "multi-file", + { + "en": "8. Supplemental documentation (optional).", + "fr": "8. Documents supplémentaires (facultatifs).", + }, + descriptions={ + "en": "Attach any supplemental documentation that will help clarify your application (e.g., Theory of Change, supporting graphics)", + "fr": "Veuillez joindre toute documentation supplémentaire qui aidera à clarifier votre demande (p. ex. théorie du changement, graphiques à l’appui).", + }, + is_required=False, + ) # Section 3 - section3 = add_section({ - 'en': 'Section 3: Policy engagement experience', - 'fr': 'Section 3: Expérience en matière d’interaction avec la sphère des politiques' - }, None, 5) - - add_question(section3, 1, 'long-text', { - 'en': '1. Please describe the kinds of engagement your organization has had with national governing institutions (e.g. government departments, bodies and agencies, commissions, parliaments, regulators, and other public-sector institutions) on any of the above topics (from Section 1, question 3)? This can include a reflection on what you have done well with communications, and what you have done not so well.', - 'fr': 'Veuillez décrire les types d’interactions que votre organisation a eus avec des institutions gouvernementales nationales (p. ex. ministères et organismes gouvernementaux, commissions, parlements, organismes de réglementation et autres institutions du secteur public) en ce qui concerne les sujets susmentionnés (à la section 1, question 3). Vous pouvez notamment faire part de vos réflexions sur ce que vous avez bien fait en matière de communication, ainsi que sur les éléments que vous devez améliorer.' - }, descriptions={ - 'en': 'Please give up to three examples of this engagement, such as requests for input, sitting on policy steering committees and meetings with policy makers.', - 'fr': 'Veuillez fournir un maximum de trois exemples de ces interactions (p. ex. demandes de consultation, participation à des comités directeurs, réunions avec des décideurs politiques).' - }, placeholders={ - 'en': 'Enter up to 350 words', - 'fr': "Entrez jusqu'à 350 mots" - }, validation_regexs={ - 'en': r'^\s*(\S+(\s+|$)){0,350}$', - 'fr': r'^\s*(\S+(\s+|$)){0,350}$', - }, validation_texts={ - 'en': 'Maximum 350 words', - 'fr': 'Maximum 350 mots' - }, is_required=True) - - add_question(section3, 2, 'long-text', { - 'en': '2. Describe and give examples of how your research has informed public debate or policy processes nationally and/or regionally. ', - 'fr': 'Décrivez votre recherche et donnez des exemples de la façon dont elle a exercé une influence sur le débat public ou les processus politiques à l’échelle nationale ou régionale.' - }, descriptions=None, placeholders={ - 'en': 'Enter up to 350 words', - 'fr': "Entrez jusqu'à 350 mots" - }, validation_regexs={ - 'en': r'^\s*(\S+(\s+|$)){0,350}$', - 'fr': r'^\s*(\S+(\s+|$)){0,350}$', - }, validation_texts={ - 'en': 'Maximum 350 words', - 'fr': 'Maximum 350 mots' - }, is_required=True) - - q3 = add_question(section3, 3, 'multi-choice', { - 'en': '3. Does your organization also work regionally and/or globally on technology policy related issues?', - 'fr': '3. Votre organisation travaille-t-elle aussi sur des questions liées aux politiques technologiques à l’échelle régionale ou mondiale?' - }, options={ - 'en': [{'label': 'Yes', 'value': 'Yes'}, {'label': 'No', 'value': 'No'}], - 'fr': [{'label': 'Oui', 'value': 'Yes'}, {'label': 'Non', 'value': 'No'}] - }, is_required=True) - - add_question(section3, 4, 'long-text', { - 'en': '3a. If yes, please describe this engagement. List any regional and global networks/association memberships if appropriate.', - 'fr': '3a. Si oui, veuillez décrire la nature de ces travaux. Dressez une liste des associations ou des réseaux auxquels vous appartenez à l’échelle régionale et mondiale, le cas échéant.' - }, descriptions=None, placeholders={ - 'en': 'Enter up to 250 words', - 'fr': "Entrez jusqu'à 250 mots" - }, validation_regexs={ - 'en': r'^\s*(\S+(\s+|$)){0,250}$', - 'fr': r'^\s*(\S+(\s+|$)){0,250}$', - }, validation_texts={ - 'en': 'Maximum 250 words', - 'fr': 'Maximum 250 mots' - }, is_required=True, depends_on_question_id=q3.id, show_for_values={ - 'en': 'Yes', - 'fr': 'Yes' - }) + section3 = add_section( + { + "en": "Section 3: Policy engagement experience", + "fr": "Section 3: Expérience en matière d’interaction avec la sphère des politiques", + }, + None, + 5, + ) + + add_question( + section3, + 1, + "long-text", + { + "en": "1. Please describe the kinds of engagement your organization has had with national governing institutions (e.g. government departments, bodies and agencies, commissions, parliaments, regulators, and other public-sector institutions) on any of the above topics (from Section 1, question 3)? This can include a reflection on what you have done well with communications, and what you have done not so well.", + "fr": "Veuillez décrire les types d’interactions que votre organisation a eus avec des institutions gouvernementales nationales (p. ex. ministères et organismes gouvernementaux, commissions, parlements, organismes de réglementation et autres institutions du secteur public) en ce qui concerne les sujets susmentionnés (à la section 1, question 3). Vous pouvez notamment faire part de vos réflexions sur ce que vous avez bien fait en matière de communication, ainsi que sur les éléments que vous devez améliorer.", + }, + descriptions={ + "en": "Please give up to three examples of this engagement, such as requests for input, sitting on policy steering committees and meetings with policy makers.", + "fr": "Veuillez fournir un maximum de trois exemples de ces interactions (p. ex. demandes de consultation, participation à des comités directeurs, réunions avec des décideurs politiques).", + }, + placeholders={"en": "Enter up to 350 words", "fr": "Entrez jusqu'à 350 mots"}, + validation_regexs={ + "en": r"^\s*(\S+(\s+|$)){0,350}$", + "fr": r"^\s*(\S+(\s+|$)){0,350}$", + }, + validation_texts={"en": "Maximum 350 words", "fr": "Maximum 350 mots"}, + is_required=True, + ) + + add_question( + section3, + 2, + "long-text", + { + "en": "2. Describe and give examples of how your research has informed public debate or policy processes nationally and/or regionally. ", + "fr": "Décrivez votre recherche et donnez des exemples de la façon dont elle a exercé une influence sur le débat public ou les processus politiques à l’échelle nationale ou régionale.", + }, + descriptions=None, + placeholders={"en": "Enter up to 350 words", "fr": "Entrez jusqu'à 350 mots"}, + validation_regexs={ + "en": r"^\s*(\S+(\s+|$)){0,350}$", + "fr": r"^\s*(\S+(\s+|$)){0,350}$", + }, + validation_texts={"en": "Maximum 350 words", "fr": "Maximum 350 mots"}, + is_required=True, + ) + + q3 = add_question( + section3, + 3, + "multi-choice", + { + "en": "3. Does your organization also work regionally and/or globally on technology policy related issues?", + "fr": "3. Votre organisation travaille-t-elle aussi sur des questions liées aux politiques technologiques à l’échelle régionale ou mondiale?", + }, + options={ + "en": [{"label": "Yes", "value": "Yes"}, {"label": "No", "value": "No"}], + "fr": [{"label": "Oui", "value": "Yes"}, {"label": "Non", "value": "No"}], + }, + is_required=True, + ) + + add_question( + section3, + 4, + "long-text", + { + "en": "3a. If yes, please describe this engagement. List any regional and global networks/association memberships if appropriate.", + "fr": "3a. Si oui, veuillez décrire la nature de ces travaux. Dressez une liste des associations ou des réseaux auxquels vous appartenez à l’échelle régionale et mondiale, le cas échéant.", + }, + descriptions=None, + placeholders={"en": "Enter up to 250 words", "fr": "Entrez jusqu'à 250 mots"}, + validation_regexs={ + "en": r"^\s*(\S+(\s+|$)){0,250}$", + "fr": r"^\s*(\S+(\s+|$)){0,250}$", + }, + validation_texts={"en": "Maximum 250 words", "fr": "Maximum 250 mots"}, + is_required=True, + depends_on_question_id=q3.id, + show_for_values={"en": "Yes", "fr": "Yes"}, + ) # Section 4 en_desc = """Please submit a full three year budget with notes using the Excel sheet available on the IDRC public website: https://www.idrc.ca/en/resources/guides-and-forms @@ -986,28 +1191,34 @@ def add_question(section, order, questionType, headlines, Insérez ce budget en tant que pièce jointe à votre demande. Dans le cas contraire, elle sera rejetée. """ - section4 = add_section({ - 'en': 'Section 4: Budget', - 'fr': 'Section 4: Budget' - }, { - 'en': en_desc, - 'fr': fr_desc - }, 6) + section4 = add_section( + {"en": "Section 4: Budget", "fr": "Section 4: Budget"}, + {"en": en_desc, "fr": fr_desc}, + 6, + ) - add_question(section4, 1, 'file', { - 'en': 'Budget', - 'fr': 'Budget' - }, is_required=True, options={ - 'en': {"accept": ".csv, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"}, - 'fr': {"accept": ".csv, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"}, - }) + add_question( + section4, + 1, + "file", + {"en": "Budget", "fr": "Budget"}, + is_required=True, + options={ + "en": { + "accept": ".csv, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel" + }, + "fr": { + "accept": ".csv, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel" + }, + }, + ) def downgrade(): Base.metadata.bind = op.get_bind() session = orm.Session(bind=Base.metadata.bind) - event = session.query(Event).filter_by(key='prc').first() + event = session.query(Event).filter_by(key="prc").first() app_form = session.query(ApplicationForm).filter_by(event_id=event.id).first() questions = session.query(Question).filter_by(application_form_id=app_form.id).all() @@ -1026,12 +1237,15 @@ def downgrade(): session.query(ApplicationForm).filter_by(event_id=event.id).delete() - event = session.query(Event).filter_by(key='prc').first() + event = session.query(Event).filter_by(key="prc").first() session.query(EventTranslation).filter_by(event_id=event.id).delete() - session.query(Event).filter_by(key='prc').delete() + session.query(Event).filter_by(key="prc").delete() session.commit() - op.alter_column('event_translation', 'name', - existing_type=sa.String(length=50), - type_=sa.String(length=100)) + op.alter_column( + "event_translation", + "name", + existing_type=sa.String(length=50), + type_=sa.String(length=100), + ) diff --git a/api/migrations/versions/cb36953371ab_.py b/api/migrations/versions/cb36953371ab_.py index b41f41df5..36d666c19 100644 --- a/api/migrations/versions/cb36953371ab_.py +++ b/api/migrations/versions/cb36953371ab_.py @@ -7,11 +7,11 @@ """ # revision identifiers, used by Alembic. -revision = 'cb36953371ab' -down_revision = ('3ec3ed0eb3c6', 'e3e47585f336') +revision = "cb36953371ab" +down_revision = ("3ec3ed0eb3c6", "e3e47585f336") -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): diff --git a/api/migrations/versions/cb391b878586_.py b/api/migrations/versions/cb391b878586_.py index f55374f3a..6959a378f 100644 --- a/api/migrations/versions/cb391b878586_.py +++ b/api/migrations/versions/cb391b878586_.py @@ -7,13 +7,14 @@ """ # revision identifiers, used by Alembic. -revision = 'cb391b878586' -down_revision = 'f349debf9e15' +revision = "cb391b878586" +down_revision = "f349debf9e15" -from alembic import op import sqlalchemy as sa -from sqlalchemy.ext.declarative import declarative_base +from alembic import op from sqlalchemy import orm +from sqlalchemy.ext.declarative import declarative_base + from app import db Base = declarative_base() @@ -21,7 +22,7 @@ class InvitationTemplate(Base): __tablename__ = "invitation_template" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) event_id = db.Column(db.Integer(), db.ForeignKey("event.id"), nullable=False) @@ -30,17 +31,18 @@ class InvitationTemplate(Base): send_for_accommodation_award_only = db.Column(db.Boolean(), nullable=False) send_for_both_travel_accommodation = db.Column(db.Boolean(), nullable=False) + class Event(Base): __tablename__ = "event" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(50), nullable=False) description = db.Column(db.String(255), nullable=False) start_date = db.Column(db.DateTime(), nullable=False) end_date = db.Column(db.DateTime(), nullable=False) - application_forms = db.relationship('ApplicationForm') + application_forms = db.relationship("ApplicationForm") def __init__(self, name, description, start_date, end_date): self.name = name @@ -63,11 +65,12 @@ def set_end_date(self, new_end_date): def get_application_form(self): return self.application_forms[0] + class ApplicationForm(Base): - __tablename__ = 'application_form' - __table_args__ = {'extend_existing': True} + __tablename__ = "application_form" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - event_id = db.Column(db.Integer(), db.ForeignKey('event.id'), nullable=False) + event_id = db.Column(db.Integer(), db.ForeignKey("event.id"), nullable=False) is_open = db.Column(db.Boolean(), nullable=False) deadline = db.Column(db.DateTime(), nullable=False) @@ -87,7 +90,8 @@ def upgrade(): template_path="Indaba 2019 - Invitation Letter - General.docx", send_for_travel_award_only=False, send_for_accommodation_award_only=False, - send_for_both_travel_accommodation=False) + send_for_both_travel_accommodation=False, + ) session.add(general_template) session.commit() @@ -97,7 +101,8 @@ def upgrade(): template_path="Indaba 2019 - Invitation Letter - Travel & Accomodation.docx", send_for_travel_award_only=False, send_for_accommodation_award_only=False, - send_for_both_travel_accommodation=True) + send_for_both_travel_accommodation=True, + ) session.add(both_template) session.commit() @@ -107,7 +112,8 @@ def upgrade(): template_path="Indaba 2019 - Invitation Letter - Travel Only.docx", send_for_travel_award_only=True, send_for_accommodation_award_only=False, - send_for_both_travel_accommodation=False) + send_for_both_travel_accommodation=False, + ) session.add(travel_only_template) session.commit() @@ -117,7 +123,8 @@ def upgrade(): template_path="Indaba 2019 - Invitation Letter - Accomodation Only.docx", send_for_travel_award_only=False, send_for_accommodation_award_only=True, - send_for_both_travel_accommodation=False) + send_for_both_travel_accommodation=False, + ) session.add(accommodation_only_template) session.commit() diff --git a/api/migrations/versions/cb503c2afd08_ai4d_fixes.py b/api/migrations/versions/cb503c2afd08_ai4d_fixes.py index 190efd8dd..c66030a57 100644 --- a/api/migrations/versions/cb503c2afd08_ai4d_fixes.py +++ b/api/migrations/versions/cb503c2afd08_ai4d_fixes.py @@ -9,26 +9,27 @@ """ # revision identifiers, used by Alembic. -revision = 'cb503c2afd08' -down_revision = '78f4d11203c5' +revision = "cb503c2afd08" +down_revision = "78f4d11203c5" + +import datetime +from enum import Enum -from alembic import op import sqlalchemy as sa -from sqlalchemy import orm +from alembic import op +from sqlalchemy import orm, select from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy import orm -from sqlalchemy import select from sqlalchemy.orm import column_property + from app import db -import datetime -from enum import Enum Base = declarative_base() + class Organisation(Base): __tablename__ = "organisation" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(50), nullable=False) @@ -40,7 +41,17 @@ class Organisation(Base): email_from = db.Column(db.String(100), nullable=True) system_url = db.Column(db.String(100), nullable=False) - def __init__(self, name, system_name, small_logo, large_logo, domain, url, email_from, system_url): + def __init__( + self, + name, + system_name, + small_logo, + large_logo, + domain, + url, + email_from, + system_url, + ): self.name = name self.small_logo = small_logo self.large_logo = large_logo @@ -50,9 +61,10 @@ def __init__(self, name, system_name, small_logo, large_logo, domain, url, email self.email_from = email_from self.system_url = system_url + class Country(Base): __tablename__ = "country" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(100), nullable=False) @@ -60,22 +72,25 @@ class Country(Base): def __init__(self, name): self.name = name + class EventType(Enum): - EVENT = 'event' - AWARD = 'award' - CALL = 'call' + EVENT = "event" + AWARD = "award" + CALL = "call" class Event(Base): __tablename__ = "event" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) start_date = db.Column(db.DateTime(), nullable=False) end_date = db.Column(db.DateTime(), nullable=False) key = db.Column(db.String(255), nullable=False, unique=True) - organisation_id = db.Column(db.Integer(), db.ForeignKey('organisation.id'), nullable=False) + organisation_id = db.Column( + db.Integer(), db.ForeignKey("organisation.id"), nullable=False + ) email_from = db.Column(db.String(255), nullable=False) url = db.Column(db.String(255), nullable=False) application_open = db.Column(db.DateTime(), nullable=False) @@ -92,7 +107,7 @@ class Event(Base): travel_grant = db.Column(db.Boolean(), nullable=False) miniconf_url = db.Column(db.String(100), nullable=True) - event_translations = db.relationship('EventTranslation', lazy='dynamic') + event_translations = db.relationship("EventTranslation", lazy="dynamic") def __init__( self, @@ -116,7 +131,7 @@ def __init__( registration_close, event_type, travel_grant, - miniconf_url=None + miniconf_url=None, ): self.start_date = start_date self.end_date = end_date @@ -148,10 +163,11 @@ def add_event_translations(self, names, descriptions): event_translation = EventTranslation(name, description, language) self.event_translations.append(event_translation) + class EventTranslation(Base): __tablename__ = "event_translation" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) event_id = db.Column(db.Integer(), db.ForeignKey("event.id"), nullable=False) @@ -159,19 +175,20 @@ class EventTranslation(Base): description = db.Column(db.String(255), nullable=False) language = db.Column(db.String(2)) - event = db.relationship('Event', foreign_keys=[event_id]) + event = db.relationship("Event", foreign_keys=[event_id]) def __init__(self, name, description, language): self.name = name self.description = description self.language = language + class ApplicationForm(Base): - __tablename__ = 'application_form' - __table_args__ = {'extend_existing': True} + __tablename__ = "application_form" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - event_id = db.Column(db.Integer(), db.ForeignKey('event.id'), nullable=False) + event_id = db.Column(db.Integer(), db.ForeignKey("event.id"), nullable=False) is_open = db.Column(db.Boolean(), nullable=False) nominations = db.Column(db.Boolean(), nullable=False) @@ -180,22 +197,29 @@ def __init__(self, event_id, is_open, nominations): self.is_open = is_open self.nominations = nominations + class Question(Base): - __tablename__ = 'question' - __table_args__ = {'extend_existing': True} + __tablename__ = "question" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) - section_id = db.Column(db.Integer(), db.ForeignKey('section.id'), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) + section_id = db.Column(db.Integer(), db.ForeignKey("section.id"), nullable=False) type = db.Column(db.String(), nullable=False) order = db.Column(db.Integer(), nullable=False) is_required = db.Column(db.Boolean(), nullable=False) - depends_on_question_id = db.Column(db.Integer(), db.ForeignKey('question.id'), nullable=True) + depends_on_question_id = db.Column( + db.Integer(), db.ForeignKey("question.id"), nullable=True + ) key = db.Column(db.String(255), nullable=True) - question_translations = db.relationship('QuestionTranslation', lazy='dynamic') + question_translations = db.relationship("QuestionTranslation", lazy="dynamic") - def __init__(self, application_form_id, section_id, order, questionType, is_required=True): + def __init__( + self, application_form_id, section_id, order, questionType, is_required=True + ): self.application_form_id = application_form_id self.section_id = section_id self.order = order @@ -203,45 +227,55 @@ def __init__(self, application_form_id, section_id, order, questionType, is_requ self.is_required = is_required def get_translation(self, language): - question_translation = self.question_translations.filter_by(language=language).first() + question_translation = self.question_translations.filter_by( + language=language + ).first() return question_translation class Section(Base): - __tablename__ = 'section' - __table_args__ = {'extend_existing': True} + __tablename__ = "section" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) order = db.Column(db.Integer(), nullable=False) - depends_on_question_id = db.Column(db.Integer(), db.ForeignKey('question.id', use_alter=True), nullable=True) + depends_on_question_id = db.Column( + db.Integer(), db.ForeignKey("question.id", use_alter=True), nullable=True + ) key = db.Column(db.String(255), nullable=True) - section_translations = db.relationship('SectionTranslation', lazy='dynamic') + section_translations = db.relationship("SectionTranslation", lazy="dynamic") - def __init__(self, application_form_id, order, depends_on_question_id=None, key=None): + def __init__( + self, application_form_id, order, depends_on_question_id=None, key=None + ): self.application_form_id = application_form_id self.order = order self.depends_on_question_id = depends_on_question_id self.key = key def get_translation(self, language): - section_translation = self.section_translations.filter_by(language=language).first() + section_translation = self.section_translations.filter_by( + language=language + ).first() return section_translation class SectionTranslation(Base): - __tablename__ = 'section_translation' - __table_args__ = {'extend_existing': True} + __tablename__ = "section_translation" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - section_id = db.Column(db.Integer(), db.ForeignKey('section.id'), nullable=False) + section_id = db.Column(db.Integer(), db.ForeignKey("section.id"), nullable=False) language = db.Column(db.String(2), nullable=False) name = db.Column(db.String(255), nullable=False) description = db.Column(db.String(), nullable=False) show_for_values = db.Column(db.JSON(), nullable=True) - section = db.relationship('Section', foreign_keys=[section_id]) + section = db.relationship("Section", foreign_keys=[section_id]) def __init__(self, section_id, language, name, description, show_for_values=None): self.section_id = section_id @@ -252,11 +286,11 @@ def __init__(self, section_id, language, name, description, show_for_values=None class QuestionTranslation(Base): - __tablename__ = 'question_translation' - __table_args__ = {'extend_existing': True} + __tablename__ = "question_translation" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - question_id = db.Column(db.Integer(), db.ForeignKey('question.id'), nullable=False) + question_id = db.Column(db.Integer(), db.ForeignKey("question.id"), nullable=False) language = db.Column(db.String(2), nullable=False) headline = db.Column(db.String(), nullable=False) description = db.Column(db.String(), nullable=True) @@ -266,7 +300,7 @@ class QuestionTranslation(Base): options = db.Column(db.JSON(), nullable=True) show_for_values = db.Column(db.JSON(), nullable=True) - question = db.relationship('Question', foreign_keys=[question_id]) + question = db.relationship("Question", foreign_keys=[question_id]) def __init__( self, @@ -278,7 +312,7 @@ def __init__( validation_regex=None, validation_text=None, options=None, - show_for_values=None + show_for_values=None, ): self.question_id = question_id self.language = language @@ -295,29 +329,37 @@ def upgrade(): Base.metadata.bind = op.get_bind() session = orm.Session(bind=Base.metadata.bind) - event = session.query(Event).filter_by(key='prc').first() + event = session.query(Event).filter_by(key="prc").first() form = session.query(ApplicationForm).filter_by(event_id=event.id).first() def get_question_by_en_headline(en_headline): - en = (session.query(QuestionTranslation) - .filter_by(headline=en_headline, language='en') - .join(Question) - .filter_by(application_form_id=form.id) - .first()) + en = ( + session.query(QuestionTranslation) + .filter_by(headline=en_headline, language="en") + .join(Question) + .filter_by(application_form_id=form.id) + .first() + ) question = en.question - fr = question.get_translation('fr') + fr = question.get_translation("fr") return question, en, fr - section_en = session.query(SectionTranslation).filter_by( - name='Section IV: Budget', language='en').first() + section_en = ( + session.query(SectionTranslation) + .filter_by(name="Section IV: Budget", language="en") + .first() + ) section_en.description = """Please submit a full three year budget with notes using the Excel template available on the IDRC public website: [https://www.idrc.ca/en/resources/guides-and-forms](https://www.idrc.ca/en/resources/guides-and-forms) (Select the ‘Proposal budget’ and download.) Applications that do not submit a complete budget in this template will not be considered. """ - section_fr = session.query(SectionTranslation).filter_by( - name='Section IV: Budget', language='fr').first() + section_fr = ( + session.query(SectionTranslation) + .filter_by(name="Section IV: Budget", language="fr") + .first() + ) section_fr.description = """Veuillez soumettre un budget triennal complet comprenant des notes en utilisant la feuille Excel qui est disponible sur le site Web public du CRDI, à la page suivante: [https://www.idrc.ca/fr/ressources/guides-et-formulaires](https://www.idrc.ca/en/resources/guides-and-forms) Cliquez sur ‘proposition de projet’, puis téléchargez la feuille Excel. Les demandes qui ne comprennent pas un budget complet préparé au moyen de ce modèle ne seront pas prises en considération. @@ -325,17 +367,21 @@ def get_question_by_en_headline(en_headline): Insérez ce budget en tant que pièce jointe à votre demande. Dans le cas contraire, elle sera rejetée. """ - _, en, fr = get_question_by_en_headline('How many people in total work in your organization?') - en.validation_regex = r'^\d*$' - en.validation_text = 'You must enter a number' - fr.validation_regex = r'^\d*$' - fr.validation_text = 'Vous devez entrer un nombre' - - _, en, fr = get_question_by_en_headline('Of these, how many are full time policy researchers?') - en.validation_regex = r'^\d*$' - en.validation_text = 'You must enter a number' - fr.validation_regex = r'^\d*$' - fr.validation_text = 'Vous devez entrer un nombre' + _, en, fr = get_question_by_en_headline( + "How many people in total work in your organization?" + ) + en.validation_regex = r"^\d*$" + en.validation_text = "You must enter a number" + fr.validation_regex = r"^\d*$" + fr.validation_text = "Vous devez entrer un nombre" + + _, en, fr = get_question_by_en_headline( + "Of these, how many are full time policy researchers?" + ) + en.validation_regex = r"^\d*$" + en.validation_text = "You must enter a number" + fr.validation_regex = r"^\d*$" + fr.validation_text = "Vous devez entrer un nombre" session.commit() diff --git a/api/migrations/versions/d4a62bfc5be5_add_email_verification_fields.py b/api/migrations/versions/d4a62bfc5be5_add_email_verification_fields.py index 5c0107e5e..55376b2de 100644 --- a/api/migrations/versions/d4a62bfc5be5_add_email_verification_fields.py +++ b/api/migrations/versions/d4a62bfc5be5_add_email_verification_fields.py @@ -1,5 +1,6 @@ import sqlalchemy as sa from alembic import op + """add email verification fields Revision ID: d4a62bfc5be5 @@ -9,17 +10,18 @@ """ # revision identifiers, used by Alembic. -revision = 'd4a62bfc5be5' -down_revision = 'fbd283484655' +revision = "d4a62bfc5be5" +down_revision = "fbd283484655" def upgrade(): - op.add_column('app_user', sa.Column( - 'verified_email', sa.Boolean(), nullable=True)) - op.add_column('app_user', sa.Column('verify_token', sa.String(length=255), - nullable=True, unique=True)) + op.add_column("app_user", sa.Column("verified_email", sa.Boolean(), nullable=True)) + op.add_column( + "app_user", + sa.Column("verify_token", sa.String(length=255), nullable=True, unique=True), + ) def downgrade(): - op.drop_column('app_user', 'verified_email') - op.drop_column('app_user', 'verify_token') + op.drop_column("app_user", "verified_email") + op.drop_column("app_user", "verify_token") diff --git a/api/migrations/versions/d7dfd413e768_add_tag_models.py b/api/migrations/versions/d7dfd413e768_add_tag_models.py index 9fd851dd6..05c6cae6f 100644 --- a/api/migrations/versions/d7dfd413e768_add_tag_models.py +++ b/api/migrations/versions/d7dfd413e768_add_tag_models.py @@ -7,44 +7,59 @@ """ # revision identifiers, used by Alembic. -revision = 'd7dfd413e768' -down_revision = '33b563f7be6b' +revision = "d7dfd413e768" +down_revision = "33b563f7be6b" -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.create_table('tag', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('event_id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['event_id'], ['event.id'], ), - sa.PrimaryKeyConstraint('id') + op.create_table( + "tag", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("event_id", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint( + ["event_id"], + ["event.id"], + ), + sa.PrimaryKeyConstraint("id"), ) - op.create_table('tag_translation', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('tag_id', sa.Integer(), nullable=False), - sa.Column('language', sa.String(length=2), nullable=False), - sa.Column('name', sa.String(), nullable=False), - sa.ForeignKeyConstraint(['tag_id'], ['tag.id'], ), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('tag_id', 'language', name='uq_tag_id_language') + op.create_table( + "tag_translation", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("tag_id", sa.Integer(), nullable=False), + sa.Column("language", sa.String(length=2), nullable=False), + sa.Column("name", sa.String(), nullable=False), + sa.ForeignKeyConstraint( + ["tag_id"], + ["tag.id"], + ), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("tag_id", "language", name="uq_tag_id_language"), ) - op.create_table('response_tag', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('response_id', sa.Integer(), nullable=False), - sa.Column('tag_id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['response_id'], ['response.id'], ), - sa.ForeignKeyConstraint(['tag_id'], ['tag.id'], ), - sa.PrimaryKeyConstraint('id') + op.create_table( + "response_tag", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("response_id", sa.Integer(), nullable=False), + sa.Column("tag_id", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint( + ["response_id"], + ["response.id"], + ), + sa.ForeignKeyConstraint( + ["tag_id"], + ["tag.id"], + ), + sa.PrimaryKeyConstraint("id"), ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('response_tag') - op.drop_table('tag_translation') - op.drop_table('tag') + op.drop_table("response_tag") + op.drop_table("tag_translation") + op.drop_table("tag") # ### end Alembic commands ### diff --git a/api/migrations/versions/d7f3adff2c9f_.py b/api/migrations/versions/d7f3adff2c9f_.py index 1558626e9..a93896091 100644 --- a/api/migrations/versions/d7f3adff2c9f_.py +++ b/api/migrations/versions/d7f3adff2c9f_.py @@ -7,11 +7,11 @@ """ # revision identifiers, used by Alembic. -revision = 'd7f3adff2c9f' -down_revision = ('3e7bdff8af80', 'b6a2a69fbb13') +revision = "d7f3adff2c9f" +down_revision = ("3e7bdff8af80", "b6a2a69fbb13") -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): diff --git a/api/migrations/versions/d80784e674a8_.py b/api/migrations/versions/d80784e674a8_.py index 25b69a3d9..6c2648a98 100644 --- a/api/migrations/versions/d80784e674a8_.py +++ b/api/migrations/versions/d80784e674a8_.py @@ -7,11 +7,11 @@ """ # revision identifiers, used by Alembic. -revision = 'd80784e674a8' -down_revision = ('f8ab033698a2', '8ba06d0a54ee') +revision = "d80784e674a8" +down_revision = ("f8ab033698a2", "8ba06d0a54ee") -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): diff --git a/api/migrations/versions/d8d17ed47c53_.py b/api/migrations/versions/d8d17ed47c53_.py index 649046b8d..e693d2d6c 100644 --- a/api/migrations/versions/d8d17ed47c53_.py +++ b/api/migrations/versions/d8d17ed47c53_.py @@ -7,20 +7,23 @@ """ # revision identifiers, used by Alembic. -revision = 'd8d17ed47c53' -down_revision = '1993beca93d7' +revision = "d8d17ed47c53" +down_revision = "1993beca93d7" -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.add_column('reference_request', sa.Column('reference_submitted', sa.Boolean(), nullable=False)) + op.add_column( + "reference_request", + sa.Column("reference_submitted", sa.Boolean(), nullable=False), + ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('reference_request', 'reference_submitted') + op.drop_column("reference_request", "reference_submitted") # ### end Alembic commands ### diff --git a/api/migrations/versions/de12a6f382c1_ai4d_review_form.py b/api/migrations/versions/de12a6f382c1_ai4d_review_form.py index 27dfa1768..c89814a47 100644 --- a/api/migrations/versions/de12a6f382c1_ai4d_review_form.py +++ b/api/migrations/versions/de12a6f382c1_ai4d_review_form.py @@ -7,29 +7,30 @@ """ # revision identifiers, used by Alembic. -revision = 'de12a6f382c1' -down_revision = '964ead196cb9' +revision = "de12a6f382c1" +down_revision = "964ead196cb9" + +import datetime +from enum import Enum -from alembic import op import sqlalchemy as sa +from alembic import op from sqlalchemy import orm from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy import orm -from app import db -import datetime -from enum import Enum +from app import db Base = declarative_base() + class EventType(Enum): - EVENT = 'event' - AWARD = 'award' + EVENT = "event" + AWARD = "award" class Event(Base): - __tablename__ = 'event' - __table_args__ = {'extend_existing': True} + __tablename__ = "event" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(50), nullable=False) @@ -37,8 +38,9 @@ class Event(Base): start_date = db.Column(db.DateTime(), nullable=False) end_date = db.Column(db.DateTime(), nullable=False) key = db.Column(db.String(255), nullable=False, unique=True) - organisation_id = db.Column(db.Integer(), db.ForeignKey( - 'organisation.id'), nullable=False) + organisation_id = db.Column( + db.Integer(), db.ForeignKey("organisation.id"), nullable=False + ) email_from = db.Column(db.String(255), nullable=False) url = db.Column(db.String(255), nullable=False) @@ -56,21 +58,23 @@ class Event(Base): class ApplicationForm(Base): - __tablename__ = 'application_form' - __table_args__ = {'extend_existing': True} + __tablename__ = "application_form" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - event_id = db.Column(db.Integer(), db.ForeignKey('event.id'), nullable=False) + event_id = db.Column(db.Integer(), db.ForeignKey("event.id"), nullable=False) is_open = db.Column(db.Boolean(), nullable=False) class Question(Base): - __tablename__ = 'question' - __table_args__ = {'extend_existing': True} + __tablename__ = "question" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) - section_id = db.Column(db.Integer(), db.ForeignKey('section.id'), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) + section_id = db.Column(db.Integer(), db.ForeignKey("section.id"), nullable=False) type = db.Column(db.String(), nullable=False) description = db.Column(db.String(), nullable=True) headline = db.Column(db.String(), nullable=False) @@ -80,17 +84,21 @@ class Question(Base): order = db.Column(db.Integer(), nullable=False) options = db.Column(db.JSON(), nullable=True) is_required = db.Column(db.Boolean(), nullable=False) - depends_on_question_id = db.Column(db.Integer(), db.ForeignKey('question.id'), nullable=True) + depends_on_question_id = db.Column( + db.Integer(), db.ForeignKey("question.id"), nullable=True + ) show_for_values = db.Column(db.JSON(), nullable=True) key = db.Column(db.String(255), nullable=True) class ReviewForm(Base): - __tablename__ = 'review_form' - __table_args__ = {'extend_existing': True} + __tablename__ = "review_form" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) is_open = db.Column(db.Boolean(), nullable=False) deadline = db.Column(db.DateTime(), nullable=False) @@ -104,12 +112,14 @@ def close(self): class ReviewQuestion(Base): - __tablename__ = 'review_question' - __table_args__ = {'extend_existing': True} + __tablename__ = "review_question" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer, primary_key=True) - review_form_id = db.Column(db.Integer(), db.ForeignKey('review_form.id'), nullable=False) - question_id = db.Column(db.Integer(), db.ForeignKey('question.id'), nullable=True) + review_form_id = db.Column( + db.Integer(), db.ForeignKey("review_form.id"), nullable=False + ) + question_id = db.Column(db.Integer(), db.ForeignKey("question.id"), nullable=True) description = db.Column(db.String(), nullable=True) headline = db.Column(db.String(), nullable=True) type = db.Column(db.String(), nullable=False) @@ -121,19 +131,21 @@ class ReviewQuestion(Base): validation_text = db.Column(db.String(), nullable=True) weight = db.Column(db.Float(), nullable=False) - def __init__(self, - review_form_id, - type, - is_required, - order, - question_id=None, - description=None, - headline=None, - placeholder=None, - options=None, - validation_regex=None, - validation_text=None, - weight=0): + def __init__( + self, + review_form_id, + type, + is_required, + order, + question_id=None, + description=None, + headline=None, + placeholder=None, + options=None, + validation_regex=None, + validation_text=None, + weight=0, + ): self.review_form_id = review_form_id self.question_id = question_id self.description = description @@ -149,17 +161,28 @@ def __init__(self, class ReviewConfiguration(Base): - __tablename__ = 'review_configuration' - __table_args__ = {'extend_existing': True} + __tablename__ = "review_configuration" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - review_form_id = db.Column(db.Integer(), db.ForeignKey('review_form.id'), nullable=False) + review_form_id = db.Column( + db.Integer(), db.ForeignKey("review_form.id"), nullable=False + ) num_reviews_required = db.Column(db.Integer(), nullable=False) num_optional_reviews = db.Column(db.Integer(), nullable=False) - drop_optional_question_id = db.Column(db.Integer(), db.ForeignKey('review_question.id'), nullable=True) + drop_optional_question_id = db.Column( + db.Integer(), db.ForeignKey("review_question.id"), nullable=True + ) drop_optional_agreement_values = db.Column(db.String(), nullable=True) - def __init__(self, review_form_id, num_reviews_required, num_optional_reviews=0, drop_optional_question_id=None, drop_optional_agreement_values=None): + def __init__( + self, + review_form_id, + num_reviews_required, + num_optional_reviews=0, + drop_optional_question_id=None, + drop_optional_agreement_values=None, + ): self.review_form_id = review_form_id self.num_reviews_required = num_reviews_required self.num_optional_reviews = num_optional_reviews @@ -168,15 +191,21 @@ def __init__(self, review_form_id, num_reviews_required, num_optional_reviews=0, def find_question(session, headline, application_form_id): - return session.query(Question).filter_by(headline=headline, application_form_id=application_form_id).first() + return ( + session.query(Question) + .filter_by(headline=headline, application_form_id=application_form_id) + .first() + ) def upgrade(): Base.metadata.bind = op.get_bind() session = orm.Session(bind=Base.metadata.bind) - event = session.query(Event).filter_by(key='ai4d2020').first() - application_form = session.query(ApplicationForm).filter_by(event_id=event.id).first() + event = session.query(Event).filter_by(key="ai4d2020").first() + application_form = ( + session.query(ApplicationForm).filter_by(event_id=event.id).first() + ) form = ReviewForm(application_form.id, datetime.datetime(2020, 7, 31)) session.add(form) session.commit() @@ -185,119 +214,205 @@ def upgrade(): session.add(config) session.commit() - project_name_question = find_question(session, 'Project Name', application_form.id) - project_name_info = ReviewQuestion(form.id, 'information', False, 1, question_id=project_name_question.id, headline='Project Name') - - proposal_question = find_question(session, 'Your Proposal', application_form.id) - proposal_info = ReviewQuestion(form.id, 'file', False, 2, question_id=proposal_question.id, headline='Proposal') - - clarity = ReviewQuestion(form.id, 'multi-choice', True, 3, description=""" + project_name_question = find_question(session, "Project Name", application_form.id) + project_name_info = ReviewQuestion( + form.id, + "information", + False, + 1, + question_id=project_name_question.id, + headline="Project Name", + ) + + proposal_question = find_question(session, "Your Proposal", application_form.id) + proposal_info = ReviewQuestion( + form.id, "file", False, 2, question_id=proposal_question.id, headline="Proposal" + ) + + clarity = ReviewQuestion( + form.id, + "multi-choice", + True, + 3, + description=""" 4: very clear and novel hypothesis, includes ML/AI/DS component 3: research hypothesis clear, not necessarily novel, includes ML/AI/DS component 2: research hypothesis clear but marginal use of ML/AI/DS -1: research hypothesis is unclear""", headline='Clarity and novelty of research hypothesis applying ML/AI/DS', - options=[ - {'value': 4, 'label': '4'}, - {'value': 3, 'label': '3'}, - {'value': 2, 'label': '2'}, - {'value': 1, 'label': '1'} - ]) - - ethical = ReviewQuestion(form.id, 'multi-choice', True, 4, description=""" +1: research hypothesis is unclear""", + headline="Clarity and novelty of research hypothesis applying ML/AI/DS", + options=[ + {"value": 4, "label": "4"}, + {"value": 3, "label": "3"}, + {"value": 2, "label": "2"}, + {"value": 1, "label": "1"}, + ], + ) + + ethical = ReviewQuestion( + form.id, + "multi-choice", + True, + 4, + description=""" 3: thorough legal, ethical and social analysis done, if research involves human participants, ethical research approval has been obtained 2: legal, ethical and social analysis is wanting, if research involves human participants, ethical research approval has been obtained 1: research involves human participants, no ethical research approval obtained -""", headline='Ethical review', - options=[ - {'value': 3, 'label': '3'}, - {'value': 2, 'label': '2'}, - {'value': 1, 'label': '1'}]) - - feasibility = ReviewQuestion(form.id, 'multi-choice', True, 5, description=""" +""", + headline="Ethical review", + options=[ + {"value": 3, "label": "3"}, + {"value": 2, "label": "2"}, + {"value": 1, "label": "1"}, + ], + ) + + feasibility = ReviewQuestion( + form.id, + "multi-choice", + True, + 5, + description=""" 4: objectives and outcomes are clearly stated and feasible 3: some objectives and outcomes not clear or not feasible 2: unclear objectives and/or outcomes 1: no objectives and/or outcomes stated -""", headline='Feasibility of objectives and outcomes', - options=[ - {'value': 4, 'label': '4'}, - {'value': 3, 'label': '3'}, - {'value': 2, 'label': '2'}, - {'value': 1, 'label': '1'}]) - - attainability = ReviewQuestion(form.id, 'multi-choice', True, 6, description=""" +""", + headline="Feasibility of objectives and outcomes", + options=[ + {"value": 4, "label": "4"}, + {"value": 3, "label": "3"}, + {"value": 2, "label": "2"}, + {"value": 1, "label": "1"}, + ], + ) + + attainability = ReviewQuestion( + form.id, + "multi-choice", + True, + 6, + description=""" 4: Activity timelines are clearly stated, reasonable and attainable within the stipulated 6 month period 3: clear activity timelines but may require more than 6 months to execute and complete 2: Timelines given but not clear, make little sense 1: no activity timelines given -""", headline='Attainability of activity timelines', - options=[ - {'value': 4, 'label': '4'}, - {'value': 3, 'label': '3'}, - {'value': 2, 'label': '2'}, - {'value': 1, 'label': '1'}]) - - challenging = ReviewQuestion(form.id, 'multi-choice', True, 7, description=""" +""", + headline="Attainability of activity timelines", + options=[ + {"value": 4, "label": "4"}, + {"value": 3, "label": "3"}, + {"value": 2, "label": "2"}, + {"value": 1, "label": "1"}, + ], + ) + + challenging = ReviewQuestion( + form.id, + "multi-choice", + True, + 7, + description=""" 3: task at hand is challenging and provides room for subsequent iterations beyond the funded 6 months phase 2: task at hand is challenging, but no evidence of potential long-term ambitions for future work 1: task at hand is not challenging, will not grow/stretch the team -""", headline='Challenging task with long term goal', - options=[ - {'value': 3, 'label': '3'}, - {'value': 2, 'label': '2'}, - {'value': 1, 'label': '1'}]) - - budget = ReviewQuestion(form.id, 'multi-choice', True, 8, description=""" +""", + headline="Challenging task with long term goal", + options=[ + {"value": 3, "label": "3"}, + {"value": 2, "label": "2"}, + {"value": 1, "label": "1"}, + ], + ) + + budget = ReviewQuestion( + form.id, + "multi-choice", + True, + 8, + description=""" 2: amount of funding requested is clearly stated, is tied in with activity timelines and schedule is feasible 1: unclear budget allocation, not tied in with activity timeline -""", headline='Budget allocation', - options=[ - {'value': 2, 'label': '2'}, - {'value': 1, 'label': '1'}]) - - team = ReviewQuestion(form.id, 'multi-choice', True, 9, description=""" +""", + headline="Budget allocation", + options=[{"value": 2, "label": "2"}, {"value": 1, "label": "1"}], + ) + + team = ReviewQuestion( + form.id, + "multi-choice", + True, + 9, + description=""" 3: details of the team/leads are given and they demonstrate a strong ability to undertake and complete the project/research at hand 2: details of the team given but they lack foundational capability to undertake and complete the work, even with mentorship support 1: little information given about the team, no demonstration of ability to undertake the research/task -""", headline='Quality of team', - options=[ - {'value': 3, 'label': '3'}, - {'value': 2, 'label': '2'}, - {'value': 1, 'label': '1'}]) - - overall = ReviewQuestion(form.id, 'multi-choice', True, 10, description="""Overall evaluation - Do you recommend this proposal for funding?""", headline='Overall evaluation', - options=[ - {'value': 3, 'label': 'Strong Recommend'}, - {'value': 2, 'label': 'Recommend'}, - {'value': 1, 'label': 'Weak Recommend'}, - {'value': 0, 'label': 'Borderline Submission'}, - {'value': -1, 'label': 'Weak Reject'}, - {'value': -2, 'label': 'Reject'}, - {'value': -3, 'label': 'Strong Reject'}]) - - confidence = ReviewQuestion(form.id, 'multi-choice', True, 11, description="""Your confidence in your assessment""", headline="Reviewer's confidence", - options=[ - {'value': 4, 'label': 'Expert'}, - {'value': 3, 'label': 'High'}, - {'value': 2, 'label': 'Medium'}, - {'value': 1, 'label': 'Low'}]) - - other = ReviewQuestion(form.id, 'long-text', True, 12, description="""Any other remarks not covered by previous questions?""", headline="Other Remarks") - - session.add_all([ - project_name_info, - proposal_info, - clarity, - ethical, - feasibility, - attainability, - challenging, - budget, - team, - overall, - confidence, - other - ]) +""", + headline="Quality of team", + options=[ + {"value": 3, "label": "3"}, + {"value": 2, "label": "2"}, + {"value": 1, "label": "1"}, + ], + ) + + overall = ReviewQuestion( + form.id, + "multi-choice", + True, + 10, + description="""Overall evaluation - Do you recommend this proposal for funding?""", + headline="Overall evaluation", + options=[ + {"value": 3, "label": "Strong Recommend"}, + {"value": 2, "label": "Recommend"}, + {"value": 1, "label": "Weak Recommend"}, + {"value": 0, "label": "Borderline Submission"}, + {"value": -1, "label": "Weak Reject"}, + {"value": -2, "label": "Reject"}, + {"value": -3, "label": "Strong Reject"}, + ], + ) + + confidence = ReviewQuestion( + form.id, + "multi-choice", + True, + 11, + description="""Your confidence in your assessment""", + headline="Reviewer's confidence", + options=[ + {"value": 4, "label": "Expert"}, + {"value": 3, "label": "High"}, + {"value": 2, "label": "Medium"}, + {"value": 1, "label": "Low"}, + ], + ) + + other = ReviewQuestion( + form.id, + "long-text", + True, + 12, + description="""Any other remarks not covered by previous questions?""", + headline="Other Remarks", + ) + + session.add_all( + [ + project_name_info, + proposal_info, + clarity, + ethical, + feasibility, + attainability, + challenging, + budget, + team, + overall, + confidence, + other, + ] + ) session.commit() @@ -305,15 +420,25 @@ def upgrade(): def downgrade(): Base.metadata.bind = op.get_bind() session = orm.Session(bind=Base.metadata.bind) - - event = session.query(Event).filter_by(key='ai4d2020').first() - application_form = session.query(ApplicationForm).filter_by(event_id=event.id).first() - form = session.query(ReviewForm).filter_by(application_form_id=application_form.id).first() + + event = session.query(Event).filter_by(key="ai4d2020").first() + application_form = ( + session.query(ApplicationForm).filter_by(event_id=event.id).first() + ) + form = ( + session.query(ReviewForm) + .filter_by(application_form_id=application_form.id) + .first() + ) if form: session.query(ReviewQuestion).filter_by(review_form_id=form.id).delete() session.query(ReviewConfiguration).filter_by(review_form_id=form.id).delete() - - form = session.query(ReviewForm).filter_by(application_form_id=application_form.id).delete() - + + form = ( + session.query(ReviewForm) + .filter_by(application_form_id=application_form.id) + .delete() + ) + session.commit() session.flush() diff --git a/api/migrations/versions/e1f99a034c22_add_submitted_to_review_response.py b/api/migrations/versions/e1f99a034c22_add_submitted_to_review_response.py index 27e3b560d..ab1b1286b 100644 --- a/api/migrations/versions/e1f99a034c22_add_submitted_to_review_response.py +++ b/api/migrations/versions/e1f99a034c22_add_submitted_to_review_response.py @@ -7,34 +7,45 @@ """ # revision identifiers, used by Alembic. -revision = 'e1f99a034c22' -down_revision = '347a14922cff' +revision = "e1f99a034c22" +down_revision = "347a14922cff" -from alembic import op import sqlalchemy as sa +from alembic import op from sqlalchemy.dialects import postgresql + def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - print('Starting Migration e1f99a034c22') - op.add_column('review_response', sa.Column('is_submitted', sa.Boolean(), nullable=True)) - - op.alter_column('review_response', 'submitted_timestamp', - existing_type=postgresql.TIMESTAMP(), - nullable=True) - - op.execute("""UPDATE review_response SET is_submitted=True, submitted_timestamp='2020-01-01'""") - op.alter_column('review_response', 'is_submitted', nullable=False) + print("Starting Migration e1f99a034c22") + op.add_column( + "review_response", sa.Column("is_submitted", sa.Boolean(), nullable=True) + ) + + op.alter_column( + "review_response", + "submitted_timestamp", + existing_type=postgresql.TIMESTAMP(), + nullable=True, + ) + + op.execute( + """UPDATE review_response SET is_submitted=True, submitted_timestamp='2020-01-01'""" + ) + op.alter_column("review_response", "is_submitted", nullable=False) op.execute("""update review_question set type='multi-file' where "order"=35""") - print('Finished Migration e1f99a034c22') + print("Finished Migration e1f99a034c22") # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.alter_column('review_response', 'submitted_timestamp', - existing_type=postgresql.TIMESTAMP(), - nullable=False) - op.drop_column('review_response', 'is_submitted') + op.alter_column( + "review_response", + "submitted_timestamp", + existing_type=postgresql.TIMESTAMP(), + nullable=False, + ) + op.drop_column("review_response", "is_submitted") # ### end Alembic commands ### diff --git a/api/migrations/versions/e3e47585f336_section_question_key.py b/api/migrations/versions/e3e47585f336_section_question_key.py index 145b41982..8c1bc7641 100644 --- a/api/migrations/versions/e3e47585f336_section_question_key.py +++ b/api/migrations/versions/e3e47585f336_section_question_key.py @@ -7,31 +7,43 @@ """ # revision identifiers, used by Alembic. -revision = 'e3e47585f336' -down_revision = 'c3c5ef958ca1' +revision = "e3e47585f336" +down_revision = "c3c5ef958ca1" -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): - op.add_column('question', sa.Column('key', sa.String(length=255), nullable=True)) - op.add_column('section', sa.Column('key', sa.String(length=255), nullable=True)) - op.execute("""UPDATE Question + op.add_column("question", sa.Column("key", sa.String(length=255), nullable=True)) + op.add_column("section", sa.Column("key", sa.String(length=255), nullable=True)) + op.execute( + """UPDATE Question SET key = 'nomination_title' FROM Section WHERE Question.section_id = Section.id AND Headline = 'Title' - AND Section.name IN ('Nominee Information', 'Doctoral Candidate Information', 'Masters Candidate Information')""") - op.execute("""UPDATE question SET key='nomination_firstname' WHERE headline='Firstname'""") - op.execute("""UPDATE question SET key='nomination_lastname' WHERE headline='Lastname'""") - op.execute("""UPDATE question SET key='nomination_email' WHERE headline='Email Address'""") + AND Section.name IN ('Nominee Information', 'Doctoral Candidate Information', 'Masters Candidate Information')""" + ) + op.execute( + """UPDATE question SET key='nomination_firstname' WHERE headline='Firstname'""" + ) + op.execute( + """UPDATE question SET key='nomination_lastname' WHERE headline='Lastname'""" + ) + op.execute( + """UPDATE question SET key='nomination_email' WHERE headline='Email Address'""" + ) + + op.execute( + """UPDATE Section SET key='nominee_section' WHERE name IN ('Nominee Information', 'Doctoral Candidate Information', 'Masters Candidate Information')""" + ) + op.execute( + """UPDATE Question SET key='nominating_capacity' WHERE headline IN ('Nominating Capacity', 'Nomination Capacity')""" + ) - op.execute("""UPDATE Section SET key='nominee_section' WHERE name IN ('Nominee Information', 'Doctoral Candidate Information', 'Masters Candidate Information')""") - op.execute("""UPDATE Question SET key='nominating_capacity' WHERE headline IN ('Nominating Capacity', 'Nomination Capacity')""") def downgrade(): - op.drop_column('section', 'key') - op.drop_column('question', 'key') - + op.drop_column("section", "key") + op.drop_column("question", "key") diff --git a/api/migrations/versions/e622c3deea5d_.py b/api/migrations/versions/e622c3deea5d_.py index 14a08426a..b8a83321d 100644 --- a/api/migrations/versions/e622c3deea5d_.py +++ b/api/migrations/versions/e622c3deea5d_.py @@ -7,32 +7,36 @@ """ # revision identifiers, used by Alembic. -revision = 'e622c3deea5d' -down_revision = 'b7006cbdd923' +revision = "e622c3deea5d" +down_revision = "b7006cbdd923" -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.create_table('reference_request', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('response_id', sa.Integer(), nullable=False), - sa.Column('title', sa.String(length=50), nullable=False), - sa.Column('firstname', sa.String(length=50), nullable=False), - sa.Column('lastname', sa.String(length=50), nullable=False), - sa.Column('relation', sa.String(length=50), nullable=False), - sa.Column('token', sa.String(length=50), nullable=False), - sa.Column('email', sa.String(length=50), nullable=True), - sa.Column('email_sent', sa.DateTime(), nullable=True), - sa.ForeignKeyConstraint(['response_id'], ['response.id'], ), - sa.PrimaryKeyConstraint('id') + op.create_table( + "reference_request", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("response_id", sa.Integer(), nullable=False), + sa.Column("title", sa.String(length=50), nullable=False), + sa.Column("firstname", sa.String(length=50), nullable=False), + sa.Column("lastname", sa.String(length=50), nullable=False), + sa.Column("relation", sa.String(length=50), nullable=False), + sa.Column("token", sa.String(length=50), nullable=False), + sa.Column("email", sa.String(length=50), nullable=True), + sa.Column("email_sent", sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint( + ["response_id"], + ["response.id"], + ), + sa.PrimaryKeyConstraint("id"), ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('reference_request') + op.drop_table("reference_request") # ### end Alembic commands ### diff --git a/api/migrations/versions/e8d1ee2746e0_event_travel_grant.py b/api/migrations/versions/e8d1ee2746e0_event_travel_grant.py index 9e706ebb2..1d118bc45 100644 --- a/api/migrations/versions/e8d1ee2746e0_event_travel_grant.py +++ b/api/migrations/versions/e8d1ee2746e0_event_travel_grant.py @@ -7,23 +7,27 @@ """ # revision identifiers, used by Alembic. -revision = 'e8d1ee2746e0' -down_revision = '7f37bf248c81' +revision = "e8d1ee2746e0" +down_revision = "7f37bf248c81" -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.add_column('event', sa.Column('travel_grant', sa.Boolean(), nullable=True)) - op.execute("""UPDATE event SET travel_grant=True WHERE name IN ('indaba 2019', 'Test Event')""") - op.execute("""UPDATE event SET travel_grant=False WHERE name NOT IN ('indaba 2019', 'Test Event')""") - op.alter_column('event', 'travel_grant', existing_type=sa.Boolean(), nullable=False) + op.add_column("event", sa.Column("travel_grant", sa.Boolean(), nullable=True)) + op.execute( + """UPDATE event SET travel_grant=True WHERE name IN ('indaba 2019', 'Test Event')""" + ) + op.execute( + """UPDATE event SET travel_grant=False WHERE name NOT IN ('indaba 2019', 'Test Event')""" + ) + op.alter_column("event", "travel_grant", existing_type=sa.Boolean(), nullable=False) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('event', 'travel_grant') + op.drop_column("event", "travel_grant") # ### end Alembic commands ### diff --git a/api/migrations/versions/ea987b257ee0_ai4d_updates.py b/api/migrations/versions/ea987b257ee0_ai4d_updates.py index f881eaa51..fa1d7fffd 100644 --- a/api/migrations/versions/ea987b257ee0_ai4d_updates.py +++ b/api/migrations/versions/ea987b257ee0_ai4d_updates.py @@ -9,26 +9,27 @@ """ # revision identifiers, used by Alembic. -revision = 'ea987b257ee0' -down_revision = '613df2d7a759' +revision = "ea987b257ee0" +down_revision = "613df2d7a759" + +import datetime +from enum import Enum -from alembic import op import sqlalchemy as sa -from sqlalchemy import orm +from alembic import op +from sqlalchemy import orm, select from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy import orm -from sqlalchemy import select from sqlalchemy.orm import column_property + from app import db -import datetime -from enum import Enum Base = declarative_base() + class Organisation(Base): __tablename__ = "organisation" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(50), nullable=False) @@ -40,7 +41,17 @@ class Organisation(Base): email_from = db.Column(db.String(100), nullable=True) system_url = db.Column(db.String(100), nullable=False) - def __init__(self, name, system_name, small_logo, large_logo, domain, url, email_from, system_url): + def __init__( + self, + name, + system_name, + small_logo, + large_logo, + domain, + url, + email_from, + system_url, + ): self.name = name self.small_logo = small_logo self.large_logo = large_logo @@ -50,9 +61,10 @@ def __init__(self, name, system_name, small_logo, large_logo, domain, url, email self.email_from = email_from self.system_url = system_url + class Country(Base): __tablename__ = "country" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(100), nullable=False) @@ -60,22 +72,25 @@ class Country(Base): def __init__(self, name): self.name = name + class EventType(Enum): - EVENT = 'event' - AWARD = 'award' - CALL = 'call' + EVENT = "event" + AWARD = "award" + CALL = "call" class Event(Base): __tablename__ = "event" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) start_date = db.Column(db.DateTime(), nullable=False) end_date = db.Column(db.DateTime(), nullable=False) key = db.Column(db.String(255), nullable=False, unique=True) - organisation_id = db.Column(db.Integer(), db.ForeignKey('organisation.id'), nullable=False) + organisation_id = db.Column( + db.Integer(), db.ForeignKey("organisation.id"), nullable=False + ) email_from = db.Column(db.String(255), nullable=False) url = db.Column(db.String(255), nullable=False) application_open = db.Column(db.DateTime(), nullable=False) @@ -92,7 +107,7 @@ class Event(Base): travel_grant = db.Column(db.Boolean(), nullable=False) miniconf_url = db.Column(db.String(100), nullable=True) - event_translations = db.relationship('EventTranslation', lazy='dynamic') + event_translations = db.relationship("EventTranslation", lazy="dynamic") def __init__( self, @@ -116,7 +131,7 @@ def __init__( registration_close, event_type, travel_grant, - miniconf_url=None + miniconf_url=None, ): self.start_date = start_date self.end_date = end_date @@ -148,10 +163,11 @@ def add_event_translations(self, names, descriptions): event_translation = EventTranslation(name, description, language) self.event_translations.append(event_translation) + class EventTranslation(Base): __tablename__ = "event_translation" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) event_id = db.Column(db.Integer(), db.ForeignKey("event.id"), nullable=False) @@ -159,19 +175,20 @@ class EventTranslation(Base): description = db.Column(db.String(255), nullable=False) language = db.Column(db.String(2)) - event = db.relationship('Event', foreign_keys=[event_id]) + event = db.relationship("Event", foreign_keys=[event_id]) def __init__(self, name, description, language): self.name = name self.description = description self.language = language + class ApplicationForm(Base): - __tablename__ = 'application_form' - __table_args__ = {'extend_existing': True} + __tablename__ = "application_form" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - event_id = db.Column(db.Integer(), db.ForeignKey('event.id'), nullable=False) + event_id = db.Column(db.Integer(), db.ForeignKey("event.id"), nullable=False) is_open = db.Column(db.Boolean(), nullable=False) nominations = db.Column(db.Boolean(), nullable=False) @@ -180,22 +197,29 @@ def __init__(self, event_id, is_open, nominations): self.is_open = is_open self.nominations = nominations + class Question(Base): - __tablename__ = 'question' - __table_args__ = {'extend_existing': True} + __tablename__ = "question" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) - section_id = db.Column(db.Integer(), db.ForeignKey('section.id'), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) + section_id = db.Column(db.Integer(), db.ForeignKey("section.id"), nullable=False) type = db.Column(db.String(), nullable=False) order = db.Column(db.Integer(), nullable=False) is_required = db.Column(db.Boolean(), nullable=False) - depends_on_question_id = db.Column(db.Integer(), db.ForeignKey('question.id'), nullable=True) + depends_on_question_id = db.Column( + db.Integer(), db.ForeignKey("question.id"), nullable=True + ) key = db.Column(db.String(255), nullable=True) - question_translations = db.relationship('QuestionTranslation', lazy='dynamic') + question_translations = db.relationship("QuestionTranslation", lazy="dynamic") - def __init__(self, application_form_id, section_id, order, questionType, is_required=True): + def __init__( + self, application_form_id, section_id, order, questionType, is_required=True + ): self.application_form_id = application_form_id self.section_id = section_id self.order = order @@ -203,45 +227,55 @@ def __init__(self, application_form_id, section_id, order, questionType, is_requ self.is_required = is_required def get_translation(self, language): - question_translation = self.question_translations.filter_by(language=language).first() + question_translation = self.question_translations.filter_by( + language=language + ).first() return question_translation class Section(Base): - __tablename__ = 'section' - __table_args__ = {'extend_existing': True} + __tablename__ = "section" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) order = db.Column(db.Integer(), nullable=False) - depends_on_question_id = db.Column(db.Integer(), db.ForeignKey('question.id', use_alter=True), nullable=True) + depends_on_question_id = db.Column( + db.Integer(), db.ForeignKey("question.id", use_alter=True), nullable=True + ) key = db.Column(db.String(255), nullable=True) - section_translations = db.relationship('SectionTranslation', lazy='dynamic') + section_translations = db.relationship("SectionTranslation", lazy="dynamic") - def __init__(self, application_form_id, order, depends_on_question_id=None, key=None): + def __init__( + self, application_form_id, order, depends_on_question_id=None, key=None + ): self.application_form_id = application_form_id self.order = order self.depends_on_question_id = depends_on_question_id self.key = key def get_translation(self, language): - section_translation = self.section_translations.filter_by(language=language).first() + section_translation = self.section_translations.filter_by( + language=language + ).first() return section_translation class SectionTranslation(Base): - __tablename__ = 'section_translation' - __table_args__ = {'extend_existing': True} + __tablename__ = "section_translation" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - section_id = db.Column(db.Integer(), db.ForeignKey('section.id'), nullable=False) + section_id = db.Column(db.Integer(), db.ForeignKey("section.id"), nullable=False) language = db.Column(db.String(2), nullable=False) name = db.Column(db.String(255), nullable=False) description = db.Column(db.String(), nullable=False) show_for_values = db.Column(db.JSON(), nullable=True) - section = db.relationship('Section', foreign_keys=[section_id]) + section = db.relationship("Section", foreign_keys=[section_id]) def __init__(self, section_id, language, name, description, show_for_values=None): self.section_id = section_id @@ -252,11 +286,11 @@ def __init__(self, section_id, language, name, description, show_for_values=None class QuestionTranslation(Base): - __tablename__ = 'question_translation' - __table_args__ = {'extend_existing': True} + __tablename__ = "question_translation" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - question_id = db.Column(db.Integer(), db.ForeignKey('question.id'), nullable=False) + question_id = db.Column(db.Integer(), db.ForeignKey("question.id"), nullable=False) language = db.Column(db.String(2), nullable=False) headline = db.Column(db.String(), nullable=False) description = db.Column(db.String(), nullable=True) @@ -266,7 +300,7 @@ class QuestionTranslation(Base): options = db.Column(db.JSON(), nullable=True) show_for_values = db.Column(db.JSON(), nullable=True) - question = db.relationship('Question', foreign_keys=[question_id]) + question = db.relationship("Question", foreign_keys=[question_id]) def __init__( self, @@ -278,7 +312,7 @@ def __init__( validation_regex=None, validation_text=None, options=None, - show_for_values=None + show_for_values=None, ): self.question_id = question_id self.language = language @@ -294,10 +328,12 @@ def __init__( class Response(Base): __tablename__ = "response" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(),db.ForeignKey("application_form.id"), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) user_id = db.Column(db.Integer(), db.ForeignKey("app_user.id"), nullable=False) is_submitted = db.Column(db.Boolean(), nullable=False) submitted_timestamp = db.Column(db.DateTime(), nullable=True) @@ -306,7 +342,7 @@ class Response(Base): started_timestamp = db.Column(db.DateTime(), nullable=True) language = db.Column(db.String(2), nullable=False) - answers = db.relationship('Answer', order_by='Answer.order') + answers = db.relationship("Answer", order_by="Answer.order") def __init__(self, application_form_id, user_id, language): self.application_form_id = application_form_id @@ -321,13 +357,17 @@ def __init__(self, application_form_id, user_id, language): class Answer(Base): __tablename__ = "answer" - __table_args__ = {'extend_existing': True} + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) response_id = db.Column(db.Integer(), db.ForeignKey("response.id"), nullable=False) question_id = db.Column(db.Integer(), db.ForeignKey("question.id"), nullable=False) value = db.Column(db.String(), nullable=False) - order = column_property(select([Question.order]).where(Question.id==question_id).correlate_except(Question)) + order = column_property( + select([Question.order]) + .where(Question.id == question_id) + .correlate_except(Question) + ) def __init__(self, response_id, question_id, value): self.response_id = response_id @@ -339,39 +379,41 @@ def upgrade(): Base.metadata.bind = op.get_bind() session = orm.Session(bind=Base.metadata.bind) - event = session.query(Event).filter_by(key='prc').first() + event = session.query(Event).filter_by(key="prc").first() form = session.query(ApplicationForm).filter_by(event_id=event.id).first() def get_question_by_en_headline(en_headline): - en = (session.query(QuestionTranslation) - .filter_by(headline=en_headline, language='en') - .join(Question) - .filter_by(application_form_id=form.id) - .first()) + en = ( + session.query(QuestionTranslation) + .filter_by(headline=en_headline, language="en") + .join(Question) + .filter_by(application_form_id=form.id) + .first() + ) question = en.question - fr = question.get_translation('fr') + fr = question.get_translation("fr") return question, en, fr - + # Remove organisation email question - question, en, fr = get_question_by_en_headline('Email Address') + question, en, fr = get_question_by_en_headline("Email Address") session.query(Answer).filter_by(question_id=question.id).delete() session.query(QuestionTranslation).filter_by(id=en.id).delete() session.query(QuestionTranslation).filter_by(id=fr.id).delete() session.query(Question).filter_by(id=question.id).delete() # Update capitalization - question, en, fr = get_question_by_en_headline('Name of Organisation') - en.headline = 'Name of organization' + question, en, fr = get_question_by_en_headline("Name of Organisation") + en.headline = "Name of organization" - question, en, fr = get_question_by_en_headline('Email Address of principal contact') - en.headline = 'Email address of principal contact' + question, en, fr = get_question_by_en_headline("Email Address of principal contact") + en.headline = "Email address of principal contact" - question, en, fr = get_question_by_en_headline('Policy research') + question, en, fr = get_question_by_en_headline("Policy research") en.description = """How will you answer the proposed research questions in the most rigorous way possible? Please include a discussion of the conceptual and theoretical framework/s, user participation, data collection and analysis. Maximum 750 words""" - question, en, fr = get_question_by_en_headline('1. Policy engagement') + question, en, fr = get_question_by_en_headline("1. Policy engagement") en.description = """Please describe the kinds of engagement your organization has had with national governing institutions (e.g. government departments, bodies and agencies, commissions, parliaments, regulators, and other public-sector institutions) on any of the above topics (from Section I, question 3)? This can include a reflection on what you have done well with communications and what you have not done so well. Please give up to three examples of this engagement, such as requests for input, sitting on policy steering committees and meetings with policy makers. @@ -383,14 +425,15 @@ def get_question_by_en_headline(en_headline): Maximum 350 mots""" - question, en, fr = get_question_by_en_headline('3. Regional Engagement') - en.headline = '3. Regional engagement' + question, en, fr = get_question_by_en_headline("3. Regional Engagement") + en.headline = "3. Regional engagement" - question, en, fr = get_question_by_en_headline('Regional Engagement') - en.headline = 'Regional engagement' + question, en, fr = get_question_by_en_headline("Regional Engagement") + en.headline = "Regional engagement" - section_en = session.query(SectionTranslation).filter_by( - name='Section IV: Budget').first() + section_en = ( + session.query(SectionTranslation).filter_by(name="Section IV: Budget").first() + ) section_en.description = """Please submit a full three year budget with notes using the Excel template available on the IDRC public website: [https://www.idrc.ca/en/resources/guides-and-forms](https://www.idrc.ca/en/resources/guides-and-forms) (Select the ‘Proposal budget’ and download.) @@ -404,20 +447,26 @@ def downgrade(): Base.metadata.bind = op.get_bind() session = orm.Session(bind=Base.metadata.bind) - event = session.query(Event).filter_by(key='prc').first() + event = session.query(Event).filter_by(key="prc").first() form = session.query(ApplicationForm).filter_by(event_id=event.id).first() def get_question_by_en_headline(en_headline): - en = (session.query(QuestionTranslation) - .filter_by(headline=en_headline, language='en') - .join(Question) - .filter_by(application_form_id=form.id) - .first()) + en = ( + session.query(QuestionTranslation) + .filter_by(headline=en_headline, language="en") + .join(Question) + .filter_by(application_form_id=form.id) + .first() + ) question = en.question - fr = question.get_translation('fr') + fr = question.get_translation("fr") return question, en, fr - def add_question(section, order, questionType, headlines, + def add_question( + section, + order, + questionType, + headlines, descriptions=None, placeholders=None, validation_regexs=None, @@ -425,8 +474,11 @@ def add_question(section, order, questionType, headlines, options=None, show_for_values=None, is_required=True, - depends_on_question_id=None): - question = Question(form.id, section.id, order, questionType, is_required=is_required) + depends_on_question_id=None, + ): + question = Question( + form.id, section.id, order, questionType, is_required=is_required + ) if depends_on_question_id is not None: question.depends_on_question_id = depends_on_question_id @@ -436,34 +488,55 @@ def add_question(section, order, questionType, headlines, translations = [] for language in headlines: - translations.append(QuestionTranslation(question.id, language, - headline=headlines[language], - description=None if descriptions is None else descriptions[language], - placeholder=None if placeholders is None else placeholders[language], - validation_regex=None if validation_regexs is None else validation_regexs[language], - validation_text=None if validation_texts is None else validation_texts[language], - options=None if options is None else options[language], - show_for_values=None if show_for_values is None else show_for_values[language])) + translations.append( + QuestionTranslation( + question.id, + language, + headline=headlines[language], + description=None + if descriptions is None + else descriptions[language], + placeholder=None + if placeholders is None + else placeholders[language], + validation_regex=None + if validation_regexs is None + else validation_regexs[language], + validation_text=None + if validation_texts is None + else validation_texts[language], + options=None if options is None else options[language], + show_for_values=None + if show_for_values is None + else show_for_values[language], + ) + ) session.add_all(translations) session.commit() return question - section_en = session.query(SectionTranslation).filter_by(name='Organization and lead applicant contact details').first() - add_question(section_en.section, 9, 'short-text', { - 'en': 'Email Address', - 'fr': 'Adresse de courriel' - }) + section_en = ( + session.query(SectionTranslation) + .filter_by(name="Organization and lead applicant contact details") + .first() + ) + add_question( + section_en.section, + 9, + "short-text", + {"en": "Email Address", "fr": "Adresse de courriel"}, + ) - question, en, fr = get_question_by_en_headline('Name of organization') - en.headline = 'Name of Organisation' + question, en, fr = get_question_by_en_headline("Name of organization") + en.headline = "Name of Organisation" - question, en, fr = get_question_by_en_headline('Email address of principal contact') - en.headline = 'Email Address of principal contact' + question, en, fr = get_question_by_en_headline("Email address of principal contact") + en.headline = "Email Address of principal contact" - question, en, fr = get_question_by_en_headline('3. Regional engagement') - en.headline = '3. Regional Engagement' + question, en, fr = get_question_by_en_headline("3. Regional engagement") + en.headline = "3. Regional Engagement" + + question, en, fr = get_question_by_en_headline("Regional engagement") + en.headline = "Regional Engagement" - question, en, fr = get_question_by_en_headline('Regional engagement') - en.headline = 'Regional Engagement' - session.commit() diff --git a/api/migrations/versions/eac3fd9d5ae8_.py b/api/migrations/versions/eac3fd9d5ae8_.py index 4fe0f702d..dc8419d70 100644 --- a/api/migrations/versions/eac3fd9d5ae8_.py +++ b/api/migrations/versions/eac3fd9d5ae8_.py @@ -8,36 +8,55 @@ # revision identifiers, used by Alembic. -revision = 'eac3fd9d5ae8' -down_revision = '865b2e2d02ff' +revision = "eac3fd9d5ae8" +down_revision = "865b2e2d02ff" -from alembic import op import sqlalchemy as sa +from alembic import op from sqlalchemy.dialects import postgresql + def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.alter_column('guest_registration', 'confirmation_email_sent_at', - existing_type=postgresql.TIMESTAMP(), - nullable=True) - op.alter_column('guest_registration', 'created_at', - existing_type=postgresql.TIMESTAMP(), - nullable=True) - op.drop_column('invitation_letter_request', 'invitation_letter_sent_at') - op.add_column('invitation_letter_request', sa.Column('invitation_letter_sent_at', sa.DateTime(), nullable=False)) + op.alter_column( + "guest_registration", + "confirmation_email_sent_at", + existing_type=postgresql.TIMESTAMP(), + nullable=True, + ) + op.alter_column( + "guest_registration", + "created_at", + existing_type=postgresql.TIMESTAMP(), + nullable=True, + ) + op.drop_column("invitation_letter_request", "invitation_letter_sent_at") + op.add_column( + "invitation_letter_request", + sa.Column("invitation_letter_sent_at", sa.DateTime(), nullable=False), + ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.alter_column('guest_registration', 'created_at', - existing_type=postgresql.TIMESTAMP(), - nullable=False) - op.alter_column('guest_registration', 'confirmation_email_sent_at', - existing_type=postgresql.TIMESTAMP(), - nullable=False) - - op.drop_column('invitation_letter_request', 'invitation_letter_sent_at') - op.add_column('invitation_letter_request', sa.Column('invitation_letter_sent_at', sa.String(), nullable=False)) - + op.alter_column( + "guest_registration", + "created_at", + existing_type=postgresql.TIMESTAMP(), + nullable=False, + ) + op.alter_column( + "guest_registration", + "confirmation_email_sent_at", + existing_type=postgresql.TIMESTAMP(), + nullable=False, + ) + + op.drop_column("invitation_letter_request", "invitation_letter_sent_at") + op.add_column( + "invitation_letter_request", + sa.Column("invitation_letter_sent_at", sa.String(), nullable=False), + ) + # ### end Alembic commands ### diff --git a/api/migrations/versions/eb6f96db82e8_eeml_review_form_update1.py b/api/migrations/versions/eb6f96db82e8_eeml_review_form_update1.py index 873d96537..d4dddd07a 100644 --- a/api/migrations/versions/eb6f96db82e8_eeml_review_form_update1.py +++ b/api/migrations/versions/eb6f96db82e8_eeml_review_form_update1.py @@ -7,36 +7,44 @@ """ # revision identifiers, used by Alembic. -revision = 'eb6f96db82e8' -down_revision = '6d34d1ab6864' +revision = "eb6f96db82e8" +down_revision = "6d34d1ab6864" -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): - op.get_bind().execute("""update review_question + op.get_bind().execute( + """update review_question set type = 'file' from question where review_question.question_id = question.id - and question.headline = 'CV'""") + and question.headline = 'CV'""" + ) - op.get_bind().execute("""update review_question + op.get_bind().execute( + """update review_question set type = 'file' from question where review_question.question_id = question.id - and question.headline = 'Extended abstract'""") + and question.headline = 'Extended abstract'""" + ) def downgrade(): - op.get_bind().execute("""update review_question + op.get_bind().execute( + """update review_question set type = 'information' from question where review_question.question_id = question.id - and question.headline = 'CV'""") + and question.headline = 'CV'""" + ) - op.get_bind().execute("""update review_question + op.get_bind().execute( + """update review_question set type = 'information' from question where review_question.question_id = question.id - and question.headline = 'Extended abstract'""") + and question.headline = 'Extended abstract'""" + ) diff --git a/api/migrations/versions/efc34996e565_.py b/api/migrations/versions/efc34996e565_.py index 6560316d5..a986a5ab9 100644 --- a/api/migrations/versions/efc34996e565_.py +++ b/api/migrations/versions/efc34996e565_.py @@ -7,23 +7,28 @@ """ # revision identifiers, used by Alembic. -revision = 'efc34996e565' -down_revision = '35cdc3c51184' +revision = "efc34996e565" +down_revision = "35cdc3c51184" -from alembic import op -import sqlalchemy as sa from datetime import datetime -from sqlalchemy.ext.declarative import declarative_base + +import sqlalchemy as sa +from alembic import op from sqlalchemy import orm +from sqlalchemy.ext.declarative import declarative_base + from app import db Base = declarative_base() + class Section(Base): - __tablename__ = 'section' - __table_args__ = {'extend_existing': True} + __tablename__ = "section" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) name = db.Column(db.String(255), unique=True, nullable=False) description = db.Column(db.String(255), nullable=False) order = db.Column(db.Integer(), unique=True, nullable=False) @@ -34,10 +39,11 @@ def __init__(self, application_form_id, name, description, order): self.description = description self.order = order + # Freeze the models class UserCategory(Base): - __tablename__ = 'user_category' - __table_args__ = {'extend_existing': True} + __tablename__ = "user_category" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(100), nullable=False) description = db.Column(db.String(500)) @@ -50,11 +56,13 @@ def __init__(self, name, description=None, group=None): class Question(Base): - __tablename__ = 'question' - __table_args__ = {'extend_existing': True} + __tablename__ = "question" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) - section_id = db.Column(db.Integer(), db.ForeignKey('section.id'), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) + section_id = db.Column(db.Integer(), db.ForeignKey("section.id"), nullable=False) type = db.Column(db.String(), nullable=False) description = db.Column(db.String(), nullable=True) headline = db.Column(db.String(), nullable=False) @@ -65,7 +73,20 @@ class Question(Base): options = db.Column(db.JSON(), nullable=True) is_required = db.Column(db.Boolean(), nullable=False) - def __init__(self, application_form_id, section_id, headline, placeholder, order, questionType, validation_regex, validation_text=None, is_required = True, description = None, options = None): + def __init__( + self, + application_form_id, + section_id, + headline, + placeholder, + order, + questionType, + validation_regex, + validation_text=None, + is_required=True, + description=None, + options=None, + ): self.application_form_id = application_form_id self.section_id = section_id self.headline = headline @@ -80,38 +101,47 @@ def __init__(self, application_form_id, section_id, headline, placeholder, order class ApplicationForm(Base): - __tablename__ = 'application_form' - __table_args__ = {'extend_existing': True} + __tablename__ = "application_form" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - event_id = db.Column(db.Integer(), db.ForeignKey('event.id'), nullable=False) + event_id = db.Column(db.Integer(), db.ForeignKey("event.id"), nullable=False) is_open = db.Column(db.Boolean(), nullable=False) deadline = db.Column(db.DateTime(), nullable=False) - def __init__(self, event_id, is_open, deadline): self.event_id = event_id self.is_open = is_open self.deadline = deadline - def upgrade(): Base.metadata.bind = op.get_bind() session = orm.Session(bind=Base.metadata.bind) # ### commands auto generated by Alembic - please adjust! ### # Update closing date for Indaba 2019 application form to 12 April 2019 - application_form = session.query(ApplicationForm).filter(ApplicationForm.event_id==1) - application_form.deadline = '12 April 2019' - application_form.description = 'This is the official application form to apply for participation in the Deep Learning Indaba to be held 25-31 August 2019 in Nairobi, Kenya. Students can also use this application form to apply for travel and accommodation awards. \n \n \n Closing date: 12 April 2019' + application_form = session.query(ApplicationForm).filter( + ApplicationForm.event_id == 1 + ) + application_form.deadline = "12 April 2019" + application_form.description = "This is the official application form to apply for participation in the Deep Learning Indaba to be held 25-31 August 2019 in Nairobi, Kenya. Students can also use this application form to apply for travel and accommodation awards. \n \n \n Closing date: 12 April 2019" # Update placeholder on "May we add your CV..." question to "Select an option..." (it currently says Select and option...) - question = session.query(Question).filter(Question.headline=='May we add your CV and email address to a database for sharing with our sponsors?').first() + question = ( + session.query(Question) + .filter( + Question.headline + == "May we add your CV and email address to a database for sharing with our sponsors?" + ) + .first() + ) question.placeholder = "Select an option" # Update "Academic" to "Academic Faculty" in user category - category = session.query(UserCategory).filter(UserCategory.name=='Academic').first() + category = ( + session.query(UserCategory).filter(UserCategory.name == "Academic").first() + ) if category is not None: - category.name = 'Academic Faculty' + category.name = "Academic Faculty" session.commit() session.flush() diff --git a/api/migrations/versions/f2cefa124236_.py b/api/migrations/versions/f2cefa124236_.py index afb6f33e1..f033fb0e6 100644 --- a/api/migrations/versions/f2cefa124236_.py +++ b/api/migrations/versions/f2cefa124236_.py @@ -7,24 +7,31 @@ """ # revision identifiers, used by Alembic. -revision = 'f2cefa124236' -down_revision = 'eac3fd9d5ae8' +revision = "f2cefa124236" +down_revision = "eac3fd9d5ae8" -from alembic import op import sqlalchemy as sa +from alembic import op from sqlalchemy.dialects import postgresql + def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.alter_column('invitation_letter_request', 'invitation_letter_sent_at', - existing_type=postgresql.TIMESTAMP(), - nullable=True) + op.alter_column( + "invitation_letter_request", + "invitation_letter_sent_at", + existing_type=postgresql.TIMESTAMP(), + nullable=True, + ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.alter_column('invitation_letter_request', 'invitation_letter_sent_at', - existing_type=postgresql.TIMESTAMP(), - nullable=False) + op.alter_column( + "invitation_letter_request", + "invitation_letter_sent_at", + existing_type=postgresql.TIMESTAMP(), + nullable=False, + ) # ### end Alembic commands ### diff --git a/api/migrations/versions/f349debf9e15_.py b/api/migrations/versions/f349debf9e15_.py index d801a9b02..7456d2418 100644 --- a/api/migrations/versions/f349debf9e15_.py +++ b/api/migrations/versions/f349debf9e15_.py @@ -7,20 +7,23 @@ """ # revision identifiers, used by Alembic. -revision = 'f349debf9e15' -down_revision = '88199ece9b26' +revision = "f349debf9e15" +down_revision = "88199ece9b26" -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.add_column('invitation_letter_request', sa.Column('passport_expiry_date', sa.DateTime(), nullable=False)) + op.add_column( + "invitation_letter_request", + sa.Column("passport_expiry_date", sa.DateTime(), nullable=False), + ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('invitation_letter_request', 'passport_expiry_date') + op.drop_column("invitation_letter_request", "passport_expiry_date") # ### end Alembic commands ### diff --git a/api/migrations/versions/f4bc3108c330_.py b/api/migrations/versions/f4bc3108c330_.py index 231df9361..355e89101 100644 --- a/api/migrations/versions/f4bc3108c330_.py +++ b/api/migrations/versions/f4bc3108c330_.py @@ -7,11 +7,12 @@ """ # revision identifiers, used by Alembic. -revision = 'f4bc3108c330' -down_revision = '2717ef90a874' +revision = "f4bc3108c330" +down_revision = "2717ef90a874" -from alembic import op import sqlalchemy as sa +from alembic import op + def upgrade(): ### commands auto generated by Alembic - please adjust! ### @@ -23,5 +24,3 @@ def downgrade(): ### commands auto generated by Alembic - please adjust! ### pass ### end Alembic commands ### - - \ No newline at end of file diff --git a/api/migrations/versions/f8ab033698a2_.py b/api/migrations/versions/f8ab033698a2_.py index 770792c16..e7007e096 100644 --- a/api/migrations/versions/f8ab033698a2_.py +++ b/api/migrations/versions/f8ab033698a2_.py @@ -7,22 +7,24 @@ """ # revision identifiers, used by Alembic. -revision = 'f8ab033698a2' -down_revision = '37fbee2da7b7' +revision = "f8ab033698a2" +down_revision = "37fbee2da7b7" -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.add_column('application_form', sa.Column('nominations', sa.Boolean(), nullable=True)) + op.add_column( + "application_form", sa.Column("nominations", sa.Boolean(), nullable=True) + ) op.execute("UPDATE application_form SET nominations = false") - op.alter_column('application_form', 'nominations', nullable=False) + op.alter_column("application_form", "nominations", nullable=False) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('application_form', 'nominations') + op.drop_column("application_form", "nominations") # ### end Alembic commands ### diff --git a/api/migrations/versions/fbd283484655_.py b/api/migrations/versions/fbd283484655_.py index 9fc5e7851..e14c1165f 100644 --- a/api/migrations/versions/fbd283484655_.py +++ b/api/migrations/versions/fbd283484655_.py @@ -7,24 +7,26 @@ """ # revision identifiers, used by Alembic. -revision = 'fbd283484655' -down_revision = '6fa47605c615' +revision = "fbd283484655" +down_revision = "6fa47605c615" -from alembic import op import sqlalchemy as sa +from alembic import op from sqlalchemy import orm -from app import db, bcrypt +from sqlalchemy.ext.declarative import DeferredReflection, declarative_base - -from sqlalchemy.ext.declarative import declarative_base, DeferredReflection +from app import bcrypt, db Base = declarative_base() + class Section(Base): - __tablename__ = 'section' - __table_args__ = {'extend_existing': True} + __tablename__ = "section" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - application_form_id = db.Column(db.Integer(), db.ForeignKey('application_form.id'), nullable=False) + application_form_id = db.Column( + db.Integer(), db.ForeignKey("application_form.id"), nullable=False + ) name = db.Column(db.String(255), unique=True, nullable=False) description = db.Column(db.String(255), nullable=False) order = db.Column(db.Integer(), unique=True, nullable=False) @@ -37,10 +39,10 @@ def __init__(self, application_form_id, name, description, order): class Question(Base): - __tablename__ = 'question' - __table_args__ = {'extend_existing': True} + __tablename__ = "question" + __table_args__ = {"extend_existing": True} id = db.Column(db.Integer(), primary_key=True) - section_id = db.Column(db.Integer(), db.ForeignKey('section.id'), nullable=False) + section_id = db.Column(db.Integer(), db.ForeignKey("section.id"), nullable=False) type = db.Column(db.String(), nullable=False) description = db.Column(db.String(), nullable=True) headline = db.Column(db.String(), nullable=False) @@ -50,7 +52,19 @@ class Question(Base): options = db.Column(db.JSON(), nullable=True) is_required = db.Column(db.Boolean(), nullable=False) - def __init__(self, application_form_id, section_id, headline, placeholder, order, questionType, validation_regex, is_required = True, description = None, options = None): + def __init__( + self, + application_form_id, + section_id, + headline, + placeholder, + order, + questionType, + validation_regex, + is_required=True, + description=None, + options=None, + ): self.section_id = section_id self.headline = headline self.placeholder = placeholder @@ -62,69 +76,78 @@ def __init__(self, application_form_id, section_id, headline, placeholder, order self.validation_regex = validation_regex - def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_index('unique_user_code', table_name='password_reset') - op.add_column('question', sa.Column('placeholder', sa.String(), nullable=True)) - op.add_column('question', sa.Column('validation_regex', sa.String(), nullable=True)) - op.create_unique_constraint('uq_section_order', 'section', ['order']) + op.drop_index("unique_user_code", table_name="password_reset") + op.add_column("question", sa.Column("placeholder", sa.String(), nullable=True)) + op.add_column("question", sa.Column("validation_regex", sa.String(), nullable=True)) + op.create_unique_constraint("uq_section_order", "section", ["order"]) update_question_data() # ### end Alembic commands ### + def update_question(session, question_id, placeholder, validation_regex): question = session.query(Question).filter(Question.id == question_id).first() question.placeholder = placeholder question.validation_regex = validation_regex + def update_question_data(): Base.metadata.bind = op.get_bind() session = orm.Session(bind=Base.metadata.bind) - update_question(session, 1, 'Enter 50 to 150 words', r'^\W*(\w+(\W+|$)){50,150}$') - update_question(session, 2, 'Enter 50 to 150 words', r'^\W*(\w+(\W+|$)){50,150}$') - update_question(session, 3, 'Enter up to 80 words, remember to include *why*', r'^\W*(\w+(\W+|$)){0,80}$') - update_question(session, 4, 'Enter up to 80 words', r'^\W*(\w+(\W+|$)){0,80}$') - update_question(session, 5, 'Enter up to 150 words', r'^\W*(\w+(\W+|$)){0,150}$') - update_question(session, 9, 'Enter up to 150 words', r'^\W*(\w+(\W+|$)){0,150}$') - update_question(session, 10, 'Departure Airport', None) - update_question(session, 18, 'Enter up to 150 words', r'^\W*(\w+(\W+|$)){0,150}$') + update_question(session, 1, "Enter 50 to 150 words", r"^\W*(\w+(\W+|$)){50,150}$") + update_question(session, 2, "Enter 50 to 150 words", r"^\W*(\w+(\W+|$)){50,150}$") + update_question( + session, + 3, + "Enter up to 80 words, remember to include *why*", + r"^\W*(\w+(\W+|$)){0,80}$", + ) + update_question(session, 4, "Enter up to 80 words", r"^\W*(\w+(\W+|$)){0,80}$") + update_question(session, 5, "Enter up to 150 words", r"^\W*(\w+(\W+|$)){0,150}$") + update_question(session, 9, "Enter up to 150 words", r"^\W*(\w+(\W+|$)){0,150}$") + update_question(session, 10, "Departure Airport", None) + update_question(session, 18, "Enter up to 150 words", r"^\W*(\w+(\W+|$)){0,150}$") cv_question = session.query(Question).filter(Question.id == 7).first() - cv_question.placeholder = 'Select and option...' + cv_question.placeholder = "Select and option..." cv_question.options = [ - {'value': 'yes', 'label': 'Yes'}, - {'value': 'no', 'label': 'No'} + {"value": "yes", "label": "Yes"}, + {"value": "no", "label": "No"}, ] travel_question = session.query(Question).filter(Question.id == 8).first() - travel_question.placeholder = 'Select an option...' + travel_question.placeholder = "Select an option..." travel_question.options = [ - {'value': 'transport-accommodation', 'label': 'Yes, transport and accommodation'}, - {'value': 'transport', 'label': 'Yes, transport only'}, - {'value': 'accommodation', 'label': 'Yes, accommodation only'}, - {'value': 'none', 'label': 'None'} + { + "value": "transport-accommodation", + "label": "Yes, transport and accommodation", + }, + {"value": "transport", "label": "Yes, transport only"}, + {"value": "accommodation", "label": "Yes, accommodation only"}, + {"value": "none", "label": "None"}, ] reward_question = session.query(Question).filter(Question.id == 11).first() - reward_question.placeholder = 'Select an option...' + reward_question.placeholder = "Select an option..." reward_question.options = [ - {'value': 'yes', 'label': 'Yes'}, - {'value': 'no', 'label': 'No'} + {"value": "yes", "label": "Yes"}, + {"value": "no", "label": "No"}, ] previous_question = session.query(Question).filter(Question.id == 15).first() - previous_question.placeholder = 'Select an option...' + previous_question.placeholder = "Select an option..." previous_question.options = [ - {'value': 'indaba-2017', 'label': 'Yes, I attended the 2017 Indaba'}, - {'value': 'indaba-2018', 'label': 'Yes, I attended the 2018 Indaba'}, - {'value': 'indaba-2017-2018', 'label': 'Yes, I attended both Indabas'}, - {'value': 'none', 'label': 'No'} + {"value": "indaba-2017", "label": "Yes, I attended the 2017 Indaba"}, + {"value": "indaba-2018", "label": "Yes, I attended the 2018 Indaba"}, + {"value": "indaba-2017-2018", "label": "Yes, I attended both Indabas"}, + {"value": "none", "label": "No"}, ] grow_question = session.query(Question).filter(Question.id == 16).first() - grow_question.placeholder = 'Enter up to 150 words' - grow_question.validation_regex = r'^\W*(\w+(\W+|$)){0,150}$' - grow_question.description = 'Maximum 150 words.' + grow_question.placeholder = "Enter up to 150 words" + grow_question.validation_regex = r"^\W*(\w+(\W+|$)){0,150}$" + grow_question.description = "Maximum 150 words." # Remove the demographic information section session.query(Question).filter(Question.section_id == 4).delete() @@ -136,8 +159,10 @@ def update_question_data(): def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_constraint('uq_section_order', 'section', type_='unique') - op.drop_column('question', 'validation_regex') - op.drop_column('question', 'placeholder') - op.create_index('unique_user_code', 'password_reset', ['user_id', 'code'], unique=True) + op.drop_constraint("uq_section_order", "section", type_="unique") + op.drop_column("question", "validation_regex") + op.drop_column("question", "placeholder") + op.create_index( + "unique_user_code", "password_reset", ["user_id", "code"], unique=True + ) # ### end Alembic commands ### diff --git a/api/migrations/versions/fc82c8e54fd7_emailtemplate_language_subject.py b/api/migrations/versions/fc82c8e54fd7_emailtemplate_language_subject.py index 91c39fef4..2756a33ca 100644 --- a/api/migrations/versions/fc82c8e54fd7_emailtemplate_language_subject.py +++ b/api/migrations/versions/fc82c8e54fd7_emailtemplate_language_subject.py @@ -7,36 +7,58 @@ """ # revision identifiers, used by Alembic. -revision = 'fc82c8e54fd7' -down_revision = '88cc4397bdf5' +revision = "fc82c8e54fd7" +down_revision = "88cc4397bdf5" -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.add_column('email_template', sa.Column('language', sa.String(length=2), nullable=True)) + op.add_column( + "email_template", sa.Column("language", sa.String(length=2), nullable=True) + ) op.execute("""UPDATE email_template SET language='en'""") - op.alter_column('email_template', 'language', nullable=False) - op.add_column('email_template', sa.Column('subject', sa.String(), nullable=True)) - - op.execute("""UPDATE email_template SET subject='FINAL REMINDER to submit you application for {event_name}' WHERE key='application-not-submitted'""") - op.execute("""UPDATE email_template SET subject='FINAL REMINDER: We do not have your application for {event_name}' WHERE key='application-not-started'""") - op.execute("""UPDATE email_template SET subject='Your application for {event_name}' WHERE key='confirmation-response'""") - op.execute("""UPDATE email_template SET subject='Withdrawal of Application for {event_name}' WHERE key='withdrawal'""") - op.execute("""UPDATE email_template SET subject='{event_name} Application Status Update' WHERE key='outcome-rejected'""") - - op.alter_column('email_template', 'subject', nullable=False) - op.drop_constraint('uq_email_template_key_event_id', 'email_template', type_='unique') - op.create_unique_constraint('uq_email_template_key_event_id', 'email_template', ['key', 'event_id', 'language']) + op.alter_column("email_template", "language", nullable=False) + op.add_column("email_template", sa.Column("subject", sa.String(), nullable=True)) + + op.execute( + """UPDATE email_template SET subject='FINAL REMINDER to submit you application for {event_name}' WHERE key='application-not-submitted'""" + ) + op.execute( + """UPDATE email_template SET subject='FINAL REMINDER: We do not have your application for {event_name}' WHERE key='application-not-started'""" + ) + op.execute( + """UPDATE email_template SET subject='Your application for {event_name}' WHERE key='confirmation-response'""" + ) + op.execute( + """UPDATE email_template SET subject='Withdrawal of Application for {event_name}' WHERE key='withdrawal'""" + ) + op.execute( + """UPDATE email_template SET subject='{event_name} Application Status Update' WHERE key='outcome-rejected'""" + ) + + op.alter_column("email_template", "subject", nullable=False) + op.drop_constraint( + "uq_email_template_key_event_id", "email_template", type_="unique" + ) + op.create_unique_constraint( + "uq_email_template_key_event_id", + "email_template", + ["key", "event_id", "language"], + ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_constraint('uq_email_template_key_event_id', 'email_template', type_='unique') - op.create_unique_constraint('uq_email_template_key_event_id', 'email_template', ['key', 'event_id']) - op.drop_column('email_template', 'subject') - op.drop_column('email_template', 'language') + op.drop_constraint( + "uq_email_template_key_event_id", "email_template", type_="unique" + ) + op.create_unique_constraint( + "uq_email_template_key_event_id", "email_template", ["key", "event_id"] + ) + op.drop_column("email_template", "subject") + op.drop_column("email_template", "language") # ### end Alembic commands ### diff --git a/api/run.py b/api/run.py index a18c872ab..b17a166ea 100644 --- a/api/run.py +++ b/api/run.py @@ -1,15 +1,17 @@ #!/usr/bin/env python -from flask_failsafe import failsafe import sys +from flask_failsafe import failsafe + @failsafe def create_app(): from app import manager + return manager # Remove encoding if upgrading to python 3. -if __name__ == '__main__': +if __name__ == "__main__": create_app().run() From 352089b28a72b9a2a722483b56052fd141b2b089 Mon Sep 17 00:00:00 2001 From: Amrit Purshotam Date: Thu, 17 Mar 2022 21:38:18 +0100 Subject: [PATCH 6/8] Add prettier formatting settings for webapp --- .circleci/config.yml | 5 ++++- .vscode/extensions.json | 3 ++- .vscode/settings.json | 20 +++++++++++++++++++- webapp/.prettierignore | 26 ++++++++++++++++++++++++++ webapp/.prettierrc.json | 3 +++ webapp/package.json | 4 +++- 6 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 webapp/.prettierignore create mode 100644 webapp/.prettierrc.json diff --git a/.circleci/config.yml b/.circleci/config.yml index 5f216c067..cb8d50c4b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -20,6 +20,9 @@ jobs: - run: name: Format with isort command: docker-compose run web isort --check --settings-path=api/setup.cfg api + - run: + name: Format with prettier + command: docker-compose run webapp yarn prettier-ci - run: name: Run migrations command: | @@ -28,7 +31,7 @@ jobs: docker-compose run web python3 api/run.py db upgrade --directory api/migrations - run: name: Run unit tests - command: docker-compose run web nosetests -v + command: docker-compose run web nosetests -v integration-tests: machine: diff --git a/.vscode/extensions.json b/.vscode/extensions.json index d2d1709fb..3ba0ec6ad 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,6 +1,7 @@ { "recommendations": [ "editorconfig.editorconfig", - "ms-python.python" + "ms-python.python", + "esbenp.prettier-vscode" ] } \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index a99b1761d..29c46224f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,5 +6,23 @@ }, "python.sortImports.args": [ "--settings-path=${workspaceFolder}/api/setup.cfg" - ] + ], + "[javascript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + }, + "[json]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + }, + "[css]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + }, + "[html]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + }, + "[jsx-tags]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + }, + "prettier.configPath": "./webapp/.prettierrc.json", + "prettier.ignorePath": "./webapp/.prettierignore", + "prettier.prettierPath": "./webapp/node_modules/prettier" } \ No newline at end of file diff --git a/webapp/.prettierignore b/webapp/.prettierignore new file mode 100644 index 000000000..ab09e01cf --- /dev/null +++ b/webapp/.prettierignore @@ -0,0 +1,26 @@ +# See https://prettier.io/docs/en/ignore.html for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Ignore md files +*.md \ No newline at end of file diff --git a/webapp/.prettierrc.json b/webapp/.prettierrc.json new file mode 100644 index 000000000..217acaf3b --- /dev/null +++ b/webapp/.prettierrc.json @@ -0,0 +1,3 @@ +{ + "endOfLine": "lf" +} \ No newline at end of file diff --git a/webapp/package.json b/webapp/package.json index 35640871b..8fad4baf0 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -50,7 +50,8 @@ "test": "react-scripts test", "eject": "react-scripts eject", "test-ci": "react-scripts test --env=jsdom --ci --coverage -u", - "cypress": "cypress open" + "cypress": "cypress open", + "prettier-ci": "prettier --check ." }, "eslintConfig": { "extends": "react-app" @@ -64,6 +65,7 @@ "devDependencies": { "@babel/runtime": "^7.12.5", "cypress": "^4.0.2", + "prettier": "2.6.0", "react-test-renderer": "^16.14.0" } } From 15bc335a53a68c1d4bbb2cdfd6bec0b2a5204372 Mon Sep 17 00:00:00 2001 From: Amrit Purshotam Date: Thu, 17 Mar 2022 21:56:44 +0100 Subject: [PATCH 7/8] Update lock file --- webapp/yarn.lock | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/webapp/yarn.lock b/webapp/yarn.lock index a96bb66dd..73db06e0f 100644 --- a/webapp/yarn.lock +++ b/webapp/yarn.lock @@ -9162,6 +9162,11 @@ preserve@^0.2.0: resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" integrity sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks= +prettier@2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.6.0.tgz#12f8f504c4d8ddb76475f441337542fa799207d4" + integrity sha512-m2FgJibYrBGGgQXNzfd0PuDGShJgRavjUoRCw1mZERIWVSXF0iLzLm+aOqTAbLnC3n6JzUhAA8uZnFVghHJ86A== + prettier@^1.14.2: version "1.16.4" resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.16.4.tgz#73e37e73e018ad2db9c76742e2647e21790c9717" From a2bb0a63c5b6cd1343c830b53dea9e53fd5cd9b7 Mon Sep 17 00:00:00 2001 From: Amrit Purshotam Date: Thu, 17 Mar 2022 22:16:42 +0100 Subject: [PATCH 8/8] Apply prettier formatting changes --- webapp/.prettierrc.json | 4 +- webapp/public/favicon/manifest.json | 80 +- webapp/public/index.html | 16 +- webapp/public/locales/en/translation.json | 844 +++---- webapp/public/locales/fr/translation.json | 844 ++++--- webapp/src/App.css | 53 +- webapp/src/App.js | 288 ++- webapp/src/History.js | 1 - webapp/src/__tests__/App.test.js | 10 +- webapp/src/components/ErrorPage.jsx | 18 +- webapp/src/components/EventStatus.js | 687 ++--- webapp/src/components/FileUpload.js | 84 +- webapp/src/components/Loading.js | 30 +- webapp/src/components/PrivateRoute.jsx | 23 +- webapp/src/components/User.js | 11 +- webapp/src/components/ViewFile.js | 54 +- .../src/components/answerValue/AnswerValue.js | 65 +- .../components/answerValue/MultiFileValue.js | 23 +- .../answerValue/__tests__/AnswerValue.test.js | 120 +- .../__tests__/MultiFileValue.test.js | 42 +- webapp/src/components/answerValue/index.js | 2 +- webapp/src/components/form/FormCheckbox.js | 13 +- webapp/src/components/form/FormCreator.js | 276 +- webapp/src/components/form/FormDate.js | 39 +- webapp/src/components/form/FormFileUpload.js | 105 +- webapp/src/components/form/FormGroup.js | 4 +- .../src/components/form/FormMultiCheckbox.js | 68 +- .../components/form/FormMultiCheckboxOther.js | 209 +- webapp/src/components/form/FormMultiFile.js | 306 ++- webapp/src/components/form/FormRadio.js | 72 +- webapp/src/components/form/FormSelect.js | 15 +- webapp/src/components/form/FormSelectOther.js | 195 +- webapp/src/components/form/FormTextArea.js | 34 +- webapp/src/components/form/FormTextBox.js | 27 +- webapp/src/components/form/FormToolTip.js | 5 +- .../src/components/form/MultiFileComponent.js | 319 ++- webapp/src/components/form/Style.css | 22 +- .../components/form/_tests_/FormDate.test.js | 29 +- .../form/_tests_/FormMultiFileTest.test.js | 97 +- webapp/src/components/index.js | 2 +- webapp/src/context/ContextProvider.js | 16 +- webapp/src/context/index.js | 2 +- webapp/src/i18n.js | 74 +- webapp/src/index.js | 26 +- .../src/pages/ResponseList/ResponseList.css | 273 +- webapp/src/pages/ResponseList/ResponseList.js | 14 +- .../components/ResponseListForm.js | 1064 ++++---- .../components/_tests_/ResponseList.test.js | 177 +- .../src/pages/ResponsePage/ResponsePage.css | 359 ++- webapp/src/pages/ResponsePage/ResponsePage.js | 1241 +++++---- .../pages/ResponsePage/ResponsePage.test.js | 179 +- .../ResponsePage/components/ReviewModal.js | 211 +- .../pages/ResponsePage/components/TagModal.js | 191 +- .../src/pages/applicationForm/Application.css | 15 +- .../src/pages/applicationForm/Application.js | 10 +- .../__tests__/Application.test.js | 14 +- .../components/ApplicationForm.js | 672 ++--- .../components/ReferenceRequest.js | 703 +++--- webapp/src/pages/applicationForm/index.js | 4 +- webapp/src/pages/attendance/Attendance.css | 2 +- .../attendance/components/AttendanceTable.js | 88 +- .../src/pages/createAccount/CreateAccount.css | 2 +- .../src/pages/createAccount/CreateAccount.js | 6 +- .../components/CreateAccountForm.js | 48 +- webapp/src/pages/createAccount/index.js | 4 +- .../src/pages/createApplicationForm/Form.css | 41 +- .../src/pages/createApplicationForm/Form.js | 10 +- .../__tests__/Form.test.js | 383 +-- .../components/ApplicationForm.js | 320 +-- .../components/Question.js | 2237 +++++++++-------- .../components/Section.js | 928 +++---- .../createApplicationForm/components/util.js | 852 ++++--- .../src/pages/createApplicationForm/index.js | 4 +- .../components/createInvitedGuestComponent.js | 62 +- .../createInvitedGuest/createInvitedGuest.js | 4 +- webapp/src/pages/createInvitedGuest/index.js | 2 +- webapp/src/pages/eventConfig/EventConfig.js | 4 +- .../components/EventConfigComponent.js | 201 +- webapp/src/pages/eventHome/EventHome.css | 4 +- webapp/src/pages/eventHome/EventHome.js | 57 +- .../eventHome/__tests__/EventHome.test.js | 10 +- webapp/src/pages/eventHome/index.js | 4 +- webapp/src/pages/eventStats/EventStats.css | 46 +- webapp/src/pages/eventStats/EventStats.js | 10 +- .../eventStats/__tests__/EventStats.test.js | 10 +- .../components/EventStatsComponent.js | 189 +- webapp/src/pages/eventStats/index.js | 2 +- webapp/src/pages/home/Home.css | 88 +- webapp/src/pages/home/Home.js | 242 +- webapp/src/pages/home/__tests__/Home.test.js | 10 +- webapp/src/pages/home/index.js | 4 +- .../invitationLetter/InvitationLetter.css | 15 +- .../invitationLetter/InvitationLetter.js | 6 +- .../invitationLetter/components/Address.js | 37 +- .../components/InvitationLetterForm.js | 56 +- webapp/src/pages/invitationLetter/index.js | 2 +- .../src/pages/invitedGuests/InvitedGuests.css | 119 +- .../src/pages/invitedGuests/InvitedGuests.js | 11 +- .../__tests__/InvitedGuest.test.js | 12 +- .../components/InvitedGuestComponent.js | 355 +-- webapp/src/pages/invitedGuests/index.js | 2 +- webapp/src/pages/login/Login.css | 30 +- webapp/src/pages/login/Login.js | 9 +- .../src/pages/login/__tests__/Login.test.js | 10 +- .../src/pages/login/components/LoginForm.js | 116 +- webapp/src/pages/offer/Offer.css | 89 +- webapp/src/pages/offer/Offer.js | 12 +- .../src/pages/offer/__tests__/Offer.test.js | 12 +- .../pages/offer/components/OfferComponent.js | 515 ++-- webapp/src/pages/profile/Profile.css | 2 +- webapp/src/pages/profile/Profile.js | 8 +- .../pages/profile/__tests__/Profile.test.js | 10 +- .../pages/profile/components/ProfileForm.js | 147 +- webapp/src/pages/profile/index.js | 4 +- webapp/src/pages/profileList/ProfileList.css | 84 +- webapp/src/pages/profileList/ProfileList.js | 14 +- .../profileList/__tests__/ProfileList.test.js | 16 +- .../components/ProfileListComponent.js | 199 +- webapp/src/pages/profileList/index.js | 2 +- webapp/src/pages/references/Reference.js | 14 +- .../references/__tests__/Reference.test.js | 16 +- .../components/ReferenceComponent.js | 298 ++- webapp/src/pages/references/index.js | 2 +- .../src/pages/registration/Registration.css | 22 +- webapp/src/pages/registration/Registration.js | 56 +- .../__tests__/Registration.test.js | 10 +- .../components/GuestRegistrationComponent.js | 382 +-- .../components/RegistrationComponent.js | 492 ++-- webapp/src/pages/registration/index.js | 4 +- .../registrationAdmin/RegistrationAdmin.js | 9 +- .../__tests__/RegistrationAdmin.test.js | 12 +- .../components/RegistrationAdminComponent.js | 174 +- webapp/src/pages/registrationAdmin/index.js | 2 +- .../src/pages/resetPassword/ResetPassword.css | 16 +- .../src/pages/resetPassword/ResetPassword.js | 56 +- .../__tests__/ResetPassword.test.js | 42 +- .../components/ConfirmPasswordResetForm.js | 69 +- .../components/RequestPasswordResetForm.js | 69 +- webapp/src/pages/review/Review.css | 90 +- webapp/src/pages/review/Review.js | 12 +- .../src/pages/review/__tests__/Review.test.js | 8 +- .../src/pages/review/components/ReviewForm.js | 1440 ++++++----- webapp/src/pages/review/index.js | 4 +- .../reviewAssignment/ReviewAssignment.css | 6 +- .../reviewAssignment/ReviewAssignment.js | 9 +- .../__tests__/ReviewAssignment.test.js | 10 +- .../components/ReviewAssignmentComponent.js | 234 +- webapp/src/pages/reviewAssignment/index.js | 2 +- .../pages/reviewDashboard/ReviewDashboard.css | 15 +- .../__tests__/ReviewDetails.test.js | 14 +- .../components/ReviewDashboard.js | 630 ++--- webapp/src/pages/reviewForm/Form.js | 10 +- .../pages/reviewForm/components/ReviewForm.js | 435 ++-- webapp/src/pages/reviewForm/index.js | 2 +- .../src/pages/reviewHistory/ReviewHistory.css | 2 +- .../src/pages/reviewHistory/ReviewHistory.js | 2 +- .../components/ReviewHistoryComponent.js | 53 +- webapp/src/pages/reviewList/ReviewList.css | 26 +- webapp/src/pages/reviewList/ReviewList.js | 12 +- .../components/ReviewListComponent.js | 389 +-- webapp/src/pages/reviewList/index.js | 4 +- webapp/src/pages/verifyEmail/VerifyEmail.css | 4 +- webapp/src/pages/verifyEmail/VerifyEmail.js | 9 +- .../verifyEmail/__tests__/VerifyEmail.test.js | 10 +- .../components/VerifyEmailComponent.js | 68 +- webapp/src/pages/verifyEmail/index.js | 2 +- webapp/src/pages/viewprofile/ViewProfile.css | 117 +- webapp/src/pages/viewprofile/ViewProfile.js | 11 +- .../viewprofile/__tests__/ViewProfile.test.js | 10 +- .../components/ViewProfileComponent.js | 196 +- webapp/src/pages/viewprofile/index.js | 2 +- webapp/src/serviceWorker.js | 40 +- .../applicationForm.service.js | 395 +-- webapp/src/services/applicationForm/index.js | 2 +- .../services/attendance/attendance.service.js | 34 +- webapp/src/services/base.service.js | 25 +- .../services/eventStats/eventStats.service.js | 122 +- webapp/src/services/eventStats/index.js | 2 +- webapp/src/services/events/events.service.js | 52 +- webapp/src/services/file/file.service.js | 57 +- webapp/src/services/file/index.js | 2 +- webapp/src/services/invitedGuests/index.js | 2 +- .../invitedGuests/invitedGuests.service.js | 78 +- webapp/src/services/offer/index.js | 2 +- webapp/src/services/offer/offer.service.js | 180 +- webapp/src/services/organisation/index.js | 2 +- .../organisation/organisation.service.js | 42 +- webapp/src/services/profilelist/index.js | 2 +- .../profilelist/profilelist.service.js | 68 +- webapp/src/services/references/index.js | 2 +- .../services/references/reference.service.js | 176 +- webapp/src/services/registration/index.js | 2 +- .../registration.admin.service.js | 24 +- .../registration/registration.service.js | 89 +- webapp/src/services/responses/index.js | 2 +- .../services/responses/responses.service.js | 232 +- webapp/src/services/reviews/index.js | 2 +- webapp/src/services/reviews/review.service.js | 328 ++- webapp/src/services/tags/index.js | 2 +- webapp/src/services/tags/tags.service.js | 226 +- webapp/src/services/user/index.js | 2 +- webapp/src/services/user/user.service.js | 90 +- webapp/src/setupTests.js | 6 +- webapp/src/utils/adminMenu.js | 38 +- webapp/src/utils/errorHandlerUtility.js | 4 +- webapp/src/utils/files.js | 28 +- webapp/src/utils/forms.js | 22 +- webapp/src/utils/user.js | 108 +- webapp/src/utils/validation/errorMessage.js | 23 +- webapp/src/utils/validation/ruleRunner.js | 24 +- webapp/src/utils/validation/rules.js | 67 +- .../src/utils/validation/validationFields.js | 18 +- 212 files changed, 14107 insertions(+), 12247 deletions(-) diff --git a/webapp/.prettierrc.json b/webapp/.prettierrc.json index 217acaf3b..a598fca41 100644 --- a/webapp/.prettierrc.json +++ b/webapp/.prettierrc.json @@ -1,3 +1,3 @@ { - "endOfLine": "lf" -} \ No newline at end of file + "endOfLine": "lf" +} diff --git a/webapp/public/favicon/manifest.json b/webapp/public/favicon/manifest.json index 013d4a6a5..3b557aab8 100644 --- a/webapp/public/favicon/manifest.json +++ b/webapp/public/favicon/manifest.json @@ -1,41 +1,41 @@ { - "name": "App", - "icons": [ - { - "src": "\/android-icon-36x36.png", - "sizes": "36x36", - "type": "image\/png", - "density": "0.75" - }, - { - "src": "\/android-icon-48x48.png", - "sizes": "48x48", - "type": "image\/png", - "density": "1.0" - }, - { - "src": "\/android-icon-72x72.png", - "sizes": "72x72", - "type": "image\/png", - "density": "1.5" - }, - { - "src": "\/android-icon-96x96.png", - "sizes": "96x96", - "type": "image\/png", - "density": "2.0" - }, - { - "src": "\/android-icon-144x144.png", - "sizes": "144x144", - "type": "image\/png", - "density": "3.0" - }, - { - "src": "\/android-icon-192x192.png", - "sizes": "192x192", - "type": "image\/png", - "density": "4.0" - } - ] -} \ No newline at end of file + "name": "App", + "icons": [ + { + "src": "/android-icon-36x36.png", + "sizes": "36x36", + "type": "image/png", + "density": "0.75" + }, + { + "src": "/android-icon-48x48.png", + "sizes": "48x48", + "type": "image/png", + "density": "1.0" + }, + { + "src": "/android-icon-72x72.png", + "sizes": "72x72", + "type": "image/png", + "density": "1.5" + }, + { + "src": "/android-icon-96x96.png", + "sizes": "96x96", + "type": "image/png", + "density": "2.0" + }, + { + "src": "/android-icon-144x144.png", + "sizes": "144x144", + "type": "image/png", + "density": "3.0" + }, + { + "src": "/android-icon-192x192.png", + "sizes": "192x192", + "type": "image/png", + "density": "4.0" + } + ] +} diff --git a/webapp/public/index.html b/webapp/public/index.html index a4df4ff6b..e225a91e6 100755 --- a/webapp/public/index.html +++ b/webapp/public/index.html @@ -73,7 +73,10 @@ href="%PUBLIC_URL%/favicon/favicon-16x16.png" /> - + - + ": "Go To Virtual Event >", - "Enjoy the Event": "Enjoy the Event", - "Now Live": "Now Live", - "Virtual Event Opening Soon!": "Virtual Event Opening Soon!", - "is opening soon - our virtual event site will be live on": "is opening soon - our virtual event site will be live on", - "Virtual Event Opens": "Virtual Event Opens", - "Starts": "Starts", - "Please try refresh the page and/or clear your cookies": "Please try refresh the page and/or clear your cookies", - "An answer is required.": "An answer is required.", - "Save for later": "Save for later", - "Saving": "Saving", - "Please fix the errors before continuing.": "Please fix the errors before continuing.", - "No answer provided.": "No answer provided.", - "Uploaded File": "Uploaded File", - "Review your Answers": "Review your Answers", - "applicationConfirmationText": "Please confirm that your answers are correct. Use the previous button to correct them if they are not. You can also exit and come back later as they have all been saved.\nClick the SUBMIT button once you are happy to submit your answers to the committee.", - "You MUST click SUBMIT before the deadline for your application to be considered!": "You MUST click SUBMIT before the deadline for your application to be considered!", - "Submit": "Submit", - "Thank you for applying!": "Thank you for applying!", - "Thank you for applying for": "Thank you for applying for", - "Your application will be reviewed by our committee and we will get back to you as soon as possible.": "Your application will be reviewed by our committee and we will get back to you as soon as possible.", - "You submitted your application on": "You submitted your application on", - "Withdraw Application": "Withdraw Application", - "Edit Application": "Edit Application", - "By continuing, your submitted application will go into draft state. You MUST press Submit again after you make your changes for your application to be considered in the selection.": "By continuing, your submitted application will go into draft state. You MUST press Submit again after you make your changes for your application to be considered in the selection.", - "Do you want to edit your application to": "Do you want to edit your application to", - "Yes - Withdraw": "Yes - Withdraw", - "No - Don't withdraw": "No - Don't withdraw", - "Yes - Edit application": "Yes - Edit application", - "No - Don't edit": "No - Don't edit", - "Step": "Step", - "Confirmation": "Confirmation", - "Saving Responses": "Saving Responses", - "Self Nomination": "Self Nomination", - "Submitted": "Submitted", - "View": "View", - "Your Nominations": "Your Nominations", - "Nominee": "Nominee", - "Status": "Status", - "New Nomination": "New Nomination", - "Title": "Title", - "Email": "Email", - "Relation": "Relation", - "Reference has been received.": "Reference has been received.", - "Email has been sent": "Email has been sent", - "ERROR": "ERROR", - "Add": "Add", - "Request References": "Request References", - "You must successfully request": "You must successfully request", - "references before continuing": "references before continuing", - "Email Address": "Email Address", - "First Name": "First Name", - "Last Name": "Last Name", - "Country of Nationality": "Country of Nationality", - "Country of Residence": "Country of Residence", - "Password": "Password", - "Confirm Password": "Confirm Password", - "Date of Birth": "Date of Birth", - "Role": "Role", - "Passport Number": "Passport Number", - "Full Name as it appears on Passport.": "Full Name as it appears on Passport.", - "Full Name": "Full Name", - "Passport Date of Expiry.": "Passport Date of Expiry.", - "Authority that issued Passport": "Authority that issued Passport", - "Work Street Address 1": "Work Street Address 1", - "Work Street Address 2": "Work Street Address 2", - "Work City": "Work City", - "Work Postal Code": "Work Postal Code", - "Work Country": "Work Country", - "Residential Street Address 1": "Residential Street Address 1", - "Residential Street Address 2": "Residential Street Address 2", - "Residential Postal Code": "Residential Postal Code", - "Residential Country": "Residential Country", - "Who the visa letter should be addressed to, e.g. Immigration Officer of X Embassy": "Who the visa letter should be addressed to, e.g. Immigration Officer of X Embassy", - "Addressed to ": "Addressed to ", - "Passwords do not match": "Passwords do not match", - "Sign Up": "Sign Up", - "Your": "Your", - "account has been created, but before you can use it, we need to verify your email address. Please check your email (and spam folder) for a message containing a link to verify your email address.": "account has been created, but before you can use it, we need to verify your email address. Please check your email (and spam folder) for a message containing a link to verify your email address.", - "if you already have an account": "if you already have an account", - "I am over 18": "I am over 18", - "I have read and agree to the ": "I have read and agree to the ", - "Create Guest": "Create Guest", - "An account with this email address already exists": "An account with this email address already exists", - "Organisation": "Organisation", - "Event Name": "Event Name", - "Description": "Description", - "Key": "Key", - "Email From": "Email From", - "URL": "URL", - "Start Date": "Start Date", - "End Date": "End Date", - "Application Open": "Application Open", - "Application Close": "Application Close", - "Review Open": "Review Open", - "Review Close": "Review Close", - "Selection Open": "Selection Open", - "Selection Close": "Selection Close", - "Offer Open": "Offer Open", - "Offer Close": "Offer Close", - "Registration Open": "Registration Open", - "Registration Close": "Registration Close", - "Cancel": "Cancel", - "Update Event": "Update Event", - "Open": "Open", - "Not Open": "Not Open", - "Closed": "Closed", - "Applications": "Applications", - "Un-submitted": "Un-submitted", - "Withdrawn": "Withdrawn", - "Completed": "Completed", - "Incomplete": "Incomplete", - "Not Allocated": "Not Allocated", - "Offers": "Offers", - "Offers Allocated": "Offers Allocated", - "Accepted": "Accepted", - "Rejected": "Rejected", - "Registrations": "Registrations", - "Total Guests": "Total Guests", - "Registered Guests": "Registered Guests", - "Welcome to": "Welcome to", - "for an account in order to apply for an event, award or call for proposals": "for an account in order to apply for an event, award or call for proposals", - "if you already have one": "if you already have one", - "Upcoming Events": "Upcoming Events", - "Awards": "Awards", - "Calls for Proposals": "Calls for Proposals", - "All": "All", - "Added": "Added", - "to the guest list": "to the guest list", - "Filter by name": "Filter by name", - "Filter by role": "Filter by role", - "No invited guests": "No invited guests", - "Download csv": "Download csv", - "Invited guest with this email already exists.": "Invited guest with this email already exists.", - "Add Guest": "Add Guest", - "User does not exist, please add these details": "User does not exist, please add these details", - "Create Invited Guest": "Create Invited Guest", - "We have re-sent your verification email, please check your inbox (and spam) and click on the link to verify your email address.": "We have re-sent your verification email, please check your inbox (and spam) and click on the link to verify your email address.", - "Sign in to your account": "Sign in to your account", - "Or": "Or", - "for a new one": "for a new one", - "Email address": "Email address", - "Forgot your password?": "Forgot your password?", - "Resend Verification Email": "Resend Verification Email", - "Your Profile": "Your Profile", - "Read-only": "Read-only", - "Save profile": "Save profile", - "Reset Your Password": "Reset Your Password", - "Reset Password": "Reset Password", - "Are you sure? Click 'Reset Password' to receive an email with a link to reset your password.": "Are you sure? Click 'Reset Password' to receive an email with a link to reset your password.", - "Type": "Type", - "There are currently no user profiles to display!": "There are currently no user profiles to display!", - "Total Profiles": "Total Profiles", - "No token specified.": "No token specified.", - "referencesClosed": "Unfortunately applications for {{eventName}} are now closed. If you still need to submit a reference for {candidateName}, please <5>contact the organisers", - "has been nominated by": "has been nominated by", - "has nominated themselves": "has nominated themselves", - "Reference": "Reference", - "Thank you! You submitted your reference on {{submittedTimestamp}}. You may update it below if you like.": "Thank you! You submitted your reference on {{submittedTimestamp}}. You may update it below if you like.", - "for": "for", - "Kindly upload your reference in support of their application in the form of a PDF (max 10Mb) below.": "Kindly upload your reference in support of their application in the form of a PDF (max 10Mb) below.", - "Upload Reference (PDF, 10Mb max)": "Upload Reference (PDF, 10Mb max)", - "Thank you! Your reference has been received and will be reviewed by our selection committee.": "Thank you! Your reference has been received and will be reviewed by our selection committee.", - "Submit reference": "Submit reference", - "Your password reset request has been processed. Please check your email for a link that will allow you to change your password.": "Your password reset request has been processed. Please check your email for a link that will allow you to change your password.", - "No Answer Provided": "No Answer Provided", - "View File": "View File", - "NO FILE UPLOADED": "NO FILE UPLOADED", - "An answer/rating is required.": "An answer/rating is required.", - "All Done!": "All Done!", - "You have completed all your reviews! Please let us know if you have any capacity for more": "You have completed all your reviews! Please let us know if you have any capacity for more", - "Thank you for your contribution!": "Thank you for your contribution!", - "Flag Response": "Flag Response", - "Response ID": "Response ID", - "Please quote this in any correspondence with admins outside of the system.": "Please quote this in any correspondence with admins outside of the system.", - "Go Back": "Go Back", - "Skip": "Skip", - "There are one or more validation errors, please correct before submitting.": "There are one or more validation errors, please correct before submitting.", - "reviewsRemaining": "You have {{reviewsRemainingCount}} reviews remaining", - "If reviewing this response revealed an issue that should be considered if this candidate were accepted, please describe it below.": "If reviewing this response revealed an issue that should be considered if this candidate were accepted, please describe it below.", - "Assign": "Assign", - "Name": "Name", - "No. Allocated": "No. Allocated", - "No. Completed": "No. Completed", - "No. to Assign": "No. to Assign", - "Total Unallocated Reviews:": "Total Unallocated Reviews:", - "Add new reviewer's email (they must already have an account)": "Add new reviewer's email (they must already have an account)", - "Review email": "Review email", - "Submitted Timestamp": "Submitted Timestamp", - "Verify Email Address": "Verify Email Address", - "verified": "Your email address has been verified. <1>Click here to login if you are not automatically redirected.", - "No user profile to display!": "No user profile to display!", - "Profile For": "Profile For", - "Personal Information": "Personal Information", - "User Application Info": "User Application Info", - "Submitted on": "Submitted on", - "Withdrawn on": "Withdrawn on", - "Add File": "Add File", - "Please enter a name": "Please enter a name", - "Upload File": "Upload File", - "Past Events": "Past Events", - "Virtual Event Over": "Virtual Event Over", - "is now over. You may still visit our virtual event site.": "is now over. You may still visit our virtual event site.", - "Thank you for participating!": "Thank you for attending!", - "Enter File Name": "Enter File Name", - "Edit Name": "Edit Name", - "Delete": "Delete", - "Response List": "Response List", - "Questions": "Questions", - "questions": "questions", - "Include un-submitted": "Include un-submitted", - "Update": "Update", - "Thank you for responding to the": "Thank you for responding to the", - "Note: You may need to change the file name to open the file on certain operating systems": "Note: You may need to change the file name to open the file on certain operating systems", - "Next": "Next", - "Previous": "Previous", - "Application Status": "Application Status", - "Reviewer": "Reviewer", - "Tags": "Tags", - "Yes": "Yes", - "Are you sure you want to delete this tag?": "Are you sure you want to delete this tag?", - "Save": "Save", - "Add Tags": "Add Tags", - "New tag": "New tag", - "Total Score": "Total Score", - "Post": "Post", - "Applications Status": "Applications Status", - "Are you sure you want to delete this reviewer?": "Are you sure you want to delete this reviewer?", - "No": "No", - "Assign Reviewer": "Assign Reviewer", - "Reviews Allocated": "Reviews Allocated", - "Reviews Completed": "Reviews Completed", - "Please fix validation errors before saving": "Please fix validation errors before saving", - "Saved": "Saved", - "You have {{numReviews}} reviews assigned, of which {{numCompleted}} are completed": "You have {{numReviews}} reviews assigned, of which {{numCompleted}} are completed", - "Edit": "Edit", - "Language": "Language", - "Action": "Action", - "You have completed all your reviews, thank you!": "You have completed all your reviews, thank you!", - "You have no reviews assigned": "You have no reviews assigned", - "Hide Completed": "Hide Completed", - "Return to review list": "Return to review list", - "Assign Reviewer (they must already have an account)": "Assign Reviewer (they must already have an account)", - "reviewsAssigned": "Assigned {{numReviewsAssigned}} reviews to {{assignedReviewerEmail}}", - "There are no more reviewers available": "There are no more reviewers available", - "Not Started": "Not Started", - "Form title": "Form title", - "Allow candidates to nominate others using this application form (Users will be able to submit multiple nominations, including for themselves. If this option is unchecked, a candidate can only apply for themselves)": "Allow candidates to nominate others using this application form (Users will be able to submit multiple nominations, including for themselves. If this option is unchecked, a candidate can only apply for themselves)", - "Application opens ": "Application opens ", - "Application closes ": "Application closes ", - "sectionPlace": "Section {{index}} of {{numSections}}", - "Add Section": "Add Section", - "Delete Section": "Delete Section", - "Duplicate Section": "Duplicate Section", - "Duplicate": "Duplicate", - "Add Question": "Add Question", - "Short Text": "Short Text", - "Long Text": "Long Text", - "Markdown": "Markdown", - "Single Choice": "Single Choice", - "Multi Choice": "Multi Choice", - "Multi Checkbox": "Multi Checkbox", - "File": "File", - "Date": "Date", - "Multi File": "Multi File", - "Choose type": "Choose type", - "Placeholder": "Placeholder", - "Value": "Value", - "Required": "Required", - "Are you sure you want to delete this question": "Are you sure you want to delete this question", - "Are you sure you want to delete this section": "Are you sure you want to delete this section", - "No - Don't delete": "No - Don't delete", - "Yes - Delete": "Yes - Delete", - "Move down": "Move down", - "Move up": "Move up", - "Move Section Down": "Move Section Down", - "Move Section Up": "Move Section Up", - "Review Dashboard": "Review Dashboard", - "Total": "Total", - "Candidate": "Candidate", - "Max and min Rererrals are required": "Max and min Rererrals are required", - "Max referral value must be greater than Min": "Max referral value must be greater than Min", - "At least one option is required": "At least one option is required", - "Label or Value cannot be empty": "Label or Value cannot be empty", - "Validation": "Validation", - "Question {{element}} is a dependency of other sections/questions. Are you sure you still want to delete it?": "Question {{element}} is a dependency of other sections/questions. Are you sure you still want to delete it?", - "Are you sure you want to delete this {{element}}": "Are you sure you want to delete this {{element}}?", - "Depends on question": "Depends on question", - "You may enter no more that {{max}} words": "You may enter no more that {{max}} words", - "You must enter at least {{min}} words": "You must enter at least {{min}} words", - "You must enter {{min}} to {{max}} words": "You must enter {{min}} to {{max}} words", - "Regular Expressions": "Regular Expressions", - "MIN": "MIN", - "MAX": "MAX", - "Enter Your Regex": "Enter Your Regex", - "Validation Text": "Validation Text", - "Word Limit": "Word Limit", - "Advanced Mode": "Advanced Mode", - "Please enter a list of file extensions, each starting with a period and separated with commas. E.g .csv,.xls": "Please enter a list of file extensions, each starting with a period and separated with commas. E.g .csv,.xls", - "Extensions (E.g .csv,.xls)": "Extensions (E.g .csv,.xls)", - "Enter Maximum Number of Referrals": "Enter Maximum Number of Referrals", - "Enter Minimum Number of Referrals": "Enter Minimum Number of Referrals", - "Maximum Number of Referrals": "Maximum Number of Referrals", - "Minimum Number of Referrals": "Minimum Number of Referrals", - "True": "True", - "False": "False", - "errorResponse": "Error: {{errorResponse}}", - "Type is Required": "Type is Required", - "Headline is Required": "Headline is Required", - "Headline": "Headline", - "Add Key": "Add Key", - "Application Form": "Application Form", - "English": "English", - "Allow candidates to nominate others using this application form(Users will be able to submit multiple nominations, including for themselves. If this option is unchecked, a candidate can only apply for themselves)": "Allow candidates to nominate others using this application form(Users will be able to submit multiple nominations, including for themselves. If this option is unchecked, a candidate can only apply for themselves)", - "Untitled Section": "Untitled Section", - "Form Settings": "Form Settings", - "Max": "Max", - "Min": "Min", - "Enter Mode": "Enter Mode", - "Label": "Label", - "": "", - " ": " ", - "Clear": "Clear", - "French": "French", - "Some Changes have not been saved. Are you sure you want to leave without saving them?": "Some Changes have not been saved. Are you sure you want to leave without saving them?", - "Details": "Details", - "Summary": "Summary", - "Stage": "Stage", - "Score Weight": "Score Weight", - "Review opens ": "Review opens ", - "Review closes ": "Review closes ", - "Review Form": "Review Form", - "Stages": "Stages", - "Add answer from application form": "Add answer from application form", - "Add New Stage": "Add New Stage", - "Select a question from Application Form": "Select a question from Application Form", - "Subheading": "Subheading", - "Numeric Input": "Numeric Input", - "Radio Buttons": "Radio Buttons", - "An application form is required to save the review form": "An application form is required to save the review form" + "Privacy Policy": "Privacy Policy", + "Powered by": "Powered by", + "cookieTitle": "This website stores cookies on your computer.", + "cookieText": "These allow us to remember who you are between pages and between visits and are used to collect information about how you interact with our website. We use this information in order to customize your experience and for analytics and metrics about our visitors. To find out more about the cookies we use, see our", + "Apply": "Apply", + "Offer": "Offer", + "Registration": "Registration", + "Registration Form": "Registration Form", + "Event Attendance": "Event Attendance", + "Event Admin": "Event Admin", + "Review Assignment": "Review Assignment", + "Invited Guests": "Invited Guests", + "Reviews": "Reviews", + "Review": "Review", + "Review History": "Review History", + "Registration Admin": "Registration Admin", + "Unconfirmed Registrations": "Unconfirmed Registrations", + "I understand": "I Understand", + "Uploaded file": "Uploaded file", + "Other": "Other", + "Other - Please Specify": "Other - Please Specify:", + "Word Count": "Word Count", + "Loading": "Loading", + "Profile": "Profile", + "Sign Out": "Sign Out", + "Sign In": "Sign In", + "Click here": "Click here", + "to download your file if it does not happen automatically": "to download your file if it does not happen automatically", + "Registered": "Registered", + "You have successfully registered for": "You have successfully registered for", + "we look forward to seeing you soon!": "we look forward to seeing you soon!", + "Pending Confirmation": "Pending Confirmation", + "Your registration is pending payment confirmation": "Your registration is pending payment confirmation", + "Registration Pending": "Registration Pending", + "Invited Guest": "Invited Guest", + "You have been invited as a": "You have been invited as a", + "please proceed to registration.": "please proceed to registration.", + "Register": "Register", + "Registration Closed": "Registration Closed", + "Registration is now closed.": "Registration is now closed.", + "Application Submitted": "Application Submitted", + "You have submitted your application, you may still edit it before the deadline.": "You have submitted your application, you may still edit it before the deadline.", + "View Application": "View Application", + "Application Withdrawn": "Application Withdrawn", + "Your application has been withdrawn, you will not be considered for": "Your application has been withdrawn, you will not be considered for", + "unless you re-submit by the deadline.": "unless you re-submit by the deadline.", + "Re-apply": "Re-apply", + "In Progress": "In Progress", + "You have not yet submitted your application, you must do so before the deadline to be considered": "You have not yet submitted your application, you must do so before the deadline to be considered", + "Continue": "Continue", + "Apply Now": "Apply Now", + "Start your application to attend": "Start your application to attend", + "Register Now": "Register Now", + "Please complete your event registration as soon as possible.": "Please complete your event registration as soon as possible.", + "Offer Rejected": "Offer Rejected", + "You have turned down your offer to attend": "You have turned down your offer to attend", + "We hope to see you at a future event!": "We hope to see you at a future event!", + "Offer Expired": "Offer Expired", + "Unfortunately your offer attend": "Unfortunately your offer attend", + "has expired.": "has expired.", + "Application Successful": "Application Successful", + "Congratulations! You have a pending offer to attend": "Congratulations! You have a pending offer to attend", + "Please accept or reject it before the expiry date.": "Please accept or reject it before the expiry date.", + "View Offer": "View Offer", + "Congratulations! You have been accepted to attend": "Congratulations! You have been accepted to attend", + "Further details will follow shortly!": "Further details will follow shortly!", + "Application Unsuccessful": "Application Unsuccessful", + "Unfortunately your application to attend": "Unfortunately your application to attend", + "was not successful. Please try again next time!": "was not successful. Please try again next time!", + "Waitlist": "Waitlist", + "You are on the waiting list for": "You are on the waiting list for", + "Please keep an eye out for further communication.": "Please keep an eye out for further communication.", + "Applied": "Applied", + "Your application is being reviewed. We will get back to you with a verdict as soon as possible.": "Your application is being reviewed. We will get back to you with a verdict as soon as possible.", + "You withdrew your application to": "You withdrew your application to", + "and applications are now closed. Your application will therefore not be considered": "and applications are now closed. Your application will therefore not be considered", + "Applications Closed": "Applications Closed", + "You started, but did not submit your application before the deadline. Your application will therefore not be considered": "You started, but did not submit your application before the deadline. Your application will therefore not be considered", + "You did not apply to": "You did not apply to", + "and applications are now closed.": "and applications are now closed.", + "Virtual Event Now Open": "Virtual Event Now Open", + "is now open! Please visit our virtual event site to attend.": "is now open! Please visit our virtual event site to attend.", + "Go To Virtual Event >": "Go To Virtual Event >", + "Enjoy the Event": "Enjoy the Event", + "Now Live": "Now Live", + "Virtual Event Opening Soon!": "Virtual Event Opening Soon!", + "is opening soon - our virtual event site will be live on": "is opening soon - our virtual event site will be live on", + "Virtual Event Opens": "Virtual Event Opens", + "Starts": "Starts", + "Please try refresh the page and/or clear your cookies": "Please try refresh the page and/or clear your cookies", + "An answer is required.": "An answer is required.", + "Save for later": "Save for later", + "Saving": "Saving", + "Please fix the errors before continuing.": "Please fix the errors before continuing.", + "No answer provided.": "No answer provided.", + "Uploaded File": "Uploaded File", + "Review your Answers": "Review your Answers", + "applicationConfirmationText": "Please confirm that your answers are correct. Use the previous button to correct them if they are not. You can also exit and come back later as they have all been saved.\nClick the SUBMIT button once you are happy to submit your answers to the committee.", + "You MUST click SUBMIT before the deadline for your application to be considered!": "You MUST click SUBMIT before the deadline for your application to be considered!", + "Submit": "Submit", + "Thank you for applying!": "Thank you for applying!", + "Thank you for applying for": "Thank you for applying for", + "Your application will be reviewed by our committee and we will get back to you as soon as possible.": "Your application will be reviewed by our committee and we will get back to you as soon as possible.", + "You submitted your application on": "You submitted your application on", + "Withdraw Application": "Withdraw Application", + "Edit Application": "Edit Application", + "By continuing, your submitted application will go into draft state. You MUST press Submit again after you make your changes for your application to be considered in the selection.": "By continuing, your submitted application will go into draft state. You MUST press Submit again after you make your changes for your application to be considered in the selection.", + "Do you want to edit your application to": "Do you want to edit your application to", + "Yes - Withdraw": "Yes - Withdraw", + "No - Don't withdraw": "No - Don't withdraw", + "Yes - Edit application": "Yes - Edit application", + "No - Don't edit": "No - Don't edit", + "Step": "Step", + "Confirmation": "Confirmation", + "Saving Responses": "Saving Responses", + "Self Nomination": "Self Nomination", + "Submitted": "Submitted", + "View": "View", + "Your Nominations": "Your Nominations", + "Nominee": "Nominee", + "Status": "Status", + "New Nomination": "New Nomination", + "Title": "Title", + "Email": "Email", + "Relation": "Relation", + "Reference has been received.": "Reference has been received.", + "Email has been sent": "Email has been sent", + "ERROR": "ERROR", + "Add": "Add", + "Request References": "Request References", + "You must successfully request": "You must successfully request", + "references before continuing": "references before continuing", + "Email Address": "Email Address", + "First Name": "First Name", + "Last Name": "Last Name", + "Country of Nationality": "Country of Nationality", + "Country of Residence": "Country of Residence", + "Password": "Password", + "Confirm Password": "Confirm Password", + "Date of Birth": "Date of Birth", + "Role": "Role", + "Passport Number": "Passport Number", + "Full Name as it appears on Passport.": "Full Name as it appears on Passport.", + "Full Name": "Full Name", + "Passport Date of Expiry.": "Passport Date of Expiry.", + "Authority that issued Passport": "Authority that issued Passport", + "Work Street Address 1": "Work Street Address 1", + "Work Street Address 2": "Work Street Address 2", + "Work City": "Work City", + "Work Postal Code": "Work Postal Code", + "Work Country": "Work Country", + "Residential Street Address 1": "Residential Street Address 1", + "Residential Street Address 2": "Residential Street Address 2", + "Residential Postal Code": "Residential Postal Code", + "Residential Country": "Residential Country", + "Who the visa letter should be addressed to, e.g. Immigration Officer of X Embassy": "Who the visa letter should be addressed to, e.g. Immigration Officer of X Embassy", + "Addressed to ": "Addressed to ", + "Passwords do not match": "Passwords do not match", + "Sign Up": "Sign Up", + "Your": "Your", + "account has been created, but before you can use it, we need to verify your email address. Please check your email (and spam folder) for a message containing a link to verify your email address.": "account has been created, but before you can use it, we need to verify your email address. Please check your email (and spam folder) for a message containing a link to verify your email address.", + "if you already have an account": "if you already have an account", + "I am over 18": "I am over 18", + "I have read and agree to the ": "I have read and agree to the ", + "Create Guest": "Create Guest", + "An account with this email address already exists": "An account with this email address already exists", + "Organisation": "Organisation", + "Event Name": "Event Name", + "Description": "Description", + "Key": "Key", + "Email From": "Email From", + "URL": "URL", + "Start Date": "Start Date", + "End Date": "End Date", + "Application Open": "Application Open", + "Application Close": "Application Close", + "Review Open": "Review Open", + "Review Close": "Review Close", + "Selection Open": "Selection Open", + "Selection Close": "Selection Close", + "Offer Open": "Offer Open", + "Offer Close": "Offer Close", + "Registration Open": "Registration Open", + "Registration Close": "Registration Close", + "Cancel": "Cancel", + "Update Event": "Update Event", + "Open": "Open", + "Not Open": "Not Open", + "Closed": "Closed", + "Applications": "Applications", + "Un-submitted": "Un-submitted", + "Withdrawn": "Withdrawn", + "Completed": "Completed", + "Incomplete": "Incomplete", + "Not Allocated": "Not Allocated", + "Offers": "Offers", + "Offers Allocated": "Offers Allocated", + "Accepted": "Accepted", + "Rejected": "Rejected", + "Registrations": "Registrations", + "Total Guests": "Total Guests", + "Registered Guests": "Registered Guests", + "Welcome to": "Welcome to", + "for an account in order to apply for an event, award or call for proposals": "for an account in order to apply for an event, award or call for proposals", + "if you already have one": "if you already have one", + "Upcoming Events": "Upcoming Events", + "Awards": "Awards", + "Calls for Proposals": "Calls for Proposals", + "All": "All", + "Added": "Added", + "to the guest list": "to the guest list", + "Filter by name": "Filter by name", + "Filter by role": "Filter by role", + "No invited guests": "No invited guests", + "Download csv": "Download csv", + "Invited guest with this email already exists.": "Invited guest with this email already exists.", + "Add Guest": "Add Guest", + "User does not exist, please add these details": "User does not exist, please add these details", + "Create Invited Guest": "Create Invited Guest", + "We have re-sent your verification email, please check your inbox (and spam) and click on the link to verify your email address.": "We have re-sent your verification email, please check your inbox (and spam) and click on the link to verify your email address.", + "Sign in to your account": "Sign in to your account", + "Or": "Or", + "for a new one": "for a new one", + "Email address": "Email address", + "Forgot your password?": "Forgot your password?", + "Resend Verification Email": "Resend Verification Email", + "Your Profile": "Your Profile", + "Read-only": "Read-only", + "Save profile": "Save profile", + "Reset Your Password": "Reset Your Password", + "Reset Password": "Reset Password", + "Are you sure? Click 'Reset Password' to receive an email with a link to reset your password.": "Are you sure? Click 'Reset Password' to receive an email with a link to reset your password.", + "Type": "Type", + "There are currently no user profiles to display!": "There are currently no user profiles to display!", + "Total Profiles": "Total Profiles", + "No token specified.": "No token specified.", + "referencesClosed": "Unfortunately applications for {{eventName}} are now closed. If you still need to submit a reference for {candidateName}, please <5>contact the organisers", + "has been nominated by": "has been nominated by", + "has nominated themselves": "has nominated themselves", + "Reference": "Reference", + "Thank you! You submitted your reference on {{submittedTimestamp}}. You may update it below if you like.": "Thank you! You submitted your reference on {{submittedTimestamp}}. You may update it below if you like.", + "for": "for", + "Kindly upload your reference in support of their application in the form of a PDF (max 10Mb) below.": "Kindly upload your reference in support of their application in the form of a PDF (max 10Mb) below.", + "Upload Reference (PDF, 10Mb max)": "Upload Reference (PDF, 10Mb max)", + "Thank you! Your reference has been received and will be reviewed by our selection committee.": "Thank you! Your reference has been received and will be reviewed by our selection committee.", + "Submit reference": "Submit reference", + "Your password reset request has been processed. Please check your email for a link that will allow you to change your password.": "Your password reset request has been processed. Please check your email for a link that will allow you to change your password.", + "No Answer Provided": "No Answer Provided", + "View File": "View File", + "NO FILE UPLOADED": "NO FILE UPLOADED", + "An answer/rating is required.": "An answer/rating is required.", + "All Done!": "All Done!", + "You have completed all your reviews! Please let us know if you have any capacity for more": "You have completed all your reviews! Please let us know if you have any capacity for more", + "Thank you for your contribution!": "Thank you for your contribution!", + "Flag Response": "Flag Response", + "Response ID": "Response ID", + "Please quote this in any correspondence with admins outside of the system.": "Please quote this in any correspondence with admins outside of the system.", + "Go Back": "Go Back", + "Skip": "Skip", + "There are one or more validation errors, please correct before submitting.": "There are one or more validation errors, please correct before submitting.", + "reviewsRemaining": "You have {{reviewsRemainingCount}} reviews remaining", + "If reviewing this response revealed an issue that should be considered if this candidate were accepted, please describe it below.": "If reviewing this response revealed an issue that should be considered if this candidate were accepted, please describe it below.", + "Assign": "Assign", + "Name": "Name", + "No. Allocated": "No. Allocated", + "No. Completed": "No. Completed", + "No. to Assign": "No. to Assign", + "Total Unallocated Reviews:": "Total Unallocated Reviews:", + "Add new reviewer's email (they must already have an account)": "Add new reviewer's email (they must already have an account)", + "Review email": "Review email", + "Submitted Timestamp": "Submitted Timestamp", + "Verify Email Address": "Verify Email Address", + "verified": "Your email address has been verified. <1>Click here to login if you are not automatically redirected.", + "No user profile to display!": "No user profile to display!", + "Profile For": "Profile For", + "Personal Information": "Personal Information", + "User Application Info": "User Application Info", + "Submitted on": "Submitted on", + "Withdrawn on": "Withdrawn on", + "Add File": "Add File", + "Please enter a name": "Please enter a name", + "Upload File": "Upload File", + "Past Events": "Past Events", + "Virtual Event Over": "Virtual Event Over", + "is now over. You may still visit our virtual event site.": "is now over. You may still visit our virtual event site.", + "Thank you for participating!": "Thank you for attending!", + "Enter File Name": "Enter File Name", + "Edit Name": "Edit Name", + "Delete": "Delete", + "Response List": "Response List", + "Questions": "Questions", + "questions": "questions", + "Include un-submitted": "Include un-submitted", + "Update": "Update", + "Thank you for responding to the": "Thank you for responding to the", + "Note: You may need to change the file name to open the file on certain operating systems": "Note: You may need to change the file name to open the file on certain operating systems", + "Next": "Next", + "Previous": "Previous", + "Application Status": "Application Status", + "Reviewer": "Reviewer", + "Tags": "Tags", + "Yes": "Yes", + "Are you sure you want to delete this tag?": "Are you sure you want to delete this tag?", + "Save": "Save", + "Add Tags": "Add Tags", + "New tag": "New tag", + "Total Score": "Total Score", + "Post": "Post", + "Applications Status": "Applications Status", + "Are you sure you want to delete this reviewer?": "Are you sure you want to delete this reviewer?", + "No": "No", + "Assign Reviewer": "Assign Reviewer", + "Reviews Allocated": "Reviews Allocated", + "Reviews Completed": "Reviews Completed", + "Please fix validation errors before saving": "Please fix validation errors before saving", + "Saved": "Saved", + "You have {{numReviews}} reviews assigned, of which {{numCompleted}} are completed": "You have {{numReviews}} reviews assigned, of which {{numCompleted}} are completed", + "Edit": "Edit", + "Language": "Language", + "Action": "Action", + "You have completed all your reviews, thank you!": "You have completed all your reviews, thank you!", + "You have no reviews assigned": "You have no reviews assigned", + "Hide Completed": "Hide Completed", + "Return to review list": "Return to review list", + "Assign Reviewer (they must already have an account)": "Assign Reviewer (they must already have an account)", + "reviewsAssigned": "Assigned {{numReviewsAssigned}} reviews to {{assignedReviewerEmail}}", + "There are no more reviewers available": "There are no more reviewers available", + "Not Started": "Not Started", + "Form title": "Form title", + "Allow candidates to nominate others using this application form (Users will be able to submit multiple nominations, including for themselves. If this option is unchecked, a candidate can only apply for themselves)": "Allow candidates to nominate others using this application form (Users will be able to submit multiple nominations, including for themselves. If this option is unchecked, a candidate can only apply for themselves)", + "Application opens ": "Application opens ", + "Application closes ": "Application closes ", + "sectionPlace": "Section {{index}} of {{numSections}}", + "Add Section": "Add Section", + "Delete Section": "Delete Section", + "Duplicate Section": "Duplicate Section", + "Duplicate": "Duplicate", + "Add Question": "Add Question", + "Short Text": "Short Text", + "Long Text": "Long Text", + "Markdown": "Markdown", + "Single Choice": "Single Choice", + "Multi Choice": "Multi Choice", + "Multi Checkbox": "Multi Checkbox", + "File": "File", + "Date": "Date", + "Multi File": "Multi File", + "Choose type": "Choose type", + "Placeholder": "Placeholder", + "Value": "Value", + "Required": "Required", + "Are you sure you want to delete this question": "Are you sure you want to delete this question", + "Are you sure you want to delete this section": "Are you sure you want to delete this section", + "No - Don't delete": "No - Don't delete", + "Yes - Delete": "Yes - Delete", + "Move down": "Move down", + "Move up": "Move up", + "Move Section Down": "Move Section Down", + "Move Section Up": "Move Section Up", + "Review Dashboard": "Review Dashboard", + "Total": "Total", + "Candidate": "Candidate", + "Max and min Rererrals are required": "Max and min Rererrals are required", + "Max referral value must be greater than Min": "Max referral value must be greater than Min", + "At least one option is required": "At least one option is required", + "Label or Value cannot be empty": "Label or Value cannot be empty", + "Validation": "Validation", + "Question {{element}} is a dependency of other sections/questions. Are you sure you still want to delete it?": "Question {{element}} is a dependency of other sections/questions. Are you sure you still want to delete it?", + "Are you sure you want to delete this {{element}}": "Are you sure you want to delete this {{element}}?", + "Depends on question": "Depends on question", + "You may enter no more that {{max}} words": "You may enter no more that {{max}} words", + "You must enter at least {{min}} words": "You must enter at least {{min}} words", + "You must enter {{min}} to {{max}} words": "You must enter {{min}} to {{max}} words", + "Regular Expressions": "Regular Expressions", + "MIN": "MIN", + "MAX": "MAX", + "Enter Your Regex": "Enter Your Regex", + "Validation Text": "Validation Text", + "Word Limit": "Word Limit", + "Advanced Mode": "Advanced Mode", + "Please enter a list of file extensions, each starting with a period and separated with commas. E.g .csv,.xls": "Please enter a list of file extensions, each starting with a period and separated with commas. E.g .csv,.xls", + "Extensions (E.g .csv,.xls)": "Extensions (E.g .csv,.xls)", + "Enter Maximum Number of Referrals": "Enter Maximum Number of Referrals", + "Enter Minimum Number of Referrals": "Enter Minimum Number of Referrals", + "Maximum Number of Referrals": "Maximum Number of Referrals", + "Minimum Number of Referrals": "Minimum Number of Referrals", + "True": "True", + "False": "False", + "errorResponse": "Error: {{errorResponse}}", + "Type is Required": "Type is Required", + "Headline is Required": "Headline is Required", + "Headline": "Headline", + "Add Key": "Add Key", + "Application Form": "Application Form", + "English": "English", + "Allow candidates to nominate others using this application form(Users will be able to submit multiple nominations, including for themselves. If this option is unchecked, a candidate can only apply for themselves)": "Allow candidates to nominate others using this application form(Users will be able to submit multiple nominations, including for themselves. If this option is unchecked, a candidate can only apply for themselves)", + "Untitled Section": "Untitled Section", + "Form Settings": "Form Settings", + "Max": "Max", + "Min": "Min", + "Enter Mode": "Enter Mode", + "Label": "Label", + "": "", + " ": " ", + "Clear": "Clear", + "French": "French", + "Some Changes have not been saved. Are you sure you want to leave without saving them?": "Some Changes have not been saved. Are you sure you want to leave without saving them?", + "Details": "Details", + "Summary": "Summary", + "Stage": "Stage", + "Score Weight": "Score Weight", + "Review opens ": "Review opens ", + "Review closes ": "Review closes ", + "Review Form": "Review Form", + "Stages": "Stages", + "Add answer from application form": "Add answer from application form", + "Add New Stage": "Add New Stage", + "Select a question from Application Form": "Select a question from Application Form", + "Subheading": "Subheading", + "Numeric Input": "Numeric Input", + "Radio Buttons": "Radio Buttons", + "An application form is required to save the review form": "An application form is required to save the review form" } diff --git a/webapp/public/locales/fr/translation.json b/webapp/public/locales/fr/translation.json index 77aaf233f..a9be07584 100644 --- a/webapp/public/locales/fr/translation.json +++ b/webapp/public/locales/fr/translation.json @@ -1,425 +1,423 @@ { - "Privacy Policy": "Politique sur la protection de la vie privée", - "Powered by": "Propulsé par", - "cookieTitle": "Ce site web conserve des témoins de connexion (« cookies ») sur votre ordinateur.", - "cookieText": "Ceux-ci nous permettent de nous rappeler votre identité entre les pages et entre les visites et servent à recueillir des données sur la façon dont vous interagissez avec notre site web. Nous utilisons ces informations afin de personnaliser votre expérience et pour des analyses et des mesures concernant nos visiteurs. Pour en savoir plus sur les témoins que nous utilisons, consultez notre", - "Apply": "Demander", - "Offer": "Offre", - "Registration": "Inscription", - "Registration Form": "Formulaire d’inscription", - "Event Attendance": "Participation à un événement", - "Event Admin": "Administration d’un événement", - "Review Assignment": "Mission d’examen", - "Invited Guests": "Invités", - "Reviews": "Examens", - "Review": "Examen", - "Review History": "Historique d’examen", - "Registration Admin": "Administration des inscriptions", - "Unconfirmed Registrations": "Inscriptions non confirmées", - "I understand": "Je comprends", - "Uploaded file": "Fichier téléchargé", - "Other": "Autres", - "Other - Please Specify": "Autre, veuillez préciser :", - "Word Count": "Nombre de mots", - "Loading": "Chargement", - "Profile": "Profil", - "Sign Out": "Déconnexion", - "Sign In": "Connexion", - "Click here": "Cliquez ici", - "to download your file if it does not happen automatically": "pour télécharger votre fichier si cela ne se fait pas automatiquement", - "Registered": "Inscrit", - "You have successfully registered for": "Vous vous êtes inscrit avec succès à", - "we look forward to seeing you soon!": "Au plaisir de vous y voir bientôt.", - "Pending Confirmation": "En attente de confirmation", - "Your registration is pending payment confirmation": "Votre inscription est en attente de la confirmation du paiement", - "Registration Pending": "Inscription en cours", - "Invited Guest": "Invité", - "You have been invited as a": "Vous avez été invité en tant que", - "please proceed to registration.": "Veuillez procéder à l’inscription.", - "Register": "Inscription", - "Registration Closed": "Inscription terminée", - "Registration is now closed.": "L’inscription est maintenant terminée.", - "Application Submitted": "Demande envoyée", - "You have submitted your application, you may still edit it before the deadline.": "Vous avez soumis votre demande, vous pouvez encore la modifier avant la date limite.", - "View Application": "Voir la demande", - "Application Withdrawn": "Demande retirée", - "Your application has been withdrawn, you will not be considered for a place at": "Votre demande a été retirée, vous ne serez pas pris en considération pour", - "unless you re-submit by the deadline.": "à moins que vous ne la présentiez à nouveau avant la date limite.", - "Re-apply": "Renouveler la demande", - "In Progress": "En cours", - "You have not yet submitted your application, you must do so before the deadline to be considered": "Vous n’avez pas encore soumis votre demande, vous devez le faire avant la date limite pour être pris en considération", - "Continue": "Poursuivre", - "Apply Now": "Faire une demande maintenant", - "Start your application to attend": "Commencez votre demande de participation", - "Register Now": "Réservez votre place dès maintenant", - "Please complete your event registration as soon as possible.": "Veuillez compléter votre inscription à l’événement dès que possible.", - "Offer Rejected": "Offre rejetée", - "You have turned down your offer to attend": "Vous avez décliné votre offre de participation", - "We hope to see you at a future event!": "Nous espérons que vous serez des nôtres lors d’un prochain événement.", - "Offer Expired": "Offre expirée", - "Unfortunately your offer attend": "Malheureusement, votre offre de participation", - "has expired.": "a expiré.", - "Application Successful": "Demande retenue", - "Congratulations! You have a pending offer to attend": "Félicitations ! Vous avez une offre de participation en cours", - "Please accept or reject it before the expiry date.": "Veuillez l’accepter ou la refuser avant la date d’expiration.", - "View Offer": "Voir l’offre", - "Congratulations! You have been accepted to attend": "Félicitations ! Vous avez été accepté pour participer", - "Further details will follow shortly!": "Des détails à ce sujet suivront sous peu.", - "Application Unsuccessful": "Demande non retenue", - "Unfortunately your application to attend": "Malheureusement, votre demande de participation", - "was not successful. Please try again next time!": "n’a pas été retenue. Veuillez réessayer la prochaine fois.", - "Waitlist": "Liste d’attente", - "You are on the waiting list for": "Vous êtes sur la liste d’attente pour", - "Please keep an eye out for further communication.": "Veuillez rester attentif aux communications futures.", - "Applied": "Demande effectuée", - "Your application is being reviewed. We will get back to you with a verdict as soon as possible.": "Votre demande est en cours d’examen. Une décision vous sera communiquée le plus tôt possible.", - "You withdrew your application to": "Vous avez retiré votre demande à", - "and applications are now closed. Your application will therefore not be considered": "et les demandes sont désormais terminées. Votre demande ne sera donc pas prise en considération.", - "Applications Closed": "Demandes terminées", - "You started, but did not submit your application before the deadline. Your application will therefore not be considered": "Vous avez commencé, mais n’avez pas soumis votre demande avant la date limite. Votre demande ne sera donc pas prise en considération.", - "You did not apply to": "Vous n’avez pas fait de demande de participation", - "and applications are now closed.": "et les demandes sont désormais terminées.", - "Virtual Event Now Open": "L’événement virtuel est maintenant ouvert", - "is now open! Please visit our virtual event site to attend.": "est maintenant ouvert. Pour y assister, veuillez visiter notre site d’événements virtuels.", - "Go To Virtual Event >": "Aller à l’événement virtuel >", - "Enjoy the Event": "Profitez bien de l’événement", - "Now Live": "Maintenant en direct", - "Virtual Event Opening Soon!": "Ouverture prochaine de l’événement virtuel.", - "is opening soon - our virtual event site will be live on": "commence bientôt - notre site virtuel de l’événement sera en direct sur", - "Virtual Event Opens": "Ouverture d’un événement virtuel", - "Starts": "débute", - "Please try refresh the page and/or clear your cookies": "Veuillez essayer de rafraîchir la page et/ou d’effacer vos témoins (« cookies »)", - "An answer is required.": "Une réponse est requise.", - "Save for later": "Enregistrer pour plus tard", - "Saving": "Enregistrement", - "Please fix the errors before continuing.": "Veuillez corriger les erreurs avant de continuer.", - "No answer provided.": "Aucune réponse n’a été fournie.", - "Uploaded File": "Fichier téléchargé", - "Review your Answers": "Révisez vos réponses", - "applicationConfirmationText": "Veuillez confirmer que vos réponses sont correctes. Utilisez le bouton précédent pour les corriger si elles ne le sont pas. Vous pouvez également sortir et revenir plus tard, car les données ont été enregistrées. Cliquez sur le bouton SOUMETTRE une fois que vous êtes prêt à soumettre vos réponses au comité.", - "You MUST click SUBMIT before the deadline for your application to be considered!": "Vous DEVEZ cliquer sur SOUMETTRE avant la date limite pour que votre demande soit prise en compte.", - "Submit": "Soumettre", - "Thank you for applying!": "Merci d’avoir soumis votre demande.", - "Thank you for applying for": "Merci d’avoir soumis votre demande", - "Your application will be reviewed by our committee and we will get back to you as soon as possible.": "Votre demande sera examinée par notre comité et nous vous répondrons dès que possible.", - "You submitted your application on": "Vous avez soumis votre demande", - "Withdraw Application": "Retirer la demande", - "Edit Application": "Modifier la demande", - "By continuing, your submitted application will go into draft state. You MUST press Submit again after you make your changes for your application to be considered in the selection.": "En continuant, votre demande soumise passera à l’état d’ébauche. Vous DEVEZ appuyer à nouveau sur « Soumettre » après avoir apporté les modifications pour que votre demande soit prise en compte dans la sélection.", - "Do you want to edit your application to": "Vous souhaitez modifier votre demande", - "Yes - Withdraw": "Oui - Retrait", - "No - Don't withdraw": "Non - Ne pas retirer", - "Yes - Edit application": "Oui - Modifier la demande", - "No - Don't edit": "Non - Ne pas modifier", - "Step": "Étape", - "Confirmation": "Confirmation", - "Saving Responses": "Enregistrement des réponses", - "Self Nomination": "Autonomination", - "Submitted": "Soumis", - "View": "Afficher", - "Your Nominations": "Vos nominations", - "Nominee": "Candidat(e)", - "Status": "État", - "New Nomination": "Nouvelle nomination", - "Title": "Titre", - "Email": "Courriel", - "Relation": "Relation", - "Reference has been received.": "La référence a été reçue.", - "Email has been sent": "Un courrier électronique a été envoyé", - "ERROR": "ERREUR", - "Add": "Ajout", - "Request References": "Demander des références", - "You must successfully request": "Vous devez demander avec succès", - "references before continuing": "références avant de continuer", - "Email Address": "Adresse électronique", - "First Name": "Prénom", - "Last Name": "Nom de famille", - "Country of Nationality": "Pays de nationalité", - "Country of Residence": "Pays de résidence", - "Password": "Mot de passe", - "Confirm Password": "Confirmer le mot de passe", - "Date of Birth": "Date de naissance", - "Role": "Rôle", - "Passport Number": "Numéro du passeport", - "Full Name as it appears on Passport.": "Nom complet tel qu’il apparaît sur le passeport", - "Full Name": "Nom au complet", - "Passport Date of Expiry.": "Date d’expiration du passeport.", - "Authority that issued Passport": "Autorité qui a délivré le passeport", - "Work Street Address 1": "Adresse postale 1 (travail)", - "Work Street Address 2": "Adresse postale 2 (travail)", - "Work City": "Ville (travail)", - "Work Postal Code": "Code postal (travail)", - "Work Country": "Pays (travail)", - "Residential Street Address 1": "Adresse postale résidentielle 1", - "Residential Street Address 2": "Adresse postale résidentielle 2", - "Residential Postal Code": "Code postal résidentiel", - "Residential Country": "Pays de résidence", - "Who the visa letter should be addressed to, e.g. Immigration Officer of X Embassy": "À qui la lettre de visa doit-elle être adressée, par exemple à l’agent d’immigration de l’ambassade X", - "Addressed to ": "Adressée à ", - "Passwords do not match": "Les mots de passe ne correspondent pas", - "Sign Up": "S’inscrire", - "Your": "Votre", - "account has been created, but before you can use it, we need to verify your email address. Please check your email (and spam folder) for a message containing a link to verify your email address.": "a été créé, mais avant que vous puissiez l’utiliser, nous devons vérifier votre adresse électronique. Veuillez vérifier votre courriel (y compris le dossier de courrier indésirable) pour trouver un message contenant un lien permettant de vérifier votre adresse électronique.", - "if you already have an account": "Si vous possédez déjà un compte", - "I am over 18": "J’ai plus de 18 ans", - "I have read and agree to the ": "J’ai lu et j’accepte ", - "Create Guest": "Créer un compte invité", - "An account with this email address already exists": "Un compte avec cette adresse électronique existe déjà", - "Organisation": "Organisation", - "Event Name": "Nom de l’événement", - "Description": "Description", - "Key": "Clé", - "Email From": "Courriel de", - "URL": "Adresse URL", - "Start Date": "Date de début", - "End Date": "Date de fin", - "Application Open": "Ouverture de la demande", - "Application Close": "Fermeture de la demande", - "Review Open": "Ouverture de l’examen", - "Review Close": "Fermeture de l’examen", - "Selection Open": "Ouverture de la sélection", - "Selection Close": "Fermeture de la sélection", - "Offer Open": "Ouverture de l’offre", - "Offer Close": "Fermeture de l’offre", - "Registration Open": "Inscription ouverte", - "Registration Close": "Inscription fermée", - "Cancel": "Annuler", - "Update Event": "Mise à jour de l’événement", - "Open": "En cours", - "Not Open": "Non ouvert", - "Closed": "Fermé", - "Applications": "Demandes", - "Un-submitted": "Non soumises", - "Withdrawn": "Retirées", - "Completed": "Terminés", - "Incomplete": "Incomplètes", - "Not Allocated": "Non attribuées", - "Offers": "Offres", - "Offers Allocated": "Offres attribuées", - "Accepted": "Acceptées", - "Rejected": "Rejetées", - "Registrations": "Inscriptions", - "Total Guests": "Nombre total d’invités", - "Registered Guests": "Invités inscrits", - "Welcome to": "Bienvenue à", - "for an account in order to apply for an event, award or call for proposals": "d’un compte afin de faire une demande de participation à un événement, un prix ou un appel à propositions", - "if you already have one": "Si vous en avez déjà un", - "Upcoming Events": "Événements à venir", - "Awards": "Prix", - "Calls for Proposals": "Appels de propositions", - "All": "Tous", - "Added": "Ajouté", - "to the guest list": "à la liste des invités", - "Filter by name": "Filtrer par nom", - "Filter by role": "Filtrer par rôle", - "No invited guests": "Nombre d’invités", - "Download csv": "Télécharger le csv", - "Invited guest with this email already exists.": "L’invité avec ce courriel existe déjà.", - "Add Guest": "Ajouter un invité", - "User does not exist, please add these details": "L’utilisateur n’existe pas, veuillez ajouter ces détails", - "Create Invited Guest": "Créer un invité", - "We have re-sent your verification email, please check your inbox (and spam) and click on the link to verify your email address.": "Nous avons envoyé à nouveau votre courriel de vérification, veuillez vérifier votre boîte de réception (et le dossier de courrier indésirable) et cliquez sur le lien pour vérifier votre adresse courriel.", - "Sign in to your account": "Connectez-vous à votre compte.", - "Or": "Ou", - "for a new one": "Pour un nouveau", - "Email address": "Adresse électronique", - "Forgot your password?": "Vous avez oublié votre mot de passe ?", - "Resend Verification Email": "Renvoyer un courriel de vérification", - "Your Profile": "Votre profil", - "Read-only": "En lecture seulement", - "Save profile": "Enregistrer le profil", - "Reset Your Password": "Réinitialiser votre mot de passe", - "Reset Password": "Réinitialisation du mot de passe", - "Are you sure? Click 'Reset Password' to receive an email with a link to reset your password.": "Êtes-vous sûr ? Cliquez sur « Réinitialiser le mot de passe » pour recevoir un courriel avec un lien pour réinitialiser votre mot de passe.", - "Type": "Type", - "There are currently no user profiles to display!": "Il n’y a aucun profil d’utilisateur à afficher pour le moment.", - "Total Profiles": "Profils totaux", - "No token specified.": "Aucun jeton n’est spécifié.", - "referencesClosed": "Malheureusement, les demandes pour {{eventName}} sont désormais terminées. Si vous devez encore soumettre une référence pour {candidateName}, veuillez <5>communiquer avec les organisateurs", - "has been nominated by": "a été nommé par", - "has nominated themselves": "s’est auto-nominé", - "Reference": "Références", - "Thank you! You submitted your reference on {{submittedTimestamp}}. You may update it below if you like.": "Merci. Vous avez soumis votre référence le {{submittedTimestamp}}. Vous pouvez la mettre à jour ci-dessous si vous le souhaitez.", - "for": "pour", - "Kindly upload your reference in support of their application in the form of a PDF (max 10Mb) below.": "Veuillez télécharger votre référence à l’appui de leur demande en format PDF (max 10 Mo) ci-dessous.", - "Upload Reference (PDF, 10Mb max)": "Télécharger la référence (PDF, 10 Mo max)", - "Thank you! Your reference has been received and will be reviewed by our selection committee.": "Merci. Votre référence a été reçue et sera examinée par notre comité de sélection.", - "Submit reference": "Soumettre une référence", - "Your password reset request has been processed. Please check your email for a link that will allow you to change your password.": "Votre demande de réinitialisation de mot de passe a été traitée. Veuillez consulter votre courriel pour trouver un lien qui vous permettra de changer votre mot de passe.", - "No Answer Provided": "Pas de réponse fournie", - "View File": "Voir le fichier", - "NO FILE UPLOADED": "AUCUN FICHIER TÉLÉCHARGÉ", - "An answer/rating is required.": "Une réponse/note est requise.", - "All Done!": "Vous avez terminé.", - "You have completed all your reviews! Please let us know if you have any capacity for more": "Vous avez terminé tous vos examens. Si vous avez d’autres disponibilités, n’hésitez pas à communiquer avec nous.", - "Thank you for your contribution!": "Je vous remercie de votre participation.", - "Flag Response": "Signaler une réponse", - "Response ID": "Numéro d’identification de la réponse", - "Please quote this in any correspondence with admins outside of the system.": "Veuillez le citer dans toute correspondance avec les administrateurs en dehors du système.", - "Go Back": "Retour en arrière", - "Skip": "Passer", - "There are one or more validation errors, please correct before submitting.": "Il y a une ou plusieurs erreurs de validation, veuillez les corriger avant de soumettre.", - "reviewsRemaining": "Il vous reste {{reviewsRemainingCount}} dossiers à examiner", - "If reviewing this response revealed an issue that should be considered if this candidate were accepted, please describe it below.": "Si l’examen de cette réponse a révélé un problème qui devrait être pris en compte si ce candidat était accepté, veuillez le décrire ci-dessous.", - "Assign": "Attribuer", - "Name": "Nom", - "No. Allocated": "Nombre attribué", - "No. Completed": "Nombre terminé", - "No. to Assign": "Nombre à attribuer", - "Total Unallocated Reviews:": "Total des examens non attribués :", - "Add new reviewer's email (they must already have an account)": "Ajouter le courriel du nouvel examinateur (il doit déjà avoir un compte)", - "Review email": "Vérifier le courrier électronique", - "Submitted Timestamp": "Horodatage soumis", - "Verify Email Address": "Vérifier l’adresse électronique", - "verified": "Votre adresse électronique a été vérifiée. <1>Cliquez ici pour vous connecter si vous n’êtes pas automatiquement redirigé.", - "No user profile to display!": "Pas de profil d’utilisateur à afficher!", - "Profile For": "Profil pour", - "Personal Information": "Renseignements personnels", - "User Application Info": "Informations sur la demande de l’utilisateur", - "Submitted on": "Soumise le ", - "Withdrawn on": "Retirée le", - "Add File": "Ajouter le fichier", - "Please enter a name": "Veuillez saisir un nom", - "Upload File": "Téléverser un fichier", - "Past Events": "Past Events", - "Virtual Event Over": "Événement virtuel terminé", - "is now over. You may still visit our virtual event site.": "est maintenant terminé. Vous pouvez toujours visiter notre site d'événements virtuels.", - "Thank you for participating!": "Merci d'avoir participé!", - "Enter File Name": "Entrez le nom du fichier", - "Edit Name": "Modifier le nom", - "Delete": "Supprimer", - "Response List": "Liste de réponse", - "Questions": "Des questions", - "questions": "des questions", - "Include un-submitted": "Inclure les non-soumis", - "Update": "Mettre à jour", - "Thank you for responding to the": "Merci d’avoir soumis votre demande", - "Note: You may need to change the file name to open the file on certain operating systems": "Remarque: vous devrez peut-être changer le nom du fichier pour ouvrir le fichier sur certains systèmes d'exploitation", - "Next": "Continuer", - "Previous": "Retourner", - "Application Status": "État de la candidature", - "Reviewer": "Critique", - "Tags": "Mots clés", - "Yes": "Oui", - "Are you sure you want to delete this tag?": "Voulez-vous vraiment supprimer ce tag?", - "Save": "Enregistrer", - "Add Tags": "Ajouter des tags", - "New tag": "Nouveau tag", - "Total Score": "Score Total", - "Post": "Publier", - "Applications Status": "Statut des applications", - "Are you sure you want to delete this reviewer?": "Voulez-vous vraiment supprimer ce réviseur?", - "No": "Non", - "Assign Reviewer": "Attribuer un réviseur", - "Reviews Allocated": "Avis attribués", - "Reviews Completed": "Examens terminés", - "Please fix validation errors before saving": "Veuillez corriger les erreurs de validation avant d'enregistrer", - "Saved": "Enregistré", - "You have {{numReviews}} reviews assigned, of which {{numCompleted}} are completed": "Vous avez {{numReviews}} avis attribués, dont {{numCompleted}} sont terminés", - "Edit": "Éditer", - "Language": "Langue", - "Action": "Action", - "You have completed all your reviews, thank you!": "Vous avez terminé tous vos commentaires, merci!", - "You have no reviews assigned": "Vous n'avez attribué aucun avis", - "Hide Completed": "Masquer terminé", - "Return to review list": "Revenir à la liste de révision", - "Assign Reviewer (they must already have an account)": "Attribuer un réviseur (ils doivent déjà avoir un compte)", - "reviewsAssigned": "{{numReviewsAssigned}} avis attribués à {{assignedReviewerEmail}}", - "There are no more reviewers available": "Il n'y a plus d'évaluateurs disponibles", - "Not Started": "Pas commencé", - "Form title": "Titre du formulaire", - "Allow candidates to nominate others using this application form (Users will be able to submit multiple nominations, including for themselves. If this option is unchecked, a candidate can only apply for themselves)": "Autoriser les candidats à en proposer d'autres à l'aide de ce formulaire de candidature (les utilisateurs pourront soumettre plusieurs candidatures, y compris pour eux-mêmes. Si cette option n'est pas cochée, un candidat ne peut postuler que pour lui-même)", - "Application opens ": "L'application s'ouvre ", - "Application closes": "L'application se ferme", - "sectionPlace": "Section {{index}} de {{numSections}}", - "Add Section": "Ajouter Section", - "Delete Section": "Suprimer Section", - "Duplicate Section": "Dupliquer section", - "Add Question": "Ajouter Question", - "Short Text": "Texte Court", - "Long Text": "Texte Long", - "Markdown": "Markdown", - "Single Choice": "Choix Unique", - "Multi Choice": "Multi Choix", - "Multi Checkbox": "Case à Cocher Multiple", - "File": "Fichier", - "Date": "Date", - "Multi File": "Multi Fichier", - "Choose type": "Choisissez la Catégorie", - "Placeholder": "Text Substituant", - "Value": "Valuer", - "Required": "Obligatoire", - "Duplicate": "Dupliquer", - "Are you sure you want to delete this question": "Êtes-vous sûr vouloir suprimer cette question", - "Are you sure you want to delete this section": "Êtes-vous sûr vouloir suprimer cette section", - "Move down": "Faire Descendre", - "Move up": "Faire Monter", - "Move Section Down": "Faire Descendre Section", - "Move Section Up": "Faire Monter Section", - "No - Don't delete": "Non - Ne pas Suprimer", - "Yes - Delete": "Oui - Suprimer", - "Review Dashboard": "Examiner le tableau de bord", - "Total": "Total", - "Candidate": "Candidat", - "Max and min Rererrals are required": "Les renvois maximum et minimum sont requis", - "Max referral value must be greater than Min": "La valeur de référence maximale doit être supérieure à Min", - "At least one option is required": "Au moins une option est requise", - "Label or Value cannot be empty": "Etiquette ou Valeur ne peuvent pas être vides", - "Validation": "Validation", - "Question {{element}} is a dependency of other sections/questions. Are you sure you still want to delete it?": "La question {{element}} est une dépendance d'autres sections / questions. Êtes-vous sûr de vouloir toujours la supprimer?", - "Are you sure you want to delete this {{element}}": "Voulez-vous vraiment supprimer cet {{element}}?", - "Depends on question": "Dépend de la question", - "You may enter no more that {{max}} words": "Vous ne pouvez saisir que {{max}} mots", - "You must enter at least {{min}} words": "Vous devez saisir au moins {{min}} mots", - "You must enter {{min}} to {{max}} words": "Vous devez saisir entre {{min}} et {{max}} mots", - "Regular Expressions": "Expressions régulières", - "MIN": "MIN", - "MAX": "MAX", - "Enter Your Regex": "Saisissez votre expression régulière", - "Validation Text": "Texte de validation", - "Word Limit": "Limite de mots", - "Advanced Mode": "Mode avancé", - "Please enter a list of file extensions, each starting with a period and separated with commas. E.g .csv,.xls": "Veuillez saisir une liste d'extensions de fichiers, chacune commençant par un point et séparée par des virgules. Ex: .csv,.xls", - "Extensions (E.g .csv,.xls)": "Extensions (Ex .csv,.xls)", - "Enter Maximum Number of Referrals": "Entrez le nombre maximum de références", - "Enter Minimum Number of Referrals": "Enter le nombre minimum de références", - "Maximum Number of Referrals": "Nombre Maximum de Références", - "Minimum Number of Referrals": "Nombre Minimum de Références", - "True": "Vrai", - "False": "Faux", - "errorResponse": "Erreur: {{errorResponse}}", - "Type is Required": "Type est Obligatoire", - "Headline is Required": "Titre est Obligatoire", - "Headline": "Titre", - "Add Key": "Ajouter une clé", - "Application Form": "Formulaire de candidature", - "English": "Anglais", - "Allow candidates to nominate others using this application form(Users will be able to submit multiple nominations, including for themselves. If this option is unchecked, a candidate can only apply for themselves)": "Autoriser les candidats à en désigner d'autres à l'aide de ce formulaire de candidature (les utilisateurs pourront soumettre plusieurs candidatures, y compris pour eux-mêmes. Si cette option n'est pas cochée, un candidat ne peut postuler que pour lui-même)", - "Untitled Section": "Section sans Titre", - "Form Settings": "Paramètres du formulaire", - "Max": "Max", - "Min": "Min", - "Enter Mode": "Entrer le Mode", - "Label": "Étiquete", - "Clear": "Dégager", - "French": "Français", - "Some Changes have not been saved. Are you sure you want to leave without saving them?": "Certaines modifications n'ont pas été enregistrées. Voulez-vous vraiment partir sans les enregistrer?", - "Details": "Des détails", - "Summary": "Résumé", - "Stage": "Étape", - "Score Weight": "Le Poids", - "Review opens ": "Ouverture de l’examen ", - "Review closes ": "Fermeture de l’examen ", - "Review Form": "Review Form", - "Stages": "Étapes", - "Add answer from application form": "Ajouter une réponse à partir du formulaire de demande", - "Add New Stage": "Ajouter une nouvelle étape", - "Select a question from Application Form": "Sélectionnez une question dans le formulaire de candidature", - " ": " ", - "Subheading": "Sous-titre", - "Numeric Input": "Saisie numérique", - "Radio Buttons": "Boutons radio", - "An application form is required to save the review form": "Un formulaire de demande est requis pour enregistrer le formulaire d'examen" - + "Privacy Policy": "Politique sur la protection de la vie privée", + "Powered by": "Propulsé par", + "cookieTitle": "Ce site web conserve des témoins de connexion (« cookies ») sur votre ordinateur.", + "cookieText": "Ceux-ci nous permettent de nous rappeler votre identité entre les pages et entre les visites et servent à recueillir des données sur la façon dont vous interagissez avec notre site web. Nous utilisons ces informations afin de personnaliser votre expérience et pour des analyses et des mesures concernant nos visiteurs. Pour en savoir plus sur les témoins que nous utilisons, consultez notre", + "Apply": "Demander", + "Offer": "Offre", + "Registration": "Inscription", + "Registration Form": "Formulaire d’inscription", + "Event Attendance": "Participation à un événement", + "Event Admin": "Administration d’un événement", + "Review Assignment": "Mission d’examen", + "Invited Guests": "Invités", + "Reviews": "Examens", + "Review": "Examen", + "Review History": "Historique d’examen", + "Registration Admin": "Administration des inscriptions", + "Unconfirmed Registrations": "Inscriptions non confirmées", + "I understand": "Je comprends", + "Uploaded file": "Fichier téléchargé", + "Other": "Autres", + "Other - Please Specify": "Autre, veuillez préciser :", + "Word Count": "Nombre de mots", + "Loading": "Chargement", + "Profile": "Profil", + "Sign Out": "Déconnexion", + "Sign In": "Connexion", + "Click here": "Cliquez ici", + "to download your file if it does not happen automatically": "pour télécharger votre fichier si cela ne se fait pas automatiquement", + "Registered": "Inscrit", + "You have successfully registered for": "Vous vous êtes inscrit avec succès à", + "we look forward to seeing you soon!": "Au plaisir de vous y voir bientôt.", + "Pending Confirmation": "En attente de confirmation", + "Your registration is pending payment confirmation": "Votre inscription est en attente de la confirmation du paiement", + "Registration Pending": "Inscription en cours", + "Invited Guest": "Invité", + "You have been invited as a": "Vous avez été invité en tant que", + "please proceed to registration.": "Veuillez procéder à l’inscription.", + "Register": "Inscription", + "Registration Closed": "Inscription terminée", + "Registration is now closed.": "L’inscription est maintenant terminée.", + "Application Submitted": "Demande envoyée", + "You have submitted your application, you may still edit it before the deadline.": "Vous avez soumis votre demande, vous pouvez encore la modifier avant la date limite.", + "View Application": "Voir la demande", + "Application Withdrawn": "Demande retirée", + "Your application has been withdrawn, you will not be considered for a place at": "Votre demande a été retirée, vous ne serez pas pris en considération pour", + "unless you re-submit by the deadline.": "à moins que vous ne la présentiez à nouveau avant la date limite.", + "Re-apply": "Renouveler la demande", + "In Progress": "En cours", + "You have not yet submitted your application, you must do so before the deadline to be considered": "Vous n’avez pas encore soumis votre demande, vous devez le faire avant la date limite pour être pris en considération", + "Continue": "Poursuivre", + "Apply Now": "Faire une demande maintenant", + "Start your application to attend": "Commencez votre demande de participation", + "Register Now": "Réservez votre place dès maintenant", + "Please complete your event registration as soon as possible.": "Veuillez compléter votre inscription à l’événement dès que possible.", + "Offer Rejected": "Offre rejetée", + "You have turned down your offer to attend": "Vous avez décliné votre offre de participation", + "We hope to see you at a future event!": "Nous espérons que vous serez des nôtres lors d’un prochain événement.", + "Offer Expired": "Offre expirée", + "Unfortunately your offer attend": "Malheureusement, votre offre de participation", + "has expired.": "a expiré.", + "Application Successful": "Demande retenue", + "Congratulations! You have a pending offer to attend": "Félicitations ! Vous avez une offre de participation en cours", + "Please accept or reject it before the expiry date.": "Veuillez l’accepter ou la refuser avant la date d’expiration.", + "View Offer": "Voir l’offre", + "Congratulations! You have been accepted to attend": "Félicitations ! Vous avez été accepté pour participer", + "Further details will follow shortly!": "Des détails à ce sujet suivront sous peu.", + "Application Unsuccessful": "Demande non retenue", + "Unfortunately your application to attend": "Malheureusement, votre demande de participation", + "was not successful. Please try again next time!": "n’a pas été retenue. Veuillez réessayer la prochaine fois.", + "Waitlist": "Liste d’attente", + "You are on the waiting list for": "Vous êtes sur la liste d’attente pour", + "Please keep an eye out for further communication.": "Veuillez rester attentif aux communications futures.", + "Applied": "Demande effectuée", + "Your application is being reviewed. We will get back to you with a verdict as soon as possible.": "Votre demande est en cours d’examen. Une décision vous sera communiquée le plus tôt possible.", + "You withdrew your application to": "Vous avez retiré votre demande à", + "and applications are now closed. Your application will therefore not be considered": "et les demandes sont désormais terminées. Votre demande ne sera donc pas prise en considération.", + "Applications Closed": "Demandes terminées", + "You started, but did not submit your application before the deadline. Your application will therefore not be considered": "Vous avez commencé, mais n’avez pas soumis votre demande avant la date limite. Votre demande ne sera donc pas prise en considération.", + "You did not apply to": "Vous n’avez pas fait de demande de participation", + "and applications are now closed.": "et les demandes sont désormais terminées.", + "Virtual Event Now Open": "L’événement virtuel est maintenant ouvert", + "is now open! Please visit our virtual event site to attend.": "est maintenant ouvert. Pour y assister, veuillez visiter notre site d’événements virtuels.", + "Go To Virtual Event >": "Aller à l’événement virtuel >", + "Enjoy the Event": "Profitez bien de l’événement", + "Now Live": "Maintenant en direct", + "Virtual Event Opening Soon!": "Ouverture prochaine de l’événement virtuel.", + "is opening soon - our virtual event site will be live on": "commence bientôt - notre site virtuel de l’événement sera en direct sur", + "Virtual Event Opens": "Ouverture d’un événement virtuel", + "Starts": "débute", + "Please try refresh the page and/or clear your cookies": "Veuillez essayer de rafraîchir la page et/ou d’effacer vos témoins (« cookies »)", + "An answer is required.": "Une réponse est requise.", + "Save for later": "Enregistrer pour plus tard", + "Saving": "Enregistrement", + "Please fix the errors before continuing.": "Veuillez corriger les erreurs avant de continuer.", + "No answer provided.": "Aucune réponse n’a été fournie.", + "Uploaded File": "Fichier téléchargé", + "Review your Answers": "Révisez vos réponses", + "applicationConfirmationText": "Veuillez confirmer que vos réponses sont correctes. Utilisez le bouton précédent pour les corriger si elles ne le sont pas. Vous pouvez également sortir et revenir plus tard, car les données ont été enregistrées. Cliquez sur le bouton SOUMETTRE une fois que vous êtes prêt à soumettre vos réponses au comité.", + "You MUST click SUBMIT before the deadline for your application to be considered!": "Vous DEVEZ cliquer sur SOUMETTRE avant la date limite pour que votre demande soit prise en compte.", + "Submit": "Soumettre", + "Thank you for applying!": "Merci d’avoir soumis votre demande.", + "Thank you for applying for": "Merci d’avoir soumis votre demande", + "Your application will be reviewed by our committee and we will get back to you as soon as possible.": "Votre demande sera examinée par notre comité et nous vous répondrons dès que possible.", + "You submitted your application on": "Vous avez soumis votre demande", + "Withdraw Application": "Retirer la demande", + "Edit Application": "Modifier la demande", + "By continuing, your submitted application will go into draft state. You MUST press Submit again after you make your changes for your application to be considered in the selection.": "En continuant, votre demande soumise passera à l’état d’ébauche. Vous DEVEZ appuyer à nouveau sur « Soumettre » après avoir apporté les modifications pour que votre demande soit prise en compte dans la sélection.", + "Do you want to edit your application to": "Vous souhaitez modifier votre demande", + "Yes - Withdraw": "Oui - Retrait", + "No - Don't withdraw": "Non - Ne pas retirer", + "Yes - Edit application": "Oui - Modifier la demande", + "No - Don't edit": "Non - Ne pas modifier", + "Step": "Étape", + "Confirmation": "Confirmation", + "Saving Responses": "Enregistrement des réponses", + "Self Nomination": "Autonomination", + "Submitted": "Soumis", + "View": "Afficher", + "Your Nominations": "Vos nominations", + "Nominee": "Candidat(e)", + "Status": "État", + "New Nomination": "Nouvelle nomination", + "Title": "Titre", + "Email": "Courriel", + "Relation": "Relation", + "Reference has been received.": "La référence a été reçue.", + "Email has been sent": "Un courrier électronique a été envoyé", + "ERROR": "ERREUR", + "Add": "Ajout", + "Request References": "Demander des références", + "You must successfully request": "Vous devez demander avec succès", + "references before continuing": "références avant de continuer", + "Email Address": "Adresse électronique", + "First Name": "Prénom", + "Last Name": "Nom de famille", + "Country of Nationality": "Pays de nationalité", + "Country of Residence": "Pays de résidence", + "Password": "Mot de passe", + "Confirm Password": "Confirmer le mot de passe", + "Date of Birth": "Date de naissance", + "Role": "Rôle", + "Passport Number": "Numéro du passeport", + "Full Name as it appears on Passport.": "Nom complet tel qu’il apparaît sur le passeport", + "Full Name": "Nom au complet", + "Passport Date of Expiry.": "Date d’expiration du passeport.", + "Authority that issued Passport": "Autorité qui a délivré le passeport", + "Work Street Address 1": "Adresse postale 1 (travail)", + "Work Street Address 2": "Adresse postale 2 (travail)", + "Work City": "Ville (travail)", + "Work Postal Code": "Code postal (travail)", + "Work Country": "Pays (travail)", + "Residential Street Address 1": "Adresse postale résidentielle 1", + "Residential Street Address 2": "Adresse postale résidentielle 2", + "Residential Postal Code": "Code postal résidentiel", + "Residential Country": "Pays de résidence", + "Who the visa letter should be addressed to, e.g. Immigration Officer of X Embassy": "À qui la lettre de visa doit-elle être adressée, par exemple à l’agent d’immigration de l’ambassade X", + "Addressed to ": "Adressée à ", + "Passwords do not match": "Les mots de passe ne correspondent pas", + "Sign Up": "S’inscrire", + "Your": "Votre", + "account has been created, but before you can use it, we need to verify your email address. Please check your email (and spam folder) for a message containing a link to verify your email address.": "a été créé, mais avant que vous puissiez l’utiliser, nous devons vérifier votre adresse électronique. Veuillez vérifier votre courriel (y compris le dossier de courrier indésirable) pour trouver un message contenant un lien permettant de vérifier votre adresse électronique.", + "if you already have an account": "Si vous possédez déjà un compte", + "I am over 18": "J’ai plus de 18 ans", + "I have read and agree to the ": "J’ai lu et j’accepte ", + "Create Guest": "Créer un compte invité", + "An account with this email address already exists": "Un compte avec cette adresse électronique existe déjà", + "Organisation": "Organisation", + "Event Name": "Nom de l’événement", + "Description": "Description", + "Key": "Clé", + "Email From": "Courriel de", + "URL": "Adresse URL", + "Start Date": "Date de début", + "End Date": "Date de fin", + "Application Open": "Ouverture de la demande", + "Application Close": "Fermeture de la demande", + "Review Open": "Ouverture de l’examen", + "Review Close": "Fermeture de l’examen", + "Selection Open": "Ouverture de la sélection", + "Selection Close": "Fermeture de la sélection", + "Offer Open": "Ouverture de l’offre", + "Offer Close": "Fermeture de l’offre", + "Registration Open": "Inscription ouverte", + "Registration Close": "Inscription fermée", + "Cancel": "Annuler", + "Update Event": "Mise à jour de l’événement", + "Open": "En cours", + "Not Open": "Non ouvert", + "Closed": "Fermé", + "Applications": "Demandes", + "Un-submitted": "Non soumises", + "Withdrawn": "Retirées", + "Completed": "Terminés", + "Incomplete": "Incomplètes", + "Not Allocated": "Non attribuées", + "Offers": "Offres", + "Offers Allocated": "Offres attribuées", + "Accepted": "Acceptées", + "Rejected": "Rejetées", + "Registrations": "Inscriptions", + "Total Guests": "Nombre total d’invités", + "Registered Guests": "Invités inscrits", + "Welcome to": "Bienvenue à", + "for an account in order to apply for an event, award or call for proposals": "d’un compte afin de faire une demande de participation à un événement, un prix ou un appel à propositions", + "if you already have one": "Si vous en avez déjà un", + "Upcoming Events": "Événements à venir", + "Awards": "Prix", + "Calls for Proposals": "Appels de propositions", + "All": "Tous", + "Added": "Ajouté", + "to the guest list": "à la liste des invités", + "Filter by name": "Filtrer par nom", + "Filter by role": "Filtrer par rôle", + "No invited guests": "Nombre d’invités", + "Download csv": "Télécharger le csv", + "Invited guest with this email already exists.": "L’invité avec ce courriel existe déjà.", + "Add Guest": "Ajouter un invité", + "User does not exist, please add these details": "L’utilisateur n’existe pas, veuillez ajouter ces détails", + "Create Invited Guest": "Créer un invité", + "We have re-sent your verification email, please check your inbox (and spam) and click on the link to verify your email address.": "Nous avons envoyé à nouveau votre courriel de vérification, veuillez vérifier votre boîte de réception (et le dossier de courrier indésirable) et cliquez sur le lien pour vérifier votre adresse courriel.", + "Sign in to your account": "Connectez-vous à votre compte.", + "Or": "Ou", + "for a new one": "Pour un nouveau", + "Email address": "Adresse électronique", + "Forgot your password?": "Vous avez oublié votre mot de passe ?", + "Resend Verification Email": "Renvoyer un courriel de vérification", + "Your Profile": "Votre profil", + "Read-only": "En lecture seulement", + "Save profile": "Enregistrer le profil", + "Reset Your Password": "Réinitialiser votre mot de passe", + "Reset Password": "Réinitialisation du mot de passe", + "Are you sure? Click 'Reset Password' to receive an email with a link to reset your password.": "Êtes-vous sûr ? Cliquez sur « Réinitialiser le mot de passe » pour recevoir un courriel avec un lien pour réinitialiser votre mot de passe.", + "Type": "Type", + "There are currently no user profiles to display!": "Il n’y a aucun profil d’utilisateur à afficher pour le moment.", + "Total Profiles": "Profils totaux", + "No token specified.": "Aucun jeton n’est spécifié.", + "referencesClosed": "Malheureusement, les demandes pour {{eventName}} sont désormais terminées. Si vous devez encore soumettre une référence pour {candidateName}, veuillez <5>communiquer avec les organisateurs", + "has been nominated by": "a été nommé par", + "has nominated themselves": "s’est auto-nominé", + "Reference": "Références", + "Thank you! You submitted your reference on {{submittedTimestamp}}. You may update it below if you like.": "Merci. Vous avez soumis votre référence le {{submittedTimestamp}}. Vous pouvez la mettre à jour ci-dessous si vous le souhaitez.", + "for": "pour", + "Kindly upload your reference in support of their application in the form of a PDF (max 10Mb) below.": "Veuillez télécharger votre référence à l’appui de leur demande en format PDF (max 10 Mo) ci-dessous.", + "Upload Reference (PDF, 10Mb max)": "Télécharger la référence (PDF, 10 Mo max)", + "Thank you! Your reference has been received and will be reviewed by our selection committee.": "Merci. Votre référence a été reçue et sera examinée par notre comité de sélection.", + "Submit reference": "Soumettre une référence", + "Your password reset request has been processed. Please check your email for a link that will allow you to change your password.": "Votre demande de réinitialisation de mot de passe a été traitée. Veuillez consulter votre courriel pour trouver un lien qui vous permettra de changer votre mot de passe.", + "No Answer Provided": "Pas de réponse fournie", + "View File": "Voir le fichier", + "NO FILE UPLOADED": "AUCUN FICHIER TÉLÉCHARGÉ", + "An answer/rating is required.": "Une réponse/note est requise.", + "All Done!": "Vous avez terminé.", + "You have completed all your reviews! Please let us know if you have any capacity for more": "Vous avez terminé tous vos examens. Si vous avez d’autres disponibilités, n’hésitez pas à communiquer avec nous.", + "Thank you for your contribution!": "Je vous remercie de votre participation.", + "Flag Response": "Signaler une réponse", + "Response ID": "Numéro d’identification de la réponse", + "Please quote this in any correspondence with admins outside of the system.": "Veuillez le citer dans toute correspondance avec les administrateurs en dehors du système.", + "Go Back": "Retour en arrière", + "Skip": "Passer", + "There are one or more validation errors, please correct before submitting.": "Il y a une ou plusieurs erreurs de validation, veuillez les corriger avant de soumettre.", + "reviewsRemaining": "Il vous reste {{reviewsRemainingCount}} dossiers à examiner", + "If reviewing this response revealed an issue that should be considered if this candidate were accepted, please describe it below.": "Si l’examen de cette réponse a révélé un problème qui devrait être pris en compte si ce candidat était accepté, veuillez le décrire ci-dessous.", + "Assign": "Attribuer", + "Name": "Nom", + "No. Allocated": "Nombre attribué", + "No. Completed": "Nombre terminé", + "No. to Assign": "Nombre à attribuer", + "Total Unallocated Reviews:": "Total des examens non attribués :", + "Add new reviewer's email (they must already have an account)": "Ajouter le courriel du nouvel examinateur (il doit déjà avoir un compte)", + "Review email": "Vérifier le courrier électronique", + "Submitted Timestamp": "Horodatage soumis", + "Verify Email Address": "Vérifier l’adresse électronique", + "verified": "Votre adresse électronique a été vérifiée. <1>Cliquez ici pour vous connecter si vous n’êtes pas automatiquement redirigé.", + "No user profile to display!": "Pas de profil d’utilisateur à afficher!", + "Profile For": "Profil pour", + "Personal Information": "Renseignements personnels", + "User Application Info": "Informations sur la demande de l’utilisateur", + "Submitted on": "Soumise le ", + "Withdrawn on": "Retirée le", + "Add File": "Ajouter le fichier", + "Please enter a name": "Veuillez saisir un nom", + "Upload File": "Téléverser un fichier", + "Past Events": "Past Events", + "Virtual Event Over": "Événement virtuel terminé", + "is now over. You may still visit our virtual event site.": "est maintenant terminé. Vous pouvez toujours visiter notre site d'événements virtuels.", + "Thank you for participating!": "Merci d'avoir participé!", + "Enter File Name": "Entrez le nom du fichier", + "Edit Name": "Modifier le nom", + "Delete": "Supprimer", + "Response List": "Liste de réponse", + "Questions": "Des questions", + "questions": "des questions", + "Include un-submitted": "Inclure les non-soumis", + "Update": "Mettre à jour", + "Thank you for responding to the": "Merci d’avoir soumis votre demande", + "Note: You may need to change the file name to open the file on certain operating systems": "Remarque: vous devrez peut-être changer le nom du fichier pour ouvrir le fichier sur certains systèmes d'exploitation", + "Next": "Continuer", + "Previous": "Retourner", + "Application Status": "État de la candidature", + "Reviewer": "Critique", + "Tags": "Mots clés", + "Yes": "Oui", + "Are you sure you want to delete this tag?": "Voulez-vous vraiment supprimer ce tag?", + "Save": "Enregistrer", + "Add Tags": "Ajouter des tags", + "New tag": "Nouveau tag", + "Total Score": "Score Total", + "Post": "Publier", + "Applications Status": "Statut des applications", + "Are you sure you want to delete this reviewer?": "Voulez-vous vraiment supprimer ce réviseur?", + "No": "Non", + "Assign Reviewer": "Attribuer un réviseur", + "Reviews Allocated": "Avis attribués", + "Reviews Completed": "Examens terminés", + "Please fix validation errors before saving": "Veuillez corriger les erreurs de validation avant d'enregistrer", + "Saved": "Enregistré", + "You have {{numReviews}} reviews assigned, of which {{numCompleted}} are completed": "Vous avez {{numReviews}} avis attribués, dont {{numCompleted}} sont terminés", + "Edit": "Éditer", + "Language": "Langue", + "Action": "Action", + "You have completed all your reviews, thank you!": "Vous avez terminé tous vos commentaires, merci!", + "You have no reviews assigned": "Vous n'avez attribué aucun avis", + "Hide Completed": "Masquer terminé", + "Return to review list": "Revenir à la liste de révision", + "Assign Reviewer (they must already have an account)": "Attribuer un réviseur (ils doivent déjà avoir un compte)", + "reviewsAssigned": "{{numReviewsAssigned}} avis attribués à {{assignedReviewerEmail}}", + "There are no more reviewers available": "Il n'y a plus d'évaluateurs disponibles", + "Not Started": "Pas commencé", + "Form title": "Titre du formulaire", + "Allow candidates to nominate others using this application form (Users will be able to submit multiple nominations, including for themselves. If this option is unchecked, a candidate can only apply for themselves)": "Autoriser les candidats à en proposer d'autres à l'aide de ce formulaire de candidature (les utilisateurs pourront soumettre plusieurs candidatures, y compris pour eux-mêmes. Si cette option n'est pas cochée, un candidat ne peut postuler que pour lui-même)", + "Application opens ": "L'application s'ouvre ", + "Application closes": "L'application se ferme", + "sectionPlace": "Section {{index}} de {{numSections}}", + "Add Section": "Ajouter Section", + "Delete Section": "Suprimer Section", + "Duplicate Section": "Dupliquer section", + "Add Question": "Ajouter Question", + "Short Text": "Texte Court", + "Long Text": "Texte Long", + "Markdown": "Markdown", + "Single Choice": "Choix Unique", + "Multi Choice": "Multi Choix", + "Multi Checkbox": "Case à Cocher Multiple", + "File": "Fichier", + "Date": "Date", + "Multi File": "Multi Fichier", + "Choose type": "Choisissez la Catégorie", + "Placeholder": "Text Substituant", + "Value": "Valuer", + "Required": "Obligatoire", + "Duplicate": "Dupliquer", + "Are you sure you want to delete this question": "Êtes-vous sûr vouloir suprimer cette question", + "Are you sure you want to delete this section": "Êtes-vous sûr vouloir suprimer cette section", + "Move down": "Faire Descendre", + "Move up": "Faire Monter", + "Move Section Down": "Faire Descendre Section", + "Move Section Up": "Faire Monter Section", + "No - Don't delete": "Non - Ne pas Suprimer", + "Yes - Delete": "Oui - Suprimer", + "Review Dashboard": "Examiner le tableau de bord", + "Total": "Total", + "Candidate": "Candidat", + "Max and min Rererrals are required": "Les renvois maximum et minimum sont requis", + "Max referral value must be greater than Min": "La valeur de référence maximale doit être supérieure à Min", + "At least one option is required": "Au moins une option est requise", + "Label or Value cannot be empty": "Etiquette ou Valeur ne peuvent pas être vides", + "Validation": "Validation", + "Question {{element}} is a dependency of other sections/questions. Are you sure you still want to delete it?": "La question {{element}} est une dépendance d'autres sections / questions. Êtes-vous sûr de vouloir toujours la supprimer?", + "Are you sure you want to delete this {{element}}": "Voulez-vous vraiment supprimer cet {{element}}?", + "Depends on question": "Dépend de la question", + "You may enter no more that {{max}} words": "Vous ne pouvez saisir que {{max}} mots", + "You must enter at least {{min}} words": "Vous devez saisir au moins {{min}} mots", + "You must enter {{min}} to {{max}} words": "Vous devez saisir entre {{min}} et {{max}} mots", + "Regular Expressions": "Expressions régulières", + "MIN": "MIN", + "MAX": "MAX", + "Enter Your Regex": "Saisissez votre expression régulière", + "Validation Text": "Texte de validation", + "Word Limit": "Limite de mots", + "Advanced Mode": "Mode avancé", + "Please enter a list of file extensions, each starting with a period and separated with commas. E.g .csv,.xls": "Veuillez saisir une liste d'extensions de fichiers, chacune commençant par un point et séparée par des virgules. Ex: .csv,.xls", + "Extensions (E.g .csv,.xls)": "Extensions (Ex .csv,.xls)", + "Enter Maximum Number of Referrals": "Entrez le nombre maximum de références", + "Enter Minimum Number of Referrals": "Enter le nombre minimum de références", + "Maximum Number of Referrals": "Nombre Maximum de Références", + "Minimum Number of Referrals": "Nombre Minimum de Références", + "True": "Vrai", + "False": "Faux", + "errorResponse": "Erreur: {{errorResponse}}", + "Type is Required": "Type est Obligatoire", + "Headline is Required": "Titre est Obligatoire", + "Headline": "Titre", + "Add Key": "Ajouter une clé", + "Application Form": "Formulaire de candidature", + "English": "Anglais", + "Allow candidates to nominate others using this application form(Users will be able to submit multiple nominations, including for themselves. If this option is unchecked, a candidate can only apply for themselves)": "Autoriser les candidats à en désigner d'autres à l'aide de ce formulaire de candidature (les utilisateurs pourront soumettre plusieurs candidatures, y compris pour eux-mêmes. Si cette option n'est pas cochée, un candidat ne peut postuler que pour lui-même)", + "Untitled Section": "Section sans Titre", + "Form Settings": "Paramètres du formulaire", + "Max": "Max", + "Min": "Min", + "Enter Mode": "Entrer le Mode", + "Label": "Étiquete", + "Clear": "Dégager", + "French": "Français", + "Some Changes have not been saved. Are you sure you want to leave without saving them?": "Certaines modifications n'ont pas été enregistrées. Voulez-vous vraiment partir sans les enregistrer?", + "Details": "Des détails", + "Summary": "Résumé", + "Stage": "Étape", + "Score Weight": "Le Poids", + "Review opens ": "Ouverture de l’examen ", + "Review closes ": "Fermeture de l’examen ", + "Review Form": "Review Form", + "Stages": "Étapes", + "Add answer from application form": "Ajouter une réponse à partir du formulaire de demande", + "Add New Stage": "Ajouter une nouvelle étape", + "Select a question from Application Form": "Sélectionnez une question dans le formulaire de candidature", + " ": " ", + "Subheading": "Sous-titre", + "Numeric Input": "Saisie numérique", + "Radio Buttons": "Boutons radio", + "An application form is required to save the review form": "Un formulaire de demande est requis pour enregistrer le formulaire d'examen" } - diff --git a/webapp/src/App.css b/webapp/src/App.css index 156515ef2..a43020812 100755 --- a/webapp/src/App.css +++ b/webapp/src/App.css @@ -11,7 +11,7 @@ } .App-header { - background-color: #1F2232; + background-color: #1f2232; display: flex; flex-direction: column; align-items: center; @@ -22,12 +22,12 @@ } .App-link { - color: #40798C; + color: #40798c; } body { - background: #F9fafb; - font-family: 'Open Sans', sans-serif; + background: #f9fafb; + font-family: "Open Sans", sans-serif; } html button:active { @@ -35,18 +35,29 @@ html button:active { outline: none; } -.Body{ - background: #F9fafb;; +.Body { + background: #f9fafb; min-height: 80vh; padding-bottom: 1em; padding-top: 2em; } -h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6 { - font-family: 'Roboto', sans-serif; +h1, +h2, +h3, +h4, +h5, +h6, +.h1, +.h2, +.h3, +.h4, +.h5, +.h6 { + font-family: "Roboto", sans-serif; } -.HomeNavBar{ +.HomeNavBar { margin-bottom: 0px; } @@ -76,7 +87,7 @@ footer .btn { padding-left: 30px; } -.link-style{ +.link-style { background: none; border: none; cursor: pointer; @@ -93,10 +104,10 @@ footer .btn { .navbar-light .navbar-nav .nav-link.active { font-weight: bold; - border-bottom: 3px solid #27A844; + border-bottom: 3px solid #27a844; } -.navbar-light .navbar-nav .nav-item .nav-link{ +.navbar-light .navbar-nav .nav-item .nav-link { padding-bottom: 0.75em; padding-top: 0.75em; } @@ -118,7 +129,7 @@ footer .btn { } .navbar-light .navbar-nav .nav-link { - color: rgba(0,0,0,.75); + color: rgba(0, 0, 0, 0.75); } .navbar-brand-main { @@ -130,7 +141,7 @@ footer .btn { margin-right: 10px; } -.alert-container{ +.alert-container { margin-top: 16px; } @@ -139,13 +150,13 @@ footer .btn { } .card { - box-shadow: 0px 2px 4px rgba(0,0,0,0.15); + box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.15); padding-top: 30px; padding-bottom: 30px; } label { - margin-bottom: .2rem; + margin-bottom: 0.2rem; } .powered-by a { @@ -161,13 +172,13 @@ label { } .language-navbar { - padding-right: .5em; - border-right: 1px solid rgba(255,255,255,.25); - margin-right: .5em; + padding-right: 0.5em; + border-right: 1px solid rgba(255, 255, 255, 0.25); + margin-right: 0.5em; } .menu-icon { - margin-right: .1em; + margin-right: 0.1em; } div.vertical-space { @@ -186,4 +197,4 @@ div.card-container { .dropdown-submenu-item { padding-left: 3em; -} \ No newline at end of file +} diff --git a/webapp/src/App.js b/webapp/src/App.js index 7a4448682..13d52300b 100755 --- a/webapp/src/App.js +++ b/webapp/src/App.js @@ -12,35 +12,38 @@ import UserDropdown from "./components/User"; import ViewFile from "./components/ViewFile"; import Reference from "./pages/references"; import CookieConsent from "react-cookie-consent"; -import ResponseList from './pages/ResponseList/ResponseList'; -import { AdminMenu } from './utils/adminMenu'; +import ResponseList from "./pages/ResponseList/ResponseList"; +import { AdminMenu } from "./utils/adminMenu"; import ReactGA from "react-ga"; import "./App.css"; import history from "./History"; -import { isEventAdmin, isRegistrationAdmin, isRegistrationVolunteer, isEventReviewer } from "./utils/user"; -import { withTranslation } from 'react-i18next'; +import { + isEventAdmin, + isRegistrationAdmin, + isRegistrationVolunteer, + isEventReviewer, +} from "./utils/user"; +import { withTranslation } from "react-i18next"; import { userService } from "./services/user"; - ReactGA.initialize("UA-136093201-1", { debug: false, - testMode: process.env.NODE_ENV === "test" + testMode: process.env.NODE_ENV === "test", }); ReactGA.pageview(window.location.pathname + window.location.search); -history.listen(location => { +history.listen((location) => { ReactGA.pageview(location.pathname + location.search); }); - class EventNav extends Component { constructor(props) { super(props); this.state = { - collapsed: true + collapsed: true, }; } @@ -49,13 +52,16 @@ class EventNav extends Component { return ( - ); } } - const EventNavTranslation = withTranslation()(EventNav); - class LanguageSelectorComponent extends Component { changeLanguage = (lang) => { // Change the language using i18next if (this.props.i18n) { - this.props.i18n.changeLanguage(lang).then(()=>{ - const currentUser = JSON.parse(localStorage.getItem("user")) + this.props.i18n.changeLanguage(lang).then(() => { + const currentUser = JSON.parse(localStorage.getItem("user")); if (currentUser) { - // We send a put request to the user service to update the language on the back-end. + // We send a put request to the user service to update the language on the back-end. // Note the language is automatically sent with every request through axios - userService.get().then(result => { + userService.get().then((result) => { userService.update({ email: result.email, firstName: result.firstname, lastName: result.lastname, - title: result.user_title + title: result.user_title, }); }); } window.location.reload(true); }); } - } + }; render() { - if (this.props.organisation && this.props.organisation.languages.length > 1) { + if ( + this.props.organisation && + this.props.organisation.languages.length > 1 + ) { return (
  • @@ -283,15 +300,22 @@ class LanguageSelectorComponent extends Component { {this.props.i18n.language}
    - {this.props.organisation.languages.map(lang => ( - + {this.props.organisation.languages.map((lang) => ( + ))}
  • -
) + + ); } - return
+ return
; } } @@ -306,21 +330,20 @@ class AppComponent extends Component { collapsed: true, eventKey: null, currentEvent: null, - error: null + error: null, }; this.refreshUser = this.refreshUser.bind(this); } componentDidMount() { - userService.authRefresh().then(user => { + userService.authRefresh().then((user) => { if (!user.error) { localStorage.setItem("user", JSON.stringify(user)); this.setState({ - user: user + user: user, }); - } - else { + } else { localStorage.removeItem("user"); } }); @@ -328,27 +351,26 @@ class AppComponent extends Component { handleLogout = () => { this.refreshUser(); - window.location = '/'; - } + window.location = "/"; + }; refreshUser() { const currentUser = JSON.parse(localStorage.getItem("user")); this.setState({ - user: currentUser + user: currentUser, }); if (currentUser) { // Send a user profile update to record the currently selected language - userService.get().then(result => { + userService.get().then((result) => { userService.update({ email: result.email, firstName: result.firstname, lastName: result.lastname, - title: result.user_title + title: result.user_title, }); }); } - } toggleMenu = () => { @@ -358,7 +380,7 @@ class AppComponent extends Component { setEvent = (eventKey, event) => { this.setState({ eventKey: eventKey, - currentEvent: event + currentEvent: event, }); }; @@ -411,18 +433,23 @@ class AppComponent extends Component { - {this.state.currentEvent && } + {this.state.currentEvent && ( + + )}
- {this.state.error &&
{this.state.error}
} + {this.state.error && ( +
{this.state.error}
+ )} ( + render={(props) => ( ( - + render={(props) => ( + )} /> ( + render={(props) => ( ( + render={(props) => ( )} /> ( + render={(props) => ( )} /> @@ -468,14 +499,15 @@ class AppComponent extends Component { ( - ( + + /> )} /> @@ -502,12 +534,12 @@ class AppComponent extends Component { target="_blank" rel="noopener noreferrer" > - {t('Privacy Policy')} + {t("Privacy Policy")} {this.props.organisation && this.props.organisation.system_name !== "Baobab" && (
- {t('Powered by')}{" "} + {t("Powered by")}{" "} -
{t('cookieTitle')}
+
{t("cookieTitle")}
- {t('cookieText')}{" "} + {t("cookieText")}{" "}
- {t('Privacy Policy')} + {t("Privacy Policy")} diff --git a/webapp/src/History.js b/webapp/src/History.js index ebc12faba..ec3d07f21 100644 --- a/webapp/src/History.js +++ b/webapp/src/History.js @@ -1,4 +1,3 @@ - import { createBrowserHistory } from "history"; export default createBrowserHistory(); diff --git a/webapp/src/__tests__/App.test.js b/webapp/src/__tests__/App.test.js index b4158c2e9..6dd1af23c 100644 --- a/webapp/src/__tests__/App.test.js +++ b/webapp/src/__tests__/App.test.js @@ -1,9 +1,9 @@ -import React from 'react'; -import {shallow} from 'enzyme'; -import App from '../App.js'; +import React from "react"; +import { shallow } from "enzyme"; +import App from "../App.js"; -test('Check if App component renders.', () => { +test("Check if App component renders.", () => { // Render App.js main component. const wrapper = shallow(); expect(wrapper.length).toEqual(1); -}); \ No newline at end of file +}); diff --git a/webapp/src/components/ErrorPage.jsx b/webapp/src/components/ErrorPage.jsx index 2cdcb6262..bc51282cf 100644 --- a/webapp/src/components/ErrorPage.jsx +++ b/webapp/src/components/ErrorPage.jsx @@ -10,13 +10,13 @@ Error Message: // Consider logging componentStack and error export const ErrorPage = ({ componentStack, error }) => { - if (errorHandler){ - var errorMessage= new Object(); + if (errorHandler) { + var errorMessage = new Object(); errorMessage.componentStack = componentStack; - errorMessage.error = error; - var jsonString= JSON.stringify(errorMessage); + errorMessage.error = error; + var jsonString = JSON.stringify(errorMessage); errorHandler.report(jsonString); - } + } const bug_mailto = "mailto:baobab@deeplearningindaba.com?subject=" + @@ -37,9 +37,11 @@ export const ErrorPage = ({ componentStack, error }) => { Error: {error.toString()}

- Please try again and if this issue persists, please{" "} - get in touch with us.
- The bug report will go to the Baobab team at the Deep Learning Indaba who maintain the application. + Please try again and if this issue persists, please{" "} + get in touch with us. +
+ The bug report will go to the Baobab team at the Deep Learning Indaba + who maintain the application.

diff --git a/webapp/src/components/EventStatus.js b/webapp/src/components/EventStatus.js index 9a4bf7e22..c4a919f0a 100644 --- a/webapp/src/components/EventStatus.js +++ b/webapp/src/components/EventStatus.js @@ -1,371 +1,434 @@ import React, { Component } from "react"; import { withRouter } from "react-router"; -import { withTranslation } from 'react-i18next'; - +import { withTranslation } from "react-i18next"; class EventStatus extends Component { - unknownStatus = (status_name, status) => { - return { - title: "ERROR", - titleClass: "text-danger", - longText: `Unknown ${status_name}: ${status}`, - shortText: "ERROR" - } - } + return { + title: "ERROR", + titleClass: "text-danger", + longText: `Unknown ${status_name}: ${status}`, + shortText: "ERROR", + }; + }; registeredStatus = (event) => { if (event.status.registration_status === "Confirmed") { - return { - title: this.props.t("Registered"), - titleClass: "text-success", - longText: this.props.t("You have successfully registered for") + " " + event.name + ", " + this.props.t("we look forward to seeing you soon!"), - shortText: this.props.t("Registered") - }; - } - else if (event.status.registration_status === "Not Confirmed") { - return { - title: this.props.t("Pending Confirmation"), - titleClass: "text-warning", - longText: this.props.t("Your registration is pending payment confirmation"), - shortText: this.props.t("Registration Pending") - }; - } - else { - return this.unknownStatus("registration status", event.status.registration_status); + return { + title: this.props.t("Registered"), + titleClass: "text-success", + longText: + this.props.t("You have successfully registered for") + + " " + + event.name + + ", " + + this.props.t("we look forward to seeing you soon!"), + shortText: this.props.t("Registered"), + }; + } else if (event.status.registration_status === "Not Confirmed") { + return { + title: this.props.t("Pending Confirmation"), + titleClass: "text-warning", + longText: this.props.t( + "Your registration is pending payment confirmation" + ), + shortText: this.props.t("Registration Pending"), + }; + } else { + return this.unknownStatus( + "registration status", + event.status.registration_status + ); } - } + }; invitedGuestStatus = (event) => { if (!event.status.registration_status) { - if (event.is_registration_open) { - return { - title: this.props.t("Invited Guest"), - titleClass: "text-success", - longText: this.props.t("You have been invited as a") + ` ${event.status.invited_guest}, ` + this.props.t("please proceed to registration."), - shortText: this.props.t("Register"), - linkClass: 'btn-success', - link: `${event.key}/registration` - }; - } - else { - return { - title: this.props.t("Registration Closed"), - titleClass: "text-danger", - longText: this.props.t(`Registration is now closed.`), - shortText: this.props.t("Registration Closed") - }; - } - } - else { - return this.registeredStatus(event); - } - } - - applicationStatus = (event) => { - const applyLink = `${event.key}/apply` - if (event.status.application_status === "Submitted") { - return { - title: this.props.t("Application Submitted"), - titleClass: "text-success", - longText: this.props.t("You have submitted your application, you may still edit it before the deadline."), - shortText: this.props.t("View Application"), - linkClass: 'btn-secondary', - link: applyLink - }; - } - else if (event.status.application_status === "Withdrawn") { + if (event.is_registration_open) { return { - title: this.props.t("Application Withdrawn"), - titleClass: "text-danger", - longText: this.props.t("Your application has been withdrawn, you will not be considered for") + ` ${event.name} ` + this.props.t("unless you re-submit by the deadline."), - shortText: this.props.t("Re-apply"), - linkClass: "btn-warning", - link: applyLink + title: this.props.t("Invited Guest"), + titleClass: "text-success", + longText: + this.props.t("You have been invited as a") + + ` ${event.status.invited_guest}, ` + + this.props.t("please proceed to registration."), + shortText: this.props.t("Register"), + linkClass: "btn-success", + link: `${event.key}/registration`, + }; + } else { + return { + title: this.props.t("Registration Closed"), + titleClass: "text-danger", + longText: this.props.t(`Registration is now closed.`), + shortText: this.props.t("Registration Closed"), }; } - else if (event.status.application_status === "Not Submitted") { - return { - title: this.props.t("In Progress"), - titleClass: "text-warning", - longText: this.props.t(`You have not yet submitted your application, you must do so before the deadline to be considered`), - shortText: this.props.t("Continue"), - linkClass: "btn-primary", - link: applyLink - }; - } - else { - return { - title: this.props.t("Apply Now"), - titleClass: "text-success", - longText: this.props.t("Start your application to attend") + ` ${event.name}`, - shortText: this.props.t("Apply Now"), - linkClass: "btn-success", - link: applyLink - }; - } - } + } else { + return this.registeredStatus(event); + } + }; + + applicationStatus = (event) => { + const applyLink = `${event.key}/apply`; + if (event.status.application_status === "Submitted") { + return { + title: this.props.t("Application Submitted"), + titleClass: "text-success", + longText: this.props.t( + "You have submitted your application, you may still edit it before the deadline." + ), + shortText: this.props.t("View Application"), + linkClass: "btn-secondary", + link: applyLink, + }; + } else if (event.status.application_status === "Withdrawn") { + return { + title: this.props.t("Application Withdrawn"), + titleClass: "text-danger", + longText: + this.props.t( + "Your application has been withdrawn, you will not be considered for" + ) + + ` ${event.name} ` + + this.props.t("unless you re-submit by the deadline."), + shortText: this.props.t("Re-apply"), + linkClass: "btn-warning", + link: applyLink, + }; + } else if (event.status.application_status === "Not Submitted") { + return { + title: this.props.t("In Progress"), + titleClass: "text-warning", + longText: this.props.t( + `You have not yet submitted your application, you must do so before the deadline to be considered` + ), + shortText: this.props.t("Continue"), + linkClass: "btn-primary", + link: applyLink, + }; + } else { + return { + title: this.props.t("Apply Now"), + titleClass: "text-success", + longText: + this.props.t("Start your application to attend") + ` ${event.name}`, + shortText: this.props.t("Apply Now"), + linkClass: "btn-success", + link: applyLink, + }; + } + }; offerStatus = (event) => { if (event.status.offer_status === "Accepted") { - if (event.status.registration_status) { - return this.registeredStatus(event); - } - else if (event.is_registration_open) { - return { - title: this.props.t("Register Now"), - titleClass: "text-success", - longText: this.props.t("You have accepted your offer to attend") + ` ${event.name}. Please complete your event registration as soon as possible.`, - shortText: this.props.t("Register Now"), - linkClass: "btn-success", - link: `${event.key}/registration` - }; - } - else { - return { - title: this.props.t("Registration Closed"), - titleClass: "text-danger", - longText: this.props.t(`Registration is now closed.`), - shortText: this.props.t("Registration Closed)") - }; - } - } - else if (event.status.offer_status === "Rejected") { + if (event.status.registration_status) { + return this.registeredStatus(event); + } else if (event.is_registration_open) { return { - title: this.props.t("Offer Rejected"), - titleClass: "text-danger", - longText: this.props.t("You have turned down your offer to attend") + ` ${event.name}.` + this.props.t("We hope to see you at a future event!"), - shortText: this.props.t("Offer Rejected") + title: this.props.t("Register Now"), + titleClass: "text-success", + longText: + this.props.t("You have accepted your offer to attend") + + ` ${event.name}. Please complete your event registration as soon as possible.`, + shortText: this.props.t("Register Now"), + linkClass: "btn-success", + link: `${event.key}/registration`, }; - } - else if (event.status.offer_status === "Expired") { + } else { return { - title: this.props.t("Offer Expired"), - titleClass: "text-danger", - longText: this.props.t("Unfortunately your offer attend") + ` ${event.name} ` + this.props.t("has expired."), - shortText: this.props.t("Offer Expired") + title: this.props.t("Registration Closed"), + titleClass: "text-danger", + longText: this.props.t(`Registration is now closed.`), + shortText: this.props.t("Registration Closed)"), }; + } + } else if (event.status.offer_status === "Rejected") { + return { + title: this.props.t("Offer Rejected"), + titleClass: "text-danger", + longText: + this.props.t("You have turned down your offer to attend") + + ` ${event.name}.` + + this.props.t("We hope to see you at a future event!"), + shortText: this.props.t("Offer Rejected"), + }; + } else if (event.status.offer_status === "Expired") { + return { + title: this.props.t("Offer Expired"), + titleClass: "text-danger", + longText: + this.props.t("Unfortunately your offer attend") + + ` ${event.name} ` + + this.props.t("has expired."), + shortText: this.props.t("Offer Expired"), + }; + } else if (event.status.offer_status === "Pending") { + return { + title: this.props.t("Application Successful"), + titleClass: "text-success", + longText: + this.props.t("Congratulations! You have a pending offer to attend") + + ` ${event.name}. ` + + this.props.t("Please accept or reject it before the expiry date."), + shortText: this.props.t("View Offer"), + linkClass: "btn-success", + link: `${event.key}/offer`, + }; + } else { + return this.unknownStatus("offer status", event.status.offer_status); } - else if (event.status.offer_status === "Pending") { - return { - title: this.props.t("Application Successful"), - titleClass: "text-success", - longText: this.props.t("Congratulations! You have a pending offer to attend") + ` ${event.name}. ` + this.props.t("Please accept or reject it before the expiry date."), - shortText: this.props.t("View Offer"), - linkClass: "btn-success", - link: `${event.key}/offer` - }; - } - else { - return this.unknownStatus("offer status", event.status.offer_status); - } - } + }; outcomeStatus = (event) => { - if (event.status.outcome_status === "ACCEPTED") { - return { - title: this.props.t("Application Successful"), - titleClass: "text-success", - longText: this.props.t("Congratulations! You have been accepted to attend") + ` ${event.name}. ` + this.props.t("Further details will follow shortly!"), - shortText: this.props.t("Application Successful") - }; - } - else if (event.status.outcome_status === "REJECTED") { - return { - title: this.props.t("Application Unsuccessful"), - titleClass: "text-danger", - longText: this.props.t("Unfortunately your application to attend") + ` ${event.name} ` + this.props.t("was not successful. Please try again next time!"), - shortText: this.props.t("Application Unsuccessful") - }; - } - else if (event.status.outcome_status === "WAITLIST") { - return { - title: this.props.t("Waitlist"), - titleClass: "text-warning", - longText: this.props.t("You are on the waiting list for") + ` ${event.name}. ` + this.props.t("Please keep an eye out for further communication."), - shortText: this.props.t("Waitlist") - }; - } - else { - return this.unknownStatus("outcome status", event.status.outcome_status); - } - } + if (event.status.outcome_status === "ACCEPTED") { + return { + title: this.props.t("Application Successful"), + titleClass: "text-success", + longText: + this.props.t("Congratulations! You have been accepted to attend") + + ` ${event.name}. ` + + this.props.t("Further details will follow shortly!"), + shortText: this.props.t("Application Successful"), + }; + } else if (event.status.outcome_status === "REJECTED") { + return { + title: this.props.t("Application Unsuccessful"), + titleClass: "text-danger", + longText: + this.props.t("Unfortunately your application to attend") + + ` ${event.name} ` + + this.props.t("was not successful. Please try again next time!"), + shortText: this.props.t("Application Unsuccessful"), + }; + } else if (event.status.outcome_status === "WAITLIST") { + return { + title: this.props.t("Waitlist"), + titleClass: "text-warning", + longText: + this.props.t("You are on the waiting list for") + + ` ${event.name}. ` + + this.props.t("Please keep an eye out for further communication."), + shortText: this.props.t("Waitlist"), + }; + } else { + return this.unknownStatus("outcome status", event.status.outcome_status); + } + }; applicationClosedStatus = (event) => { - if (event.status.application_status === "Submitted") { - return { - title: this.props.t("Application Submitted"), - titleClass: "text-success", - longText: this.props.t("Your application is being reviewed. We will get back to you with a verdict as soon as possible."), - shortText: this.props.t("Applied") - } - } - else if (event.status.application_status === "Withdrawn") { - return { - title: this.props.t("Application Withdrawn"), - titleClass: "text-danger", - longText: this.props.t("You withdrew your application to") + ` ${event.name} ` + this.props.t("and applications are now closed. Your application will therefore not be considered"), - shortText: this.props.t("Application Withdrawn") - } - } - else if (event.status.application_status === "Not Submitted") { - return { - title: this.props.t("Applications Closed"), - titleClass: "text-danger", - longText: this.props.t("You started, but did not submit your application before the deadline. Your application will therefore not be considered"), - shortText: this.props.t("Applications Closed") - } - } - else { - return { - title: this.props.t("Applications Closed"), - titleClass: "text-danger", - longText: this.props.t("You did not apply to") + ` ${event.name} ` + this.props.t("and applications are now closed."), - shortText: this.props.t("Applications Closed") - } - } - } + if (event.status.application_status === "Submitted") { + return { + title: this.props.t("Application Submitted"), + titleClass: "text-success", + longText: this.props.t( + "Your application is being reviewed. We will get back to you with a verdict as soon as possible." + ), + shortText: this.props.t("Applied"), + }; + } else if (event.status.application_status === "Withdrawn") { + return { + title: this.props.t("Application Withdrawn"), + titleClass: "text-danger", + longText: + this.props.t("You withdrew your application to") + + ` ${event.name} ` + + this.props.t( + "and applications are now closed. Your application will therefore not be considered" + ), + shortText: this.props.t("Application Withdrawn"), + }; + } else if (event.status.application_status === "Not Submitted") { + return { + title: this.props.t("Applications Closed"), + titleClass: "text-danger", + longText: this.props.t( + "You started, but did not submit your application before the deadline. Your application will therefore not be considered" + ), + shortText: this.props.t("Applications Closed"), + }; + } else { + return { + title: this.props.t("Applications Closed"), + titleClass: "text-danger", + longText: + this.props.t("You did not apply to") + + ` ${event.name} ` + + this.props.t("and applications are now closed."), + shortText: this.props.t("Applications Closed"), + }; + } + }; buildMiniConfUrl = (event) => { - const link = - "https://" + - event.miniconf_url + - "/index.html?token=" + - JSON.parse(localStorage.getItem("user"))["token"] + - "&event_id=" + - event.id + - "&redirect_back_url=" + - window.location.href + - "&verify_token_url=" + - process.env.REACT_APP_API_URL + - "/api/v1/validate-user-event-attendee" + - "&origin=" + - window.location.origin; + const link = + "https://" + + event.miniconf_url + + "/index.html?token=" + + JSON.parse(localStorage.getItem("user"))["token"] + + "&event_id=" + + event.id + + "&redirect_back_url=" + + window.location.href + + "&verify_token_url=" + + process.env.REACT_APP_API_URL + + "/api/v1/validate-user-event-attendee" + + "&origin=" + + window.location.origin; - return link - } + return link; + }; pastEventStatus = (event) => { if (event.miniconf_url) { - return { - title: this.props.t("Virtual Event Over"), - titleClass: "text-success", - longText: `${event.name} ` + this.props.t("is now over. You may still visit our virtual event site."), - shortText: this.props.t('Go To Virtual Event >'), - linkClass: "btn-success", - link: this.buildMiniConfUrl(event) - } - } - else { - return { - title: this.props.t("Thank you for participating!"), - titleClass: "text-success", - shortText: this.props.t("Thank you for participating!") - } - } - } + return { + title: this.props.t("Virtual Event Over"), + titleClass: "text-success", + longText: + `${event.name} ` + + this.props.t( + "is now over. You may still visit our virtual event site." + ), + shortText: this.props.t("Go To Virtual Event >"), + linkClass: "btn-success", + link: this.buildMiniConfUrl(event), + }; + } else { + return { + title: this.props.t("Thank you for participating!"), + titleClass: "text-success", + shortText: this.props.t("Thank you for participating!"), + }; + } + }; eventStatus = (event) => { - if (event.miniconf_url) { - return { - title: this.props.t("Virtual Event Now Open"), - titleClass: "text-success", - longText: `${event.name} ` + this.props.t("is now open! Please visit our virtual event site to attend."), - shortText: this.props.t('Go To Virtual Event >'), - linkClass: "btn-success", - link: this.buildMiniConfUrl(event) - } - } - else { - return { - title: this.props.t("Enjoy the Event"), - titleClass: "text-success", - shortText: this.props.t("Now Live") - } - } - - } + if (event.miniconf_url) { + return { + title: this.props.t("Virtual Event Now Open"), + titleClass: "text-success", + longText: + `${event.name} ` + + this.props.t( + "is now open! Please visit our virtual event site to attend." + ), + shortText: this.props.t("Go To Virtual Event >"), + linkClass: "btn-success", + link: this.buildMiniConfUrl(event), + }; + } else { + return { + title: this.props.t("Enjoy the Event"), + titleClass: "text-success", + shortText: this.props.t("Now Live"), + }; + } + }; eventComingSoonStatus = (event) => { if (event.miniconf_url) { - return { - title: this.props.t("Virtual Event Opening Soon!"), - titleClass: "text-success", - longText: `${event.name} ` + this.props.t("is opening soon - our virtual event site will be live on") + ` ${event.start_date}.`, - shortText: this.props.t("Virtual Event Opens") + ` ${event.start_date}`, - } - } - else { - return { - title: this.props.t("Enjoy the Event"), - titleClass: "text-success", - shortText: `Starts ${event.start_date}!` - } + return { + title: this.props.t("Virtual Event Opening Soon!"), + titleClass: "text-success", + longText: + `${event.name} ` + + this.props.t( + "is opening soon - our virtual event site will be live on" + ) + + ` ${event.start_date}.`, + shortText: this.props.t("Virtual Event Opens") + ` ${event.start_date}`, + }; + } else { + return { + title: this.props.t("Enjoy the Event"), + titleClass: "text-success", + shortText: `Starts ${event.start_date}!`, + }; } - } + }; mapStatus = (event) => { - if (event.status === null) { - return this.unknownStatus("Status", this.props.t("Please try refresh the page and/or clear your cookies")); - } + if (event.status === null) { + return this.unknownStatus( + "Status", + this.props.t("Please try refresh the page and/or clear your cookies") + ); + } - if (!event.is_event_opening && event.status.is_event_attendee) { - return this.pastEventStatus(event); - } + if (!event.is_event_opening && event.status.is_event_attendee) { + return this.pastEventStatus(event); + } - if (!event.is_registration_open && event.is_event_opening && event.status.is_event_attendee) { - return this.eventComingSoonStatus(event); - } + if ( + !event.is_registration_open && + event.is_event_opening && + event.status.is_event_attendee + ) { + return this.eventComingSoonStatus(event); + } - if (event.is_event_open && event.status.is_event_attendee) { - return this.eventStatus(event); - } + if (event.is_event_open && event.status.is_event_attendee) { + return this.eventStatus(event); + } - if (event.status.invited_guest) { - return this.invitedGuestStatus(event); - } - - if (event.is_application_open) { - return this.applicationStatus(event); - } + if (event.status.invited_guest) { + return this.invitedGuestStatus(event); + } - if (event.status.offer_status) { - return this.offerStatus(event); - } + if (event.is_application_open) { + return this.applicationStatus(event); + } - if (event.status.outcome_status) { - return this.outcomeStatus(event); - } + if (event.status.offer_status) { + return this.offerStatus(event); + } - return this.applicationClosedStatus(event); + if (event.status.outcome_status) { + return this.outcomeStatus(event); + } - } + return this.applicationClosedStatus(event); + }; renderButton = (definition) => { - return {definition.shortText} - } + return ( + + {definition.shortText} + + ); + }; render() { const definition = this.mapStatus(this.props.event); if (this.props.longForm) { - return
-

{definition.title}

-

{definition.longText}


- {definition.shortText && definition.link && this.renderButton(definition)} + return ( +
+

{definition.title}

+

{definition.longText}

+
+ {definition.shortText && + definition.link && + this.renderButton(definition)}
- } - else { - if (definition.shortText && definition.link && definition.title !== definition.shortText) { - return
- {definition.title}
{this.renderButton(definition)} -
- } - else if (definition.shortText && definition.link) { - return this.renderButton(definition); - } - else { - return {definition.shortText} - } + ); + } else { + if ( + definition.shortText && + definition.link && + definition.title !== definition.shortText + ) { + return ( +
+ {definition.title} +
+ {this.renderButton(definition)} +
+ ); + } else if (definition.shortText && definition.link) { + return this.renderButton(definition); + } else { + return {definition.shortText}; + } } } } diff --git a/webapp/src/components/FileUpload.js b/webapp/src/components/FileUpload.js index 853df881c..b4fb15626 100644 --- a/webapp/src/components/FileUpload.js +++ b/webapp/src/components/FileUpload.js @@ -4,64 +4,68 @@ import { fileService } from "../services/file/file.service"; import { withRouter } from "react-router"; class FileUploadComponent extends Component { - constructor(props) { - super(props); - this.state = { - uploadPercentComplete: 0, - uploading: false, - uploaded: false, - uploadError: "" - }; - } - - handleUploadFile = file => { - this.setState({ - uploading: true - }, () => { + constructor(props) { + super(props); + this.state = { + uploadPercentComplete: 0, + uploading: false, + uploaded: false, + uploadError: "", + }; + } + + handleUploadFile = (file) => { + this.setState( + { + uploading: true, + }, + () => { fileService - .uploadFile(file, progressEvent => { + .uploadFile(file, (progressEvent) => { const percentCompleted = Math.round( - (progressEvent.loaded * 100) / progressEvent.total); + (progressEvent.loaded * 100) / progressEvent.total + ); this.setState({ - uploadPercentComplete: percentCompleted + uploadPercentComplete: percentCompleted, }); }) - .then(response => { + .then((response) => { if (response.fileId && this.props.onChange) { this.props.onChange( this.props.id, JSON.stringify({ filename: response.fileId, - rename: file.name + rename: file.name, }) ); } this.setState({ uploaded: response.fileId !== "", uploadError: response.error, - uploading: false + uploading: false, }); }); } - ); - }; - - render() { - return ( - - ); - } + ); + }; + + render() { + return ( + + ); } +} - export default withRouter(FileUploadComponent); \ No newline at end of file +export default withRouter(FileUploadComponent); diff --git a/webapp/src/components/Loading.js b/webapp/src/components/Loading.js index 7c9fe1f8b..20e393b5f 100644 --- a/webapp/src/components/Loading.js +++ b/webapp/src/components/Loading.js @@ -1,24 +1,20 @@ import React, { Component } from "react"; class Loading extends Component { + render() { + const loadingStyle = { + width: "3rem", + height: "3rem", + }; - render() { - const loadingStyle = { - width: "3rem", - height: "3rem" - }; - - return ( -
-
- Loading... -
-
- ); - } - + return ( +
+
+ Loading... +
+
+ ); + } } export default Loading; diff --git a/webapp/src/components/PrivateRoute.jsx b/webapp/src/components/PrivateRoute.jsx index 997e30d6d..498c95959 100644 --- a/webapp/src/components/PrivateRoute.jsx +++ b/webapp/src/components/PrivateRoute.jsx @@ -1,10 +1,17 @@ -import React from 'react'; -import { Route, Redirect } from 'react-router-dom'; +import React from "react"; +import { Route, Redirect } from "react-router-dom"; export const PrivateRoute = ({ component: Component, ...rest }) => ( - ( - localStorage.getItem('user') - ? - : - )} /> -) \ No newline at end of file + + localStorage.getItem("user") ? ( + + ) : ( + + ) + } + /> +); diff --git a/webapp/src/components/User.js b/webapp/src/components/User.js index 736bcb66d..39bd01dee 100644 --- a/webapp/src/components/User.js +++ b/webapp/src/components/User.js @@ -3,18 +3,18 @@ import { NavLink } from "react-router-dom"; import { withRouter } from "react-router"; import { userService } from "../services/user"; import isEqual from "lodash.isequal"; -import { withTranslation } from 'react-i18next'; +import { withTranslation } from "react-i18next"; class UserDropdown extends Component { constructor(props) { super(props); this.state = { - user: props.user + user: props.user, }; } - componentWillReceiveProps = props => { + componentWillReceiveProps = (props) => { let userFromStorage = JSON.parse(localStorage.getItem("user")); if (!isEqual(userFromStorage, props.user)) { this.setState({ user: userFromStorage }); @@ -54,7 +54,10 @@ class UserDropdown extends Component { > {t("Profile")} -
diff --git a/webapp/src/components/ViewFile.js b/webapp/src/components/ViewFile.js index e7ced894d..67a3bd7f5 100644 --- a/webapp/src/components/ViewFile.js +++ b/webapp/src/components/ViewFile.js @@ -1,36 +1,36 @@ import React, { Component } from "react"; import { withRouter } from "react-router"; -import { withTranslation } from 'react-i18next'; -import { getDownloadURL } from '../utils/files'; +import { withTranslation } from "react-i18next"; +import { getDownloadURL } from "../utils/files"; class ViewFile extends Component { - - constructor(props) { - super(props); - this.state = { - fileLocation: null - } - } + constructor(props) { + super(props); + this.state = { + fileLocation: null, + }; + } - componentDidMount() { - if (this.props.match && this.props.match.params) { - const filename = this.props.match.params.filename; - const fileLocation = getDownloadURL(filename); - this.setState({ - fileLocation: fileLocation - }); - window.location = fileLocation; - } + componentDidMount() { + if (this.props.match && this.props.match.params) { + const filename = this.props.match.params.filename; + const fileLocation = getDownloadURL(filename); + this.setState({ + fileLocation: fileLocation, + }); + window.location = fileLocation; } + } - render() { - const t = this.props.t; - return ( -
- {t("Click here")} {t("to download your file if it does not happen automatically")}. -
- ); - } + render() { + const t = this.props.t; + return ( +
+ {t("Click here")}{" "} + {t("to download your file if it does not happen automatically")}. +
+ ); } +} - export default withRouter(withTranslation()(ViewFile)); \ No newline at end of file +export default withRouter(withTranslation()(ViewFile)); diff --git a/webapp/src/components/answerValue/AnswerValue.js b/webapp/src/components/answerValue/AnswerValue.js index bc789a14f..d7dfea048 100644 --- a/webapp/src/components/answerValue/AnswerValue.js +++ b/webapp/src/components/answerValue/AnswerValue.js @@ -1,16 +1,16 @@ -import Linkify from 'react-linkify'; +import Linkify from "react-linkify"; import React from "react"; -import { withTranslation } from 'react-i18next'; +import { withTranslation } from "react-i18next"; -import { getDownloadURL } from '../../utils/files'; -import MultiFileValue from './MultiFileValue' +import { getDownloadURL } from "../../utils/files"; +import MultiFileValue from "./MultiFileValue"; const FILE = "file"; -const INFORMATION = 'information' +const INFORMATION = "information"; const MULTI_CHOICE = "multi-choice"; -const MULTI_FILE = 'multi-file'; +const MULTI_FILE = "multi-file"; -function AnswerValue({answer, question, t}) { +function AnswerValue({ answer, question, t }) { if (answer && answer.question_type) { if (answer.question_type === INFORMATION) { return ""; @@ -24,32 +24,45 @@ function AnswerValue({answer, question, t}) { if (answer && answer.value) { switch (question.type) { case MULTI_CHOICE: - const opts = question.options || answer.options - const options = opts.filter(o => o.value === answer.value); + const opts = question.options || answer.options; + const options = opts.filter((o) => o.value === answer.value); if (options && options.length > 0) { return options[0].label; - } - else { + } else { return answer.value; } case FILE: - return
- {t("Uploaded File")} -
- - *{t("Note: You may need to change the file name to open the file on certain operating systems")} - -
+ return ( +
+ + {t("Uploaded File")} + +
+ + * + {t( + "Note: You may need to change the file name to open the file on certain operating systems" + )} + +
+ ); case MULTI_FILE: - return
- -
- - *{t("Note: You may need to change the file name to open the file on certain operating systems")} - -
+ return ( +
+ +
+ + * + {t( + "Note: You may need to change the file name to open the file on certain operating systems" + )} + +
+ ); default: - return {answer.value} + return ( + {answer.value} + ); } } return t("No answer provided."); diff --git a/webapp/src/components/answerValue/MultiFileValue.js b/webapp/src/components/answerValue/MultiFileValue.js index 5cc93dbee..e6a77752b 100644 --- a/webapp/src/components/answerValue/MultiFileValue.js +++ b/webapp/src/components/answerValue/MultiFileValue.js @@ -1,17 +1,20 @@ import React from "react"; -import { getDownloadURL } from '../../utils/files'; +import { getDownloadURL } from "../../utils/files"; -function MultiFileValue({value}) { +function MultiFileValue({ value }) { const files = JSON.parse(value); - return ; + return ( + + ); } export default MultiFileValue; diff --git a/webapp/src/components/answerValue/__tests__/AnswerValue.test.js b/webapp/src/components/answerValue/__tests__/AnswerValue.test.js index 481701720..3bb0f3eeb 100644 --- a/webapp/src/components/answerValue/__tests__/AnswerValue.test.js +++ b/webapp/src/components/answerValue/__tests__/AnswerValue.test.js @@ -1,65 +1,64 @@ -import Linkify from 'react-linkify'; -import React from 'react'; -import { shallow } from 'enzyme'; +import Linkify from "react-linkify"; +import React from "react"; +import { shallow } from "enzyme"; -import AnswerValue from '../AnswerValue'; -import MultiFileValue from '../MultiFileValue' +import AnswerValue from "../AnswerValue"; +import MultiFileValue from "../MultiFileValue"; const baseUrl = process.env.REACT_APP_API_URL; -jest.mock('react-i18next', () => ({ +jest.mock("react-i18next", () => ({ // this mock makes sure any components using the translate HoC receive the t function as a prop - withTranslation: () => Component => { - Component.defaultProps = { ...Component.defaultProps, t: v => v }; - return Component; + withTranslation: () => (Component) => { + Component.defaultProps = { ...Component.defaultProps, t: (v) => v }; + return Component; }, })); -describe('AnswerValue Tests', () => { - - it('Renders AnswerValue component.', () => { +describe("AnswerValue Tests", () => { + it("Renders AnswerValue component.", () => { const answer = { - value: 'Lorem' - } + value: "Lorem", + }; const question = { - type: 'information' - } + type: "information", + }; const wrapper = shallow( ); expect(wrapper.length).toEqual(1); }); - it('Renders file.', () => { - const answer = {value: 'lorem.txt'} - const question = {type: 'file'} + it("Renders file.", () => { + const answer = { value: "lorem.txt" }; + const question = { type: "file" }; const wrapper = shallow( ); - const href = baseUrl + "/api/v1/file?filename=" + answer.value - expect(wrapper.contains( - - Uploaded File - - )).toEqual(true); + const href = baseUrl + "/api/v1/file?filename=" + answer.value; + expect( + wrapper.contains( + + Uploaded File + + ) + ).toEqual(true); }); - it('Renders multi-file.', () => { - const answer = {value: ['lorem.txt']} - const question = {type: 'multi-file'} + it("Renders multi-file.", () => { + const answer = { value: ["lorem.txt"] }; + const question = { type: "multi-file" }; const wrapper = shallow( ); - expect(wrapper.contains( - - )); + expect(wrapper.contains()); }); it('Renders nothing for "information" type.', () => { - const answer = {} + const answer = {}; const question = { - type: 'information' - } + type: "information", + }; const wrapper = shallow( ); @@ -67,26 +66,26 @@ describe('AnswerValue Tests', () => { }); it('Renders nothing for "information" answer question type.', () => { - const answer = {question_type: 'information'} - const question = {} + const answer = { question_type: "information" }; + const question = {}; const wrapper = shallow( ); expect(wrapper.equals("")).toEqual(true); }); - it('Renders multi-choice.', () => { + it("Renders multi-choice.", () => { const answer = { - value: 'ipsum' - } + value: "ipsum", + }; const question = { - type: 'multi-choice', + type: "multi-choice", options: [ - { label: 'Lorem', value: 'lorem' }, - { label: 'Ipsum', value: 'ipsum' }, - { label: 'Dolor', value: 'dolor' } - ] - } + { label: "Lorem", value: "lorem" }, + { label: "Ipsum", value: "ipsum" }, + { label: "Dolor", value: "dolor" }, + ], + }; const wrapper = shallow( ); @@ -94,31 +93,32 @@ describe('AnswerValue Tests', () => { }); it('Renders "No answer provided" when answer has no value.', () => { - const answer = {value: null} + const answer = { value: null }; const question = { - type: 'multi-choice', + type: "multi-choice", options: [ - { label: 'Lorem', value: 'lorem' }, - { label: 'Ipsum', value: 'ipsum' }, - { label: 'Dolor', value: 'dolor' } - ] - } + { label: "Lorem", value: "lorem" }, + { label: "Ipsum", value: "ipsum" }, + { label: "Dolor", value: "dolor" }, + ], + }; const wrapper = shallow( ); expect(wrapper.equals("No answer provided.")).toEqual(true); }); - it('Renders linkified answer value for all other types.', () => { - const answer = { value: 'lorem' } - const question = { type: 'ipsum' } + it("Renders linkified answer value for all other types.", () => { + const answer = { value: "lorem" }; + const question = { type: "ipsum" }; const wrapper = shallow( ); - expect(wrapper.contains( - lorem - )).toEqual(true); + expect( + wrapper.contains( + lorem + ) + ).toEqual(true); }); - -}); \ No newline at end of file +}); diff --git a/webapp/src/components/answerValue/__tests__/MultiFileValue.test.js b/webapp/src/components/answerValue/__tests__/MultiFileValue.test.js index 5c9553ea0..5e815b6f7 100644 --- a/webapp/src/components/answerValue/__tests__/MultiFileValue.test.js +++ b/webapp/src/components/answerValue/__tests__/MultiFileValue.test.js @@ -5,31 +5,27 @@ import MultiFileValue from "../MultiFileValue"; const prefix = process.env.REACT_APP_API_URL + "/api/v1/file?filename="; -describe('MultiFileValue Tests', () => { - - it('Renders MultiFileValue component.', () => { - const value = `[{"id":1,"file":"1.tst","name":"lorem.tst"},{"id":2,"file":"2.tst","name":"ipsum.tst"}]` - const wrapper = shallow( - - ); +describe("MultiFileValue Tests", () => { + it("Renders MultiFileValue component.", () => { + const value = `[{"id":1,"file":"1.tst","name":"lorem.tst"},{"id":2,"file":"2.tst","name":"ipsum.tst"}]`; + const wrapper = shallow(); expect(wrapper.length).toEqual(1); }); - it('Renders list of files.', () => { - const value = `[{"id":1,"filename":"1.tst","rename":"lorem.tst"},{"id":2,"filename":"2.tst","rename":"ipsum.tst"}]` - const wrapper = shallow( - - ); - expect(wrapper.contains( - - )).toEqual(true); + it("Renders list of files.", () => { + const value = `[{"id":1,"filename":"1.tst","rename":"lorem.tst"},{"id":2,"filename":"2.tst","rename":"ipsum.tst"}]`; + const wrapper = shallow(); + expect( + wrapper.contains( + + ) + ).toEqual(true); }); - }); diff --git a/webapp/src/components/answerValue/index.js b/webapp/src/components/answerValue/index.js index f43bbc25c..8734ff405 100644 --- a/webapp/src/components/answerValue/index.js +++ b/webapp/src/components/answerValue/index.js @@ -1,3 +1,3 @@ -import AnswerValue from './AnswerValue'; +import AnswerValue from "./AnswerValue"; export default AnswerValue; diff --git a/webapp/src/components/form/FormCheckbox.js b/webapp/src/components/form/FormCheckbox.js index f4439285b..6efd887e1 100644 --- a/webapp/src/components/form/FormCheckbox.js +++ b/webapp/src/components/form/FormCheckbox.js @@ -15,7 +15,11 @@ class FormCheckbox extends React.Component { } } - linkRenderer = (props) => {props.children} + linkRenderer = (props) => ( + + {props.children} + + ); render() { return ( @@ -25,7 +29,10 @@ class FormCheckbox extends React.Component { errorText={this.props.errorText} >
- + {this.props.description ? ( ) : ( @@ -44,7 +51,7 @@ class FormCheckbox extends React.Component { checked={this.props.value} onChange={this.props.onChange} min={this.props.min || null} - ref={input => { + ref={(input) => { this.nameInput = input; }} tabIndex={this.props.tabIndex} diff --git a/webapp/src/components/form/FormCreator.js b/webapp/src/components/form/FormCreator.js index 7cd635277..e4754ad50 100644 --- a/webapp/src/components/form/FormCreator.js +++ b/webapp/src/components/form/FormCreator.js @@ -1,61 +1,91 @@ -import React, { useEffect, createRef } from 'react'; -import { Redirect, Prompt } from 'react-router-dom'; -import { Trans } from 'react-i18next'; +import React, { useEffect, createRef } from "react"; +import { Redirect, Prompt } from "react-router-dom"; +import { Trans } from "react-i18next"; import { default as ReactSelect } from "react-select"; -import _ from 'lodash'; -import Section from '../../pages/createApplicationForm/components/Section'; -import Loading from '../Loading'; +import _ from "lodash"; +import Section from "../../pages/createApplicationForm/components/Section"; +import Loading from "../Loading"; import { - option, AnimateSections, - drop, drag, StageModal - } from '../../pages/createApplicationForm/components/util'; - import { dateFormat, TopBar } from '../../utils/forms'; - - + option, + AnimateSections, + drop, + drag, + StageModal, +} from "../../pages/createApplicationForm/components/util"; +import { dateFormat, TopBar } from "../../utils/forms"; const FormCreator = ({ - languages, event: evt, t, sections, - setSections, setNominate, language, - setLanguage, dragId, setDragId, - applyTransition, setApplytransition, - parentDropable, setParentDropable, - homeRedirect, initialState, errorResponse, - disableSaveBtn, setDisableSaveBtn, - events, setEvent, eventService, addSection, - handleSave, isSaved, isReview, addQuestion, - addAnswerFromAppForm, appSections, leaveStage, - setLeaveStage, showingModal, setShowingModal, - title, EventMeta,hasKey, hasDependancy, - hasSpecialQuestion, setTotalStages, - memoizedStage, setStg, memoizedTotalStages, - isNewStage, isReviewLoading + languages, + event: evt, + t, + sections, + setSections, + setNominate, + language, + setLanguage, + dragId, + setDragId, + applyTransition, + setApplytransition, + parentDropable, + setParentDropable, + homeRedirect, + initialState, + errorResponse, + disableSaveBtn, + setDisableSaveBtn, + events, + setEvent, + eventService, + addSection, + handleSave, + isSaved, + isReview, + addQuestion, + addAnswerFromAppForm, + appSections, + leaveStage, + setLeaveStage, + showingModal, + setShowingModal, + title, + EventMeta, + hasKey, + hasDependancy, + hasSpecialQuestion, + setTotalStages, + memoizedStage, + setStg, + memoizedTotalStages, + isNewStage, + isReviewLoading, }) => { const lang = languages; const saved = _.isEqual(initialState, sections); let maxSurrogateId = 1; - sections.forEach(s => { + sections.forEach((s) => { if (s.backendId > maxSurrogateId) { maxSurrogateId = s.backendId; } - s.questions.forEach(q => { + s.questions.forEach((q) => { if (q.backendId > maxSurrogateId) { - maxSurrogateId = q.backendId + maxSurrogateId = q.backendId; } if (q.surrogate_id > maxSurrogateId) { - maxSurrogateId = q.surrogate_id + maxSurrogateId = q.surrogate_id; } - }) - }) + }); + }); useEffect(() => { const eventId = evt.id; - eventService.getEvent(eventId).then( res => { + eventService.getEvent(eventId).then((res) => { setEvent({ loading: false, event: res.event, - error: res.error - }) + error: res.error, + }); }); }, []); @@ -64,21 +94,21 @@ const FormCreator = ({ setTimeout(() => { setNominate(val); }, 1); - } + }; const handleLanguageChange = (e) => { setLanguage(e); - } + }; const handleSection = (input) => { setSections(input); - } + }; const handleDrag = (e) => { - if(parentDropable) { + if (parentDropable) { drag(e, setDragId); } - } + }; const handleDragOver = (e) => { e.preventDefault(); @@ -91,28 +121,30 @@ const FormCreator = ({ elements: sections, dragId, setState: setSections, - setAnimation: setApplytransition + setAnimation: setApplytransition, }); } - } + }; const options = () => { - return lang.map(l => option({ - value: l.code, - label: l.description, - t - })); - } + return lang.map((l) => + option({ + value: l.code, + label: l.description, + t, + }) + ); + }; let isSaveDisabled = false; - sections.forEach(s => { - for(let key of Object.keys(s.name)) { + sections.forEach((s) => { + for (let key of Object.keys(s.name)) { if (!s.name[key]) { isSaveDisabled = true; } } - s.questions.forEach(q => { - for(let key of Object.keys(q.headline)) { + s.questions.forEach((q) => { + for (let key of Object.keys(q.headline)) { if (!q.headline[key]) { isSaveDisabled = true; } @@ -120,22 +152,16 @@ const FormCreator = ({ if (!q.type) { isSaveDisabled = true; } - }) - }) - - + }); + }); - const {loading, event: evnt, error } = events; + const { loading, event: evnt, error } = events; if (loading || isReviewLoading) { - return + return ; } if (error) { - return ( -
- {error} -
- ) + return
{error}
; } const eventKey = evt.key; @@ -143,38 +169,40 @@ const FormCreator = ({ <> + message={t( + "Some Changes have not been saved. Are you sure you want to leave without saving them?" + )} + /> {homeRedirect && } {errorResponse ? ( -
- {{errorResponse}} +
+ + {{ errorResponse }} +
) : ( -
- +
+
+ style={{ textAlign: "end", width: "61%" }} + className="add-section-btn-wrapper" + > + />
+ /> handleLanguageChange(e)} + onChange={(e) => handleLanguageChange(e)} value={language} defaultValue={language} - className='select-language' + className="select-language" styles={{ control: (base, state) => ({ ...base, boxShadow: "none", border: state.isFocused && "none", - transition: state.isFocused && 'color,background-color 1.5s ease-out', - background: state.isFocused && 'lightgray', - color: '#fff' + transition: + state.isFocused && "color,background-color 1.5s ease-out", + background: state.isFocused && "lightgray", + color: "#fff", }), option: (base, state) => ({ - ...base, - backgroundColor: state.isFocused && "#1f2d3e", - color: state.isFocused && "#fff" - }) + ...base, + backgroundColor: state.isFocused && "#1f2d3e", + color: state.isFocused && "#fff", + }), }} menuPlacement="auto" /> @@ -223,44 +252,41 @@ const FormCreator = ({ applyTransition={applyTransition} setApplytransition={setApplytransition} > - { - sections - .map((section, i) => ( -
- )) - } + {sections.map((section, i) => ( +
+ ))}
)} - ) -} + ); +}; export default FormCreator; diff --git a/webapp/src/components/form/FormDate.js b/webapp/src/components/form/FormDate.js index 1a835735e..e7983613c 100644 --- a/webapp/src/components/form/FormDate.js +++ b/webapp/src/components/form/FormDate.js @@ -1,9 +1,9 @@ import React from "react"; -import DateTimePicker from 'react-datetime-picker' +import DateTimePicker from "react-datetime-picker"; import FormGroup from "./FormGroup"; import FormToolTip from "./FormToolTip"; import "./Style.css"; -import * as moment from 'moment'; +import * as moment from "moment"; import ReactMarkdown from "react-markdown"; class FormDate extends React.Component { @@ -13,25 +13,25 @@ class FormDate extends React.Component { shouldDisplayError = () => { return this.props.showError && this.props.errorText !== ""; - }; componentWillReceiveProps(nextProps) { if (nextProps.showFocus) { this.dateInput.focus(); } - - - } - onChange = value => { + onChange = (value) => { if (value && this.props.onChange) { this.props.onChange(moment(value).format("YYYY-MM-DD")); } - } + }; - linkRenderer = (props) => {props.children} + linkRenderer = (props) => ( + + {props.children} + + ); render() { return ( @@ -41,26 +41,33 @@ class FormDate extends React.Component { errorText={this.props.errorText} >
- + {this.props.description ? ( ) : ( -
- )} - +
+ )}
{ + ref={(input) => { this.dateInput = input; }} - className={this.props.id && this.props.errorText && this.props.errorText.length > 0 ? "react-datetime-picker error" : "react-datetime-picker"} + className={ + this.props.id && + this.props.errorText && + this.props.errorText.length > 0 + ? "react-datetime-picker error" + : "react-datetime-picker" + } onChange={this.onChange} value={this.props.value ? new Date(this.props.value) : null} format="y-MM-dd" /> -
); diff --git a/webapp/src/components/form/FormFileUpload.js b/webapp/src/components/form/FormFileUpload.js index df7420883..d13dba43f 100644 --- a/webapp/src/components/form/FormFileUpload.js +++ b/webapp/src/components/form/FormFileUpload.js @@ -2,7 +2,7 @@ import React from "react"; import FormGroup from "./FormGroup"; import FormToolTip from "./FormToolTip"; import "./Style.css"; -import { withTranslation } from 'react-i18next'; +import { withTranslation } from "react-i18next"; import { getDownloadURL } from "../../utils/files"; import ReactMarkdown from "react-markdown"; @@ -17,17 +17,21 @@ class FormFileUpload extends React.Component { } } - onChange = event => { - if (this.props.uploadFile) { - this.props.uploadFile(event.target.files[0]); - } - } + onChange = (event) => { + if (this.props.uploadFile) { + this.props.uploadFile(event.target.files[0]); + } + }; - linkRenderer = (props) => {props.children} + linkRenderer = (props) => ( + + {props.children} + + ); render() { var progressStyle = { - width: this.props.uploadPercentComplete.toString() + "%" + width: this.props.uploadPercentComplete.toString() + "%", }; const t = this.props.t; @@ -38,39 +42,60 @@ class FormFileUpload extends React.Component { id={this.props.id + "-group"} errorText={this.props.errorText} > -
- - {this.props.description ? ( - - ) : ( -
- )} -
- { - this.nameInput = input; - }} - accept={this.props.options && this.props.options.accept ? this.props.options.accept : ".pdf, application/pdf" } - tabIndex={this.props.tabIndex} - autoFocus={this.props.autoFocus} - required={this.props.required || null} - disabled={this.props.uploading} +
+ - {this.props.uploading &&
-
-
} - {(this.props.uploaded || this.props.value) && - {t('Uploaded file')}} + {this.props.description ? ( + + ) : ( +
+ )} +
+ { + this.nameInput = input; + }} + accept={ + this.props.options && this.props.options.accept + ? this.props.options.accept + : ".pdf, application/pdf" + } + tabIndex={this.props.tabIndex} + autoFocus={this.props.autoFocus} + required={this.props.required || null} + disabled={this.props.uploading} + /> + {this.props.uploading && ( +
+
+
+ )} + {(this.props.uploaded || this.props.value) && ( + + {t("Uploaded file")} + + )}
); diff --git a/webapp/src/components/form/FormGroup.js b/webapp/src/components/form/FormGroup.js index 045f9c05d..5f526acbc 100644 --- a/webapp/src/components/form/FormGroup.js +++ b/webapp/src/components/form/FormGroup.js @@ -6,9 +6,7 @@ class FormGroup extends React.Component { return (
{this.props.children} -
- {this.props.errorText} -
+
{this.props.errorText}
); } diff --git a/webapp/src/components/form/FormMultiCheckbox.js b/webapp/src/components/form/FormMultiCheckbox.js index d3f06a2c1..ca96e217a 100644 --- a/webapp/src/components/form/FormMultiCheckbox.js +++ b/webapp/src/components/form/FormMultiCheckbox.js @@ -9,17 +9,17 @@ class FormMultiCheckbox extends React.Component { super(props); this.state = { options: [], - checked: [] + checked: [], }; } componentDidMount() { let checkedValues = []; if (this.props.defaultValue) { - checkedValues = this.props.defaultValue.split(' ; '); + checkedValues = this.props.defaultValue.split(" ; "); } this.setState({ - checked: checkedValues + checked: checkedValues, }); } @@ -34,21 +34,24 @@ class FormMultiCheckbox extends React.Component { } onCheckChanged = (option, checked) => { - this.setState(prevState => { - var newChecked = prevState.checked.filter(c => c !== option.value); - if (checked) { - newChecked.push(option.value); + this.setState( + (prevState) => { + var newChecked = prevState.checked.filter((c) => c !== option.value); + if (checked) { + newChecked.push(option.value); + } + newChecked = newChecked.sort(); + return { + checked: newChecked, + }; + }, + () => { + if (this.props.onChange) { + this.props.onChange(this.state.checked.join(" ; ")); + } } - newChecked = newChecked.sort(); - return { - checked: newChecked - }; - }, () => { - if (this.props.onChange) { - this.props.onChange(this.state.checked.join(' ; ')); - } - }); - } + ); + }; renderFormCheckbox = (option) => { let id = "checkbox_" + this.props.id + "_" + option.value; @@ -63,20 +66,26 @@ class FormMultiCheckbox extends React.Component { } type="checkbox" checked={this.state.checked.includes(option.value)} - onChange={e => { + onChange={(e) => { this.onCheckChanged(option, e.target.checked); }} - ref={input => { + ref={(input) => { this.nameInput = input; }} - key={this.props.id + '_check_' + option.value} + key={this.props.id + "_check_" + option.value} /> - +
); - } + }; - linkRenderer = (props) => {props.children} + linkRenderer = (props) => ( + + {props.children} + + ); render() { return ( @@ -88,15 +97,20 @@ class FormMultiCheckbox extends React.Component { autoFocus={this.props.autoFocus} >
- + {this.props.description ? ( ) : ( -
- )} +
+ )}
- {this.props.options.map((option) => this.renderFormCheckbox(option))} + {this.props.options.map((option) => + this.renderFormCheckbox(option) + )}
diff --git a/webapp/src/components/form/FormMultiCheckboxOther.js b/webapp/src/components/form/FormMultiCheckboxOther.js index cce18ff5e..2837d6940 100644 --- a/webapp/src/components/form/FormMultiCheckboxOther.js +++ b/webapp/src/components/form/FormMultiCheckboxOther.js @@ -4,114 +4,127 @@ import React from "react"; import "./Style.css"; import _ from "lodash"; import FormMultiCheckbox from "./FormMultiCheckbox"; -import { withTranslation } from 'react-i18next'; +import { withTranslation } from "react-i18next"; class FormMultiCheckboxOther extends React.Component { - constructor(props) { - super(props); - this.state = { - options: [], - values: [], - showOther: false, - otherText: "", - }; + constructor(props) { + super(props); + this.state = { + options: [], + values: [], + showOther: false, + otherText: "", + }; + } + + getDefaultValues = () => { + let defaultValues = this.props.defaultValue.split(" ; "); + const otherValue = _.find( + defaultValues, + (v) => !this.props.options.some((o) => o.value === v) + ); + if (otherValue) { + defaultValues = defaultValues.filter((v) => v !== otherValue); + defaultValues.push("other"); } - - getDefaultValues = () => { - let defaultValues = this.props.defaultValue.split(' ; '); - const otherValue = _.find(defaultValues, v => !this.props.options.some(o => o.value === v)); - if (otherValue) { - defaultValues = defaultValues.filter(v => v !== otherValue); - defaultValues.push('other'); - } - return { - defaultValues: defaultValues, - otherValue: otherValue - }; - } - - componentDidMount() { - const options = this.props.options; - if (!options.some(o => o.value === "other")) { - options.push({ - 'value': 'other', - 'label': this.props.t("Other") + '...' - }); - } - - const defaultValues = this.getDefaultValues(); - if (defaultValues.defaultValues.some(d => d === "other")) { - this.setState({ - showOther: true, - otherText: defaultValues.otherValue - }); - } - - - this.setState({ - options: options, - }); + return { + defaultValues: defaultValues, + otherValue: otherValue, + }; + }; + + componentDidMount() { + const options = this.props.options; + if (!options.some((o) => o.value === "other")) { + options.push({ + value: "other", + label: this.props.t("Other") + "...", + }); } - notifyValueChanged = () => { - const values = this.state.values.filter(v => v !== "other"); - if (this.state.showOther) { - values.push(this.state.otherText); - } - if (this.props.onChange) { - this.props.onChange(values.join(' ; ')); - } + const defaultValues = this.getDefaultValues(); + if (defaultValues.defaultValues.some((d) => d === "other")) { + this.setState({ + showOther: true, + otherText: defaultValues.otherValue, + }); } - onChange = (value) => { - let values = value.split(' ; '); + this.setState({ + options: options, + }); + } - this.setState({ - showOther: values.some(v => v === "other"), - values: values - }, () => { - this.notifyValueChanged(); - }); + notifyValueChanged = () => { + const values = this.state.values.filter((v) => v !== "other"); + if (this.state.showOther) { + values.push(this.state.otherText); } - - onOtherChanged = e => { - this.setState({ - otherText: e.target.value.replace(";", "") - }, () => { this.notifyValueChanged(); }); - } - - render() { - const t = this.props.t; - - return
- - - {this.state.showOther && -
- -
- -
-
} -
+ if (this.props.onChange) { + this.props.onChange(values.join(" ; ")); } - + }; + + onChange = (value) => { + let values = value.split(" ; "); + + this.setState( + { + showOther: values.some((v) => v === "other"), + values: values, + }, + () => { + this.notifyValueChanged(); + } + ); + }; + + onOtherChanged = (e) => { + this.setState( + { + otherText: e.target.value.replace(";", ""), + }, + () => { + this.notifyValueChanged(); + } + ); + }; + + render() { + const t = this.props.t; + + return ( +
+ + {this.state.showOther && ( +
+ +
+ +
+
+ )} +
+ ); + } } export default withTranslation()(FormMultiCheckboxOther); diff --git a/webapp/src/components/form/FormMultiFile.js b/webapp/src/components/form/FormMultiFile.js index e7d13debc..19cfbfbc7 100644 --- a/webapp/src/components/form/FormMultiFile.js +++ b/webapp/src/components/form/FormMultiFile.js @@ -1,160 +1,188 @@ import React, { Component } from "react"; import { withTranslation } from "react-i18next"; import FormGroup from "./FormGroup"; -import MultiFileComponent from './MultiFileComponent'; +import MultiFileComponent from "./MultiFileComponent"; import "./Style.css"; import _ from "lodash"; import ReactMarkdown from "react-markdown"; import FormToolTip from "./FormToolTip"; - export class FormMultiFile extends Component { - constructor(props) { - super(props); - - const initLength = (props.options && props.options.num_uploads) || 1; - const initList = [] - for (let i = 0; i < initLength; i++) { - initList.push({ id: i+1, name: null, file: null }); - } + constructor(props) { + super(props); - this.state = { - fileList: initList, - addError: false - } + const initLength = (props.options && props.options.num_uploads) || 1; + const initList = []; + for (let i = 0; i < initLength; i++) { + initList.push({ id: i + 1, name: null, file: null }); } - componentWillMount() { - if (this.props.value) { - this.setState({ - fileList: JSON.parse(this.props.value), - }) - } + this.state = { + fileList: initList, + addError: false, + }; + } + + componentWillMount() { + if (this.props.value) { + this.setState({ + fileList: JSON.parse(this.props.value), + }); + } + } + + // add file + addFile = () => { + let handleList = this.state.fileList; + // check against empty fields + let condition = handleList.every((val) => val.file); + + // variables + let addError = condition ? false : true; + + // add item if there is no empty fields + if (condition) { + handleList.push({ + id: _.max(handleList.map((h) => h.id)) + 1, + name: null, + file: null, + }); } - - // add file - addFile = () => { - let handleList = this.state.fileList; - // check against empty fields - let condition = handleList.every(val => val.file); - - // variables - let addError = condition ? false : true; - - - // add item if there is no empty fields - if (condition) { - handleList.push({ - id: _.max(handleList.map(h=>h.id)) + 1, - name: null, - file: null + this.setState( + { + addError: addError, + fileList: handleList, + }, + () => { + condition = true; + addError = false; + } + ); + }; + + del = (id) => { + const newList = this.state.fileList.filter((h) => h.id !== id); + + // setState and Callback functions + this.setState( + { + fileList: newList, + }, + () => { + if (this.props.onChange) { + const filteredList = this.state.fileList.filter((f) => f.file); + this.props.onChange(JSON.stringify(filteredList)); + } + } + ); + }; + + //handle upload + handleUpload = (id, file, name, rename) => { + console.log( + "Uploading file with id: ", + id, + " file: ", + file, + " name: ", + name, + " rename: ", + rename + ); + const handleList = this.state.fileList; + return new Promise((resolve, reject) => { + // Add new value + const existing = _.find(handleList, (h) => h.id === id); + if (!existing.file) { + // New file, upload it + if (this.props.uploadFile) { + this.props.uploadFile(file).then((fileId) => { + existing.name = name; + existing.filename = fileId; + existing.file = fileId; + existing.rename = rename; + this.setState({ fileList: handleList }, () => { + if (this.props.onChange) { + const filteredList = this.state.fileList.filter((f) => f.file); + this.props.onChange(JSON.stringify(filteredList)); + } + resolve(fileId); }); + // TODO: Handle errors from upload + }); } - - this.setState({ - addError: addError, - fileList: handleList - }, () => { - condition = true; - addError = false - }) - } - - del = (id) => { - const newList = this.state.fileList.filter(h=>h.id !== id); - - // setState and Callback functions - this.setState({ - fileList: newList - }, - () => { - if (this.props.onChange) { - const filteredList = this.state.fileList.filter(f=>f.file); - this.props.onChange(JSON.stringify(filteredList)); - } - }) - } - - - //handle upload - handleUpload = (id, file, name, rename) => { - console.log("Uploading file with id: ", id, " file: ", file, " name: ", name, " rename: ", rename); - const handleList = this.state.fileList; - return new Promise((resolve, reject) => { - // Add new value - const existing = _.find(handleList, h=>h.id === id); - if (!existing.file) { // New file, upload it - if (this.props.uploadFile) { - this.props.uploadFile(file).then(fileId => { - existing.name = name; - existing.filename = fileId; - existing.file = fileId; - existing.rename = rename; - this.setState({fileList: handleList}, () => { - if (this.props.onChange) { - const filteredList = this.state.fileList.filter(f=>f.file); - this.props.onChange(JSON.stringify(filteredList)); - } - resolve(fileId); - }); - // TODO: Handle errors from upload - }) - } - } - else { // Already uploaded, update metadata - existing.name = name; - this.setState({fileList: handleList}, () => { - if (this.props.onChange) { - const filteredList = this.state.fileList.filter(f=>f.file); - this.props.onChange(JSON.stringify(filteredList)); - } - resolve(existing.file); - }); - } + } else { + // Already uploaded, update metadata + existing.name = name; + this.setState({ fileList: handleList }, () => { + if (this.props.onChange) { + const filteredList = this.state.fileList.filter((f) => f.file); + this.props.onChange(JSON.stringify(filteredList)); + } + resolve(existing.file); }); + } + }); + }; + + linkRenderer = (props) => ( + + {props.children} + + ); + + render() { + const t = this.props.t; + let canAddFile = true; + if (this.props.options && this.props.options.num_uploads) { + canAddFile = this.props.options.num_uploads > this.state.fileList.length; } - linkRenderer = (props) => {props.children} - - render() { - const t = this.props.t - let canAddFile = true; - if (this.props.options && this.props.options.num_uploads) { - canAddFile = this.props.options.num_uploads > this.state.fileList.length; - } - - return ( -
- -
- - {this.props.description ? ( - - ) : ( -
- )} -
- - {this.state.fileList.map(val => { - return - })} - - {canAddFile && } -

{this.props.errorText}

- -
- ); - } - + return ( +
+ +
+ + {this.props.description ? ( + + ) : ( +
+ )} +
+ + {this.state.fileList.map((val) => { + return ( + + ); + })} + + {canAddFile && ( + + )} +
+ {" "} +

{this.props.errorText}

{" "} +
+ +
+ ); + } } export default withTranslation()(FormMultiFile); - diff --git a/webapp/src/components/form/FormRadio.js b/webapp/src/components/form/FormRadio.js index c63cd9d6a..428bfc490 100644 --- a/webapp/src/components/form/FormRadio.js +++ b/webapp/src/components/form/FormRadio.js @@ -8,7 +8,11 @@ class FormRadio extends React.Component { return this.props.showError && this.props.errorText !== ""; }; - linkRenderer = (props) => {props.children} + linkRenderer = (props) => ( + + {props.children} + + ); render() { return ( @@ -17,31 +21,47 @@ class FormRadio extends React.Component { id={this.props.id + "-group"} errorText={this.props.errorText} > - {this.props.label &&
- -
} - {this.props.options.map(o=>{ - return ( -
- - -
- ); - })} + {this.props.label && ( +
+ +
+ )} + {this.props.options.map((o) => { + return ( +
+ + +
+ ); + })}
); diff --git a/webapp/src/components/form/FormSelect.js b/webapp/src/components/form/FormSelect.js index d9f46b7eb..41f4aa486 100644 --- a/webapp/src/components/form/FormSelect.js +++ b/webapp/src/components/form/FormSelect.js @@ -16,13 +16,17 @@ class FormSelect extends React.Component { } } - linkRenderer = (props) => {props.children} + linkRenderer = (props) => ( + + {props.children} + + ); render() { const { id, options, placeholder, onChange, defaultValue } = this.props; let value = this.props.value; if (defaultValue) { - value = options.filter(option => option.value === defaultValue); + value = options.filter((option) => option.value === defaultValue); } return (
@@ -31,7 +35,10 @@ class FormSelect extends React.Component { errorText={this.props.errorText} >
- + {this.props.description ? ( ) : ( @@ -43,7 +50,7 @@ class FormSelect extends React.Component { options={options} placeholder={placeholder} value={value} - onChange={e => onChange(id, e)} + onChange={(e) => onChange(id, e)} defaultValue={value || null} className={ this.shouldDisplayError() diff --git a/webapp/src/components/form/FormSelectOther.js b/webapp/src/components/form/FormSelectOther.js index f8c86d1d5..cd7806e84 100644 --- a/webapp/src/components/form/FormSelectOther.js +++ b/webapp/src/components/form/FormSelectOther.js @@ -3,106 +3,129 @@ import React from "react"; import "./Style.css"; import FormSelect from "./FormSelect"; -import { withTranslation } from 'react-i18next'; +import { withTranslation } from "react-i18next"; class FormSelectOther extends React.Component { - constructor(props) { - super(props); - this.state = { - options: [], - value: [], - showOther: false, - otherText: "" - }; - } - - componentDidMount() { - const options = this.props.options; - if (!options.some(o => o.value === "other")) { - options.push({ - 'value': 'other', - 'label': this.props.t("Other") + '...' - }); - } - - let otherText = ""; - let showOther = false; - if (this.props.defaultValue && !options.some(o=>o.value === this.props.defaultValue)) { - otherText = this.props.defaultValue; - showOther = true; - } + constructor(props) { + super(props); + this.state = { + options: [], + value: [], + showOther: false, + otherText: "", + }; + } - this.setState({ - options: options, - otherText: otherText, - showOther: showOther - }); + componentDidMount() { + const options = this.props.options; + if (!options.some((o) => o.value === "other")) { + options.push({ + value: "other", + label: this.props.t("Other") + "...", + }); } - getDefaultValue = ()=>{ - let defaultValue = this.props.defaultValue; - if (this.props.defaultValue && !this.props.options.some(o=>o.value === this.props.defaultValue)) { - defaultValue = "other"; - } - return defaultValue; + let otherText = ""; + let showOther = false; + if ( + this.props.defaultValue && + !options.some((o) => o.value === this.props.defaultValue) + ) { + otherText = this.props.defaultValue; + showOther = true; } - notifyValueChanged = () => { - const value = this.state.showOther ? this.state.otherText : this.state.value; - if (this.props.onChange) { - this.props.onChange(value); - } - } + this.setState({ + options: options, + otherText: otherText, + showOther: showOther, + }); + } - onChange = (_, dropdown) => { - const value = dropdown.value.toString(); - this.setState({ - showOther: value === "other", - value: value - }, () => { - this.notifyValueChanged(); - }); + getDefaultValue = () => { + let defaultValue = this.props.defaultValue; + if ( + this.props.defaultValue && + !this.props.options.some((o) => o.value === this.props.defaultValue) + ) { + defaultValue = "other"; } + return defaultValue; + }; - onOtherChanged = e => { - this.setState({ - otherText: e.target.value - }, () => { this.notifyValueChanged(); }); + notifyValueChanged = () => { + const value = this.state.showOther + ? this.state.otherText + : this.state.value; + if (this.props.onChange) { + this.props.onChange(value); } + }; - render() { - const t = this.props.t; - return
+ onChange = (_, dropdown) => { + const value = dropdown.value.toString(); + this.setState( + { + showOther: value === "other", + value: value, + }, + () => { + this.notifyValueChanged(); + } + ); + }; - - {this.state.showOther && -
- -
- -
-
} -
- } + onOtherChanged = (e) => { + this.setState( + { + otherText: e.target.value, + }, + () => { + this.notifyValueChanged(); + } + ); + }; + render() { + const t = this.props.t; + return ( +
+ + {this.state.showOther && ( +
+ +
+ +
+
+ )} +
+ ); + } } export default withTranslation()(FormSelectOther); diff --git a/webapp/src/components/form/FormTextArea.js b/webapp/src/components/form/FormTextArea.js index 3792c9870..2618dbaaf 100644 --- a/webapp/src/components/form/FormTextArea.js +++ b/webapp/src/components/form/FormTextArea.js @@ -1,15 +1,15 @@ import React from "react"; import FormGroup from "./FormGroup"; import FormToolTip from "./FormToolTip"; -import { withTranslation } from 'react-i18next'; +import { withTranslation } from "react-i18next"; import ReactMarkdown from "react-markdown"; class FormTextArea extends React.Component { state = { words: 0, characters: 0, - } - + }; + shouldDisplayError = () => { return this.props.showError && this.props.errorText !== ""; }; @@ -34,16 +34,20 @@ class FormTextArea extends React.Component { } this.setState({ - words - }) - return words + words, + }); + return words; }; - linkRenderer = (props) => {props.children} + linkRenderer = (props) => ( + + {props.children} + + ); render() { const t = this.props.t; - + return (
- + {this.props.description ? ( ) : ( @@ -73,16 +80,17 @@ class FormTextArea extends React.Component { onKeyUp={this.getWordCount} onMouseUp={this.getWordCount} onMouseDown={this.getWordCount} - ref={input => { + ref={(input) => { this.nameInput = input; }} - key={"text_"+this.props.key} + key={"text_" + this.props.key} tabIndex={this.props.tabIndex} autoFocus={this.props.autoFocus} /> - {t('Word Count') + ': ' + this.state.words} + + {t("Word Count") + ": " + this.state.words} + -
); } diff --git a/webapp/src/components/form/FormTextBox.js b/webapp/src/components/form/FormTextBox.js index 54f29666f..f4d3528aa 100644 --- a/webapp/src/components/form/FormTextBox.js +++ b/webapp/src/components/form/FormTextBox.js @@ -15,7 +15,11 @@ class FormTextBox extends React.Component { } } - linkRenderer = (props) => {props.children} + linkRenderer = (props) => ( + + {props.children} + + ); render() { return ( @@ -25,30 +29,29 @@ class FormTextBox extends React.Component { errorText={this.props.errorText} >
- + {this.props.description ? ( ) : ( -
- )} +
+ )}
{ + ref={(input) => { this.nameInput = input; }} tabIndex={this.props.tabIndex} diff --git a/webapp/src/components/form/FormToolTip.js b/webapp/src/components/form/FormToolTip.js index 52122b36b..97631f80a 100644 --- a/webapp/src/components/form/FormToolTip.js +++ b/webapp/src/components/form/FormToolTip.js @@ -8,7 +8,10 @@ class FormToolTip extends React.Component { if (hasDescription) { tooltip = (
- +
); diff --git a/webapp/src/components/form/MultiFileComponent.js b/webapp/src/components/form/MultiFileComponent.js index dc9fda389..7cd0a6175 100644 --- a/webapp/src/components/form/MultiFileComponent.js +++ b/webapp/src/components/form/MultiFileComponent.js @@ -1,155 +1,196 @@ import React from "react"; import "./Style.css"; -import tick from '../../images/tick.png' -import { withTranslation } from 'react-i18next'; +import tick from "../../images/tick.png"; +import { withTranslation } from "react-i18next"; import ReactToolTip from "react-tooltip"; -import { getDownloadURL } from '../../utils/files'; - - class MultiFileComponent extends React.Component { - constructor(props) { - super(props); - this.state = { - delete: false, - btnSubmit: false, - isSubmitted: false, - name: null, - file: null, - } - } - - // handleChange - handleChange = event => { - const value = event.target.value; - - this.setState({ - name: value - }); - +import { getDownloadURL } from "../../utils/files"; + +class MultiFileComponent extends React.Component { + constructor(props) { + super(props); + this.state = { + delete: false, + btnSubmit: false, + isSubmitted: false, + name: null, + file: null, }; + } - // handleUpload - handleUpload = event => { - const value = event.target.files[0]; - let name = this.state.name; - const rename = value.name; + // handleChange + handleChange = (event) => { + const value = event.target.value; - if (!this.state.name || this.state.name == "") { - name = value.name; - } + this.setState({ + name: value, + }); + }; - if (this.props.handleUpload) { - this.props.handleUpload(this.props.value.id, value, name, rename).then(() => { - this.setState({ - isSubmitted: true - }); - }); - } - }; + // handleUpload + handleUpload = (event) => { + const value = event.target.files[0]; + let name = this.state.name; + const rename = value.name; - // Name Change - nameChange = event => { - event.preventDefault() - this.setState({ - isSubmitted: false, - name: "", - btnSubmit: true, - prevName: event.target.value - }) + if (!this.state.name || this.state.name == "") { + name = value.name; } - // delete File - del = event => { - event.preventDefault() - - this.setState({ - delete: true - }, () => this.props.del(this.props.value.id)) - + if (this.props.handleUpload) { + this.props + .handleUpload(this.props.value.id, value, name, rename) + .then(() => { + this.setState({ + isSubmitted: true, + }); + }); } - - - //Submit - submit = event => { - event.preventDefault(); - if(!this.state.name.length) { - this.setState({ - error: true - }) - } - else { - this.setState({ - btnSubmit: false, - isSubmitted: true, - error: false - }, () => { - this.props.handleUpload(this.props.value.id, this.props.value.file, this.state.name) - }) + }; + + // Name Change + nameChange = (event) => { + event.preventDefault(); + this.setState({ + isSubmitted: false, + name: "", + btnSubmit: true, + prevName: event.target.value, + }); + }; + + // delete File + del = (event) => { + event.preventDefault(); + + this.setState( + { + delete: true, + }, + () => this.props.del(this.props.value.id) + ); + }; + + //Submit + submit = (event) => { + event.preventDefault(); + if (!this.state.name.length) { + this.setState({ + error: true, + }); + } else { + this.setState( + { + btnSubmit: false, + isSubmitted: true, + error: false, + }, + () => { + this.props.handleUpload( + this.props.value.id, + this.props.value.file, + this.state.name + ); } + ); } - - handleInput() { - if (!this.props.value.file) { - return ( - ) - } - else { - return (
{this.props.value.file.name}
) - } - } - - render() { - const { - btnSubmit, - isSubmitted, - name - } = this.state; - - const file = this.props.value.file; - - const t = this.props.t - - const placeholder = this.props.value.name ? `${this.props.value.name}` : (this.props.placeholder || t("Enter File Name")); - - return ( - -
- - - -
- -
- - - - - - - - - -
- {this.state.error &&

{t("Please enter a name")}

} - -
- {this.handleInput()} -
-
- -
- - ) + }; + + handleInput() { + if (!this.props.value.file) { + return ( + + ); + } else { + return ( +
+ +
{this.props.value.file.name}
+
+ ); } + } + + render() { + const { btnSubmit, isSubmitted, name } = this.state; + + const file = this.props.value.file; + + const t = this.props.t; + + const placeholder = this.props.value.name + ? `${this.props.value.name}` + : this.props.placeholder || t("Enter File Name"); + + return ( +
+ + +
+
+ + + + + + + + + + + + + + + +
+ {this.state.error && ( +

{t("Please enter a name")}

+ )} + +
+ {this.handleInput()} +
+
+
+ ); + } } -export default withTranslation()(MultiFileComponent) - - - - +export default withTranslation()(MultiFileComponent); diff --git a/webapp/src/components/form/Style.css b/webapp/src/components/form/Style.css index e8bc010af..9795c7d83 100644 --- a/webapp/src/components/form/Style.css +++ b/webapp/src/components/form/Style.css @@ -23,7 +23,6 @@ border-radius: 5px; } - /* MultiFileComponent */ .upload-item-wrapper { @@ -46,7 +45,7 @@ .file-name.enter .input-field { border: 1px solid hsl(169deg 37% 57%); - } +} .input-field { width: 40%; @@ -57,7 +56,6 @@ margin-bottom: 0; } - .upload-item-container input#file-upload-button { background: hsl(211deg 100% 50%); color: white; @@ -72,8 +70,6 @@ border: 1px solid red; } - - .file-name { display: flex; margin-bottom: 10px; @@ -125,8 +121,8 @@ display: none; } -.bin.show{ -display: block; +.bin.show { + display: block; } .file-input { @@ -139,19 +135,19 @@ display: block; } .file-uploaded { -display: flex; + display: flex; } .file-uploaded img { width: 20px; height: 20px; margin: 0 5px; - } +} - .view { - margin: 0 10px; - } +.view { + margin: 0 10px; +} .multi-checkbox-list { margin-top: 1em; -} \ No newline at end of file +} diff --git a/webapp/src/components/form/_tests_/FormDate.test.js b/webapp/src/components/form/_tests_/FormDate.test.js index 2c774fdae..53ea58152 100644 --- a/webapp/src/components/form/_tests_/FormDate.test.js +++ b/webapp/src/components/form/_tests_/FormDate.test.js @@ -1,18 +1,17 @@ -import React from 'react'; -import { shallow, mount } from 'enzyme'; -import FormDate from '../FormDate' +import React from "react"; +import { shallow, mount } from "enzyme"; +import FormDate from "../FormDate"; -describe('FormDate Componenet Tests', () => { - it('Check if FormDate renders', () => { - const wrapper = shallow(); - expect(wrapper.length).toEqual(1); - }) +describe("FormDate Componenet Tests", () => { + it("Check if FormDate renders", () => { + const wrapper = shallow(); + expect(wrapper.length).toEqual(1); + }); - it('Check if FormDate renders error in IU', () => { - const wrapper = shallow(); + it("Check if FormDate renders error in IU", () => { + const wrapper = shallow(); - wrapper.setProps({ id: "123", errorText: "There is an error" }) - expect(wrapper.exists('.react-datetime-picker.error')).toEqual(true); - }) - -}); \ No newline at end of file + wrapper.setProps({ id: "123", errorText: "There is an error" }); + expect(wrapper.exists(".react-datetime-picker.error")).toEqual(true); + }); +}); diff --git a/webapp/src/components/form/_tests_/FormMultiFileTest.test.js b/webapp/src/components/form/_tests_/FormMultiFileTest.test.js index 5edab8f92..a2d644ac2 100644 --- a/webapp/src/components/form/_tests_/FormMultiFileTest.test.js +++ b/webapp/src/components/form/_tests_/FormMultiFileTest.test.js @@ -1,58 +1,57 @@ - -import React from "react" -import { shallow, mount } from 'enzyme'; -import FormMultiFile from '../FormMultiFile.js'; - - -jest.mock('react-i18next', () => ({ - // this mock makes sure any components using the translate HoC receive the t function as a prop - withTranslation: () => Component => { - Component.defaultProps = { ...Component.defaultProps, t: () => "" }; - return Component; - }, +import React from "react"; +import { shallow, mount } from "enzyme"; +import FormMultiFile from "../FormMultiFile.js"; + +jest.mock("react-i18next", () => ({ + // this mock makes sure any components using the translate HoC receive the t function as a prop + withTranslation: () => (Component) => { + Component.defaultProps = { ...Component.defaultProps, t: () => "" }; + return Component; + }, })); const onSubmitSpy = jest.fn(); const uploadFile = onSubmitSpy; // FormMultiFile and Tests -describe('FormMultiFile Tests', () => { - it('Check if FormMultiFile Page renders.', () => { - const wrapper = shallow(); - expect(wrapper.length).toEqual(1); +describe("FormMultiFile Tests", () => { + it("Check if FormMultiFile Page renders.", () => { + const wrapper = shallow(); + expect(wrapper.length).toEqual(1); + }); + + it("Check if FormMultiFile state updates in rendered().", () => { + const wrapper = shallow(); + wrapper.setState({ + fileList: [ + { id: 1, name: null, file: null }, + { id: 2, name: null, file: null }, + ], }); - - it('Check if FormMultiFile state updates in rendered().', () => { - const wrapper = shallow(); - wrapper.setState({ fileList: [{ id: 1, name: null, file: null }, { id: 2, name: null, file: null }] }); - expect(wrapper.find('.multi-file-component').length).toEqual(2); + expect(wrapper.find(".multi-file-component").length).toEqual(2); + }); + + it("Check if FormMultiFile adds upload components render().", () => { + const wrapper = shallow(); + wrapper.setState({ + fileList: [ + { id: 1, name: "test", file: "test" }, + { id: 2, name: "test", file: "test" }, + ], }); - - - it('Check if FormMultiFile adds upload components render().', () => { - const wrapper = shallow(); - wrapper.setState({ fileList: [{ id: 1, name: "test", file: "test" }, { id: 2, name: "test", file: "test" }] }); - wrapper.instance().addFile(); - expect(wrapper.find('.multi-file-component').length).toEqual(3); + wrapper.instance().addFile(); + expect(wrapper.find(".multi-file-component").length).toEqual(3); + }); + + it("Check if FormMultiFile adds components in render() when props change.", () => { + const wrapper = shallow(); + wrapper.setState({ + fileList: [ + { id: 1, name: "test", file: "test" }, + { id: 2, name: "test", file: "test" }, + ], }); - - - it('Check if FormMultiFile adds components in render() when props change.', () => { - const wrapper = shallow(); - wrapper.setState({ fileList: [{ id: 1, name: "test", file: "test" }, { id: 2, name: "test", file: "test" }] }); - wrapper.instance().handleUpload(); - expect(wrapper.find('.multi-file-component').length).toEqual(2); - }); - -}) - - - - - - - - - - - + wrapper.instance().handleUpload(); + expect(wrapper.find(".multi-file-component").length).toEqual(2); + }); +}); diff --git a/webapp/src/components/index.js b/webapp/src/components/index.js index e0d535e8a..deac56e08 100644 --- a/webapp/src/components/index.js +++ b/webapp/src/components/index.js @@ -1 +1 @@ -export * from './PrivateRoute'; +export * from "./PrivateRoute"; diff --git a/webapp/src/context/ContextProvider.js b/webapp/src/context/ContextProvider.js index 384b954d1..7431d927a 100644 --- a/webapp/src/context/ContextProvider.js +++ b/webapp/src/context/ContextProvider.js @@ -1,17 +1,13 @@ -import React, { useState } from 'react'; -import AppContext from '.'; +import React, { useState } from "react"; +import AppContext from "."; const ContextProvider = ({ children }) => { const [appFormData, setAppFormData] = useState(null); const context = { appFormData, - setAppFormData - } ; - return ( - - {children} - - ); -} + setAppFormData, + }; + return {children}; +}; export default ContextProvider; diff --git a/webapp/src/context/index.js b/webapp/src/context/index.js index bd2372867..aaa6db760 100644 --- a/webapp/src/context/index.js +++ b/webapp/src/context/index.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React from "react"; const AppContext = React.createContext({}); diff --git a/webapp/src/i18n.js b/webapp/src/i18n.js index 6093bd497..ac8863b49 100644 --- a/webapp/src/i18n.js +++ b/webapp/src/i18n.js @@ -1,20 +1,20 @@ -import i18n from 'i18next'; -import { initReactI18next } from 'react-i18next'; - -import Backend from 'i18next-http-backend'; -import LanguageDetector from 'i18next-browser-languagedetector'; +import i18n from "i18next"; +import { initReactI18next } from "react-i18next"; +import Backend from "i18next-http-backend"; +import LanguageDetector from "i18next-browser-languagedetector"; function i18nInit(organisation) { const orgLanguageDetector = { - name: 'orgLanguageDetector', - + name: "orgLanguageDetector", + lookup(options) { let found = []; - - if (typeof navigator !== 'undefined') { - if (navigator.languages) { // chrome only; not an array, so can't use .push.apply instead of iterating - for (let i=0; i < navigator.languages.length; i++) { + + if (typeof navigator !== "undefined") { + if (navigator.languages) { + // chrome only; not an array, so can't use .push.apply instead of iterating + for (let i = 0; i < navigator.languages.length; i++) { found.push(navigator.languages[i]); } } @@ -25,46 +25,52 @@ function i18nInit(organisation) { found.push(navigator.language); } } - + if (found.length == 0) { return undefined; } - - found = found.map(lang => lang.slice(0, 2)); - const orgLanguageCodes = organisation.languages.map(l => l.code); + + found = found.map((lang) => lang.slice(0, 2)); + const orgLanguageCodes = organisation.languages.map((l) => l.code); let selectedLanguage = null; - found.forEach(f => { + found.forEach((f) => { if (selectedLanguage == null && orgLanguageCodes.includes(f)) { - selectedLanguage = f; + selectedLanguage = f; } - }) + }); return selectedLanguage || undefined; - } + }, }; - + const languageDetectorOptions = { // order and from where user language should be detected - order: ['querystring', 'localStorage', 'cookie', 'sessionStorage', 'orgLanguageDetector'] + order: [ + "querystring", + "localStorage", + "cookie", + "sessionStorage", + "orgLanguageDetector", + ], }; - + const languageDetector = new LanguageDetector(null, languageDetectorOptions); languageDetector.addDetector(orgLanguageDetector); i18n - .use(Backend) - .use(languageDetector) - .use(initReactI18next) - .init({ - fallbackLng: 'en', - debug: true, - interpolation: { - escapeValue: false, // not needed for react as it escapes by default - }, - keySeparator: false - }); + .use(Backend) + .use(languageDetector) + .use(initReactI18next) + .init({ + fallbackLng: "en", + debug: true, + interpolation: { + escapeValue: false, // not needed for react as it escapes by default + }, + keySeparator: false, + }); return i18n; } -export default i18nInit; \ No newline at end of file +export default i18nInit; diff --git a/webapp/src/index.js b/webapp/src/index.js index cf6e5b67b..185b8676d 100755 --- a/webapp/src/index.js +++ b/webapp/src/index.js @@ -6,12 +6,12 @@ import "./index.css"; import * as serviceWorker from "./serviceWorker"; import { ErrorPage } from "./components/ErrorPage"; import { ErrorBoundary } from "react-error-boundary"; -import i18nInit from './i18n'; +import i18nInit from "./i18n"; import Loading from "./components/Loading"; import { organisationService } from "./services/organisation/organisation.service"; -import ContextProvider from './context/ContextProvider'; +import ContextProvider from "./context/ContextProvider"; -const App = React.lazy(() => import('./App')); // Lazy-loaded +const App = React.lazy(() => import("./App")); // Lazy-loaded require("dotenv").config(); @@ -21,16 +21,16 @@ class Bootstrap extends Component { this.state = { organisation: null, - loading: true + loading: true, }; } componentDidMount() { - organisationService.getOrganisation().then(response => { + organisationService.getOrganisation().then((response) => { this.setState({ organisation: response.organisation, error: response.error, - loading: false + loading: false, }); if (response.organisation) { document.title = @@ -44,12 +44,14 @@ class Bootstrap extends Component { render() { if (this.state.loading) { - return + return ; } - return }> - - + return ( + }> + + + ); } } @@ -58,8 +60,8 @@ ReactDOM.render( - - , document.getElementById("root") + , + document.getElementById("root") ); // If you want your app to work offline and load faster, you can change diff --git a/webapp/src/pages/ResponseList/ResponseList.css b/webapp/src/pages/ResponseList/ResponseList.css index c32bf6906..d9357b52c 100644 --- a/webapp/src/pages/ResponseList/ResponseList.css +++ b/webapp/src/pages/ResponseList/ResponseList.css @@ -1,271 +1,264 @@ .label-top { - color: grey; - font-size: 17px; + color: grey; + font-size: 17px; } .lists-wrapper .btn { - margin-right: 5px; + margin-right: 5px; } - /* Drop Down Lists Wrapper and Buttons */ .lists-wrapper { - display: flex; - align-items: center; - flex-wrap: wrap; + display: flex; + align-items: center; + flex-wrap: wrap; } /* Pills */ .pills { - display: flex; - min-width: 1%; + display: flex; + min-width: 1%; } .badge { - margin: 0 5px; - padding: 4px 6px; + margin: 0 5px; + padding: 4px 6px; } .badge:hover { - cursor: pointer; + cursor: pointer; } .pills i { - padding: 0 3px; + padding: 0 3px; } - /* Tags */ .tags { - min-width: 1px; + min-width: 1px; } -.tag-list { - min-width: 1px; - height: 1px; - opacity: 0; - margin-left: 0; - transition: 300ms; - pointer-events: none; - overflow: scroll; +.tag-list { + min-width: 1px; + height: 1px; + opacity: 0; + margin-left: 0; + transition: 300ms; + pointer-events: none; + overflow: scroll; } .tag-list.show { - height: 240px; - opacity: 1; - margin-right: 15px; - transition: 300ms; + height: 240px; + opacity: 1; + margin-right: 15px; + transition: 300ms; } .tag-item { - display: flex; - margin: 5px 0; - opacity: 1; - pointer-events: none; + display: flex; + margin: 5px 0; + opacity: 1; + pointer-events: none; } -.tag-list.show .tag-item{ - pointer-events: all; +.tag-list.show .tag-item { + pointer-events: all; } .tag-item.hide { - display: flex; - margin: 5px 0; - color: lightgrey; - display: none; + display: flex; + margin: 5px 0; + color: lightgrey; + display: none; } .btn.tag { - background: none; - border: 1px solid grey; - pointer-events: all; - opacity: 1; + background: none; + border: 1px solid grey; + pointer-events: all; + opacity: 1; } .btn.tag.hide { - background: none; - pointer-events: none; - opacity: 0.5; + background: none; + pointer-events: none; + opacity: 0.5; } .btn.tags { - background: hsl(0deg, 0%, 91%); - margin: 0; - font-size: 12px; - width: 15%; - text-align: left; + background: hsl(0deg, 0%, 91%); + margin: 0; + font-size: 12px; + width: 15%; + text-align: left; } - /*Questions*/ .question-wrapper { - text-align: left; - width: 40vw; - transition: 300ms; + text-align: left; + width: 40vw; + transition: 300ms; } .question-wrapper.wide { - text-align: left; - width: 87vw; - transition: 300ms; + text-align: left; + width: 87vw; + transition: 300ms; } .question-wrapper .heading { - color: grey; - font-weight: 300; - margin-top: 30px; - transition: 300ms; + color: grey; + font-weight: 300; + margin-top: 30px; + transition: 300ms; } .question-wrapper .heading.short { - color: grey; - font-weight: 300; - margin-top: 2px; - transition: 300ms; + color: grey; + font-weight: 300; + margin-top: 2px; + transition: 300ms; } .btn-secondary { - opacity: 1; - pointer-events: all; - } + opacity: 1; + pointer-events: all; +} - .btn-secondary.hide { - opacity: 0.5; - pointer-events: none; + opacity: 0.5; + pointer-events: none; } html .btn-update { - background:hsl(214deg, 98%, 65%); + background: hsl(214deg, 98%, 65%); } - html .btn-primary.green { - background: hsl(120, 58%, 42%); - border: darkgreen; - color: white; - } + background: hsl(120, 58%, 42%); + border: darkgreen; + color: white; +} - .btn.reset:hover{ - background: hsl(0, 0%, 91%); - border: 1px solid lightgrey; - } +.btn.reset:hover { + background: hsl(0, 0%, 91%); + border: 1px solid lightgrey; +} /* Inputs */ #label { - margin-left: 20px; + margin-left: 20px; } .question-item #label { - margin-left: 20px; + margin-left: 20px; } .form-check-input.input { - margin-left: 2px; + margin-left: 2px; } .form-check-label { - display: block; + display: block; } .total-q { - color: grey; + color: grey; } .questions-item { - display: flex; - width: 30%; - opacity: 1; - pointer-events: all; + display: flex; + width: 30%; + opacity: 1; + pointer-events: all; } .questions-item.hide { - display: flex; - width: 30%; - opacity: 0.5; - pointer-events: none; + display: flex; + width: 30%; + opacity: 0.5; + pointer-events: none; } - .question-list { - height: 0px; - opacity: 0; - z-index: -10; - margin: 10px 0; - transition: 300ms; - overflow: scroll; - display: flex; - flex-wrap: wrap; - pointer-events: none; + height: 0px; + opacity: 0; + z-index: -10; + margin: 10px 0; + transition: 300ms; + overflow: scroll; + display: flex; + flex-wrap: wrap; + pointer-events: none; } .question-list.show { - height: 240px; - opacity: 1; - z-index: 0; - transition: 300ms; - pointer-events: all; + height: 240px; + opacity: 1; + z-index: 0; + transition: 300ms; + pointer-events: all; } /* React Table */ .react-table { - text-align: left; - width: 88vw; - display: flex; - justify-content: center; + text-align: left; + width: 88vw; + display: flex; + justify-content: center; } .ReactTable { - width: 100%; - max-height: 66vh + width: 100%; + max-height: 66vh; } .Tooltip { - width: 35vw; - word-break: break-all; - overflow-wrap: break-word; - display: block; + width: 35vw; + word-break: break-all; + overflow-wrap: break-word; + display: block; } - html .ReactTable .myCol { - max-width: 300px; - white-space: normal; - max-height: 200px; - overflow: scroll; - margin-bottom: 10px + max-width: 300px; + white-space: normal; + max-height: 200px; + overflow: scroll; + margin-bottom: 10px; } html .table-nav-link { - color: hsl(214deg 4% 32%); - background: hsl(0deg 0% 93%); - padding: 3px 17px; - border-radius: 4px; + color: hsl(214deg 4% 32%); + background: hsl(0deg 0% 93%); + padding: 3px 17px; + border-radius: 4px; } .pills.table .btn-primary { - background: rgb(243, 243, 243); - color: black; - margin: 1px; + background: rgb(243, 243, 243); + color: black; + margin: 1px; } html .pills.table { - margin-bottom: 0; + margin-bottom: 0; } .review-assign-container { - text-align: center; - width: 88vw; - -webkit-justify-content: center; - justify-content: center; - margin-top: 5em; + text-align: center; + width: 88vw; + -webkit-justify-content: center; + justify-content: center; + margin-top: 5em; } .assign-button { - margin-left: 1em; - margin-top: 5px; -} \ No newline at end of file + margin-left: 1em; + margin-top: 5px; +} diff --git a/webapp/src/pages/ResponseList/ResponseList.js b/webapp/src/pages/ResponseList/ResponseList.js index 71e969636..7484f0a70 100644 --- a/webapp/src/pages/ResponseList/ResponseList.js +++ b/webapp/src/pages/ResponseList/ResponseList.js @@ -1,13 +1,11 @@ -import React, { Component } from 'react'; -import { withTranslation } from 'react-i18next'; -import ResponseListForm from './components/ResponseListForm' - +import React, { Component } from "react"; +import { withTranslation } from "react-i18next"; +import ResponseListForm from "./components/ResponseListForm"; class ResponseList extends Component { - - render() { - return - } + render() { + return ; + } } export default withTranslation()(ResponseList); diff --git a/webapp/src/pages/ResponseList/components/ResponseListForm.js b/webapp/src/pages/ResponseList/components/ResponseListForm.js index 9736ba5b4..0f8a913f6 100644 --- a/webapp/src/pages/ResponseList/components/ResponseListForm.js +++ b/webapp/src/pages/ResponseList/components/ResponseListForm.js @@ -1,493 +1,635 @@ -import React, { Component } from 'react'; -import '../ResponseList.css'; -import { Trans, withTranslation } from 'react-i18next' -import { responsesService } from '../../../services/responses/responses.service'; -import ReactTable from 'react-table'; +import React, { Component } from "react"; +import "../ResponseList.css"; +import { Trans, withTranslation } from "react-i18next"; +import { responsesService } from "../../../services/responses/responses.service"; +import ReactTable from "react-table"; import "react-table/react-table.css"; -import ReactTooltip from 'react-tooltip'; +import ReactTooltip from "react-tooltip"; import { NavLink } from "react-router-dom"; -import { tagsService } from '../../../services/tags/tags.service'; -import { applicationFormService } from '../../../services/applicationForm/applicationForm.service'; +import { tagsService } from "../../../services/tags/tags.service"; +import { applicationFormService } from "../../../services/applicationForm/applicationForm.service"; import AnswerValue from "../../../components/answerValue"; import Loading from "../../../components/Loading"; import FormTextBox from "../../../components/form/FormTextBox"; -import { reviewService } from '../../../services/reviews/review.service'; +import { reviewService } from "../../../services/reviews/review.service"; class ResponseListForm extends Component { - constructor(props) { - super(props); - this.state = { - questions: [], - selectedQuestions: [], - toggleList: false, - responseTable: null, - btnUpdate: false, - selectedTags: [], - includeUnsubmitted: false, - responses: null, - tags: null, - error: null, - isLoading: true - } - } - - componentWillMount() { - Promise.all([ - tagsService.getTagList(this.props.event.id), - responsesService.getResponseList(this.props.event.id, false, []), - applicationFormService.getQuestionList(this.props.event.id) - ]).then(responses => { - console.log(responses) - this.setState({ - tags: responses[0].tags, - responses: responses[1].responses, - questions: responses[2].questions ? responses[2].questions.filter(q => q.type !== "information") : null, - error: responses[0].error || responses[1].error || responses[2].error, - isLoading: false - }, this.handleData); - }); - } - - refreshResponses() { - this.toggleList(false); - const { selectedTags, selectedQuestions, includeUnsubmitted } = this.state; - - responsesService.getResponseList(this.props.event.id, includeUnsubmitted, selectedQuestions).then(resp => { - this.setState({ - responses: resp.responses.filter(r => selectedTags.length == 0 || r.tags.map(t => t.name).some(t => selectedTags.includes(t))), - error: resp.error - }, this.handleData); - }); + constructor(props) { + super(props); + this.state = { + questions: [], + selectedQuestions: [], + toggleList: false, + responseTable: null, + btnUpdate: false, + selectedTags: [], + includeUnsubmitted: false, + responses: null, + tags: null, + error: null, + isLoading: true, + }; + } + + componentWillMount() { + Promise.all([ + tagsService.getTagList(this.props.event.id), + responsesService.getResponseList(this.props.event.id, false, []), + applicationFormService.getQuestionList(this.props.event.id), + ]).then((responses) => { + console.log(responses); + this.setState( + { + tags: responses[0].tags, + responses: responses[1].responses, + questions: responses[2].questions + ? responses[2].questions.filter((q) => q.type !== "information") + : null, + error: responses[0].error || responses[1].error || responses[2].error, + isLoading: false, + }, + this.handleData + ); + }); + } + + refreshResponses() { + this.toggleList(false); + const { selectedTags, selectedQuestions, includeUnsubmitted } = this.state; + + responsesService + .getResponseList( + this.props.event.id, + includeUnsubmitted, + selectedQuestions + ) + .then((resp) => { + this.setState( + { + responses: resp.responses.filter( + (r) => + selectedTags.length == 0 || + r.tags.map((t) => t.name).some((t) => selectedTags.includes(t)) + ), + error: resp.error, + }, + this.handleData + ); + }); + } + + // Handle/Format Data + handleData = () => { + const { questions, responses } = this.state; + + if (!responses) { + console.log("ERROR: responses is not defined: ", responses); + return; } - // Handle/Format Data - handleData = () => { - const { questions, responses } = this.state; - - if (!responses) { - console.log('ERROR: responses is not defined: ', responses); - return; + this.setState({ + selectedResponseIds: responses.map((r) => r.response_id), + }); + + // disable question list + this.toggleList(false); + + // Handle Answers and Reviews + + // TODO: Change this to a map and don't mutate the state. + responses.forEach((val) => { + let handleAnswers = []; + let handleReviews = []; + let handleTags = []; + + // Create Response Id Link + if (this.props.event) { + val.response_id = ( + + {val.response_id} + + ); + } + + val.answers.forEach((answer) => { + const question = questions.find( + (q) => answer.question_id === q.question_id + ); + handleAnswers.push([ + { + headline: answer.headline, + value: ( +
+ + +
+ ), + }, + ]); + }); + + // extract only the tag names + val.tags.forEach((tag) => { + if (tag) { + handleTags.push(
{tag.name}
); } - - this.setState({ - selectedResponseIds: responses.map(r => r.response_id) + }); + + // extract only the reviewers name + val.reviewers.forEach((review) => { + review + ? handleReviews.push(review.reviewer_name) + : handleReviews.push(""); + }); + + // add User Title as new column + function userTitleCol(row, user_title, firstname, lastname) { + return (row.user = user_title + " " + firstname + " " + lastname); + } + + // envoke and store new columns for UserTitle + // combine user credentials + userTitleCol(val, val.user_title, val.firstname, val.lastname); + + // insert Answers values as columns + if (handleAnswers.length) { + handleAnswers.forEach((answer, index) => { + let key = answer[0].headline; + val[key] = answer[0].value; }); - - // disable question list - this.toggleList(false); - - // Handle Answers and Reviews - - // TODO: Change this to a map and don't mutate the state. - responses.forEach(val => { - let handleAnswers = []; - let handleReviews = []; - let handleTags = []; - - // Create Response Id Link - if (this.props.event) { - val.response_id = - {val.response_id} - ; - } - - val.answers.forEach(answer => { - const question = questions.find(q => answer.question_id === q.question_id); - handleAnswers.push([{ - headline: answer.headline, - value: ( -
- - -
- ) - }]) - }) - - // extract only the tag names - val.tags.forEach(tag => { - if (tag) { - handleTags.push(
{tag.name}
); - } - }) - - // extract only the reviewers name - val.reviewers.forEach(review => { - review ? handleReviews.push(review.reviewer_name) : handleReviews.push(""); - }) - - // add User Title as new column - function userTitleCol(row, user_title, firstname, lastname) { - return row.user = user_title + " " + firstname + " " + lastname; - } - - // envoke and store new columns for UserTitle - // combine user credentials - userTitleCol(val, val.user_title, val.firstname, val.lastname); - - // insert Answers values as columns - if (handleAnswers.length) { - handleAnswers.forEach((answer, index) => { - let key = answer[0].headline; - val[key] = answer[0].value; - }); - handleAnswers = []; - }; - // insert new reviews values as columns - if (handleReviews.length) { - handleReviews.forEach((review, index) => { - let num = (index) + (1); - let key = this.props.t("Reviewer") + " " + num; - val[key] = review; - handleReviews = []; - }) - }; - - val.tags = handleTags; - val.is_submitted = val.is_submitted ? "True" : "False"; - val.is_withdrawn = val.is_withdrawn ? "True" : "False"; - - // delete original review and answer rows as they don't need to be displayed with all their data - delete val.answers; - delete val.reviewers; - delete val.user_title; - delete val.firstname; - delete val.lastname; - - }) - - this.setState({ - responseTable: responses, - btnUpdate: false + handleAnswers = []; + } + // insert new reviews values as columns + if (handleReviews.length) { + handleReviews.forEach((review, index) => { + let num = index + 1; + let key = this.props.t("Reviewer") + " " + num; + val[key] = review; + handleReviews = []; }); - } - - // Tag Selection State - tagSelector(name) { - const list = this.state.selectedTags; - const duplicateTag = list.indexOf(name); // test against duplicates - - duplicateTag == -1 ? list.push(name) : list.splice(duplicateTag, 1); - - this.setState({ - selectedTags: list, - btnUpdate: true - }) - } - - // Delete Pill function - deletePill(val) { - this.tagSelector(val); - this.refreshResponses(); - } - - - // Question selection state - questionSelector(question) { - const selected = this.state.selectedQuestions; - let duplicate = selected.indexOf(question) - - duplicate == -1 ? selected.push(question) : selected.splice(duplicate, 1); - this.setState({ - selectedQuestions: selected, - btnUpdate: true - }) - } - - - // Toggle List - toggleList(list, type) { - this.setState({ - toggleList: !list ? type : false - }) - } - - - // Generate table columns - generateCols() { - let colFormat = []; - // Find the row with greatest col count and assign the col values to React Table - if (this.state.responseTable) { - - // function - function readColumns(rows) { - let tableColumns = ["response_id", "user", "start_date", "is_submitted", "is_withdrawn", "submitted_date", "tags"]; - rows.map(val => { - let newColumns = Object.keys(val); - newColumns.forEach(val => { - if (!tableColumns.includes(val)) { - tableColumns.push(val) - }; - }); - }); - return tableColumns - }; - - // function - function widthCalc(colItem) { - if (colItem.includes('question')) { - return 200 - }; - - if (colItem.includes('user') || colItem.includes('Review') || colItem.includes('date')) { - return 180 - } - else { - return 100 - } + } + + val.tags = handleTags; + val.is_submitted = val.is_submitted ? "True" : "False"; + val.is_withdrawn = val.is_withdrawn ? "True" : "False"; + + // delete original review and answer rows as they don't need to be displayed with all their data + delete val.answers; + delete val.reviewers; + delete val.user_title; + delete val.firstname; + delete val.lastname; + }); + + this.setState({ + responseTable: responses, + btnUpdate: false, + }); + }; + + // Tag Selection State + tagSelector(name) { + const list = this.state.selectedTags; + const duplicateTag = list.indexOf(name); // test against duplicates + + duplicateTag == -1 ? list.push(name) : list.splice(duplicateTag, 1); + + this.setState({ + selectedTags: list, + btnUpdate: true, + }); + } + + // Delete Pill function + deletePill(val) { + this.tagSelector(val); + this.refreshResponses(); + } + + // Question selection state + questionSelector(question) { + const selected = this.state.selectedQuestions; + let duplicate = selected.indexOf(question); + + duplicate == -1 ? selected.push(question) : selected.splice(duplicate, 1); + this.setState({ + selectedQuestions: selected, + btnUpdate: true, + }); + } + + // Toggle List + toggleList(list, type) { + this.setState({ + toggleList: !list ? type : false, + }); + } + + // Generate table columns + generateCols() { + let colFormat = []; + // Find the row with greatest col count and assign the col values to React Table + if (this.state.responseTable) { + // function + function readColumns(rows) { + let tableColumns = [ + "response_id", + "user", + "start_date", + "is_submitted", + "is_withdrawn", + "submitted_date", + "tags", + ]; + rows.map((val) => { + let newColumns = Object.keys(val); + newColumns.forEach((val) => { + if (!tableColumns.includes(val)) { + tableColumns.push(val); } + }); + }); + return tableColumns; + } - let col = readColumns(this.state.responseTable); - - // TODO: Make columns deterministic, add translations for headers - colFormat = col.map(val => ({ id: val, Header: val, accessor: val, className: "myCol", width: widthCalc(val) })); + // function + function widthCalc(colItem) { + if (colItem.includes("question")) { + return 200; } - return colFormat - } - - - - renderReset() { - const { - selectedQuestions, - toggleList, - selectedTags, - } = this.state - if (!toggleList) { - if (selectedTags.length || selectedQuestions.length) { - return - } + if ( + colItem.includes("user") || + colItem.includes("Review") || + colItem.includes("date") + ) { + return 180; + } else { + return 100; } + } + + let col = readColumns(this.state.responseTable); + + // TODO: Make columns deterministic, add translations for headers + colFormat = col.map((val) => ({ + id: val, + Header: val, + accessor: val, + className: "myCol", + width: widthCalc(val), + })); } - // Reset state, question and tag list UI - reset() { - // reset checkboxes - document.querySelectorAll('input[type=checkbox]').forEach(el => el.checked = false); + return colFormat; + } - // disable question list - this.toggleList(true) - - this.setState({ - selectedQuestions: [], - selectedTags: [] - }, () => this.refreshResponses()) + renderReset() { + const { selectedQuestions, toggleList, selectedTags } = this.state; + if (!toggleList) { + if (selectedTags.length || selectedQuestions.length) { + return ( + + ); + } } - - toggleUnsubmitted = () => { + } + + // Reset state, question and tag list UI + reset() { + // reset checkboxes + document + .querySelectorAll("input[type=checkbox]") + .forEach((el) => (el.checked = false)); + + // disable question list + this.toggleList(true); + + this.setState( + { + selectedQuestions: [], + selectedTags: [], + }, + () => this.refreshResponses() + ); + } + + toggleUnsubmitted = () => { + this.setState( + { + includeUnsubmitted: !this.state.includeUnsubmitted, + }, + () => this.refreshResponses() + ); + }; + + handleChange = (event) => { + const value = event.target.value; + this.setState({ + newReviewerEmail: value, + reviewerAssignError: "", + reviewerAssignSuccess: "", + }); + }; + + assignReviewer = () => { + reviewService + .assignResponsesToReviewer( + this.props.event.id, + this.state.selectedResponseIds, + this.state.newReviewerEmail + ) + .then((response) => { this.setState({ - includeUnsubmitted: !this.state.includeUnsubmitted - }, () => this.refreshResponses()); - } - - handleChange = event => { - const value = event.target.value; - this.setState({ - newReviewerEmail: value, - reviewerAssignError: "", - reviewerAssignSuccess: "" + reviewerAssignError: response.error, + newReviewerEmail: response.error ? this.state.newReviewerEmail : "", + numReviewsAssigned: response.error + ? 0 + : this.state.selectedResponseIds.length, + assignedReviewerEmail: response.error + ? "" + : this.state.newReviewerEmail, + reviewerAssignSuccess: !response.error, }); - }; + if (!response.error) { + this.refreshResponses(); + } + }); + }; + + render() { + // Translation + const t = this.props.t; + + // State values + const { + questions, + toggleList, + responseTable, + btnUpdate, + tags, + selectedTags, + selectedQuestions, + error, + isLoading, + numReviewsAssigned, + assignedReviewerEmail, + reviewerAssignError, + newReviewerEmail, + reviewerAssignSuccess, + } = this.state; + + if (error) { + return ( +
{error}
+ ); + } - assignReviewer = () => { - reviewService.assignResponsesToReviewer(this.props.event.id, this.state.selectedResponseIds, this.state.newReviewerEmail) - .then(response => { - this.setState({ - reviewerAssignError: response.error, - newReviewerEmail: response.error ? this.state.newReviewerEmail : "", - numReviewsAssigned: response.error ? 0 : this.state.selectedResponseIds.length, - assignedReviewerEmail: response.error ? "" : this.state.newReviewerEmail, - reviewerAssignSuccess: !response.error - }); - if (!response.error) { - this.refreshResponses(); - } - }); + if (isLoading) { + return ; } - render() { - // Translation - const t = this.props.t; - - // State values - const { - questions, - toggleList, - responseTable, - btnUpdate, - tags, - selectedTags, - selectedQuestions, - error, - isLoading, - numReviewsAssigned, - assignedReviewerEmail, - reviewerAssignError, - newReviewerEmail, - reviewerAssignSuccess - } = this.state - - if (error) { - return
{error}
; - } + // Generate Col + const columns = this.generateCols(); + const renderReset = this.renderReset(); - if (isLoading) { - return (); + return ( +
+
-
- {/*Heading*/} -

{t('Response List')}

- {/*CheckBox*/} -
- this.toggleUnsubmitted()} className="form-check-input input" type="checkbox" value="" id="defaultCheck1" /> - -
- - {/* Wrapper for drop down lists */} -
- - {/*Tags Dropdown*/} -
- {toggleList == "tag" ? : - - } -
- - {/*Questions DropDown*/} -
- {toggleList == "question" ? - - - : } - - - {/* Reset Button */} - {renderReset} - - {/*Update Table*/} - {toggleList == "question" && questions.length && - {questions.length} {t('questions')} - } -
- - - {/*Pills*/} -
- {selectedTags && - selectedTags.map(val => { - return this.deletePill(val)} className="badge badge-primary">{val} - }) - } -
- + > + {/*Heading*/} +

+ {t("Response List")} +

+ {/*CheckBox*/} +
+ this.toggleUnsubmitted()} + className="form-check-input input" + type="checkbox" + value="" + id="defaultCheck1" + /> + +
+ + {/* Wrapper for drop down lists */} +
+ {/*Tags Dropdown*/} +
+ {toggleList == "tag" ? ( + + ) : ( + + )} +
+ + {/*Questions DropDown*/} +
+ {toggleList == "question" ? ( + + ) : ( + + )} + + {/* Reset Button */} + {renderReset} + + {/*Update Table*/} + {toggleList == "question" && questions.length && ( + + {questions.length} {t("questions")} + + )} +
+ + {/*Pills*/} +
+ {selectedTags && + selectedTags.map((val) => { + return ( + this.deletePill(val)} + className="badge badge-primary" + > + {val} + + ); + })} +
+
+ + {/* List Section */} +
+ {/*Tag List*/} +
+ {tags && + tags.map((val) => { + return ( +
+
- - - {/* List Section */} -
- - {/*Tag List*/} -
- {tags && - tags.map(val => { - return
- -
- })} - {/* Update Button */} - -
- - {/* List Questions */} -
- {questions && questions.length && questions.map(val => { - return
- this.questionSelector(val.question_id)} className="question-list-inputs" type="checkbox" value="" id={val.question_id} /> - -
- }) - } -
- + ); + })} + {/* Update Button */} +
+ + {/* List Questions */} +
+ {questions && + questions.length && + questions.map((val) => { + return ( +
+ this.questionSelector(val.question_id)} + className="question-list-inputs" + type="checkbox" + value="" + id={val.question_id} + /> +
- - -
- - -
- {/* Response Table */} - {!toggleList && - - } -
- -
- - - - - {reviewerAssignError && - {JSON.stringify(this.state.reviewerAssignError)} - } - - {reviewerAssignSuccess && - Assigned {{numReviewsAssigned}} reviews to {{assignedReviewerEmail}} - } - -
- -
- ) - } + ); + })} +
+
+
+ +
+ {/* Response Table */} + {!toggleList && ( + + )} +
+ +
+ + + + + {reviewerAssignError && ( + + {JSON.stringify(this.state.reviewerAssignError)} + + )} + + {reviewerAssignSuccess && ( + + + Assigned {{ numReviewsAssigned }} reviews to{" "} + {{ assignedReviewerEmail }} + + + )} +
+ + ); + } } -export default withTranslation()(ResponseListForm); \ No newline at end of file +export default withTranslation()(ResponseListForm); diff --git a/webapp/src/pages/ResponseList/components/_tests_/ResponseList.test.js b/webapp/src/pages/ResponseList/components/_tests_/ResponseList.test.js index 8096956b5..eec1f1db1 100644 --- a/webapp/src/pages/ResponseList/components/_tests_/ResponseList.test.js +++ b/webapp/src/pages/ResponseList/components/_tests_/ResponseList.test.js @@ -1,135 +1,148 @@ import React from "react"; import { shallow, mount } from "enzyme"; import ResponseList from "../ResponseListForm"; -import { applicationFormService } from '../../../../services/applicationForm/applicationForm.service'; -import { responsesService } from '../../../../services/responses/responses.service'; -import { tagsService } from '../../../../services/tags/tags.service'; +import { applicationFormService } from "../../../../services/applicationForm/applicationForm.service"; +import { responsesService } from "../../../../services/responses/responses.service"; +import { tagsService } from "../../../../services/tags/tags.service"; -jest.mock('react-i18next', () => ({ +jest.mock("react-i18next", () => ({ // this mock makes sure any components using the translate HoC receive the t function as a prop - withTranslation: () => Component => { + withTranslation: () => (Component) => { Component.defaultProps = { ...Component.defaultProps, t: () => "" }; return Component; }, })); - // Mock Props const props = { event: { - id: 3 + id: 3, }, match: { params: { id: 1, - eventKey: "1234" - } - } -} - - -const mockData = [ - { - "response_id": 1, - "user_title": "Mr", - "firstname": "Jimmy", - "lastname": "Fallon", - "start_date": "2020-06-01 [...]", // ISO8601 format - "is_submitted": true, - "is_withdrawn": false, - "submitted_date": "2020-06-03 [...]", // ISO8601 format - "language": "en", - "answers": [ - {"question_id": 3, "value": "Hello world", "type": "short-text", "options": null}, - { "question_id": 5, "value": "harry-potter", "multi-choice": null, "options": [{ "label": "Harry Potter", "value": "harry-potter" }, { "label": "X-men", "value": "x-men" }] } - ], - "tags": [ // <-------- NEW - {"id": 5, "name": "Education"}, - {"id": 7, "name": "Healthcare"} - ] + eventKey: "1234", }, - { - "response_id": 2, - "user_title": "Ms", - "firstname": "Halle", - "lastname": "Berry", - "start_date": "2020-06-01 [...]", // ISO8601 format - "is_submitted": true, - "is_withdrawn": false, - "submitted_date": "2020-06-03 [...]", // ISO8601 format - "language": "en", - "answers": [ - {"question_id": 3, "value": "Hello world", "type": "short-text", "options": null}, - { "question_id": 5, "value": "x-men", "multi-choice": null, "options": [{ "label": "Harry Potter", "value": "harry-potter" }, { "label": "X-men", "value": "x-men" }] } - ], - "tags": [ // <-------- NEW - {"id": 2, "name": "Desk Reject"}, - {"id": 7, "name": "Healthcare"} - ] - } -] - + }, +}; +const mockData = [ + { + response_id: 1, + user_title: "Mr", + firstname: "Jimmy", + lastname: "Fallon", + start_date: "2020-06-01 [...]", // ISO8601 format + is_submitted: true, + is_withdrawn: false, + submitted_date: "2020-06-03 [...]", // ISO8601 format + language: "en", + answers: [ + { + question_id: 3, + value: "Hello world", + type: "short-text", + options: null, + }, + { + question_id: 5, + value: "harry-potter", + "multi-choice": null, + options: [ + { label: "Harry Potter", value: "harry-potter" }, + { label: "X-men", value: "x-men" }, + ], + }, + ], + tags: [ + // <-------- NEW + { id: 5, name: "Education" }, + { id: 7, name: "Healthcare" }, + ], + }, + { + response_id: 2, + user_title: "Ms", + firstname: "Halle", + lastname: "Berry", + start_date: "2020-06-01 [...]", // ISO8601 format + is_submitted: true, + is_withdrawn: false, + submitted_date: "2020-06-03 [...]", // ISO8601 format + language: "en", + answers: [ + { + question_id: 3, + value: "Hello world", + type: "short-text", + options: null, + }, + { + question_id: 5, + value: "x-men", + "multi-choice": null, + options: [ + { label: "Harry Potter", value: "harry-potter" }, + { label: "X-men", value: "x-men" }, + ], + }, + ], + tags: [ + // <-------- NEW + { id: 2, name: "Desk Reject" }, + { id: 7, name: "Healthcare" }, + ], + }, +]; test("Check if ResponseList Page renders.", () => { const wrapper = shallow(); expect(wrapper.length).toEqual(1); }); - test("Check if Question API call works.", async () => { - const response = await applicationFormService.getQuestionList(props.event.id) + const response = await applicationFormService.getQuestionList(props.event.id); expect(response.error).toBeTruthy(); }); - test("Check if responsesService API call works.", async () => { - const response = await responsesService.getResponseList(props.event.id, false, []); + const response = await responsesService.getResponseList( + props.event.id, + false, + [] + ); expect(response.error).toBeTruthy(); }); - -test("Check if tag API call is successful.", async () => { +test("Check if tag API call is successful.", async () => { const response = await tagsService.getTagList(props.event.id); expect(response.error).toBeTruthy(); }); - test("Check if React Table renders.", async () => { - const wrapper = shallow(); + const wrapper = shallow(); wrapper.setState({ toggleList: null, - isLoading: false - }) - expect(wrapper.find('.ReactTable').length).toEqual(1); + isLoading: false, + }); + expect(wrapper.find(".ReactTable").length).toEqual(1); }); - test("Check if toggleList function displays list.", () => { - const wrapper = shallow(); + const wrapper = shallow(); let listStatus = wrapper.state().toggleList; wrapper.setState({ - isLoading: false - }) + isLoading: false, + }); wrapper.instance().toggleList(listStatus, "tag"); - expect(wrapper.find('.tag-list.show').length).toEqual(1); + expect(wrapper.find(".tag-list.show").length).toEqual(1); }); - test("Check if Col function is succesfull.", async () => { - const wrapper = shallow(); + const wrapper = shallow(); wrapper.setState({ - responseTable: mockData - }) + responseTable: mockData, + }); let columns = await wrapper.instance().generateCols(); expect(columns.length).toBeTruthy(); }); - - - - - - - - - diff --git a/webapp/src/pages/ResponsePage/ResponsePage.css b/webapp/src/pages/ResponsePage/ResponsePage.css index df24df32f..2dda8c593 100644 --- a/webapp/src/pages/ResponsePage/ResponsePage.css +++ b/webapp/src/pages/ResponsePage/ResponsePage.css @@ -1,383 +1,374 @@ /* Universal */ .table-wrapper.response-page { - width: 87vw; - margin-top: 20px; + width: 87vw; + margin-top: 20px; } - .table-wrapper.response-page h2 { - color: hsl(0, 0%, 25%); - font-weight: 300; - margin-top: 2px; - text-align: left; - margin-bottom: 5px; + color: hsl(0, 0%, 25%); + font-weight: 300; + margin-top: 2px; + text-align: left; + margin-bottom: 5px; } .response-details h4 { - color: hsl(213deg 16% 49%);; - font-weight: 300; - padding-left: 7px; - margin-top: 5px; - text-align: left; + color: hsl(213deg 16% 49%); + font-weight: 300; + padding-left: 7px; + margin-top: 5px; + text-align: left; } .table-wrapper.response-page button { - margin-bottom: 10px; + margin-bottom: 10px; } .table-wrapper.response-page label { - font-size: 12px; - color: grey; - margin: 0 1px; + font-size: 12px; + color: grey; + margin: 0 1px; } - /* Flex Box Classes */ .flex { - display: flex; + display: flex; } .baseline { - align-items: baseline; + align-items: baseline; } .center { - justify-content: center; + justify-content: center; } - /* General and User Details Heading */ .headings-lower { - display: flex; - width: 100%; - justify-content: space-between; - margin-top: 8px; - flex-wrap: wrap; - padding: 2%; - box-shadow: 1px 2px 6px hsl(0, 0%, 53%); - border-radius: 4px; + display: flex; + width: 100%; + justify-content: space-between; + margin-top: 8px; + flex-wrap: wrap; + padding: 2%; + box-shadow: 1px 2px 6px hsl(0, 0%, 53%); + border-radius: 4px; } .user-details { - padding: 0 2px + padding: 0 2px; } .user-details.right { - padding: 0 10px + padding: 0 10px; } html .headings-lower label { - color: hsl(0deg 0% 55%); + color: hsl(0deg 0% 55%); } .user-details { - display: flex; - text-align: left; - flex-direction: column; - justify-content: center; + display: flex; + text-align: left; + flex-direction: column; + justify-content: center; } - .user-details.right { - display: flex; - flex-direction: column; - align-items: flex-start; + display: flex; + flex-direction: column; + align-items: flex-start; } .user-details h4 { - color: hsl(0, 0%, 44%); - margin: 0; + color: hsl(0, 0%, 44%); + margin: 0; } .tags .btn-light { - font-size: 13px; - background: hsl(0deg 0% 100%); - border: 1px solid hsl(0deg, 1%, 74%); + font-size: 13px; + background: hsl(0deg 0% 100%); + border: 1px solid hsl(0deg, 1%, 74%); } .badge { - margin: 2px; + margin: 2px; } .badge-info { - background: hsl(188deg, 100%, 23%); - font-size: 14px; + background: hsl(188deg, 100%, 23%); + font-size: 14px; } .fa-trash-alt { - padding: 0 5px; + padding: 0 5px; } /* Tag List */ .tag-response { - opacity: 0; - pointer-events: none; - overflow: scroll; - transition: 300ms; - height: 1px; - margin-top: 2px; + opacity: 0; + pointer-events: none; + overflow: scroll; + transition: 300ms; + height: 1px; + margin-top: 2px; } .tag-response.show { - opacity: 1; - pointer-events: all; - transition: 300ms; - height: 180px; + opacity: 1; + pointer-events: all; + transition: 300ms; + height: 180px; } .badge.add-tags { - background: hsl(208, 74%, 57%); - color: white; + background: hsl(208, 74%, 57%); + color: white; } .badge.add-tags.active { - background: hsl(120, 0%, 57%); + background: hsl(120, 0%, 57%); } .tag-item { - display: flex; - margin: 5px 0; - opacity: 1; - pointer-events: none; + display: flex; + margin: 5px 0; + opacity: 1; + pointer-events: none; } .btn.tag { - background: none; - border: 1px solid hsl(0, 0%, 75%); - pointer-events: all; - opacity: 1; + background: none; + border: 1px solid hsl(0, 0%, 75%); + pointer-events: all; + opacity: 1; } .badge-light { - border: 1px solid grey; + border: 1px solid grey; } .tags { - display: flex; - flex-wrap: wrap; - min-width: 1px; + display: flex; + flex-wrap: wrap; + min-width: 1px; } .tags .btn.badge-info { - background: hsl(28deg, 79%, 54%); + background: hsl(28deg, 79%, 54%); } .tag-response .btn.tag { - background: hsl(0deg, 0%, 90%); - padding: 2px 4px; - border: none; - color: hsl(0deg 0% 32%); + background: hsl(0deg, 0%, 90%); + padding: 2px 4px; + border: none; + color: hsl(0deg 0% 32%); } .tag-response .btn { - width: 100%; - font-size: 12px; - text-align: left; - margin: 0; + width: 100%; + font-size: 12px; + text-align: left; + margin: 0; } /* New Tag Modal */ .new-tag-inputs { - margin: 5px 0; - display: flex; - flex-direction: column; - width: 100%; + margin: 5px 0; + display: flex; + flex-direction: column; + width: 100%; } .new-tag-inputs label { - text-align: left; + text-align: left; } .new-tag-inputs input { - width: 100%; + width: 100%; } /* Reviewers Section */ .reviewers-section .list { - display: flex; - flex-wrap: wrap; - align-items: center; - margin: 12px 0; + display: flex; + flex-wrap: wrap; + align-items: center; + margin: 12px 0; } .reviewers-section .list .reviewer { - box-shadow: 1px 1px 5px hsl(0deg, 0%, 75%); - margin-right: 20px; - padding: 3px 15px; - border-radius: 8px; - margin: 10px 5px; - height: 3.5em; + box-shadow: 1px 1px 5px hsl(0deg, 0%, 75%); + margin-right: 20px; + padding: 3px 15px; + border-radius: 8px; + margin: 10px 5px; + height: 3.5em; } .reviewers-section .list .add-reviewer .btn { - margin: 0 10px; - font-size: 13px; - border: 1px solid hsl(0, 0%, 91%); + margin: 0 10px; + font-size: 13px; + border: 1px solid hsl(0, 0%, 91%); } -.reviewers-section p { - margin: 0 8px 0 0; - font-size: 13px; +.reviewers-section p { + margin: 0 8px 0 0; + font-size: 13px; } .reviewers-section h5 { - margin-top: 11px; - color: grey; + margin-top: 11px; + color: grey; } .reviewer div { - display: flex; + display: flex; } .divider { - height: 1px; - background: lightgrey; - width: 100%; - margin: 35px auto; + height: 1px; + background: lightgrey; + width: 100%; + margin: 35px auto; } /* Review Modal */ .review-select { - background: none; - border: none; - outline: none; - text-align: left; - box-shadow: 1px 2px 6px hsl(0, 0%, 53%); - cursor: pointer; - border-radius: 10px; - padding: 5px 30px 5px 15px; + background: none; + border: none; + outline: none; + text-align: left; + box-shadow: 1px 2px 6px hsl(0, 0%, 53%); + cursor: pointer; + border-radius: 10px; + padding: 5px 30px 5px 15px; } .review-select label { - color: hsl(208deg, 84%, 59%) + color: hsl(208deg, 84%, 59%); } - .review-select:hover { - background: hsl(0, 0%, 96%); - color: hsl(0, 0%, 19%); + background: hsl(0, 0%, 96%); + color: hsl(0, 0%, 19%); } .review-select div { - display: flex; + display: flex; } html button:focus { - border: none; - outline: none; + border: none; + outline: none; } - .review-select.active { - background:hsl(208deg, 84%, 59%); - border: none; - outline: none; + background: hsl(208deg, 84%, 59%); + border: none; + outline: none; } .trash-review { - background: none; - border: none; - outline: none + background: none; + border: none; + outline: none; } .review-select p { -color: hsl(0, 0%, 38%); -font-size: 12px; -margin-right: 8px; -margin-bottom: 4px; + color: hsl(0, 0%, 38%); + font-size: 12px; + margin-right: 8px; + margin-bottom: 4px; } .review-select.active label { -color: white; -font-size: 15px; + color: white; + font-size: 15px; } .review-select.active p { - color: hsl(0, 0%, 100%); + color: hsl(0, 0%, 100%); } .review-completed { - color: hsl(120, 41%, 47%); + color: hsl(120, 41%, 47%); } .review-not-started { - color: hsl(0, 64%, 61%); + color: hsl(0, 64%, 61%); } .review-started { - color: #e8a64a; + color: #e8a64a; } - /* Response Details Section and Sub-Sections */ .response-details { - margin-top: 20px; - width: 100%; - padding: 2%; - background: white; - box-shadow: 1px 2px 6px hsl(0, 0%, 53%); - min-height: 40px; - padding-bottom: 40px; - text-align: left; - border-radius: 4px; + margin-top: 20px; + width: 100%; + padding: 2%; + background: white; + box-shadow: 1px 2px 6px hsl(0, 0%, 53%); + min-height: 40px; + padding-bottom: 40px; + text-align: left; + border-radius: 4px; } .section { - padding: 10px 0; + padding: 10px 0; } - /* Q&A Sections */ .Q-A { - display: flex; - flex-wrap: wrap; + display: flex; + flex-wrap: wrap; } .question-answer-block { - width: 100%; - padding: 10px; - border: 1px solid #e8e8e8; - margin: 5px; - background: hsl(0deg 0% 98%); - border-radius: 2px; + width: 100%; + padding: 10px; + border: 1px solid #e8e8e8; + margin: 5px; + background: hsl(0deg 0% 98%); + border-radius: 2px; } .question-answer-block p { - color: grey; - font-size: 17px; + color: grey; + font-size: 17px; } .question-answer-block p.answer { - color: #212529; - font-size: 17px; - white-space: pre-wrap; + color: #212529; + font-size: 17px; + white-space: pre-wrap; } - html .answerer { - font-weight: bold; - font-size: 13px; +html .answerer { + font-weight: bold; + font-size: 13px; } .answer.file { - color: skyblue; - text-decoration: underline; + color: skyblue; + text-decoration: underline; } - /* Response Details Type Face */ .response-details h3 { - color: hsl(213deg 16% 49%); - margin: 0; - padding-left: 7px; - padding-bottom: 4px; - padding-right: 4px; + color: hsl(213deg 16% 49%); + margin: 0; + padding-left: 7px; + padding-bottom: 4px; + padding-right: 4px; } .question-answer-block .question-description { - font-size: 0.9em; + font-size: 0.9em; } .question-answer-block .question-headline { - color: #666; -} \ No newline at end of file + color: #666; +} diff --git a/webapp/src/pages/ResponsePage/ResponsePage.js b/webapp/src/pages/ResponsePage/ResponsePage.js index 4ab2bc622..e9b340609 100644 --- a/webapp/src/pages/ResponsePage/ResponsePage.js +++ b/webapp/src/pages/ResponsePage/ResponsePage.js @@ -1,578 +1,705 @@ -import React, { Component } from 'react' +import React, { Component } from "react"; import "react-table/react-table.css"; -import './ResponsePage.css' -import { withTranslation } from 'react-i18next'; -import ReviewModal from './components/ReviewModal'; -import TagModal from './components/TagModal'; -import { eventService } from '../../services/events/events.service'; -import { applicationFormService } from '../../services/applicationForm/applicationForm.service'; -import { reviewService } from '../../services/reviews/review.service'; -import { tagsService } from '../../services/tags/tags.service'; -import { responsesService } from '../../services/responses/responses.service'; +import "./ResponsePage.css"; +import { withTranslation } from "react-i18next"; +import ReviewModal from "./components/ReviewModal"; +import TagModal from "./components/TagModal"; +import { eventService } from "../../services/events/events.service"; +import { applicationFormService } from "../../services/applicationForm/applicationForm.service"; +import { reviewService } from "../../services/reviews/review.service"; +import { tagsService } from "../../services/tags/tags.service"; +import { responsesService } from "../../services/responses/responses.service"; import AnswerValue from "../../components/answerValue"; import Loading from "../../components/Loading"; import { ConfirmModal } from "react-bootstrap4-modal"; -import moment from 'moment' -import { getDownloadURL } from '../../utils/files'; - +import moment from "moment"; +import { getDownloadURL } from "../../utils/files"; class ResponsePage extends Component { - constructor(props) { - super(props); - this.state = { - tagMenu: false, - error: false, - addNewTags: [], - eventLanguages: [], - isLoading: true, - removeTagModalVisible: false, - removeReviewerModalVisible: false, - tagToRemove: null, - reviewToRemove: null, - } - }; - - componentDidMount() { - - Promise.all([ - eventService.getEvent(this.props.event.id), - 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({ - eventLanguages: responses[0].event ? Object.keys(responses[0].event.name) : null, - 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, - isLoading: false - }, this.handleData); - }); - }; - - - // Misc Functions - - goBack() { - this.props.history.goBack(); - }; - - error(error) { - this.setState({ - error: error, - removeTagModalVisible: false, - removeReviewerModalVisible: false, - tagToRemove: null, - reviewToRemove: null - - }); - }; - - // Toggle Tag Menu - toggleTags(list) { - this.setState({ - tagMenu: list ? false : true - }); + constructor(props) { + super(props); + this.state = { + tagMenu: false, + error: false, + addNewTags: [], + eventLanguages: [], + isLoading: true, + removeTagModalVisible: false, + removeReviewerModalVisible: false, + tagToRemove: null, + reviewToRemove: null, }; - - - // Render Page HTML - // Generate Applciation Status - - formatDate = (dateString) => { - return moment(dateString).format('D MMM YYYY, H:mm:ss [(UTC)]'); + } + + componentDidMount() { + Promise.all([ + eventService.getEvent(this.props.event.id), + 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( + { + eventLanguages: responses[0].event + ? Object.keys(responses[0].event.name) + : null, + 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, + isLoading: false, + }, + this.handleData + ); + }); + } + + // Misc Functions + + goBack() { + this.props.history.goBack(); + } + + error(error) { + this.setState({ + error: error, + removeTagModalVisible: false, + removeReviewerModalVisible: false, + tagToRemove: null, + reviewToRemove: null, + }); + } + + // Toggle Tag Menu + toggleTags(list) { + this.setState({ + tagMenu: list ? false : true, + }); + } + + // Render Page HTML + // Generate Applciation Status + + formatDate = (dateString) => { + return moment(dateString).format("D MMM YYYY, H:mm:ss [(UTC)]"); + }; + + applicationStatus() { + const data = this.state.applicationData; + if (data) { + const unsubmitted = !data.is_submitted && !data.is_withdrawn; + const submitted = data.is_submitted; + const withdrawn = data.is_withdrawn; + + if (unsubmitted) { + return ( + + Unsubmitted{" "} + {this.formatDate(data.started_timestamp)} + + ); + } + if (submitted) { + return ( + + Submitted{" "} + {this.formatDate(data.started_timestamp)} + + ); + } + if (withdrawn) { + return ( + + Withdrawn{" "} + {this.formatDate(data.started_timestamp)} + + ); + } } - - applicationStatus() { - const data = this.state.applicationData; - if (data) { - const unsubmitted = !data.is_submitted && !data.is_withdrawn; - const submitted = data.is_submitted; - const withdrawn = data.is_withdrawn; - - if (unsubmitted) { - return Unsubmitted {this.formatDate(data.started_timestamp)} - }; - if (submitted) { - return Submitted {this.formatDate(data.started_timestamp)} - }; - if (withdrawn) { - return Withdrawn {this.formatDate(data.started_timestamp)} - }; - }; - }; - - // Render Sections - renderSections() { - const applicationForm = this.state.applicationForm; - const applicationData = this.state.applicationData; - let html = []; - - // main function - if (applicationForm && applicationData) { - applicationForm.sections.forEach(section => { - html.push(
- { /*Heading*/} -

{section.name}

- { /*Q & A*/} -
- {this.renderResponses(section)} -
-
) - }); - }; - - return html - }; - - // Render questions and answers - renderResponses(section) { - const applicationData = this.state.applicationData; - const questions = section.questions.map(q => { - const a = applicationData.answers.find(a => a.question_id === q.id); - if (q.type === "information") { - return

{q.headline}

- }; - return
-

{q.headline} - {q.description &&
{q.description}
} -

-
+ } + + // Render Sections + renderSections() { + const applicationForm = this.state.applicationForm; + const applicationData = this.state.applicationData; + let html = []; + + // main function + if (applicationForm && applicationData) { + applicationForm.sections.forEach((section) => { + html.push( +
+ {/*Heading*/} +
+

{section.name}

- }); - return questions - }; - - // Render Answers - renderAnswer(id, type, headline, options) { - const applicationData = this.state.applicationData; - const baseUrl = process.env.REACT_APP_API_URL; + {/*Q & A*/} +
{this.renderResponses(section)}
+
+ ); + }); + } - const a = applicationData.answers.find(a => a.question_id === id); - if (!a) { - return this.props.t("No answer provided."); + return html; + } + + // Render questions and answers + renderResponses(section) { + const applicationData = this.state.applicationData; + const questions = section.questions.map((q) => { + const a = applicationData.answers.find((a) => a.question_id === q.id); + if (q.type === "information") { + return

{q.headline}

; + } + return ( +
+

+ {q.headline} + {q.description && ( + +
+ {q.description} +
+ )} +

+
+ +
+
+ ); + }); + return questions; + } + + // Render Answers + renderAnswer(id, type, headline, options) { + const applicationData = this.state.applicationData; + const baseUrl = process.env.REACT_APP_API_URL; + + const a = applicationData.answers.find((a) => a.question_id === id); + if (!a) { + return this.props.t("No answer provided."); + } else { + // file + if (type == "file") { + return ( + + {this.props.t("Uploaded File")} + + ); + } + // multi-file + else if (type == "multi-file") { + const answerFiles = JSON.parse(a.value); + let files = []; + if (Array.isArray(answerFiles) && answerFiles.length > 0) { + files = answerFiles.map((file) => ( + + )); + } else { + files = "No files uploaded"; } - else { - // file - if (type == "file") { - return {this.props.t("Uploaded File")} - } - // multi-file - else if (type == "multi-file") { - const answerFiles = JSON.parse(a.value); - let files = []; - if (Array.isArray(answerFiles) && answerFiles.length > 0) { - files = answerFiles.map(file => ) - } - else { - files = "No files uploaded"; - } - return
{files}
- } - // choice - else if (type.includes("choice")) { - let choices = []; - options.forEach(opt => { - if (a.value == opt.value) { - choices.push(
{opt.label}
) - }; - }); - return
{choices}
- } - // other - else { - return

{a.value}

- }; - }; - }; - - // render Delete Modal - renderDeleteTagModal() { - const t = this.props.t; - - return -

- {t('Are you sure you want to delete this tag?')} -

-
- - }; - - - // Tag Functions - // Post Response API - postResponseTag(tagId, responseId, eventId) { - const updateApplicationData = this.state.applicationData; - const updateTagList = this.state.tagList; - - responsesService.tagResponse(responseId, tagId, eventId) - .then(response => { - if (response.status == 201) { - const getTag = this.state.tagList.find(t => t.id === response.responseTag.tag_id); - updateApplicationData.tags.push({ "name": getTag.name, "id": getTag.id }); - updateTagList.splice(getTag, 1); - - this.setState({ - applicationData: updateApplicationData, - tagList: updateTagList - }); - } - else { - this.error(response.error); - } - }); - }; - - // Post Tag List API - addTag = (tagTranslations) => { - if (Object.keys(tagTranslations).length) { - const newTag = { - name: tagTranslations - }; - - - tagsService.addTag(newTag, this.props.event.id).then(response => { - if (response.status == 201) { - const updateTagList = this.state.tagList; - const newTag = { - id: response.tag.id, - name: response.tag.name[this.props.i18n.language] - }; - updateTagList.push(newTag); - this.setState({ - tagList: updateTagList - }, () => { - this.postResponseTag(response.tag.id, this.props.match.params.id, this.props.event.id); - }); - } - else { - this.error(response.error); - } - }); - }; - }; - - // Del Tag - removeTag(tag_id) { - this.setState({ - removeTagModalVisible: true, - tagToRemove: tag_id + return
{files}
; + } + // choice + else if (type.includes("choice")) { + let choices = []; + options.forEach((opt) => { + if (a.value == opt.value) { + choices.push(
{opt.label}
); + } }); - }; - - confirmRemoveTag = () => { - const updateApplicationData = this.state.applicationData; - const { tagToRemove } = this.state; - - responsesService.removeTag( - parseInt(this.props.match.params.id), - tagToRemove, - this.props.event.id - ).then(response => { - if (response.status == 200) { - const index = updateApplicationData.tags.findIndex(t => t.id === tagToRemove); - if (index >= 0) - updateApplicationData.tags.splice(index, 1); - - this.setState({ - applicationData: updateApplicationData, - removeTagModalVisible: false, - tagToRemove: null - }); - } - else { - this.cancelRemoveTag(); - this.error(response.message); + return
{choices}
; + } + // other + else { + return ( +
+

{a.value}

+
+ ); + } + } + } + + // render Delete Modal + renderDeleteTagModal() { + const t = this.props.t; + + return ( + +

{t("Are you sure you want to delete this tag?")}

+
+ ); + } + + // Tag Functions + // Post Response API + postResponseTag(tagId, responseId, eventId) { + const updateApplicationData = this.state.applicationData; + const updateTagList = this.state.tagList; + + responsesService + .tagResponse(responseId, tagId, eventId) + .then((response) => { + if (response.status == 201) { + const getTag = this.state.tagList.find( + (t) => t.id === response.responseTag.tag_id + ); + updateApplicationData.tags.push({ name: getTag.name, id: getTag.id }); + updateTagList.splice(getTag, 1); + + this.setState({ + applicationData: updateApplicationData, + tagList: updateTagList, + }); + } else { + this.error(response.error); + } + }); + } + + // Post Tag List API + addTag = (tagTranslations) => { + if (Object.keys(tagTranslations).length) { + const newTag = { + name: tagTranslations, + }; + + tagsService.addTag(newTag, this.props.event.id).then((response) => { + if (response.status == 201) { + const updateTagList = this.state.tagList; + const newTag = { + id: response.tag.id, + name: response.tag.name[this.props.i18n.language], + }; + updateTagList.push(newTag); + this.setState( + { + tagList: updateTagList, + }, + () => { + this.postResponseTag( + response.tag.id, + this.props.match.params.id, + this.props.event.id + ); } - }); - }; - - cancelRemoveTag = () => { - this.setState({ - tagToRemove: null, + ); + } else { + this.error(response.error); + } + }); + } + }; + + // Del Tag + removeTag(tag_id) { + this.setState({ + removeTagModalVisible: true, + tagToRemove: tag_id, + }); + } + + confirmRemoveTag = () => { + const updateApplicationData = this.state.applicationData; + const { tagToRemove } = this.state; + + responsesService + .removeTag( + parseInt(this.props.match.params.id), + tagToRemove, + this.props.event.id + ) + .then((response) => { + if (response.status == 200) { + const index = updateApplicationData.tags.findIndex( + (t) => t.id === tagToRemove + ); + if (index >= 0) updateApplicationData.tags.splice(index, 1); + + this.setState({ + applicationData: updateApplicationData, removeTagModalVisible: false, - }); - }; - - renderTags() { - const data = this.state.applicationData; - - if (data) { - let tags = data.tags ? data.tags.map(tag => { - return - {tag.name} - this.removeTag(tag.id)} className="far fa-trash-alt cursor-pointer"> - }) - : - null - return tags - }; - }; - - renderTagModal() { - const { eventLanguages } = this.state; - - if (eventLanguages) { - return this.addTag(tag)} - eventLanguages={eventLanguages} - /> - }; - }; - - - // Reviews - // Render Reviews - renderReviews() { - - if (this.state.applicationData) { - if (this.state.applicationData.reviewers) { - const reviews = this.state.applicationData.reviewers.map((val, index) => { - let num = index + 1; - // {"reviewer_user_id": 4, "user_title": "Mr", "firstname": "Joe", "lastname": "Soap", "status": "completed"}, - return
- -
-

{val.user_title} {val.firstname} {val.lastname}

- - {val.status === "completed" &&

{this.props.t("Completed")}

} - {val.status === "started" &&

{this.props.t("In Progress")}

} - {val.status === "not_started" && -

- {this.props.t("Not Started")} - -

- } -
-
- }); - - return reviews - }; - } - - }; - - // Remove Reviewer - removeReview(reviewer_user_id) { - this.setState({ - removeReviewerModalVisible: true, - reviewToRemove: reviewer_user_id - }); - }; - - removeReviewerService() { - const { applicationData, reviewToRemove } = this.state; - const { event } = this.props; - - reviewService.deleteResponseReviewer(event.id, applicationData.id, reviewToRemove) - .then(response => { - if (response.error) { - this.error(response.error); - } else { - const newReviewers = applicationData.reviewers.filter(r => r === null || r.reviewer_user_id !== reviewToRemove); - - this.setState({ - applicationData: { - ...applicationData, - reviewers: newReviewers - }, - removeReviewerModalVisible: false - }); - } - }) - }; - - cancelRemoveReviewer() { - this.setState({ - removeReviewerModalVisible: false, - reviewToRemove: null - }); - }; - - postReviewerService(reviewer) { - const { applicationData } = this.state; - - reviewService.assignResponsesToReviewer(this.props.event.id, [applicationData.id], reviewer.email) - .then(response => { - if (response.error) { - this.error(response.error); - } - else { - const newReviewer = { - "reviewer_user_id": reviewer.reviewer_user_id, - "user_title": reviewer.user_title, - "firstname": reviewer.firstname, - "lastname": reviewer.lastname, - "completed": false - } - - const newReviewers = [...applicationData.reviewers, newReviewer]; - - this.setState({ - applicationData: { - ...applicationData, - reviewers: newReviewers - } - }); - } - }); - }; - - // Render Review Modal - renderReviewerModal() { - 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))} - event={this.props.event} - t={this.props.t} + tagToRemove: null, + }); + } else { + this.cancelRemoveTag(); + this.error(response.message); + } + }); + }; + + cancelRemoveTag = () => { + this.setState({ + tagToRemove: null, + removeTagModalVisible: false, + }); + }; + + renderTags() { + const data = this.state.applicationData; + + if (data) { + let tags = data.tags + ? data.tags.map((tag) => { + return ( + + {tag.name} + this.removeTag(tag.id)} + className="far fa-trash-alt cursor-pointer" + > + + ); + }) + : null; + return tags; + } + } + + renderTagModal() { + const { eventLanguages } = this.state; + + if (eventLanguages) { + return ( + this.addTag(tag)} + eventLanguages={eventLanguages} /> - }; - - renderDeleteReviewerModal() { - const t = this.props.t; - - return this.removeReviewerService(e)} - onCancel={(e) => this.cancelRemoveReviewer(e)} - okText={t("Yes")} - cancelText={t("No")}> -

- {t('Are you sure you want to delete this reviewer?')} -

-
- }; - - - render() { - const { applicationData, tagList, tagMenu, error, eventLanguages, isLoading } = this.state; - // Translation - const t = this.props.t; - - // Loading - if (isLoading) { - return - }; - - return ( -
- {/* API Error */} - {error && -
-

{JSON.stringify(error)}

-
- } - - {/* Add Tag Modal*/} - {this.renderTagModal()} - {this.renderReviewerModal()} - {this.renderDeleteTagModal()} - {this.renderDeleteReviewerModal()} - - - {/* Headings */} - {applicationData && -
-
-

{applicationData.user_title} {applicationData.firstname} {applicationData.lastname}

-
- {this.renderTags()} - this.toggleTags(tagMenu)} className={tagMenu && tagList.length || tagMenu && eventLanguages.length - ? "btn btn-light active" - : "btn btn-light add-tags"}> - {t('Add tags')} - -
- - {/*Tag List*/} -
- {tagList && - tagList.map(val => { - return
- -
- })} - - - - -
-
- - {/* User details Right Tab */} -
-
-

{this.applicationStatus()}

- -
- -
-
- } - - + ); + } + } + + // Reviews + // Render Reviews + renderReviews() { + if (this.state.applicationData) { + if (this.state.applicationData.reviewers) { + const reviews = this.state.applicationData.reviewers.map( + (val, index) => { + let num = index + 1; + // {"reviewer_user_id": 4, "user_title": "Mr", "firstname": "Joe", "lastname": "Soap", "status": "completed"}, + return ( +
+ +
+

+ {val.user_title} {val.firstname} {val.lastname} +

+ + {val.status === "completed" && ( +

+ {this.props.t("Completed")} +

+ )} + {val.status === "started" && ( +

+ {this.props.t("In Progress")} +

+ )} + {val.status === "not_started" && ( +

+ {this.props.t("Not Started")} + +

+ )} +
+
+ ); + } + ); + + return reviews; + } + } + } + + // Remove Reviewer + removeReview(reviewer_user_id) { + this.setState({ + removeReviewerModalVisible: true, + reviewToRemove: reviewer_user_id, + }); + } + + removeReviewerService() { + const { applicationData, reviewToRemove } = this.state; + const { event } = this.props; + + reviewService + .deleteResponseReviewer(event.id, applicationData.id, reviewToRemove) + .then((response) => { + if (response.error) { + this.error(response.error); + } else { + const newReviewers = applicationData.reviewers.filter( + (r) => r === null || r.reviewer_user_id !== reviewToRemove + ); + + this.setState({ + applicationData: { + ...applicationData, + reviewers: newReviewers, + }, + removeReviewerModalVisible: false, + }); + } + }); + } + + cancelRemoveReviewer() { + this.setState({ + removeReviewerModalVisible: false, + reviewToRemove: null, + }); + } + + postReviewerService(reviewer) { + const { applicationData } = this.state; + + reviewService + .assignResponsesToReviewer( + this.props.event.id, + [applicationData.id], + reviewer.email + ) + .then((response) => { + if (response.error) { + this.error(response.error); + } else { + const newReviewer = { + reviewer_user_id: reviewer.reviewer_user_id, + user_title: reviewer.user_title, + firstname: reviewer.firstname, + lastname: reviewer.lastname, + completed: false, + }; + + const newReviewers = [...applicationData.reviewers, newReviewer]; + + this.setState({ + applicationData: { + ...applicationData, + reviewers: newReviewers, + }, + }); + } + }); + } + + // Render Review Modal + renderReviewerModal() { + return ( + 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 + ) + )} + event={this.props.event} + t={this.props.t} + /> + ); + } + + renderDeleteReviewerModal() { + const t = this.props.t; + + return ( + this.removeReviewerService(e)} + onCancel={(e) => this.cancelRemoveReviewer(e)} + okText={t("Yes")} + cancelText={t("No")} + > +

{t("Are you sure you want to delete this reviewer?")}

+
+ ); + } + + render() { + const { + applicationData, + tagList, + tagMenu, + error, + eventLanguages, + isLoading, + } = this.state; + // Translation + const t = this.props.t; + + // Loading + if (isLoading) { + return ; + } - {/*Response Data*/} - {applicationData && -
- {/* Reviews */} -
-

{t('Reviewers')}

-
- {this.renderReviews()} -
- -
-
- -
-
- - {this.renderSections()} -
+ return ( +
+ {/* API Error */} + {error && ( +
+

{JSON.stringify(error)}

+
+ )} + + {/* Add Tag Modal*/} + {this.renderTagModal()} + {this.renderReviewerModal()} + {this.renderDeleteTagModal()} + {this.renderDeleteReviewerModal()} + + {/* Headings */} + {applicationData && ( +
+
+

+ {applicationData.user_title} {applicationData.firstname}{" "} + {applicationData.lastname} +

+
+ {this.renderTags()} + this.toggleTags(tagMenu)} + className={ + (tagMenu && tagList.length) || + (tagMenu && eventLanguages.length) + ? "btn btn-light active" + : "btn btn-light add-tags" + } + > + {t("Add tags")} + +
+ + {/*Tag List*/} +
+ {tagList && + tagList.map((val) => { + return ( +
+ +
+ ); + })} + + +
+
+ {/* User details Right Tab */} +
+
+ {" "} +

{this.applicationStatus()}

+ +
- ) - }; -}; +
+ )} + + {/*Response Data*/} + {applicationData && ( +
+ {/* Reviews */} +
+

{t("Reviewers")}

+
+ {this.renderReviews()} +
+ +
+
+ +
+
+ + {this.renderSections()} +
+ )} +
+ ); + } +} export default withTranslation()(ResponsePage); diff --git a/webapp/src/pages/ResponsePage/ResponsePage.test.js b/webapp/src/pages/ResponsePage/ResponsePage.test.js index 9b2cec126..e487dbe81 100644 --- a/webapp/src/pages/ResponsePage/ResponsePage.test.js +++ b/webapp/src/pages/ResponsePage/ResponsePage.test.js @@ -1,116 +1,117 @@ import React from "react"; import { shallow } from "enzyme"; import ResponsePage from "./ResponsePage"; -import { applicationFormService } from '../../services/applicationForm/applicationForm.service'; -import { responsesService } from '../../services/responses/responses.service'; +import { applicationFormService } from "../../services/applicationForm/applicationForm.service"; +import { responsesService } from "../../services/responses/responses.service"; -jest.mock('react-i18next', () => ({ - // this mock makes sure any components using the translate HoC receive the t function as a prop - withTranslation: () => Component => { - Component.defaultProps = { ...Component.defaultProps, t: () => "" }; - return Component; - }, +jest.mock("react-i18next", () => ({ + // this mock makes sure any components using the translate HoC receive the t function as a prop + withTranslation: () => (Component) => { + Component.defaultProps = { ...Component.defaultProps, t: () => "" }; + return Component; + }, })); // Mock Props const props = { - event: { - id: 3 + event: { + id: 3, + }, + match: { + params: { + id: 1, + eventKey: "1234", }, - match: { - params: { - id: 1, - eventKey: "1234" - } - } -} + }, +}; /* The form data API call requires Authentication and therefore we need to insert formData manualy */ const formData = { - sections: [ - { - depends_on_question_id: null, - description: "This is the official application form to apply for participation in the Deep Learning Indaba to be held 25-31 August 2019 in Nairobi, Kenya. Students can also use this application form to apply for travel and accommodation awards. \n \n \n Closing date: 30 April 2019", - id: 12, - name: "Indaba 2019 Application Form", - order: 1, - questions: [], - show_for_values: false - }, - { - depends_on_question_id: null, - description: "This is the official application form to apply for participation in the Deep Learning Indaba to be held 25-31 August 2019 in Nairobi, Kenya. Students can also use this application form to apply for travel and accommodation awards. \n \n \n Closing date: 30 April 2019", - id: 12, - name: "Indaba 2019 Application Form", - order: 1, - questions: [], - show_for_values: false - } - ] -} - -const reviewersData = [ + sections: [ { - reviewer_user_id: 1, - user_title: "Mr", - firstname: "Joe", - lastname: "Bloggs", - status: "completed" + depends_on_question_id: null, + description: + "This is the official application form to apply for participation in the Deep Learning Indaba to be held 25-31 August 2019 in Nairobi, Kenya. Students can also use this application form to apply for travel and accommodation awards. \n \n \n Closing date: 30 April 2019", + id: 12, + name: "Indaba 2019 Application Form", + order: 1, + questions: [], + show_for_values: false, }, { - reviewer_user_id: 2, - user_title: "Ms", - firstname: "Jane", - lastname: "Bloggs", - status: "started" - } -] + depends_on_question_id: null, + description: + "This is the official application form to apply for participation in the Deep Learning Indaba to be held 25-31 August 2019 in Nairobi, Kenya. Students can also use this application form to apply for travel and accommodation awards. \n \n \n Closing date: 30 April 2019", + id: 12, + name: "Indaba 2019 Application Form", + order: 1, + questions: [], + show_for_values: false, + }, + ], +}; + +const reviewersData = [ + { + reviewer_user_id: 1, + user_title: "Mr", + firstname: "Joe", + lastname: "Bloggs", + status: "completed", + }, + { + reviewer_user_id: 2, + user_title: "Ms", + firstname: "Jane", + lastname: "Bloggs", + status: "started", + }, +]; const applicationData = { - id: 1, - application_form_id: 1, - user_id: 1, - is_submitted: false, - submitted_timestamp: null, - is_withdrawn: false, - withdrawn_timestamp: null, - started_timestamp: "2020-01-01", - answers: [], - language: "en", - user_title: "Mx", - firstname: "Finn", - lastname: "Dog", - tags: [], - reviewers: [] -} + id: 1, + application_form_id: 1, + user_id: 1, + is_submitted: false, + submitted_timestamp: null, + is_withdrawn: false, + withdrawn_timestamp: null, + started_timestamp: "2020-01-01", + answers: [], + language: "en", + user_title: "Mx", + firstname: "Finn", + lastname: "Dog", + tags: [], + reviewers: [], +}; // Tests test("Check if Response Page renders.", () => { - const wrapper = shallow(); - expect(wrapper.length).toEqual(1); + const wrapper = shallow(); + expect(wrapper.length).toEqual(1); }); test("Check if Question and Answer html renders.", async () => { - const wrapper = shallow(); - wrapper.setState({ - applicationForm: formData, - reviewers: reviewersData, - applicationData: applicationData, - isLoading: false - }) - expect(wrapper.find('.Q-A').length).toBeTruthy(); + const wrapper = shallow(); + wrapper.setState({ + applicationForm: formData, + reviewers: reviewersData, + applicationData: applicationData, + isLoading: false, + }); + expect(wrapper.find(".Q-A").length).toBeTruthy(); }); - test("Check if tag list renders.", async () => { - const wrapper = shallow(); - wrapper.setState({ - isLoading: false, - applicationData: applicationData, - reviewers: reviewersData, - tagMenu: true, - tagList: [{headline: ""},{headline: ""}], - eventLanguages: ["En", "Fr"] - }) - expect(wrapper.find('.tag-response.show').length).toEqual(1); + const wrapper = shallow(); + wrapper.setState({ + isLoading: false, + applicationData: applicationData, + reviewers: reviewersData, + tagMenu: true, + tagList: [{ headline: "" }, { headline: "" }], + eventLanguages: ["En", "Fr"], + }); + expect(wrapper.find(".tag-response.show").length).toEqual(1); }); diff --git a/webapp/src/pages/ResponsePage/components/ReviewModal.js b/webapp/src/pages/ResponsePage/components/ReviewModal.js index b5b5ed36f..03d7af683 100644 --- a/webapp/src/pages/ResponsePage/components/ReviewModal.js +++ b/webapp/src/pages/ResponsePage/components/ReviewModal.js @@ -1,102 +1,121 @@ -import React, { Component } from 'react'; - +import React, { Component } from "react"; class ReviewModal extends Component { - constructor(props) { - super(props); - this.state = { - selectedReviewer: null - } + constructor(props) { + super(props); + this.state = { + selectedReviewer: null, }; - - - handlePost(reviewers) { - this.props.handlePost(reviewers); - - this.setState({ - selectedReviewer: null - }); - } - - - - handleSelect(reviewer) { - this.setState({ - selectedReviewer: reviewer - }); - }; - - - - renderModal() { - const { selectedReviewer } = this.state; - - const { reviewers, t } = this.props; - - - if (this.props.event) { - return ( - + )}
); } diff --git a/webapp/src/pages/viewprofile/index.js b/webapp/src/pages/viewprofile/index.js index a842ccc9f..02ae896f9 100644 --- a/webapp/src/pages/viewprofile/index.js +++ b/webapp/src/pages/viewprofile/index.js @@ -1,3 +1,3 @@ import ViewProfile from "./ViewProfile"; -export default ViewProfile; \ No newline at end of file +export default ViewProfile; diff --git a/webapp/src/serviceWorker.js b/webapp/src/serviceWorker.js index 978d56561..c2456cc35 100755 --- a/webapp/src/serviceWorker.js +++ b/webapp/src/serviceWorker.js @@ -11,9 +11,9 @@ // opt-in, read http://bit.ly/CRA-PWA const isLocalhost = Boolean( - window.location.hostname === 'localhost' || + window.location.hostname === "localhost" || // [::1] is the IPv6 localhost address. - window.location.hostname === '[::1]' || + window.location.hostname === "[::1]" || // 127.0.0.1/8 is considered localhost for IPv4. window.location.hostname.match( /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ @@ -21,7 +21,7 @@ const isLocalhost = Boolean( ); export function register(config) { - if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { + if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) { // The URL constructor is available in all browsers that support SW. const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); if (publicUrl.origin !== window.location.origin) { @@ -31,7 +31,7 @@ export function register(config) { return; } - window.addEventListener('load', () => { + window.addEventListener("load", () => { const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; if (isLocalhost) { @@ -42,8 +42,8 @@ export function register(config) { // service worker/PWA documentation. navigator.serviceWorker.ready.then(() => { console.log( - 'This web app is being served cache-first by a service ' + - 'worker. To learn more, visit http://bit.ly/CRA-PWA' + "This web app is being served cache-first by a service " + + "worker. To learn more, visit http://bit.ly/CRA-PWA" ); }); } else { @@ -57,21 +57,21 @@ export function register(config) { function registerValidSW(swUrl, config) { navigator.serviceWorker .register(swUrl) - .then(registration => { + .then((registration) => { registration.onupdatefound = () => { const installingWorker = registration.installing; if (installingWorker === null) { return; } installingWorker.onstatechange = () => { - if (installingWorker.state === 'installed') { + if (installingWorker.state === "installed") { if (navigator.serviceWorker.controller) { // At this point, the updated precached content has been fetched, // but the previous service worker will still serve the older // content until all client tabs are closed. console.log( - 'New content is available and will be used when all ' + - 'tabs for this page are closed. See http://bit.ly/CRA-PWA.' + "New content is available and will be used when all " + + "tabs for this page are closed. See http://bit.ly/CRA-PWA." ); // Execute callback @@ -82,7 +82,7 @@ function registerValidSW(swUrl, config) { // At this point, everything has been precached. // It's the perfect time to display a // "Content is cached for offline use." message. - console.log('Content is cached for offline use.'); + console.log("Content is cached for offline use."); // Execute callback if (config && config.onSuccess) { @@ -93,23 +93,23 @@ function registerValidSW(swUrl, config) { }; }; }) - .catch(error => { - console.error('Error during service worker registration:', error); + .catch((error) => { + console.error("Error during service worker registration:", error); }); } function checkValidServiceWorker(swUrl, config) { // Check if the service worker can be found. If it can't reload the page. fetch(swUrl) - .then(response => { + .then((response) => { // Ensure service worker exists, and that we really are getting a JS file. - const contentType = response.headers.get('content-type'); + const contentType = response.headers.get("content-type"); if ( response.status === 404 || - (contentType !== null && contentType.indexOf('javascript') === -1) + (contentType !== null && contentType.indexOf("javascript") === -1) ) { // No service worker found. Probably a different app. Reload the page. - navigator.serviceWorker.ready.then(registration => { + navigator.serviceWorker.ready.then((registration) => { registration.unregister().then(() => { window.location.reload(); }); @@ -121,14 +121,14 @@ function checkValidServiceWorker(swUrl, config) { }) .catch(() => { console.log( - 'No internet connection found. App is running in offline mode.' + "No internet connection found. App is running in offline mode." ); }); } export function unregister() { - if ('serviceWorker' in navigator) { - navigator.serviceWorker.ready.then(registration => { + if ("serviceWorker" in navigator) { + navigator.serviceWorker.ready.then((registration) => { registration.unregister(); }); } diff --git a/webapp/src/services/applicationForm/applicationForm.service.js b/webapp/src/services/applicationForm/applicationForm.service.js index 283b56997..304828570 100644 --- a/webapp/src/services/applicationForm/applicationForm.service.js +++ b/webapp/src/services/applicationForm/applicationForm.service.js @@ -1,125 +1,128 @@ -import axios from 'axios'; -import {authHeader} from '../base.service'; +import axios from "axios"; +import { authHeader } from "../base.service"; const baseUrl = process.env.REACT_APP_API_URL; export const applicationFormService = { - getForEvent, - submit, - getResponse, - updateResponse, - withdraw, - getQuestionList, - getDetailsForEvent, - updateApplicationForm + getForEvent, + submit, + getResponse, + updateResponse, + withdraw, + getQuestionList, + getDetailsForEvent, + updateApplicationForm, }; - function getForEvent(eventId) { - return axios - .get(baseUrl + "/api/v1/application-form?event_id=" + eventId, { 'headers': authHeader() }) - .then(response => { - let formSpec = null; - if (response) formSpec = response.data; - return { - formSpec: formSpec, - status: response.status, - message: response.statusText - } + return axios + .get(baseUrl + "/api/v1/application-form?event_id=" + eventId, { + headers: authHeader(), + }) + .then((response) => { + let formSpec = null; + if (response) formSpec = response.data; + return { + formSpec: formSpec, + status: response.status, + message: response.statusText, + }; }) - .catch(function(error){ - return{ - formSpec:null, + .catch(function (error) { + return { + formSpec: null, error: error.response && error.response.data - ? error.response.data.message - : error.message, - status: error.response && error.response.status + ? error.response.data.message + : error.message, + status: error.response && error.response.status, }; - }); + }); } function getResponse(eventId) { return axios - .get(baseUrl + "/api/v1/response?event_id=" + eventId, { 'headers': authHeader() }) - .then(resp => { - let response = null; - if (resp) response = resp.data; - return { - response: response, - status: resp.status, - message: resp.statusText - } + .get(baseUrl + "/api/v1/response?event_id=" + eventId, { + headers: authHeader(), }) - .catch(error => { + .then((resp) => { + let response = null; + if (resp) response = resp.data; + return { + response: response, + status: resp.status, + message: resp.statusText, + }; + }) + .catch((error) => { if (error.response) { return { - response: null, - status: error.response.status, - message: error.response.statusText + response: null, + status: error.response.status, + message: error.response.statusText, }; } else { // The request was made but no response was received return { response: null, status: null, - message: error.message + message: error.message, }; } }); } function submit(applicationFormId, isSubmitted, answers) { - let response = { - "application_form_id": applicationFormId, - "is_submitted": isSubmitted, - "answers": answers - } + let response = { + application_form_id: applicationFormId, + is_submitted: isSubmitted, + answers: answers, + }; - return axios.post(baseUrl + `/api/v1/response`, response, {headers: authHeader()}) - .then(resp=> { + return axios + .post(baseUrl + `/api/v1/response`, response, { headers: authHeader() }) + .then((resp) => { + return { + response_id: resp.data.id, + is_submitted: resp.data.is_submitted, + submitted_timestamp: resp.data.submitted_timestamp, + status: response.status, + message: response.statusText, + }; + }) + .catch((error) => { + if (error.response) { return { - response_id: resp.data.id, - is_submitted: resp.data.is_submitted, - submitted_timestamp: resp.data.submitted_timestamp, - status: response.status, - message: response.statusText + response_id: null, + status: error.response.status, + message: error.response.statusText, + is_submitted: false, + submitted_timestamp: null, + }; + } else { + // The request was made but no response was received + return { + response_id: null, + status: null, + message: error.message, + is_submitted: false, + submitted_timestamp: null, }; - }) - .catch(error => { - if (error.response) { - return { - response_id: null, - status: error.response.status, - message: error.response.statusText, - is_submitted: false, - submitted_timestamp: null - }; - } else { - // The request was made but no response was received - return { - response_id: null, - status: null, - message: error.message, - is_submitted: false, - submitted_timestamp: null - }; - } - }) + } + }); } - - function updateResponse(response_id, applicationFormId, isSubmitted, answers) { let response = { - "id": response_id, - "application_form_id": applicationFormId, - "is_submitted": isSubmitted, - "answers": answers - } + id: response_id, + application_form_id: applicationFormId, + is_submitted: isSubmitted, + answers: answers, + }; - return axios.put(baseUrl + `/api/v1/response`, response, {headers: authHeader()}) - .then(resp=> { + return axios + .put(baseUrl + `/api/v1/response`, response, { headers: authHeader() }) + .then((resp) => { return { response_id: resp.data.id, is_submitted: resp.data.is_submitted, @@ -128,14 +131,14 @@ function updateResponse(response_id, applicationFormId, isSubmitted, answers) { message: response.statusText, }; }) - .catch(error => { + .catch((error) => { if (error.response) { return { response_id: null, status: error.response.status, message: error.response.statusText, is_submitted: response.is_submitted, - submitted_timestamp: null + submitted_timestamp: null, }; } else { // The request was made but no response was received @@ -144,134 +147,156 @@ function updateResponse(response_id, applicationFormId, isSubmitted, answers) { status: null, message: error.message, is_submitted: response.is_submitted, - submitted_timestamp: null + submitted_timestamp: null, }; } - }) - } - + }); +} + function withdraw(id) { - return axios.delete(baseUrl + `/api/v1/response`, {'headers': authHeader(), 'data': {'id': id} }) - .then(resp=> { + return axios + .delete(baseUrl + `/api/v1/response`, { + headers: authHeader(), + data: { id: id }, + }) + .then((resp) => { + return { + isError: false, + status: resp.status, + message: resp.statusText, + }; + }) + .catch((error) => { + if (error.response) { + return { + isError: true, + status: error.response.status, + message: error.response.statusText, + }; + } else { + // The request was made but no response was received return { - isError: false, - status: resp.status, - message: resp.statusText + isError: true, + status: null, + message: error.message, }; - }) - .catch(error => { - if (error.response) { - return { - isError: true, - status: error.response.status, - message: error.response.statusText, - }; - } else { - // The request was made but no response was received - return { - isError: true, - status: null, - message: error.message, - }; - } - }); + } + }); } function getQuestionList(eventId) { return axios - .get(baseUrl + "/api/v1/questions", { - "headers": authHeader(), - "params": { - event_id: eventId - } + .get(baseUrl + "/api/v1/questions", { + headers: authHeader(), + params: { + event_id: eventId, + }, }) - .then(response => { - let questions = null; - if (response) questions = response.data; - return { - questions: questions, - status: response.status, - message: response.statusText - } + .then((response) => { + let questions = null; + if (response) questions = response.data; + return { + questions: questions, + status: response.status, + message: response.statusText, + }; }) - .catch(function(error){ - return{ - questions: null, - error: - error.response && error.response.data - ? error.response.data.message - : error.message, - status: error.response && error.response.status - }; + .catch(function (error) { + return { + questions: null, + error: + error.response && error.response.data + ? error.response.data.message + : error.message, + status: error.response && error.response.status, + }; }); } function getDetailsForEvent(eventId) { return axios - .get(baseUrl + "/api/v1/application-form-detail?event_id=" + eventId, - { 'headers': authHeader() }) - .then(response => { + .get(baseUrl + "/api/v1/application-form-detail?event_id=" + eventId, { + headers: authHeader(), + }) + .then((response) => { let formSpec = null; if (response) formSpec = response.data; return { - formSpec: formSpec, - status: response.status, - message: response.statusText - } - }) - .catch(function(error){ - return{ - formSpec:null, - error: - error.response && error.response.data - ? error.response.data.message - : error.message, - status: error.response && error.response.status - }; - }); + formSpec: formSpec, + status: response.status, + message: response.statusText, + }; + }) + .catch(function (error) { + return { + formSpec: null, + error: + error.response && error.response.data + ? error.response.data.message + : error.message, + status: error.response && error.response.status, + }; + }); } -export function updateApplicationForm(id, event_id, is_open, nominations, sections) { +export function updateApplicationForm( + id, + event_id, + is_open, + nominations, + sections +) { let form = { - "id": id, - 'event_id': event_id, - "is_open": is_open, - "nominations": nominations, - "sections": sections - } + id: id, + event_id: event_id, + is_open: is_open, + nominations: nominations, + sections: sections, + }; - return axios.put(baseUrl + `/api/v1/application-form-detail`, form, {headers: authHeader()}) - .then(resp=> { + return axios + .put(baseUrl + `/api/v1/application-form-detail`, form, { + headers: authHeader(), + }) + .then((resp) => { return resp; }) - .catch(error => { + .catch((error) => { if (error.response) { return error.response; } else { // The request was made but no response was received return error; } - }) - } + }); +} + +export function createApplicationForm( + event_id, + is_open, + nominations, + sections +) { + let form = { + event_id: event_id, + is_open: is_open, + nominations: nominations, + sections: sections, + }; - export function createApplicationForm(event_id, is_open, nominations, sections) { - let form = { - 'event_id': event_id, - "is_open": is_open, - "nominations": nominations, - "sections": sections - } - - return axios.post(baseUrl + `/api/v1/application-form-detail`, form, {headers: authHeader()}) - .then(resp=> { - return resp; - }) - .catch(error => { - if (error.response) { - return error.response; - } else { - // The request was made but no response was received - return error; - } - }) - } \ No newline at end of file + return axios + .post(baseUrl + `/api/v1/application-form-detail`, form, { + headers: authHeader(), + }) + .then((resp) => { + return resp; + }) + .catch((error) => { + if (error.response) { + return error.response; + } else { + // The request was made but no response was received + return error; + } + }); +} diff --git a/webapp/src/services/applicationForm/index.js b/webapp/src/services/applicationForm/index.js index b396ea397..f55ae88a0 100644 --- a/webapp/src/services/applicationForm/index.js +++ b/webapp/src/services/applicationForm/index.js @@ -1 +1 @@ -export * from './applicationForm.service'; \ No newline at end of file +export * from "./applicationForm.service"; diff --git a/webapp/src/services/attendance/attendance.service.js b/webapp/src/services/attendance/attendance.service.js index 6cc7d8773..d000c67d8 100644 --- a/webapp/src/services/attendance/attendance.service.js +++ b/webapp/src/services/attendance/attendance.service.js @@ -6,7 +6,7 @@ const baseUrl = process.env.REACT_APP_API_URL; export const attendanceService = { getAttendanceList, confirm, - undoConfirmation + undoConfirmation, }; function getAttendanceList(eventId, exclude_already_signed_in) { @@ -18,23 +18,23 @@ function getAttendanceList(eventId, exclude_already_signed_in) { "&exclude_already_signed_in=" + exclude_already_signed_in, { - headers: authHeader() + headers: authHeader(), } ) - .then(function(response) { + .then(function (response) { return { data: response.data, - error: "" + error: "", }; }) - .catch(function(error) { + .catch(function (error) { return { data: null, error: error.response && error.response.data ? error.response.data.message : error.message, - statusCode: error.response && error.response.status + statusCode: error.response && error.response.status, }; }); } @@ -42,28 +42,28 @@ function getAttendanceList(eventId, exclude_already_signed_in) { function confirm(eventId, userId) { const data = { user_id: userId, - event_id: eventId + event_id: eventId, }; return axios .post(baseUrl + "/api/v1/attendance", data, { - headers: authHeader() + headers: authHeader(), }) - .then(function(response) { + .then(function (response) { return { data: response.data, error: "", - statusCode: response.status + statusCode: response.status, }; }) - .catch(function(error) { + .catch(function (error) { return { data: null, error: error.response && error.response.data ? error.response.data.message : error.message, - statusCode: error.response && error.response.status + statusCode: error.response && error.response.status, }; }); } @@ -71,25 +71,25 @@ function confirm(eventId, userId) { function undoConfirmation(eventId, userId) { const data = { user_id: userId, - event_id: eventId + event_id: eventId, }; return axios .delete(baseUrl + `/api/v1/attendance`, { headers: authHeader(), data }) - .then(function(response) { + .then(function (response) { return { data: response.data, error: "", - statusCode: response.status + statusCode: response.status, }; }) - .catch(function(error) { + .catch(function (error) { return { data: null, error: error.response && error.response.data ? error.response.data.message : error.message, - statusCode: error.response && error.response.status + statusCode: error.response && error.response.status, }; }); } diff --git a/webapp/src/services/base.service.js b/webapp/src/services/base.service.js index dc1bee416..4876e2bd5 100644 --- a/webapp/src/services/base.service.js +++ b/webapp/src/services/base.service.js @@ -1,6 +1,6 @@ import axios from "axios"; import history from "../History"; -import i18n from 'i18next'; +import i18n from "i18next"; export function authHeader() { // return authorization header with basic auth credentials @@ -36,12 +36,11 @@ axios.interceptors.response.use( // but only if you aren't on the home or sign up page localStorage.removeItem("user"); if ( - history && history.location && - ( - history.location.pathname !== "/" && - history.location.pathname !== "/createAccount" && - history.location.pathname !== "/resetPassword" - ) + history && + history.location && + history.location.pathname !== "/" && + history.location.pathname !== "/createAccount" && + history.location.pathname !== "/resetPassword" ) { history.push("/login"); } @@ -49,10 +48,8 @@ axios.interceptors.response.use( } ); -axios.interceptors.request.use( - request => { - request.params = request.params || {}; - request.params['language'] = i18n.language; - return request; - } -) +axios.interceptors.request.use((request) => { + request.params = request.params || {}; + request.params["language"] = i18n.language; + return request; +}); diff --git a/webapp/src/services/eventStats/eventStats.service.js b/webapp/src/services/eventStats/eventStats.service.js index cfee04b5b..f521e3e87 100644 --- a/webapp/src/services/eventStats/eventStats.service.js +++ b/webapp/src/services/eventStats/eventStats.service.js @@ -4,67 +4,79 @@ import { authHeader } from "../base.service"; const baseUrl = process.env.REACT_APP_API_URL; export const eventStatsService = { - getStats, - sendReminderToBegin, - sendReminderToSubmit -} + getStats, + sendReminderToBegin, + sendReminderToSubmit, +}; function getStats(eventId) { - return axios - .get(baseUrl + "/api/v1/eventstats?event_id=" + eventId, { headers: authHeader() }) - .then(function(response) { - return { - stats: response.data, - error: "" - }; - }) - .catch(function(error) { - return { - stats: null, - error: (error.response && error.response.data) ? error.response.data.message : error.message - } - }); + return axios + .get(baseUrl + "/api/v1/eventstats?event_id=" + eventId, { + headers: authHeader(), + }) + .then(function (response) { + return { + stats: response.data, + error: "", + }; + }) + .catch(function (error) { + return { + stats: null, + error: + error.response && error.response.data + ? error.response.data.message + : error.message, + }; + }); } function sendReminderToSubmit(eventId) { - let event = { - "event_id": eventId, - }; - return axios - .post(baseUrl + "/api/v1/reminder-unsubmitted", event, { headers: authHeader() }) - .then(function(response) { - return { - msg: "Reminders sent", - error: "" - } - }) - .catch(function(error) { - return { - msg: "", - error: (error.response && error.response.data) ? error.response.data.message : error.message - } - }) + let event = { + event_id: eventId, + }; + return axios + .post(baseUrl + "/api/v1/reminder-unsubmitted", event, { + headers: authHeader(), + }) + .then(function (response) { + return { + msg: "Reminders sent", + error: "", + }; + }) + .catch(function (error) { + return { + msg: "", + error: + error.response && error.response.data + ? error.response.data.message + : error.message, + }; + }); } function sendReminderToBegin(eventId) { - let event = { - "event_id": eventId, - }; - return axios - .post(baseUrl + "/api/v1/reminder-not-started", event, { headers: authHeader() }) - .then(function(response) { - return { - msg: "Reminders sent", - error: "" - } - }) - .catch(function(error) { - return { - msg: "Failed", - error: (error.response && error.response.data) ? error.response.data.message : error.message - } - }) + let event = { + event_id: eventId, + }; + return axios + .post(baseUrl + "/api/v1/reminder-not-started", event, { + headers: authHeader(), + }) + .then(function (response) { + return { + msg: "Reminders sent", + error: "", + }; + }) + .catch(function (error) { + return { + msg: "Failed", + error: + error.response && error.response.data + ? error.response.data.message + : error.message, + }; + }); } - - - diff --git a/webapp/src/services/eventStats/index.js b/webapp/src/services/eventStats/index.js index cd0062808..793afde93 100644 --- a/webapp/src/services/eventStats/index.js +++ b/webapp/src/services/eventStats/index.js @@ -1 +1 @@ -export * from './eventStats.service'; \ No newline at end of file +export * from "./eventStats.service"; diff --git a/webapp/src/services/events/events.service.js b/webapp/src/services/events/events.service.js index aaba03b63..bdfcf86bc 100644 --- a/webapp/src/services/events/events.service.js +++ b/webapp/src/services/events/events.service.js @@ -8,27 +8,29 @@ export const eventService = { create, update, getEvents, - getByKey + getByKey, }; export function getEvent(event_id) { return axios - .get(baseUrl + `/api/v1/event?id=` + event_id + "&cache_bust=1", { headers: authHeader() }) - .then(response => { + .get(baseUrl + `/api/v1/event?id=` + event_id + "&cache_bust=1", { + headers: authHeader(), + }) + .then((response) => { return { event: response.data, error: "", - statusCode: response.status + statusCode: response.status, }; }) - .catch(error => { + .catch((error) => { return { event: null, error: error.response && error.response.data ? error.response.data.message : error.message, - statusCode: error.response && error.response.status + statusCode: error.response && error.response.status, }; }); } @@ -55,25 +57,25 @@ export function create(event) { offer_open: event.offer_open, offer_close: event.offer_close, registration_open: event.registration_open, - registration_close: event.registration_close + registration_close: event.registration_close, }, { headers: authHeader() } ) - .then(response => { + .then((response) => { return { event: response.data, error: "", - statusCode: response.status + statusCode: response.status, }; }) - .catch(error => { + .catch((error) => { return { event: null, error: error.response && error.response.data ? error.response.data.message : error.message, - statusCode: error.response && error.response.status + statusCode: error.response && error.response.status, }; }); } @@ -101,25 +103,25 @@ export function update(event) { offer_open: event.offer_open, offer_close: event.offer_close, registration_open: event.registration_open, - registration_close: event.registration_close + registration_close: event.registration_close, }, { headers: authHeader() } ) - .then(response => { + .then((response) => { return { event: response.data, error: "", - statusCode: response.status + statusCode: response.status, }; }) - .catch(error => { + .catch((error) => { return { event: null, error: error.response && error.response.data ? error.response.data.message : error.message, - statusCode: error.response && error.response.status + statusCode: error.response && error.response.status, }; }); } @@ -127,21 +129,21 @@ export function update(event) { function getEvents() { return axios .get(baseUrl + `/api/v1/events?cache_bust=1`, { headers: authHeader() }) - .then(function(response) { + .then(function (response) { return { events: response.data, error: "", - statusCode: response.status + statusCode: response.status, }; }) - .catch(function(error) { + .catch(function (error) { return { events: null, error: error.response && error.response.data ? error.response.data.message : error.message, - statusCode: error.response && error.response.status + statusCode: error.response && error.response.status, }; }); } @@ -149,23 +151,23 @@ function getEvents() { function getByKey(event_key) { return axios .get(baseUrl + `/api/v1/event-by-key?event_key=${event_key}&cache_bust=1`, { - headers: authHeader() + headers: authHeader(), }) - .then(function(response) { + .then(function (response) { return { event: response.data, error: "", - statusCode: response.status + statusCode: response.status, }; }) - .catch(function(error) { + .catch(function (error) { return { event: null, error: error.response && error.response.data ? error.response.data.message : error.message, - statusCode: error.response && error.response.status + statusCode: error.response && error.response.status, }; }); } diff --git a/webapp/src/services/file/file.service.js b/webapp/src/services/file/file.service.js index ad6025ab3..72e047c8a 100644 --- a/webapp/src/services/file/file.service.js +++ b/webapp/src/services/file/file.service.js @@ -1,34 +1,39 @@ -import axios from 'axios'; -import {authHeader} from '../base.service'; +import axios from "axios"; +import { authHeader } from "../base.service"; const baseUrl = process.env.REACT_APP_API_URL; export const fileService = { - uploadFile -} + uploadFile, +}; function uploadFile(file, onUploadProgress) { - var config = { - onUploadProgress: onUploadProgress, - headers: { - ...authHeader(), - 'Content-Type': 'multipart/form-data' - } - } + var config = { + onUploadProgress: onUploadProgress, + headers: { + ...authHeader(), + "Content-Type": "multipart/form-data", + }, + }; - let formData = new FormData(); - formData.append("file", file); + let formData = new FormData(); + formData.append("file", file); - return axios.post(baseUrl + "/api/v1/file", formData, config) - .then(response => { - return { - fileId: response.data.file_id, - error: "" - } - }).catch(error => { - return { - fileId: "", - error: (error.response && error.response.data) ? error.response.data.message : error.message - } - }); -} \ No newline at end of file + return axios + .post(baseUrl + "/api/v1/file", formData, config) + .then((response) => { + return { + fileId: response.data.file_id, + error: "", + }; + }) + .catch((error) => { + return { + fileId: "", + error: + error.response && error.response.data + ? error.response.data.message + : error.message, + }; + }); +} diff --git a/webapp/src/services/file/index.js b/webapp/src/services/file/index.js index b9b821a50..5d5458a28 100644 --- a/webapp/src/services/file/index.js +++ b/webapp/src/services/file/index.js @@ -1 +1 @@ -export * from './file.service'; \ No newline at end of file +export * from "./file.service"; diff --git a/webapp/src/services/invitedGuests/index.js b/webapp/src/services/invitedGuests/index.js index 8ba202cc3..b6a0a2f45 100644 --- a/webapp/src/services/invitedGuests/index.js +++ b/webapp/src/services/invitedGuests/index.js @@ -1 +1 @@ -export * from "./invitedGuest.service"; \ No newline at end of file +export * from "./invitedGuest.service"; diff --git a/webapp/src/services/invitedGuests/invitedGuests.service.js b/webapp/src/services/invitedGuests/invitedGuests.service.js index 96c68d1eb..09c728f6e 100644 --- a/webapp/src/services/invitedGuests/invitedGuests.service.js +++ b/webapp/src/services/invitedGuests/invitedGuests.service.js @@ -1,5 +1,5 @@ import axios from "axios"; -import { authHeader } from '../base.service'; +import { authHeader } from "../base.service"; const baseUrl = process.env.REACT_APP_API_URL; // const UNKNOWN_COUNTRY = 260; @@ -9,8 +9,8 @@ export const invitedGuestServices = { addInvitedGuest, createInvitedGuest, getRoles, - determineIfInvitedGuest -} + determineIfInvitedGuest, +}; const roleOptions = [ { value: "Speaker", label: "Speaker" }, @@ -21,7 +21,7 @@ const roleOptions = [ { value: "Dignitary", label: "Dignitary" }, { value: "Indaba X", label: "Indaba X" }, { value: "Sponsor", label: "Sponsor" }, - { value: "Press", label: "Press" } + { value: "Press", label: "Press" }, ]; function getRoles() { @@ -31,12 +31,12 @@ function getRoles() { function getInvitedGuestList(eventId) { return axios .get(baseUrl + "/api/v1/invitedGuestList?event_id=" + eventId, { - headers: authHeader() - }) + headers: authHeader(), + }) .then(function (response) { return { form: response.data, - error: "" + error: "", }; }) .catch(function (error) { @@ -45,7 +45,7 @@ function getInvitedGuestList(eventId) { error: error.response && error.response.data ? error.response.data.message - : error.message + : error.message, }; }); } @@ -62,41 +62,42 @@ function addInvitedGuest(email_address, event_Id, role) { .then(function (response) { return { msg: "succeeded", - response: response - } + response: response, + }; }) .catch(function (error) { if (error.response && error.response.status === 404) { - return { msg: "404" } - } - else if (error.response && error.response.status === 409) { - return { msg: "409" } - } - else { + return { msg: "404" }; + } else if (error.response && error.response.status === 409) { + return { msg: "409" }; + } else { return { msg: "Failed", - error: (error.response && error.response.data) ? error.response.data.message : error.message, - } + error: + error.response && error.response.data + ? error.response.data.message + : error.message, + }; } - }) + }); } function determineIfInvitedGuest(event_id) { return axios .get(baseUrl + "/api/v1/checkIfInvitedGuest?event_id=" + event_id, { - headers: authHeader() + headers: authHeader(), }) - .then(function(response) { + .then(function (response) { return { msg: "Guest Found", - statusCode: "200" + statusCode: "200", }; }) - .catch(function(error) { + .catch(function (error) { if (error.response && error.response.status === 404) { return { msg: "Guest Not Found", - statusCode: "404" + statusCode: "404", }; } else { return { @@ -104,13 +105,12 @@ function determineIfInvitedGuest(event_id) { error: error.response && error.response.data ? error.response.data.message - : error.message + : error.message, }; } }); } - function createInvitedGuest(user, event_Id, role) { let data = { event_id: event_Id, @@ -119,26 +119,30 @@ function createInvitedGuest(user, event_Id, role) { lastname: user.lastName, user_title: user.title, role: role, - policy_agreed: true + policy_agreed: true, }; - + return axios - .post(baseUrl + "/api/v1/invitedGuest/create", data, { headers: authHeader() }) + .post(baseUrl + "/api/v1/invitedGuest/create", data, { + headers: authHeader(), + }) .then(function (response) { - return { msg: "succeeded", - response: response - } + response: response, + }; }) .catch(function (error) { if (error.response && error.response.status === 409) { - return { msg: "409" } + return { msg: "409" }; } else { return { msg: "Failed", - error: (error.response && error.response.data) ? error.response.data.message : error.message, - } + error: + error.response && error.response.data + ? error.response.data.message + : error.message, + }; } - }) -} \ No newline at end of file + }); +} diff --git a/webapp/src/services/offer/index.js b/webapp/src/services/offer/index.js index 3d12fc75a..efe570818 100644 --- a/webapp/src/services/offer/index.js +++ b/webapp/src/services/offer/index.js @@ -1 +1 @@ -export * from "./offer.service"; \ No newline at end of file +export * from "./offer.service"; diff --git a/webapp/src/services/offer/offer.service.js b/webapp/src/services/offer/offer.service.js index 625bc9f66..aba29425e 100644 --- a/webapp/src/services/offer/offer.service.js +++ b/webapp/src/services/offer/offer.service.js @@ -1,94 +1,112 @@ import axios from "axios"; -import {authHeader} from '../base.service'; +import { authHeader } from "../base.service"; const baseUrl = process.env.REACT_APP_API_URL; export const offerServices = { - getOffer, - addOffer, - updateOffer -} - -function getOffer(event_id){ - return axios.get(baseUrl + "/api/v1/offer?event_id=" +event_id,{ - headers: authHeader() - }) - .then(function(response){ - return{ - offer: response.data, - error:"", - statusCode: response.status - }; - }) - .catch(function(error){ - return{ - offer:null, - error: - error.response && error.response.data - ? error.response.data.message - : error.message, - statusCode: error.response && error.response.status - }; - }); + getOffer, + addOffer, + updateOffer, +}; +function getOffer(event_id) { + return axios + .get(baseUrl + "/api/v1/offer?event_id=" + event_id, { + headers: authHeader(), + }) + .then(function (response) { + return { + offer: response.data, + error: "", + statusCode: response.status, + }; + }) + .catch(function (error) { + return { + offer: null, + error: + error.response && error.response.data + ? error.response.data.message + : error.message, + statusCode: error.response && error.response.status, + }; + }); } -function addOffer(user_id, event_id,offer_date,expiry_date,payment_required,travel_award,accommodation_award, accepted_accommodation_award, accepted_travel_award){ - let data = { - user_id:user_id, - event_id:event_id, - offer_date:offer_date, - expiry_date:expiry_date, - payment_required:payment_required, - travel_award:travel_award, - accommodation_award:accommodation_award, - accepted_accommodation_award:accepted_accommodation_award, - accepted_travel_award: accepted_travel_award - } +function addOffer( + user_id, + event_id, + offer_date, + expiry_date, + payment_required, + travel_award, + accommodation_award, + accepted_accommodation_award, + accepted_travel_award +) { + let data = { + user_id: user_id, + event_id: event_id, + offer_date: offer_date, + expiry_date: expiry_date, + payment_required: payment_required, + travel_award: travel_award, + accommodation_award: accommodation_award, + accepted_accommodation_award: accepted_accommodation_award, + accepted_travel_award: accepted_travel_award, + }; - return axios - .post(baseUrl + 'api/v1/offer',data,{headers:authHeader()}) - .then(function (response){ - return{ - message:"succeeded", - response: response - } - }) - .catch(function(error) { - return { - message: null, - error: - error.response && error.response.data - ? error.response.data.message - : error.message - }; - }); + return axios + .post(baseUrl + "api/v1/offer", data, { headers: authHeader() }) + .then(function (response) { + return { + message: "succeeded", + response: response, + }; + }) + .catch(function (error) { + return { + message: null, + error: + error.response && error.response.data + ? error.response.data.message + : error.message, + }; + }); } -function updateOffer(offer_id, event_id, candidate_response, rejected_reason, accepted_accommodation_award, accepted_travel_award){ - let data = { - offer_id:offer_id, - event_id:event_id, - candidate_response:candidate_response, - rejected_reason:rejected_reason, - accepted_travel_award:accepted_travel_award, - accepted_accommodation_award:accepted_accommodation_award - }; +function updateOffer( + offer_id, + event_id, + candidate_response, + rejected_reason, + accepted_accommodation_award, + accepted_travel_award +) { + let data = { + offer_id: offer_id, + event_id: event_id, + candidate_response: candidate_response, + rejected_reason: rejected_reason, + accepted_travel_award: accepted_travel_award, + accepted_accommodation_award: accepted_accommodation_award, + }; - return axios - .put(baseUrl + "/api/v1/offer", data,{ headers: authHeader() }) - .then(function(response){ - return{ - message:"succeeded", - response:response - } - }).catch(function(error) { - return { - message: null, - error: - error.response && error.response.data - ? error.response.data.message - : error.message - }; - }); + return axios + .put(baseUrl + "/api/v1/offer", data, { headers: authHeader() }) + .then(function (response) { + return { + message: "succeeded", + response: response, + }; + }) + .catch(function (error) { + return { + message: null, + error: + error.response && error.response.data + ? error.response.data.message + : error.message, + }; + }); } diff --git a/webapp/src/services/organisation/index.js b/webapp/src/services/organisation/index.js index f77ed2eeb..aabbe6d18 100644 --- a/webapp/src/services/organisation/index.js +++ b/webapp/src/services/organisation/index.js @@ -1 +1 @@ -export * from "./organisation.service"; \ No newline at end of file +export * from "./organisation.service"; diff --git a/webapp/src/services/organisation/organisation.service.js b/webapp/src/services/organisation/organisation.service.js index abf13d6ae..be4fedb89 100644 --- a/webapp/src/services/organisation/organisation.service.js +++ b/webapp/src/services/organisation/organisation.service.js @@ -3,27 +3,27 @@ import axios from "axios"; const baseUrl = process.env.REACT_APP_API_URL; export const organisationService = { - getOrganisation -} + getOrganisation, +}; function getOrganisation() { - return axios.get(baseUrl + "/api/v1/organisation") - .then(function (response) { - return { - organisation: response.data, - error: "", - statusCode: response.status - }; - }) - .catch(function (error) { - return { - organisation: null, - error: - error.response && error.response.data - ? error.response.data.message - : error.message, - statusCode: error.response && error.response.status - }; - }); + return axios + .get(baseUrl + "/api/v1/organisation") + .then(function (response) { + return { + organisation: response.data, + error: "", + statusCode: response.status, + }; + }) + .catch(function (error) { + return { + organisation: null, + error: + error.response && error.response.data + ? error.response.data.message + : error.message, + statusCode: error.response && error.response.status, + }; + }); } - diff --git a/webapp/src/services/profilelist/index.js b/webapp/src/services/profilelist/index.js index 8d6d0c1a4..2dfe70570 100644 --- a/webapp/src/services/profilelist/index.js +++ b/webapp/src/services/profilelist/index.js @@ -1 +1 @@ -export * from "./profilelist.service"; \ No newline at end of file +export * from "./profilelist.service"; diff --git a/webapp/src/services/profilelist/profilelist.service.js b/webapp/src/services/profilelist/profilelist.service.js index c16798bb1..673e41e52 100644 --- a/webapp/src/services/profilelist/profilelist.service.js +++ b/webapp/src/services/profilelist/profilelist.service.js @@ -4,37 +4,45 @@ import { authHeader } from "../base.service"; const baseUrl = process.env.REACT_APP_API_URL; export const profileService = { - getProfilesList, - getUserProfile -} + getProfilesList, + getUserProfile, +}; -function getProfilesList(event_id){ - return axios - .get(baseUrl+"/api/v1/userprofilelist?event_id="+event_id, { headers: authHeader()}) - .then((response)=>{ - return { List: response.data, error: ""}; - }) - .catch((error)=>{ - //handling an error, then assign an empty / null array - return {List:[], - error: error.response && error.response.data - ? error.response.data.message : - error.message } - }); +function getProfilesList(event_id) { + return axios + .get(baseUrl + "/api/v1/userprofilelist?event_id=" + event_id, { + headers: authHeader(), + }) + .then((response) => { + return { List: response.data, error: "" }; + }) + .catch((error) => { + //handling an error, then assign an empty / null array + return { + List: [], + error: + error.response && error.response.data + ? error.response.data.message + : error.message, + }; + }); } -function getUserProfile(user_id){ - return axios - .get(baseUrl+"/api/v1/userprofile?user_id="+user_id,{headers: authHeader()}) - .then((response)=>{ - return response.data; - }) - .catch((error)=>{ - return { user:{}, - error: error.response && error.response.data ? - error.response.data.message: - error.message } - }); +function getUserProfile(user_id) { + return axios + .get(baseUrl + "/api/v1/userprofile?user_id=" + user_id, { + headers: authHeader(), + }) + .then((response) => { + return response.data; + }) + .catch((error) => { + return { + user: {}, + error: + error.response && error.response.data + ? error.response.data.message + : error.message, + }; + }); } - - diff --git a/webapp/src/services/references/index.js b/webapp/src/services/references/index.js index 49f80f66c..367e7a2e9 100644 --- a/webapp/src/services/references/index.js +++ b/webapp/src/services/references/index.js @@ -1 +1 @@ -export * from "./reference.service"; \ No newline at end of file +export * from "./reference.service"; diff --git a/webapp/src/services/references/reference.service.js b/webapp/src/services/references/reference.service.js index ad97af4b9..0e1d8692a 100644 --- a/webapp/src/services/references/reference.service.js +++ b/webapp/src/services/references/reference.service.js @@ -4,100 +4,108 @@ import { authHeader, extractErrorMessage } from "../base.service"; const baseUrl = process.env.REACT_APP_API_URL; export const referenceService = { - getReferenceRequestDetails, - submitReference, - getReferenceRequests, - requestReference -} + getReferenceRequestDetails, + submitReference, + getReferenceRequests, + requestReference, +}; function getReferenceRequestDetails(token) { - return axios - .get(baseUrl + `/api/v1/reference-request/detail?token=${token}`, { headers: authHeader() }) - .then((response) => { - return { - details: response.data, - error: "" - } - }) - .catch((error) => { - return { - details: null, - error: extractErrorMessage(error) - } - }); + return axios + .get(baseUrl + `/api/v1/reference-request/detail?token=${token}`, { + headers: authHeader(), + }) + .then((response) => { + return { + details: response.data, + error: "", + }; + }) + .catch((error) => { + return { + details: null, + error: extractErrorMessage(error), + }; + }); } function submitReference(token, uploadedDocument, shouldUpdate) { - let reference = { - token: token, - uploaded_document: uploadedDocument - }; + let reference = { + token: token, + uploaded_document: uploadedDocument, + }; - const promise = shouldUpdate - ? axios.put(baseUrl + "/api/v1/reference", reference, { headers: authHeader() }) - : axios.post(baseUrl + "/api/v1/reference", reference, { headers: authHeader() }) + const promise = shouldUpdate + ? axios.put(baseUrl + "/api/v1/reference", reference, { + headers: authHeader(), + }) + : axios.post(baseUrl + "/api/v1/reference", reference, { + headers: authHeader(), + }); - return promise - .then(function (response) { - return { - error: "" - } - }) - .catch(error => { - return { - error: extractErrorMessage(error) - } - }); + return promise + .then(function (response) { + return { + error: "", + }; + }) + .catch((error) => { + return { + error: extractErrorMessage(error), + }; + }); } - function getReferenceRequests(responseId) { - return axios - .get( - baseUrl + - `/api/v1/reference-request/list?response_id=${responseId}`, - { - headers: authHeader() - } - ) - .then(function (response) { - return { - requests: response.data, - error: "" - }; - }) - .catch(function (error) { - return { - requests: null, - error: extractErrorMessage(error), - statusCode: error.response && error.response.status - }; - }); + return axios + .get(baseUrl + `/api/v1/reference-request/list?response_id=${responseId}`, { + headers: authHeader(), + }) + .then(function (response) { + return { + requests: response.data, + error: "", + }; + }) + .catch(function (error) { + return { + requests: null, + error: extractErrorMessage(error), + statusCode: error.response && error.response.status, + }; + }); } -function requestReference(responseId, title, firstname, lastname, email, relation) { - let data = { - response_id: responseId, - title: title, - firstname: firstname, - lastname: lastname, - email: email, - relation: relation - }; +function requestReference( + responseId, + title, + firstname, + lastname, + email, + relation +) { + let data = { + response_id: responseId, + title: title, + firstname: firstname, + lastname: lastname, + email: email, + relation: relation, + }; - return axios - .post(baseUrl + "/api/v1/reference-request", data, { - headers: authHeader() - }) - .then(function (response) { - return { - error: "", - referenceRequest: response.data - }; - }) - .catch(function (error) { - return { - error: extractErrorMessage(error) - }; - }); -} \ No newline at end of file + return axios + .post(baseUrl + "/api/v1/reference-request", data, { + headers: authHeader(), + }) + .then(function (response) { + return { + error: "", + referenceRequest: response.data, + }; + }) + .catch(function (error) { + return { + error: extractErrorMessage(error), + }; + }); +} diff --git a/webapp/src/services/registration/index.js b/webapp/src/services/registration/index.js index 5ac8a27c8..9c16d967d 100644 --- a/webapp/src/services/registration/index.js +++ b/webapp/src/services/registration/index.js @@ -1 +1 @@ -export * from "./registration.service"; \ No newline at end of file +export * from "./registration.service"; diff --git a/webapp/src/services/registration/registration.admin.service.js b/webapp/src/services/registration/registration.admin.service.js index 2acc79913..eb2de0114 100644 --- a/webapp/src/services/registration/registration.admin.service.js +++ b/webapp/src/services/registration/registration.admin.service.js @@ -5,56 +5,56 @@ const baseUrl = process.env.REACT_APP_API_URL; export const registrationAdminService = { getUnconfirmed, - confirm + confirm, }; function getUnconfirmed(eventId) { return axios .get(baseUrl + "/api/v1/registration/unconfirmed?event_id=" + eventId, { - headers: authHeader() + headers: authHeader(), }) - .then(function(response) { + .then(function (response) { return { data: response.data, - error: "" + error: "", }; }) - .catch(function(error) { + .catch(function (error) { return { data: null, error: error.response && error.response.data ? error.response.data.message : error.message, - statusCode: error.response && error.response.status + statusCode: error.response && error.response.status, }; }); } function confirm(registrationId) { const data = { - registration_id: registrationId + registration_id: registrationId, }; return axios .post(baseUrl + "/api/v1/registration/confirm", data, { - headers: authHeader() + headers: authHeader(), }) - .then(function(response) { + .then(function (response) { return { data: response.data, error: "", - statusCode: response.status + statusCode: response.status, }; }) - .catch(function(error) { + .catch(function (error) { return { data: null, error: error.response && error.response.data ? error.response.data.message : error.message, - statusCode: error.response && error.response.status + statusCode: error.response && error.response.status, }; }); } diff --git a/webapp/src/services/registration/registration.service.js b/webapp/src/services/registration/registration.service.js index abe9d3116..f2c636034 100644 --- a/webapp/src/services/registration/registration.service.js +++ b/webapp/src/services/registration/registration.service.js @@ -10,7 +10,7 @@ export const registrationService = { getGuestRegistration, submitGuestResponse, getGuestRegistrationResponse, - requestInvitationLetter + requestInvitationLetter, }; function getRegistrationForm(eventId, offerId) { @@ -22,23 +22,23 @@ function getRegistrationForm(eventId, offerId) { "&offer_id=" + offerId, { - headers: authHeader() + headers: authHeader(), } ) - .then(function(response) { + .then(function (response) { return { form: response.data, - error: "" + error: "", }; }) - .catch(function(error) { + .catch(function (error) { return { form: null, error: error.response && error.response.data ? error.response.data.message : error.message, - statusCode: error.response && error.response.status + statusCode: error.response && error.response.status, }; }); } @@ -46,21 +46,21 @@ function getRegistrationForm(eventId, offerId) { function getRegistrationResponse(id) { return axios .get(baseUrl + "/api/v1/registration-response", { - headers: authHeader() + headers: authHeader(), }) - .then(function(response) { + .then(function (response) { return { form: response.data, - error: "" + error: "", }; }) - .catch(function(error) { + .catch(function (error) { return { form: null, error: error.response && error.response.data ? error.response.data.message - : error.message + : error.message, }; }); } @@ -68,24 +68,24 @@ function getRegistrationResponse(id) { function submitResponse(data, shouldUpdate) { const promise = shouldUpdate ? axios.put(baseUrl + "/api/v1/registration-response", data, { - headers: authHeader() + headers: authHeader(), }) : axios.post(baseUrl + "/api/v1/registration-response", data, { - headers: authHeader() + headers: authHeader(), }); return promise - .then(function(response) { + .then(function (response) { return { error: "", - form: response + form: response, }; }) - .catch(function(error) { + .catch(function (error) { return { error: error.response && error.response.data && error.response.data.message ? error.response.data.message - : error.message + : error.message, }; }); } @@ -93,17 +93,17 @@ function submitResponse(data, shouldUpdate) { function getGuestRegistration(event_id) { return axios .get(baseUrl + "/api/v1/guest-registration-form?event_id=" + event_id, { - headers: authHeader() + headers: authHeader(), }) - .then(function(response) { + .then(function (response) { return { msg: "succeeded", form: response.data, - error: "" + error: "", }; }) - .catch(function(error) { + .catch(function (error) { if (error.response && error.response.status === 404) { return { msg: "404" }; } else { @@ -112,7 +112,7 @@ function getGuestRegistration(event_id) { error: error.response && error.response.data ? error.response.data.message - : error.message + : error.message, }; } }); @@ -121,21 +121,21 @@ function getGuestRegistration(event_id) { function getGuestRegistrationResponse() { return axios .get(baseUrl + "/api/v1/guest-registration", { - headers: authHeader() + headers: authHeader(), }) - .then(function(response) { + .then(function (response) { return { form: response.data, - error: "" + error: "", }; }) - .catch(function(error) { + .catch(function (error) { return { form: null, error: error.response && error.response.data ? error.response.data.message - : error.message + : error.message, }; }); } @@ -143,24 +143,24 @@ function getGuestRegistrationResponse() { function submitGuestResponse(data, shouldUpdate) { const promise = shouldUpdate ? axios.put(baseUrl + "/api/v1/guest-registration", data, { - headers: authHeader() + headers: authHeader(), }) : axios.post(baseUrl + "/api/v1/guest-registration", data, { - headers: authHeader() + headers: authHeader(), }); return promise - .then(function(response) { + .then(function (response) { return { error: "", - form: response + form: response, }; }) - .catch(function(error) { + .catch(function (error) { return { error: error.response && error.response.data && error.response.data.message ? error.response.data.message - : error.message + : error.message, }; }); } @@ -174,22 +174,23 @@ function requestInvitationLetter(user, event_id) { passport_name: user.fullNameOnPassport, passport_no: user.passportNumber, passport_issued_by: user.passportIssuedByAuthority, - passport_expiry_date: user.passportExpiryDate + passport_expiry_date: user.passportExpiryDate, }; return axios .post(baseUrl + `/api/v1/invitation-letter`, invitationLetterReq, { - headers: authHeader() + headers: authHeader(), }) - .then(response => { + .then((response) => { return response; }) - .catch(error => { - return { - invitationLetterId: null, - status: error.response ? error.response.status : null, - error: error.response && error.response.data - ? error.response.data.message - : error.message - }; + .catch((error) => { + return { + invitationLetterId: null, + status: error.response ? error.response.status : null, + error: + error.response && error.response.data + ? error.response.data.message + : error.message, + }; }); } diff --git a/webapp/src/services/responses/index.js b/webapp/src/services/responses/index.js index bd3ae29a9..6e501d70a 100644 --- a/webapp/src/services/responses/index.js +++ b/webapp/src/services/responses/index.js @@ -1 +1 @@ -export * from './responses.service'; \ No newline at end of file +export * from "./responses.service"; diff --git a/webapp/src/services/responses/responses.service.js b/webapp/src/services/responses/responses.service.js index 08848afec..ca037a1c0 100644 --- a/webapp/src/services/responses/responses.service.js +++ b/webapp/src/services/responses/responses.service.js @@ -1,129 +1,133 @@ -import axios from 'axios'; -import {authHeader} from '../base.service'; +import axios from "axios"; +import { authHeader } from "../base.service"; const baseUrl = process.env.REACT_APP_API_URL; export const responsesService = { - getResponseList, - getResponseDetail, - tagResponse, - removeTag -} + getResponseList, + getResponseDetail, + tagResponse, + removeTag, +}; function getResponseList(eventId, includeUnsubmitted, questionIds) { - return axios - .get(baseUrl + "/api/v1/responses", { - "headers": authHeader(), - "params": { - event_id: eventId, - include_unsubmitted: includeUnsubmitted, - question_ids: questionIds - } - }) - .then(response => { - let responses = null; - if (response) responses = response.data; - return { - responses: responses, - status: response.status, - message: response.statusText - } - }) - .catch(function(error){ - return{ - responses: null, - error: - error.response && error.response.data - ? error.response.data.message - : error.message, - status: error.response && error.response.status - }; - }); + return axios + .get(baseUrl + "/api/v1/responses", { + headers: authHeader(), + params: { + event_id: eventId, + include_unsubmitted: includeUnsubmitted, + question_ids: questionIds, + }, + }) + .then((response) => { + let responses = null; + if (response) responses = response.data; + return { + responses: responses, + status: response.status, + message: response.statusText, + }; + }) + .catch(function (error) { + return { + responses: null, + error: + error.response && error.response.data + ? error.response.data.message + : error.message, + status: error.response && error.response.status, + }; + }); } function getResponseDetail(responseId, eventId) { - return axios - .get(baseUrl + "/api/v1/responsedetail", { - "headers": authHeader(), - "params": { - response_id: responseId, - event_id: eventId - } - }) - .then(response => { - let detail = null; - if (response) detail = response.data; - return { - detail: detail, - status: response.status, - message: response.statusText - } - }) - .catch(function(error){ - return{ - detail: null, - error: - error.response && error.response.data - ? error.response.data.message - : error.message, - status: error.response && error.response.status - }; - }); + return axios + .get(baseUrl + "/api/v1/responsedetail", { + headers: authHeader(), + params: { + response_id: responseId, + event_id: eventId, + }, + }) + .then((response) => { + let detail = null; + if (response) detail = response.data; + return { + detail: detail, + status: response.status, + message: response.statusText, + }; + }) + .catch(function (error) { + return { + detail: null, + error: + error.response && error.response.data + ? error.response.data.message + : error.message, + status: error.response && error.response.status, + }; + }); } function tagResponse(responseId, tagId, eventId) { - return axios - .post(baseUrl + "/api/v1/responsetag", { - response_id: responseId, - tag_id: tagId, - event_id: eventId - }, { - "headers": authHeader() - }) - .then(response => { - let responseTag = null; - if (response) responseTag = response.data; - return { - responseTag: responseTag, - status: response.status, - message: response.statusText - } - }) - .catch(function(error){ - return{ - responseTag: null, - error: - error.response && error.response.data - ? error.response.data.message - : error.message, - status: error.response && error.response.status - }; - }); + return axios + .post( + baseUrl + "/api/v1/responsetag", + { + response_id: responseId, + tag_id: tagId, + event_id: eventId, + }, + { + headers: authHeader(), + } + ) + .then((response) => { + let responseTag = null; + if (response) responseTag = response.data; + return { + responseTag: responseTag, + status: response.status, + message: response.statusText, + }; + }) + .catch(function (error) { + return { + responseTag: null, + error: + error.response && error.response.data + ? error.response.data.message + : error.message, + status: error.response && error.response.status, + }; + }); } function removeTag(responseId, tagId, eventId) { - return axios - .delete(baseUrl + "/api/v1/responsetag", { - "headers": authHeader(), - "params": { - response_id: responseId, - tag_id: tagId, - event_id: eventId - } - }) - .then(response => { - return { - status: response.status, - message: response.statusText - } - }) - .catch(function(error){ - return{ - error: - error.response && error.response.data - ? error.response.data.message - : error.message, - status: error.response && error.response.status - }; - }); -} \ No newline at end of file + return axios + .delete(baseUrl + "/api/v1/responsetag", { + headers: authHeader(), + params: { + response_id: responseId, + tag_id: tagId, + event_id: eventId, + }, + }) + .then((response) => { + return { + status: response.status, + message: response.statusText, + }; + }) + .catch(function (error) { + return { + error: + error.response && error.response.data + ? error.response.data.message + : error.message, + status: error.response && error.response.status, + }; + }); +} diff --git a/webapp/src/services/reviews/index.js b/webapp/src/services/reviews/index.js index 5446607f7..0e8221945 100644 --- a/webapp/src/services/reviews/index.js +++ b/webapp/src/services/reviews/index.js @@ -1 +1 @@ -export * from './review.service'; \ No newline at end of file +export * from "./review.service"; diff --git a/webapp/src/services/reviews/review.service.js b/webapp/src/services/reviews/review.service.js index 6de523992..82d820f6d 100644 --- a/webapp/src/services/reviews/review.service.js +++ b/webapp/src/services/reviews/review.service.js @@ -20,27 +20,27 @@ export const reviewService = { getReviewStage, getReviewFormDetails, updateReviewForm, - createReviewForm + createReviewForm, }; function getReviewForm(eventId, skip) { return axios .get(baseUrl + "/api/v1/review?event_id=" + eventId + "&skip=" + skip, { - headers: authHeader() + headers: authHeader(), }) - .then(function(response) { + .then(function (response) { return { form: response.data, - error: "" + error: "", }; }) - .catch(function(error) { + .catch(function (error) { return { form: null, error: error.response && error.response.data ? error.response.data.message - : error.message + : error.message, }; }); } @@ -48,43 +48,47 @@ function getReviewForm(eventId, skip) { function getReviewResponse(id) { return axios .get(baseUrl + "/api/v1/reviewresponse?id=" + id, { - headers: authHeader() + headers: authHeader(), }) - .then(function(response) { + .then(function (response) { return { form: response.data, - error: "" + error: "", }; }) - .catch(function(error) { + .catch(function (error) { return { form: null, error: error.response && error.response.data ? error.response.data.message - : error.message + : error.message, }; }); } function getResponseReview(responseId, eventId) { return axios - .get(baseUrl + `/api/v1/responsereview?response_id=${responseId}&event_id=${eventId}`, { - headers: authHeader() - }) - .then(function(response) { + .get( + baseUrl + + `/api/v1/responsereview?response_id=${responseId}&event_id=${eventId}`, + { + headers: authHeader(), + } + ) + .then(function (response) { return { form: response.data, - error: "" + error: "", }; }) - .catch(function(error) { + .catch(function (error) { return { form: null, error: error.response && error.response.data ? error.response.data.message - : error.message + : error.message, }; }); } @@ -94,27 +98,31 @@ function submit(responseId, reviewFormId, scores, shouldUpdate, isSubmitted) { response_id: responseId, review_form_id: reviewFormId, scores: scores, - is_submitted: isSubmitted + is_submitted: isSubmitted, }; - const promise = shouldUpdate - ? axios.put(baseUrl + "/api/v1/reviewresponse", review, { headers: authHeader() }) - : axios.post(baseUrl + "/api/v1/reviewresponse", review, { headers: authHeader() }) - + const promise = shouldUpdate + ? axios.put(baseUrl + "/api/v1/reviewresponse", review, { + headers: authHeader(), + }) + : axios.post(baseUrl + "/api/v1/reviewresponse", review, { + headers: authHeader(), + }); + return promise - .then(function(response) { + .then(function (response) { return { reviewResponse: response.data, - error: "" + error: "", }; }) - .catch(function(error) { + .catch(function (error) { return { reviewResponse: null, error: error.response && error.response.data ? error.response.data.message - : error.message + : error.message, }; }); } @@ -122,22 +130,21 @@ function submit(responseId, reviewFormId, scores, shouldUpdate, isSubmitted) { function getReviewAssignments(eventId) { return axios .get(baseUrl + "/api/v1/reviewassignment?event_id=" + eventId, { - headers: authHeader() - + headers: authHeader(), }) - .then(function(response) { + .then(function (response) { return { reviewers: response.data, - error: "" + error: "", }; }) - .catch(function(error) { + .catch(function (error) { return { reviewers: null, error: error.response && error.response.data ? error.response.data.message - : error.message + : error.message, }; }); } @@ -146,24 +153,24 @@ function assignReviews(eventId, reviewerUserEmail, numReviews) { let assignment = { event_id: eventId, reviewer_user_email: reviewerUserEmail, - num_reviews: numReviews + num_reviews: numReviews, }; return axios .post(baseUrl + "/api/v1/reviewassignment", assignment, { - headers: authHeader() + headers: authHeader(), }) - .then(function(response) { + .then(function (response) { return { - error: "" + error: "", }; }) - .catch(function(error) { + .catch(function (error) { return { error: error.response && error.response.data ? error.response.data.message - : error.message + : error.message, }; }); } @@ -171,21 +178,21 @@ function assignReviews(eventId, reviewerUserEmail, numReviews) { function getReviewSummary(eventId) { return axios .get(baseUrl + "/api/v1/reviewassignment/summary?event_id=" + eventId, { - headers: authHeader() + headers: authHeader(), }) - .then(function(response) { + .then(function (response) { return { reviewSummary: response.data, - error: "" + error: "", }; }) - .catch(function(error) { + .catch(function (error) { return { reviewSummary: null, error: error.response && error.response.data ? error.response.data.message - : error.message + : error.message, }; }); } @@ -208,48 +215,44 @@ function getReviewHistory( "&sort_column=" + sort_column, { - headers: authHeader() + headers: authHeader(), } ) - .then(function(response) { + .then(function (response) { return { reviewHistory: response.data, - error: "" + error: "", }; }) - .catch(function(error) { + .catch(function (error) { return { reviewHistory: null, error: error.response && error.response.data ? error.response.data.message - : error.message + : error.message, }; }); } - function getReviewList(eventId) { return axios - .get( - baseUrl + "/api/v1/reviewlist?event_id=" + eventId, - { - headers: authHeader() - } - ) - .then(function(response) { + .get(baseUrl + "/api/v1/reviewlist?event_id=" + eventId, { + headers: authHeader(), + }) + .then(function (response) { return { reviewList: response.data, - error: "" + error: "", }; }) - .catch(function(error) { + .catch(function (error) { return { reviewList: null, error: error.response && error.response.data ? error.response.data.message - : error.message + : error.message, }; }); } @@ -258,24 +261,24 @@ function assignResponsesToReviewer(eventId, responseIds, reviewerEmail) { const assignment = { event_id: eventId, response_ids: responseIds, - reviewer_email: reviewerEmail + reviewer_email: reviewerEmail, }; return axios .post(baseUrl + "/api/v1/assignresponsereviewer", assignment, { - headers: authHeader() + headers: authHeader(), }) - .then(function(response) { + .then(function (response) { return { - error: "" + error: "", }; }) - .catch(function(error) { + .catch(function (error) { return { error: error.response && error.response.data ? error.response.data.message - : error.message + : error.message, }; }); } @@ -284,188 +287,179 @@ function deleteResponseReviewer(eventId, responseId, reviewerUserId) { const deleteParams = { event_id: eventId, response_id: responseId, - reviewer_user_id: reviewerUserId + reviewer_user_id: reviewerUserId, }; return axios .delete(baseUrl + "/api/v1/assignresponsereviewer", { headers: authHeader(), - params: deleteParams + params: deleteParams, }) - .then(function(response) { + .then(function (response) { return { - error: "" + error: "", }; }) - .catch(function(error) { + .catch(function (error) { return { error: error.response && error.response.data ? error.response.data.message - : error.message + : error.message, }; }); } function getReviewDetails(eventId) { return axios - .get( - baseUrl + "/api/v1/reviewresponsedetaillist?event_id=" + eventId, - { - headers: authHeader() - } - ) - .then(function(response) { + .get(baseUrl + "/api/v1/reviewresponsedetaillist?event_id=" + eventId, { + headers: authHeader(), + }) + .then(function (response) { return { reviewList: response.data, - error: "" + error: "", }; }) - .catch(function(error) { + .catch(function (error) { return { reviewList: null, - error: extractErrorMessage(error) + error: extractErrorMessage(error), }; }); } function getReviewSummaryList(eventId) { return axios - .get( - baseUrl + "/api/v1/reviewresponsesummarylist?event_id=" + eventId, - { - headers: authHeader() - } - ) - .then(function(response) { + .get(baseUrl + "/api/v1/reviewresponsesummarylist?event_id=" + eventId, { + headers: authHeader(), + }) + .then(function (response) { return { reviewList: response.data, - error: "" + error: "", }; }) - .catch(function(error) { + .catch(function (error) { return { reviewList: null, - error: extractErrorMessage(error) + error: extractErrorMessage(error), }; }); } function getReviewStage(eventId) { return axios - .get( - baseUrl + "/api/v1/reviewstage?event_id=" + eventId, - { - headers: authHeader() - } - ) - .then(function(response) { + .get(baseUrl + "/api/v1/reviewstage?event_id=" + eventId, { + headers: authHeader(), + }) + .then(function (response) { return { data: response.data, - error: "" + error: "", }; }) - .catch(function(error) { + .catch(function (error) { return { data: null, - error: extractErrorMessage(error) + error: extractErrorMessage(error), }; }); } -function getReviewFormDetails(eventId,stage) { +function getReviewFormDetails(eventId, stage) { const url = stage - ? `/api/v1/review-form-detail?event_id=${eventId}&stage=${stage}` - : `/api/v1/review-form-detail?event_id=${eventId}` + ? `/api/v1/review-form-detail?event_id=${eventId}&stage=${stage}` + : `/api/v1/review-form-detail?event_id=${eventId}`; return axios - .get( - baseUrl + url, - { - headers: authHeader() - } - ) - .then(function(res) { + .get(baseUrl + url, { + headers: authHeader(), + }) + .then(function (res) { return { data: res.data, - error: "" + error: "", }; }) - .catch(function(error) { + .catch(function (error) { return { data: null, - error: extractErrorMessage(error) + error: extractErrorMessage(error), }; }); } function updateReviewForm({ - id, eventId, isOpen, applicationFormId, - stage, deadline, active, sectionsToSave + id, + eventId, + isOpen, + applicationFormId, + stage, + deadline, + active, + sectionsToSave, }) { const form = { - "id": id, - 'event_id': eventId, - "is_open": isOpen, - "application_form_id": applicationFormId, - "stage": stage, - "deadline": deadline, - "active": active, - "sections": sectionsToSave - } + id: id, + event_id: eventId, + is_open: isOpen, + application_form_id: applicationFormId, + stage: stage, + deadline: deadline, + active: active, + sections: sectionsToSave, + }; return axios - .put( - baseUrl + `/api/v1/review-form-detail`, - form, - { - headers: authHeader() - }, - ) - .then(res => { + .put(baseUrl + `/api/v1/review-form-detail`, form, { + headers: authHeader(), + }) + .then((res) => { return { data: res.data, - error: "" + error: "", }; }) - .catch(error => { + .catch((error) => { return { data: null, - error: extractErrorMessage(error) + error: extractErrorMessage(error), }; }); } function createReviewForm({ - eventId, isOpen, applicationFormId, - stage, deadline, active, sectionsToSave + eventId, + isOpen, + applicationFormId, + stage, + deadline, + active, + sectionsToSave, }) { const form = { - 'event_id': eventId, - "is_open": isOpen, - "application_form_id": applicationFormId, - "stage": stage, - "deadline": deadline, - "active": active, - "sections": sectionsToSave - } + event_id: eventId, + is_open: isOpen, + application_form_id: applicationFormId, + stage: stage, + deadline: deadline, + active: active, + sections: sectionsToSave, + }; return axios - .post( - baseUrl + '/api/v1/review-form-detail', - form, - { - headers: authHeader() - }, - ) - .then(res => { - return { - data: res.data, - error: "", - status: res.status - }; - }) - .catch(error => { - return { - data: null, - error: extractErrorMessage(error) - }; - }); + .post(baseUrl + "/api/v1/review-form-detail", form, { + headers: authHeader(), + }) + .then((res) => { + return { + data: res.data, + error: "", + status: res.status, + }; + }) + .catch((error) => { + return { + data: null, + error: extractErrorMessage(error), + }; + }); } diff --git a/webapp/src/services/tags/index.js b/webapp/src/services/tags/index.js index 9841c3354..4a6bcc7b7 100644 --- a/webapp/src/services/tags/index.js +++ b/webapp/src/services/tags/index.js @@ -1 +1 @@ -export * from './tags.service'; \ No newline at end of file +export * from "./tags.service"; diff --git a/webapp/src/services/tags/tags.service.js b/webapp/src/services/tags/tags.service.js index 7b84d0f68..65bc87d46 100644 --- a/webapp/src/services/tags/tags.service.js +++ b/webapp/src/services/tags/tags.service.js @@ -1,128 +1,128 @@ -import axios from 'axios'; -import {authHeader} from '../base.service'; +import axios from "axios"; +import { authHeader } from "../base.service"; const baseUrl = process.env.REACT_APP_API_URL; export const tagsService = { - getTag, - addTag, - updateTag, - getTagList -} + getTag, + addTag, + updateTag, + getTagList, +}; function getTag(tagId, eventId) { - return axios - .get(baseUrl + "/api/v1/tag", { - "headers": authHeader(), - "params": { - id: tagId, - event_id: eventId - } - }) - .then(response => { - let tag = null; - if (response) tag = response.data; - return { - tag: tag, - status: response.status, - message: response.statusText - } - }) - .catch(function(error){ - return{ - tag: null, - error: - error.response && error.response.data - ? error.response.data.message - : error.message, - status: error.response && error.response.status - }; - }); + return axios + .get(baseUrl + "/api/v1/tag", { + headers: authHeader(), + params: { + id: tagId, + event_id: eventId, + }, + }) + .then((response) => { + let tag = null; + if (response) tag = response.data; + return { + tag: tag, + status: response.status, + message: response.statusText, + }; + }) + .catch(function (error) { + return { + tag: null, + error: + error.response && error.response.data + ? error.response.data.message + : error.message, + status: error.response && error.response.status, + }; + }); } function addTag(tag, eventId) { - return axios - .post(baseUrl + "/api/v1/tag", tag, { - "headers": authHeader(), - "params": { - event_id: eventId - } - }) - .then(response => { - let tag = null; - if (response) tag = response.data; - return { - tag: tag, - status: response.status, - message: response.statusText - } - }) - .catch(function(error){ - return{ - tag: null, - error: - error.response && error.response.data - ? error.response.data.message - : error.message, - status: error.response && error.response.status - }; - }); + return axios + .post(baseUrl + "/api/v1/tag", tag, { + headers: authHeader(), + params: { + event_id: eventId, + }, + }) + .then((response) => { + let tag = null; + if (response) tag = response.data; + return { + tag: tag, + status: response.status, + message: response.statusText, + }; + }) + .catch(function (error) { + return { + tag: null, + error: + error.response && error.response.data + ? error.response.data.message + : error.message, + status: error.response && error.response.status, + }; + }); } function updateTag(tag, eventId) { - return axios - .put(baseUrl + "/api/v1/tag", tag, { - "headers": authHeader(), - "params": { - event_id: eventId - } - }) - .then(response => { - let tag = null; - if (response) tag = response.data; - return { - tag: tag, - status: response.status, - message: response.statusText - } - }) - .catch(function(error){ - return{ - tag: null, - error: - error.response && error.response.data - ? error.response.data.message - : error.message, - status: error.response && error.response.status - }; - }); + return axios + .put(baseUrl + "/api/v1/tag", tag, { + headers: authHeader(), + params: { + event_id: eventId, + }, + }) + .then((response) => { + let tag = null; + if (response) tag = response.data; + return { + tag: tag, + status: response.status, + message: response.statusText, + }; + }) + .catch(function (error) { + return { + tag: null, + error: + error.response && error.response.data + ? error.response.data.message + : error.message, + status: error.response && error.response.status, + }; + }); } function getTagList(eventId) { - return axios - .get(baseUrl + "/api/v1/tags", { - "headers": authHeader(), - "params": { - event_id: eventId - } - }) - .then(response => { - let tags = null; - if (response) tags = response.data; - return { - tags: tags, - status: response.status, - message: response.statusText - } - }) - .catch(function(error){ - return{ - tags: null, - error: - error.response && error.response.data - ? error.response.data.message - : error.message, - status: error.response && error.response.status - }; - }); -} \ No newline at end of file + return axios + .get(baseUrl + "/api/v1/tags", { + headers: authHeader(), + params: { + event_id: eventId, + }, + }) + .then((response) => { + let tags = null; + if (response) tags = response.data; + return { + tags: tags, + status: response.status, + message: response.statusText, + }; + }) + .catch(function (error) { + return { + tags: null, + error: + error.response && error.response.data + ? error.response.data.message + : error.message, + status: error.response && error.response.status, + }; + }); +} diff --git a/webapp/src/services/user/index.js b/webapp/src/services/user/index.js index fefa75d9f..37f6938cc 100644 --- a/webapp/src/services/user/index.js +++ b/webapp/src/services/user/index.js @@ -1 +1 @@ -export * from './user.service'; \ No newline at end of file +export * from "./user.service"; diff --git a/webapp/src/services/user/user.service.js b/webapp/src/services/user/user.service.js index 438eb8b50..960f4a9a0 100644 --- a/webapp/src/services/user/user.service.js +++ b/webapp/src/services/user/user.service.js @@ -14,16 +14,16 @@ export const userService = { verifyEmail, resendVerification, addComment, - authRefresh + authRefresh, }; function login(email, password) { return axios .post(baseUrl + `/api/v1/authenticate`, { email: email, - password: password + password: password, }) - .then(response => { + .then((response) => { let user = null; if (response) user = response.data; @@ -45,9 +45,9 @@ function create(user) { lastname: user.lastName, user_title: user.title, password: user.password, - policy_agreed: user.agreePrivacyPolicy + policy_agreed: user.agreePrivacyPolicy, }) - .then(response => { + .then((response) => { let user = null; if (response) user = response.data; return user; @@ -63,11 +63,11 @@ function update(user) { firstname: user.firstName, lastname: user.lastName, user_title: user.title, - password: "" + password: "", }, { headers: authHeader() } ) - .then(response => { + .then((response) => { let user = null; if (response) user = response.data; localStorage.setItem("user", JSON.stringify(user)); @@ -78,27 +78,27 @@ function update(user) { function deleteAccount() { return axios .delete(baseUrl + `/api/v1/user`, { headers: authHeader() }) - .then(response => { + .then((response) => { localStorage.removeItem("user"); return { status: response.status, - message: response.statusText + message: response.statusText, }; }) - .catch(error => { + .catch((error) => { if (error.response) { return { status: error.response.status, message: error.response.data ? error.response.data.message - : error.response.statusText + : error.response.statusText, }; } else { // The request was made but no response was received return { status: null, - message: error.message + message: error.message, }; } }); @@ -112,12 +112,12 @@ function logout() { function get() { return axios .get(baseUrl + `/api/v1/user`, { headers: authHeader() }) - .then(function(response) { + .then(function (response) { // handle success const user = response.data; return user; }) - .catch(function(error) { + .catch(function (error) { // handle error return { error: error }; }); @@ -125,12 +125,12 @@ function get() { function authRefresh() { return axios - .get(baseUrl + '/api/v1/authenticationrefresh', { headers: authHeader() }) - .then(function(response) { + .get(baseUrl + "/api/v1/authenticationrefresh", { headers: authHeader() }) + .then(function (response) { const user = response.data; return user; }) - .catch(function(error) { + .catch(function (error) { // handle error return { error: error }; }); @@ -139,27 +139,27 @@ function authRefresh() { function requestPasswordReset(email) { return axios .post(baseUrl + `/api/v1/password-reset/request`, { - email: email + email: email, }) - .then(response => { + .then((response) => { return { status: response.status, - message: response.statusText + message: response.statusText, }; }) - .catch(error => { + .catch((error) => { if (error.response) { return { status: error.response.status, message: error.response.data ? error.response.data.message - : error.response.statusText + : error.response.statusText, }; } else { // The request was made but no response was received return { status: null, - message: error.message + message: error.message, }; } }); @@ -169,27 +169,27 @@ function confirmPasswordReset(password, code) { return axios .post(baseUrl + `/api/v1/password-reset/confirm`, { password: password, - code: code + code: code, }) - .then(response => { + .then((response) => { return { status: response.status, - message: response.statusText + message: response.statusText, }; }) - .catch(error => { + .catch((error) => { if (error.response) { return { status: error.response.status, message: error.response.data ? error.response.data.message - : error.response.statusText + : error.response.statusText, }; } else { // The request was made but no response was received return { status: null, - message: error.message + message: error.message, }; } }); @@ -199,17 +199,17 @@ function verifyEmail(token) { console.log("Verifying email with token: " + token); return axios .get(baseUrl + "/api/v1/verify-email?token=" + token) - .then(response => { + .then((response) => { return { - error: "" + error: "", }; }) - .catch(error => { + .catch((error) => { return { error: error.response && error.response.data ? error.response.data.message - : error.message + : error.message, }; }); } @@ -217,41 +217,41 @@ function verifyEmail(token) { function resendVerification(email) { return axios .get(baseUrl + "/api/v1/resend-verification-email?email=" + email) - .then(response => { + .then((response) => { return { - error: "" + error: "", }; }) - .catch(error => { + .catch((error) => { return { error: error.response && error.response.data ? error.response.data.message - : error.message + : error.message, }; }); } function addComment(event_id, user_id, comment) { let data = { - event_id: event_id, + event_id: event_id, user_id: user_id, - comment: comment + comment: comment, }; - + return axios .post(baseUrl + "/api/v1/user-comment", data, { headers: authHeader() }) - .then(response => { + .then((response) => { return { - error: "" + error: "", }; }) - .catch(error => { + .catch((error) => { return { error: error.response && error.response.data ? error.response.data.message - : error.message + : error.message, }; }); -} \ No newline at end of file +} diff --git a/webapp/src/setupTests.js b/webapp/src/setupTests.js index c0ef6ef64..1b86882f3 100644 --- a/webapp/src/setupTests.js +++ b/webapp/src/setupTests.js @@ -1,5 +1,5 @@ -const Enzyme = require('enzyme'); -const EnzymeAdapter = require('enzyme-adapter-react-16'); +const Enzyme = require("enzyme"); +const EnzymeAdapter = require("enzyme-adapter-react-16"); // Setup enzyme's react adapter -Enzyme.configure({ adapter: new EnzymeAdapter() }); \ No newline at end of file +Enzyme.configure({ adapter: new EnzymeAdapter() }); diff --git a/webapp/src/utils/adminMenu.js b/webapp/src/utils/adminMenu.js index c13beb573..2d0cd4e7c 100644 --- a/webapp/src/utils/adminMenu.js +++ b/webapp/src/utils/adminMenu.js @@ -1,21 +1,21 @@ -import React from 'react'; +import React from "react"; export const AdminMenu = (props) => { - return ( -
  • - -
    - {props.children} -
    -
  • - ) -} + return ( +
  • + +
    + {props.children} +
    +
  • + ); +}; diff --git a/webapp/src/utils/errorHandlerUtility.js b/webapp/src/utils/errorHandlerUtility.js index 377ac3f2a..f1b680a7a 100644 --- a/webapp/src/utils/errorHandlerUtility.js +++ b/webapp/src/utils/errorHandlerUtility.js @@ -7,11 +7,11 @@ const environment = process.env.NODE_ENV; let errorHandler; // TODO change Baobab to [event] if (environment === "production") { - if(api_key){ + if (api_key) { errorHandler = new StackdriverErrorReporter(); errorHandler.start({ key: api_key, - projectId: "baobab" + projectId: "baobab", }); } } else { diff --git a/webapp/src/utils/files.js b/webapp/src/utils/files.js index bd4c76cbd..1a106ed64 100644 --- a/webapp/src/utils/files.js +++ b/webapp/src/utils/files.js @@ -5,17 +5,17 @@ * @returns {String} */ export function getDownloadURL(value) { - const baseUrl = process.env.REACT_APP_API_URL; - let url = baseUrl + "/api/v1/file?filename="; - if (value) { - try { - const { filename, rename } = JSON.parse(value); - url += filename + "&rename=" + encodeURIComponent(rename); - } catch (e) { - url += value; - } + const baseUrl = process.env.REACT_APP_API_URL; + let url = baseUrl + "/api/v1/file?filename="; + if (value) { + try { + const { filename, rename } = JSON.parse(value); + url += filename + "&rename=" + encodeURIComponent(rename); + } catch (e) { + url += value; } - return url; + } + return url; } /** @@ -24,15 +24,15 @@ export function getDownloadURL(value) { * @param {String} filename Exported file's name */ export function downloadCSV(csv, filename) { - const csvData = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); + const csvData = new Blob([csv], { type: "text/csv;charset=utf-8;" }); if (navigator.msSaveBlob) { navigator.msSaveBlob(csvData, filename); } else { - var link = document.createElement('a'); + var link = document.createElement("a"); link.href = window.URL.createObjectURL(csvData); - link.setAttribute('download', filename); + link.setAttribute("download", filename); document.body.appendChild(link); link.click(); document.body.removeChild(link); } -}; +} diff --git a/webapp/src/utils/forms.js b/webapp/src/utils/forms.js index 8ffac77f4..fb6322c07 100644 --- a/webapp/src/utils/forms.js +++ b/webapp/src/utils/forms.js @@ -1,15 +1,15 @@ -import React from 'react'; -import icon from '../pages/createApplicationForm/icon.svg'; +import React from "react"; +import icon from "../pages/createApplicationForm/icon.svg"; export const dateFormat = (date) => { - return new Date(date).toLocaleDateString('en-GB', { - weekday: 'short', - month: 'long', - day: '2-digit', - year: 'numeric', - hour: '2-digit' - }) -} + return new Date(date).toLocaleDateString("en-GB", { + weekday: "short", + month: "long", + day: "2-digit", + year: "numeric", + hour: "2-digit", + }); +}; export const TopBar = ({ title, t }) => { return ( @@ -20,4 +20,4 @@ export const TopBar = ({ title, t }) => {
    ); -} +}; diff --git a/webapp/src/utils/user.js b/webapp/src/utils/user.js index 1939905b2..beb6871d8 100644 --- a/webapp/src/utils/user.js +++ b/webapp/src/utils/user.js @@ -1,58 +1,58 @@ export const isEventAdmin = (user, event) => { - if (!user) { - return false; - } - return ( - user.is_admin || - (user.roles && - user.roles.some( - r => r.role === "admin" && event && r.event_id === event.id - )) - ); - }; - - export const isRegistrationAdmin = (user, event) => { - if (!user) { - return false; - } - return ( - user.is_admin || - (user.roles && - user.roles.some( - r => - (r.role === "admin" || r.role === "registration-admin") && - event && - r.event_id === event.id - )) - ); - }; + if (!user) { + return false; + } + return ( + user.is_admin || + (user.roles && + user.roles.some( + (r) => r.role === "admin" && event && r.event_id === event.id + )) + ); +}; - export const isRegistrationVolunteer = (user, event) => { - if (!user) { - return false; - } - return ( - user.is_admin || - (user.roles && - user.roles.some( - r => - (r.role === "admin" || - r.role === "registration-admin" || - r.role === "registration-volunteer") && - event && - r.event_id === event.id - )) - ); - }; +export const isRegistrationAdmin = (user, event) => { + if (!user) { + return false; + } + return ( + user.is_admin || + (user.roles && + user.roles.some( + (r) => + (r.role === "admin" || r.role === "registration-admin") && + event && + r.event_id === event.id + )) + ); +}; - export const isEventReviewer = (user, event) => { - if (!user) { - return false; - } - return ( - user.roles && +export const isRegistrationVolunteer = (user, event) => { + if (!user) { + return false; + } + return ( + user.is_admin || + (user.roles && user.roles.some( - r => r.role === "reviewer" && event && r.event_id === event.id - ) - ); - }; \ No newline at end of file + (r) => + (r.role === "admin" || + r.role === "registration-admin" || + r.role === "registration-volunteer") && + event && + r.event_id === event.id + )) + ); +}; + +export const isEventReviewer = (user, event) => { + if (!user) { + return false; + } + return ( + user.roles && + user.roles.some( + (r) => r.role === "reviewer" && event && r.event_id === event.id + ) + ); +}; diff --git a/webapp/src/utils/validation/errorMessage.js b/webapp/src/utils/validation/errorMessage.js index c64fe3d12..dba115135 100644 --- a/webapp/src/utils/validation/errorMessage.js +++ b/webapp/src/utils/validation/errorMessage.js @@ -1,17 +1,18 @@ // TODO: HOW TO TRANSLATE THESE? -export const isRequiredDropdown = fieldName => `${fieldName} is required`; -export const isRequiredText = fieldName => `${fieldName} is required`; -export const isRequiredCheckBox = fieldName => `${fieldName} is required`; -export const isNotValidEmail = fieldName => +export const isRequiredDropdown = (fieldName) => `${fieldName} is required`; +export const isRequiredText = (fieldName) => `${fieldName} is required`; +export const isRequiredCheckBox = (fieldName) => `${fieldName} is required`; +export const isNotValidEmail = (fieldName) => `${fieldName} is not a valid email address`; -export const isNotValid = fieldName => `${fieldName} is not valid`; -export const minLength = length => { - return fieldName => `${fieldName} must be at least ${length} characters`; +export const isNotValid = (fieldName) => `${fieldName} is not valid`; +export const minLength = (length) => { + return (fieldName) => `${fieldName} must be at least ${length} characters`; }; -export const maxLength = length => { - return fieldName => `${fieldName} must be less than ${length} characters`; +export const maxLength = (length) => { + return (fieldName) => `${fieldName} must be less than ${length} characters`; }; -export const isNotValidUsername = fieldName => +export const isNotValidUsername = (fieldName) => `Please enter a valid email or cell phone number`; -export const isNotValidDate = fieldName => `${fieldName} is not a valid date.`; +export const isNotValidDate = (fieldName) => + `${fieldName} is not a valid date.`; export const matchingPasswords = () => `Passwords do not match.`; diff --git a/webapp/src/utils/validation/ruleRunner.js b/webapp/src/utils/validation/ruleRunner.js index 28219b1cc..b63a76b6e 100644 --- a/webapp/src/utils/validation/ruleRunner.js +++ b/webapp/src/utils/validation/ruleRunner.js @@ -1,20 +1,20 @@ export const ruleRunner = (rule, ...validations) => { - return state => { + return (state) => { for (let v of validations) { - let errorMessageFunc = v(state[rule.name]) + let errorMessageFunc = v(state[rule.name]); if (errorMessageFunc) { - return { [rule.name]: errorMessageFunc(rule.display) } + return { [rule.name]: errorMessageFunc(rule.display) }; } } - return null - } -} + return null; + }; +}; export const run = (state, runners) => { - let array = [] - runners.forEach(function(runner) { + let array = []; + runners.forEach(function (runner) { if (runner(state)) { - array.push(runner(state)) + array.push(runner(state)); } - }) - return array -} + }); + return array; +}; diff --git a/webapp/src/utils/validation/rules.js b/webapp/src/utils/validation/rules.js index 4dbabb5bd..c1496c042 100644 --- a/webapp/src/utils/validation/rules.js +++ b/webapp/src/utils/validation/rules.js @@ -1,14 +1,14 @@ import * as ErrorMessages from "./errorMessage.js"; -import * as validate from 'validate.js'; +import * as validate from "validate.js"; validate.validators.regex = (value, options, key, attributes) => { - let regExp = new RegExp(options.pattern); + let regExp = new RegExp(options.pattern); if (!regExp.test(value)) { return options.message; } }; -export const requiredText = text => { +export const requiredText = (text) => { if (text) { return null; } else { @@ -16,14 +16,14 @@ export const requiredText = text => { } }; -export const isValidDate = d => { +export const isValidDate = (d) => { let date = new Date(d); if (date instanceof Date && !isNaN(date)) { return null; } else return ErrorMessages.isNotValidDate; }; -export const requiredDropdown = text => { +export const requiredDropdown = (text) => { if (text) { return null; } else { @@ -31,7 +31,7 @@ export const requiredDropdown = text => { } }; -export const requiredCheckBox = checked => { +export const requiredCheckBox = (checked) => { if (checked) { return null; } else { @@ -39,9 +39,10 @@ export const requiredCheckBox = checked => { } }; -export const validEmail = text => { +export const validEmail = (text) => { if (text) { - var re = /^(([^<>()\]\\.,;:\s@"]+(\.[^<>()\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; + var re = + /^(([^<>()\]\\.,;:\s@"]+(\.[^<>()\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; if (!re.test(text.toLowerCase())) { return ErrorMessages.isNotValid; } @@ -49,19 +50,19 @@ export const validEmail = text => { return null; }; -export const minLength = length => { - return text => { +export const minLength = (length) => { + return (text) => { return text.length >= length ? null : ErrorMessages.minLength(length); }; }; -export const maxLength = length => { - return text => { +export const maxLength = (length) => { + return (text) => { return text.length <= length ? null : ErrorMessages.maxLength(length); }; }; -export const validPhoneNumber = text => { +export const validPhoneNumber = (text) => { if (text) { var re = /^[(]{0,1}[0-9]{3}[)]{0,1}[-\s]{0,1}[0-9]{3}[-\s]{0,1}[0-9]{4}$/; if (!re.test(text.toLowerCase())) { @@ -71,7 +72,7 @@ export const validPhoneNumber = text => { return null; }; -export const validUsername = text => { +export const validUsername = (text) => { if (text) { if (isNaN(text)) { if (validEmail(text)) { @@ -84,7 +85,7 @@ export const validUsername = text => { return null; }; -export const validName = text => { +export const validName = (text) => { if (text) { var re = /^[-A-Za-z ]+$/; if (re.test(text)) { @@ -94,27 +95,31 @@ export const validName = text => { return ErrorMessages.isNotValid; }; -export const validatePassword = text => { - return validate({password: text}, { - password: { - presence: true, - length: { - minimum: 6, - message: "must be at least 6 characters" - }, - regex: { - pattern: /^(?=.{6,}$)(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*\W).*$/, - message: "should contain at least 1 uppercase letter, 1 lowercase letter, 1 digit, 1 special character and must be at least 6 characters" +export const validatePassword = (text) => { + return validate( + { password: text }, + { + password: { + presence: true, + length: { + minimum: 6, + message: "must be at least 6 characters", + }, + regex: { + pattern: /^(?=.{6,}$)(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*\W).*$/, + message: + "should contain at least 1 uppercase letter, 1 lowercase letter, 1 digit, 1 special character and must be at least 6 characters", + }, }, } - }); -} + ); +}; export const matchingPasswords = (password, confirmPassword) => { - if(password && confirmPassword){ - if(password !== confirmPassword){ + if (password && confirmPassword) { + if (password !== confirmPassword) { return ErrorMessages.matchingPasswords; } } return null; -} \ No newline at end of file +}; diff --git a/webapp/src/utils/validation/validationFields.js b/webapp/src/utils/validation/validationFields.js index 08d913e0a..410e8512b 100644 --- a/webapp/src/utils/validation/validationFields.js +++ b/webapp/src/utils/validation/validationFields.js @@ -13,15 +13,15 @@ const validationFields = { fullNameOnPassport: { name: "fullNameOnPassport", display: "Full Name", - description: "Full Name as it appears on Passport." + description: "Full Name as it appears on Passport.", }, passportExpiryDate: { name: "passportExpiryDate", - display: "Passport Date of Expiry." + display: "Passport Date of Expiry.", }, passportIssuedByAuthority: { name: "passportIssuedByAuthority", - display: "Authority that issued Passport" + display: "Authority that issued Passport", }, workStreet1: { name: "workStreet1", display: "Work Street Address 1" }, workStreet2: { name: "workStreet2", display: "Work Street Address 2" }, @@ -30,27 +30,27 @@ const validationFields = { workCountry: { name: "workCountry", display: "Work Country" }, residentialStreet1: { name: "residentialStreet1", - display: "Residential Street Address 1" + display: "Residential Street Address 1", }, residentialStreet2: { name: "residentialStreet2", - display: "Residential Street Address 2" + display: "Residential Street Address 2", }, residentialCity: { name: "residentialCity", display: "Residential City" }, residentialPostalCode: { name: "residentialPostalCode", - display: "Residential Postal Code" + display: "Residential Postal Code", }, residentialCountry: { name: "residentialCountry", - display: "Residential Country" + display: "Residential Country", }, letterAddressedTo: { name: "letterAddressedTo", display: "Addressed to ", description: - "Who the visa letter should be addressed to, e.g. Immigration Officer of X Embassy" - } + "Who the visa letter should be addressed to, e.g. Immigration Officer of X Embassy", + }, }; export default validationFields;