diff --git a/api/app/models/bookings/exam.py b/api/app/models/bookings/exam.py index bc903a55e..bdfda45a7 100644 --- a/api/app/models/bookings/exam.py +++ b/api/app/models/bookings/exam.py @@ -49,6 +49,8 @@ class Exam(Base): payee_email = db.Column(db.String(50), nullable=True) payee_name = db.Column(db.String(50), nullable=True) payee_phone = db.Column(db.String(50), nullable=True) + candidates_list = db.Column(db.JSON, nullable=True) + is_pesticide = db.Column(db.Integer, nullable=True, default=0) booking = db.relationship("Booking") exam_type = db.relationship("ExamType") diff --git a/api/app/resources/bookings/booking/booking_post.py b/api/app/resources/bookings/booking/booking_post.py index 73e129344..919f177f7 100644 --- a/api/app/resources/bookings/booking/booking_post.py +++ b/api/app/resources/bookings/booking/booking_post.py @@ -55,11 +55,23 @@ def post(self): db.session.add(booking) db.session.commit() - else: + elif type(i_id) == int: booking.invigilators.append(Invigilator.query.filter_by(invigilator_id=i_id).first_or_404()) db.session.add(booking) db.session.commit() + + elif type(i_id) == list: + + if len(i_id) == 0: + db.session.add(booking) + db.session.commit() + + else: + for value in i_id: + booking.invigilators.append(Invigilator.query.filter_by(invigilator_id=value).first_or_404()) + db.session.add(booking) + db.session.commit() result = self.booking_schema.dump(booking) diff --git a/api/app/resources/bookings/exam/exam_bcmp.py b/api/app/resources/bookings/exam/exam_bcmp.py new file mode 100644 index 000000000..dfa9ed7a5 --- /dev/null +++ b/api/app/resources/bookings/exam/exam_bcmp.py @@ -0,0 +1,80 @@ +'''Copyright 2018 Province of British Columbia + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License.''' + +import logging +import copy +import json +from flask import request, g +from flask_restx import Resource +from app.models.theq import CSR, Office +from app.models.bookings import ExamType, Invigilator +from app.schemas.bookings import ExamSchema, CandidateSchema +from qsystem import api, api_call_with_retry, db, oidc +from app.utilities.bcmp_service import BCMPService + +from app.resources.bookings.exam.exam_post import ExamPost + +@api.route("/exams/bcmp/", methods=["POST"]) +class ExamBcmpPost(Resource): + + exam_schema = ExamSchema() + bcmp_service = BCMPService() + + @oidc.accept_token(require_token=True) + @api_call_with_retry + def post(self): + + csr = CSR.find_by_username(g.oidc_token_info['username']) + + json_data = request.get_json() + + exam, warning = self.exam_schema.load(json_data) + + print("json_data: ") + print(json_data) + + if warning: + logging.warning("WARNING: %s", warning) + return {"message": warning}, 422 + + if not (exam.office_id == csr.office_id or csr.liaison_designate == 1): + return {"The Exam Office ID and CSR Office ID do not match!"}, 403 + + formatted_data = ExamPost.format_data(self, json_data, exam) + exam = formatted_data["exam"] + + invigilator = None + if exam.invigilator_id: + invigilator = Invigilator.query.filter_by(invigilator_id=exam.invigilator_id).first() + + bcmp_response = None + if json_data["ind_or_group"] == "individual": + + exam_fees = json_data["fees"] + + logging.info("Creating individual pesticide exam") + bcmp_response = self.bcmp_service.create_individual_exam(exam, exam_fees, invigilator, formatted_data["pesticide_office"], g.oidc_token_info) + + else: + + logging.info("Creating Group pesticide exam") + bcmp_response = self.bcmp_service.create_group_exam_bcmp(exam, formatted_data["candidates_list_bcmp"], invigilator, formatted_data["pesticide_office"], g.oidc_token_info) + + + if bcmp_response: + return {"bcmp_job_id": bcmp_response['jobId'], + "errors": {}}, 201 + else: + return {"message": "create_group_exam_bcmp failed", + "error": bcmp_response}, 403 diff --git a/api/app/resources/bookings/exam/exam_bulk_status.py b/api/app/resources/bookings/exam/exam_bulk_status.py index 0687cadfb..292fe5214 100644 --- a/api/app/resources/bookings/exam/exam_bulk_status.py +++ b/api/app/resources/bookings/exam/exam_bulk_status.py @@ -13,18 +13,21 @@ limitations under the License.''' import logging +import json from flask import g -from flask_restplus import Resource +from flask_restx import Resource from sqlalchemy import exc from app.models.bookings import Exam +from app.schemas.bookings import ExamSchema from app.models.theq import CSR from app.utilities.bcmp_service import BCMPService -from qsystem import api, oidc +from qsystem import api, oidc, db @api.route("/exams/bcmp_status/", methods=["POST"]) class ExamList(Resource): bcmp_service = BCMPService() + exam_schema = ExamSchema() @oidc.accept_token(require_token=True) def post(self): @@ -32,9 +35,32 @@ def post(self): try: exams = Exam.query.filter_by(upload_received_ind=0).filter(Exam.bcmp_job_id.isnot(None)) - self.bcmp_service.bulk_check_exam_status(exams) + bcmp_response = self.bcmp_service.bulk_check_exam_status(exams) - return {}, 200 + job_ids = [] + for job in bcmp_response["jobs"]: + if job["jobStatus"] == "RESPONSE_UPLOADED": + job_ids.append(job["jobId"]) + + print("job_ids to update: ") + print(job_ids) + + exams_tobe_updated = None + + if len(job_ids) != 0: + exams_tobe_updated = Exam.query.filter(Exam.bcmp_job_id.in_(job_ids)) + + for exam in exams_tobe_updated: + exam_upd, warn = self.exam_schema.load({'upload_received_ind': 1}, instance=exam, partial=True) + db.session.add(exam_upd) + + try: + db.session.commit() + except: + db.session.rollback() + raise + + return {"exams_updated": exams_tobe_updated}, 200 except exc.SQLAlchemyError as error: logging.error(error, exc_info=True) diff --git a/api/app/resources/bookings/exam/exam_download.py b/api/app/resources/bookings/exam/exam_download.py index 0aed9a52d..3bd875cbf 100644 --- a/api/app/resources/bookings/exam/exam_download.py +++ b/api/app/resources/bookings/exam/exam_download.py @@ -13,7 +13,7 @@ limitations under the License.''' from flask import g, Response -from flask_restplus import Resource +from flask_restx import Resource import io import logging import urllib @@ -40,11 +40,11 @@ def get(self, exam_id): if not (exam.office_id == csr.office_id or csr.liaison_designate == 1): return {"The Exam Office ID and CSR Office ID do not match!"}, 403 - status = self.bcmp_service.check_exam_status(exam) - print(status) + job = self.bcmp_service.check_exam_status(exam) + print(job) - if status == 'PACKAGE_GENERATED': - package_url = status["jobProperties"]["EXAM_PACKAGE_URL"] + if job['jobStatus'] == 'PACKAGE_GENERATED': + package_url = job["jobProperties"]["EXAM_PACKAGE_URL"] req = urllib.request.Request(package_url) response = urllib.request.urlopen(req).read() exam_file = io.BytesIO(response) @@ -58,7 +58,7 @@ def get(self, exam_id): "Content-Type": "application/pdf" }) else: - return {'message': 'API is down'}, 400 + return {'message': 'Package not yet generated', 'status': job['jobStatus']}, 400 # test_url = 'http://www.pdf995.com/samples/pdf.pdf' # req = urllib.request.Request(test_url) # response = urllib.request.urlopen(req).read() diff --git a/api/app/resources/bookings/exam/exam_email_invigilator.py b/api/app/resources/bookings/exam/exam_email_invigilator.py index 793608036..b9defb438 100644 --- a/api/app/resources/bookings/exam/exam_email_invigilator.py +++ b/api/app/resources/bookings/exam/exam_email_invigilator.py @@ -13,7 +13,7 @@ limitations under the License.''' from flask import g, request -from flask_restplus import Resource +from flask_restx import Resource import logging from sqlalchemy import exc from app.models.theq import CSR diff --git a/api/app/resources/bookings/exam/exam_post.py b/api/app/resources/bookings/exam/exam_post.py index fe5793c63..358cbe16d 100644 --- a/api/app/resources/bookings/exam/exam_post.py +++ b/api/app/resources/bookings/exam/exam_post.py @@ -13,16 +13,17 @@ limitations under the License.''' import logging +import copy +import json from flask import request, g from flask_restx import Resource from app.models.theq import CSR, Office -from flask_restplus import Resource -from app.models.bookings import ExamType -from app.schemas.bookings import ExamSchema +from flask_restx import Resource +from app.models.bookings import ExamType, Invigilator +from app.schemas.bookings import ExamSchema, CandidateSchema from qsystem import api, api_call_with_retry, db, oidc from app.utilities.bcmp_service import BCMPService - @api.route("/exams/", methods=["POST"]) class ExamPost(Resource): @@ -33,46 +34,34 @@ class ExamPost(Resource): @api_call_with_retry def post(self): + is_bcmp_req = True if request.args.get('bcmp_pesticide') else False + + print("is_bcmp_req: ") + print(is_bcmp_req) + csr = CSR.find_by_username(g.oidc_token_info['username']) json_data = request.get_json() exam, warning = self.exam_schema.load(json_data) + print("json_data: ") + print(json_data) + if warning: logging.warning("WARNING: %s", warning) return {"message": warning}, 422 - print("+=+=+=+= NAME: %s +=+=+=+=" % exam.examinee_name) - - exam_type = ExamType.query.filter_by(exam_type_id=exam.exam_type_id).first() - - if not exam_type: - exam_type = ExamType.query.filter_by(pesticide_exam_ind=1, group_exam_ind=1).first() - exam.exam_type = exam_type - - if exam_type.pesticide_exam_ind: - if not exam_type.group_exam_ind: - logging.info("Create BCMP exam since this is a pesticide exam") - - if json_data["sbc_managed"] != "sbc": - print("Setting non-SBC shit") - pesticide_office = Office.query.filter_by(office_name="Pesticide Offsite").first() - exam.office_id = pesticide_office.office_id - - if exam_type.group_exam_ind: - logging.info("Creating group pesticide exam") - bcmp_response = self.bcmp_service.create_group_exam(exam) - else: - logging.info("Creating individual pesticide exam") - bcmp_response = self.bcmp_service.create_individual_exam(exam, exam_type) - - if bcmp_response: - exam.bcmp_job_id = bcmp_response['jobId'] - else: - print("Do the group exam shit here") - else: - if not (exam.office_id == csr.office_id or csr.liaison_designate == 1): - return {"The Exam Office ID and CSR Office ID do not match!"}, 403 + + if not (exam.office_id == csr.office_id or csr.liaison_designate == 1): + return {"The Exam Office ID and CSR Office ID do not match!"}, 403 + + if exam.is_pesticide: + formatted_data = self.format_data(json_data, exam) + exam = formatted_data["exam"] + job = self.bcmp_service.check_exam_status(exam) + print(job) + if job and job['jobProperties'] and job['jobProperties']['JOB_ID']: + exam.event_id = job['jobProperties']['JOB_ID'] db.session.add(exam) db.session.commit() @@ -81,3 +70,64 @@ def post(self): return {"exam": result.data, "errors": result.errors}, 201 + + + + ## formating data to save on bcmp + def format_data(self, json_data, exam): + + candidates_list_bcmp = [] + + pesticide_office = None + if json_data["sbc_managed"] == "sbc": + pesticide_office = Office.query.filter_by(office_id=exam.office_id).first() + else: + pesticide_office = Office.query.filter_by(office_name="Pesticide Offsite").first() + exam.office_id = pesticide_office.office_id + + if json_data["ind_or_group"] == "individual": + + exam_type = ExamType.query.filter_by(exam_type_id=exam.exam_type_id).first() + + if not exam_type: + exam_type = ExamType.query.filter_by(pesticide_exam_ind=1, group_exam_ind=1).first() + exam.exam_type = exam_type + + else: + logging.info("For Group Exams") + + exam_type = ExamType.query.filter_by(exam_type_name="Group Pesticide Exam").first() + if exam_type: + exam.exam_type_id = exam_type.exam_type_id + exam.exam_type = exam_type + + if json_data["candidates"]: + candidates = json_data["candidates"] + candidates_list = [] + for candidate in candidates: + candidate_temp = {} + candidate_temp["examinee_name"] = candidate["name"] + candidate_temp["examinee_email"] = candidate["email"] + candidate_temp["exam_type_id"] = candidate["exam_type_id"] + candidate_temp["fees"] = candidate["fees"] + candidate_temp["payee_ind"] = 1 if (candidate["billTo"] == "candidate") else 0 + candidate_temp["receipt"] = candidate["receipt"] + candidate_temp["receipt_number"] = candidate["receipt"] + candidate_temp["payee_name"] = candidate["payeeName"] + candidate_temp["payee_email"] = candidate["payeeEmail"] + candidates_list.append(candidate_temp) + # for bcmp service + candidates_bcmp = copy.deepcopy(candidate_temp) + exam_type = ExamType.query.filter_by(exam_type_id=candidate["exam_type_id"]).first() + if exam_type.exam_type_name: + candidates_bcmp["exam_type"] = exam_type.exam_type_name + candidates_list_bcmp.append(candidates_bcmp) + + exam.candidates_list = candidates_list + + return { + 'exam': exam, + 'candidates_list_bcmp': candidates_list_bcmp, + 'pesticide_office': pesticide_office, + } + diff --git a/api/app/resources/bookings/exam/exam_transfer.py b/api/app/resources/bookings/exam/exam_transfer.py index c03ab3d16..18ee2b6d1 100644 --- a/api/app/resources/bookings/exam/exam_transfer.py +++ b/api/app/resources/bookings/exam/exam_transfer.py @@ -13,7 +13,7 @@ limitations under the License.''' from flask import g -from flask_restplus import Resource +from flask_restx import Resource import logging from sqlalchemy import exc from app.models.theq import CSR @@ -37,8 +37,14 @@ def post(self, exam_id): if not (exam.office_id == csr.office_id or csr.liaison_designate == 1): return {"The Exam Office ID and CSR Office ID do not match!"}, 403 - self.bcmp_service.send_exam_to_bcmp(exam) - return {} + bcmp_response = self.bcmp_service.send_exam_to_bcmp(exam) + + if bcmp_response: + return {"bcmp": bcmp_response, + "errors": {}}, 202 + else: + return {"message": "create_group_exam_bcmp failed", + "error": bcmp_response}, 403 except exc.SQLAlchemyError as error: logging.error(error, exc_info=True) diff --git a/api/app/resources/bookings/exam/exam_upload.py b/api/app/resources/bookings/exam/exam_upload.py index 70b0deb41..9ea15710a 100644 --- a/api/app/resources/bookings/exam/exam_upload.py +++ b/api/app/resources/bookings/exam/exam_upload.py @@ -13,7 +13,7 @@ limitations under the License.''' from flask import g, request -from flask_restplus import Resource +from flask_restx import Resource import logging from sqlalchemy import exc from app.models.theq import CSR diff --git a/api/app/resources/bookings/invigilator/invigilator_list.py b/api/app/resources/bookings/invigilator/invigilator_list.py index ddc362312..66cadb3ae 100644 --- a/api/app/resources/bookings/invigilator/invigilator_list.py +++ b/api/app/resources/bookings/invigilator/invigilator_list.py @@ -13,7 +13,7 @@ limitations under the License.''' import logging -from flask import g +from flask import request, g from flask_restx import Resource from sqlalchemy import exc from app.models.bookings import Invigilator @@ -44,24 +44,3 @@ def get(self): logging.error(error, exc_info=True) return {"message": "api is down"}, 500 -@api.route("/pesticide_invigilators/", methods=["GET"]) -class InvigilatorList(Resource): - - invigilator_schema = InvigilatorSchema(many=True) - - @oidc.accept_token(require_token=True) - def get(self): - - csr = CSR.find_by_username(g.oidc_token_info['username']) - - try: - invigilators = Invigilator.query.filter_by(office_id=csr.office_id)\ - .filter(Invigilator.deleted.is_(None)) - - result = self.invigilator_schema.dump(invigilators) - return {'invigilators': result.data, - 'errors': result.errors}, 200 - - except exc.SQLAlchemyError as error: - logging.error(error, exc_info=True) - return {"message": "api is down"}, 500 diff --git a/api/app/resources/bookings/invigilator/invigilator_list_offsite.py b/api/app/resources/bookings/invigilator/invigilator_list_offsite.py new file mode 100644 index 000000000..74d241390 --- /dev/null +++ b/api/app/resources/bookings/invigilator/invigilator_list_offsite.py @@ -0,0 +1,48 @@ +'''Copyright 2018 Province of British Columbia + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License.''' + +import logging +from flask import request, g +from flask_restx import Resource +from sqlalchemy import exc +from app.models.bookings import Invigilator +from app.models.theq import CSR, Office +from app.schemas.bookings import InvigilatorSchema +from qsystem import api, oidc + + +@api.route("/invigilators/offsite/", methods=["GET"]) +class InvigilatorListOffsiteGet(Resource): + + invigilator_schema = InvigilatorSchema(many=True) + + @oidc.accept_token(require_token=True) + def get(self): + + csr = CSR.find_by_username(g.oidc_token_info['username']) + + pesticide_office = Office.query.filter_by(office_name="Pesticide Offsite").first() + + try: + invigilators = Invigilator.query.filter_by(office_id=pesticide_office.office_id)\ + .filter(Invigilator.deleted.is_(None)) + + result = self.invigilator_schema.dump(invigilators) + return {'invigilators': result.data, + 'errors': result.errors}, 200 + + except exc.SQLAlchemyError as error: + logging.error(error, exc_info=True) + return {"message": "api is down"}, 500 + diff --git a/api/app/resources/theq/citizen/citizen_generic_invite.py b/api/app/resources/theq/citizen/citizen_generic_invite.py index d5911cde0..60d7b996e 100644 --- a/api/app/resources/theq/citizen/citizen_generic_invite.py +++ b/api/app/resources/theq/citizen/citizen_generic_invite.py @@ -15,9 +15,85 @@ from filelock import FileLock from flask import g, request from flask_restx import Resource -from qsystem import api, api_call_with_retry, db, oidc, socketio, my_print +from qsystem import api, api_call_with_retry, db, oidc, socketio, my_print, get_key from app.models.theq import Citizen, CSR, CitizenState, Period, PeriodState, ServiceReq, SRState from app.schemas.theq import CitizenSchema +from datetime import datetime +from pprint import pprint + + +@oidc.accept_token(require_token=True) +@api_call_with_retry +def csr_find_by_user(): + csr = CSR.find_by_username(g.oidc_token_info['username']) + return csr + +@oidc.accept_token(require_token=True) +@api_call_with_retry +def find_active(): + active_citizen_state = CitizenState.query.filter_by(cs_state_name='Active').first() + return active_citizen_state + +@oidc.accept_token(require_token=True) +@api_call_with_retry +def find_wait(): + waiting_period_state = PeriodState.get_state_by_name("Waiting") + return waiting_period_state + +@oidc.accept_token(require_token=True) +@api_call_with_retry +def find_citizen(counter_id, active_citizen_state, csr, waiting_period_state): + citizen = Citizen.query \ + .filter_by(counter_id=counter_id, cs_id=active_citizen_state.cs_id, office_id=csr.office_id) \ + .join(Citizen.service_reqs) \ + .join(ServiceReq.periods) \ + .filter_by(ps_id=waiting_period_state.ps_id) \ + .filter(Period.time_end.is_(None)) \ + .order_by(Citizen.priority, Citizen.citizen_id) \ + .first() + return citizen + +@oidc.accept_token(require_token=True) +@api_call_with_retry +def find_citizen2(active_citizen_state, csr, waiting_period_state): + citizen = Citizen.query \ + .filter_by(cs_id=active_citizen_state.cs_id, office_id=csr.office_id) \ + .join(Citizen.service_reqs) \ + .join(ServiceReq.periods) \ + .filter_by(ps_id=waiting_period_state.ps_id) \ + .filter(Period.time_end.is_(None)) \ + .order_by(Citizen.priority, Citizen.citizen_id) \ + .first() + return citizen + +@oidc.accept_token(require_token=True) +@api_call_with_retry +def find_active_sr(citizen): + active_service_request = citizen.get_active_service_request() + return active_service_request + +@oidc.accept_token(require_token=True) +@api_call_with_retry +def invite_active_sr(active_service_request,csr,citizen): + active_service_request.invite(csr, invite_type="generic", sr_count=len(citizen.service_reqs)) + +@oidc.accept_token(require_token=True) +@api_call_with_retry +def find_active_ss(): + active_service_state = SRState.get_state_by_name("Active") + return active_service_state + +@oidc.accept_token(require_token=True) +@api_call_with_retry +def find_active_sr(citizen): + active_service_request = citizen.get_active_service_request() + return active_service_request + +@oidc.accept_token(require_token=True) +@api_call_with_retry +def find_active_sr(citizen): + active_service_request = citizen.get_active_service_request() + return active_service_request @api.route("/citizens/invite/", methods=['POST']) class CitizenGenericInvite(Resource): @@ -26,16 +102,26 @@ class CitizenGenericInvite(Resource): citizens_schema = CitizenSchema(many=True) @oidc.accept_token(require_token=True) - @api_call_with_retry + #@api_call_with_retry def post(self): - - csr = CSR.find_by_username(g.oidc_token_info['username']) + #print("==> In Python /citizens/invitetest") + y = 0 + #for x in range(0, 25): + key = "DR->" + get_key() + #print("") + y = y + 1 + #print("DATETIME:", datetime.now(), "starting loop:", y, "==>Key : ", key) + csr = csr_find_by_user() + #print("DATETIME:", datetime.now(), "==>Key : ", key,"===>AFTER CALL TO csr_find_by_user:", csr) lock = FileLock("lock/invite_citizen_{}.lock".format(csr.office_id)) - with lock: - active_citizen_state = CitizenState.query.filter_by(cs_state_name='Active').first() - waiting_period_state = PeriodState.get_state_by_name("Waiting") + #active_citizen_state = find_active() + active_citizen_state = citizen_state + #print("DATETIME:", datetime.now(), "==>Key : ", key, "===>AFTER CALL TO find_Active:", active_citizen_state) + + waiting_period_state = find_wait() + #print("DATETIME:", datetime.now(), "==>Key : ", key, "===>AFTER CALL TO find_wait:", waiting_period_state) citizen = None json_data = request.get_json() @@ -44,25 +130,13 @@ def post(self): else: counter_id = int(csr.counter_id) - citizen = Citizen.query \ - .filter_by(counter_id=counter_id, cs_id=active_citizen_state.cs_id, office_id=csr.office_id) \ - .join(Citizen.service_reqs) \ - .join(ServiceReq.periods) \ - .filter_by(ps_id=waiting_period_state.ps_id) \ - .filter(Period.time_end.is_(None)) \ - .order_by(Citizen.priority, Citizen.citizen_id) \ - .first() + citizen = find_citizen(counter_id,active_citizen_state, csr, waiting_period_state) + #print("DATETIME:", datetime.now(), "==>Key : ", key, "===>AFTER CALL TO find_citizen:", citizen) # If no matching citizen with the same counter type, get next one if citizen is None: - citizen = Citizen.query \ - .filter_by(cs_id=active_citizen_state.cs_id, office_id=csr.office_id) \ - .join(Citizen.service_reqs) \ - .join(ServiceReq.periods) \ - .filter_by(ps_id=waiting_period_state.ps_id) \ - .filter(Period.time_end.is_(None)) \ - .order_by(Citizen.priority, Citizen.citizen_id) \ - .first() + citizen = find_citizen2(active_citizen_state, csr, waiting_period_state) + #print("DATETIME:", datetime.now(), "==>Key : ", key, "===>AFTER CALL TO find_citizen2:", citizen) if citizen is None: return {"message": "There is no citizen to invite"}, 400 @@ -70,16 +144,21 @@ def post(self): my_print("==> POST /citizens/invite/ Citizen: " + str(citizen.citizen_id) + ', Ticket: ' + citizen.ticket_number) db.session.refresh(citizen) - active_service_request = citizen.get_active_service_request() + + active_service_request = find_active_sr(citizen) + #print("DATETIME:", datetime.now(), "==>Key : ", key, "===>AFTER CALL TO find_active_sr:", citizen) try: - active_service_request.invite(csr, invite_type="generic", sr_count = len(citizen.service_reqs)) + invite_active_sr(active_service_request,csr,citizen) + #print("DATETIME:", datetime.now(), "==>Key : ", key, "===>AFTER CALL TO invite_active_sr:") + except TypeError: return {"message": "Error inviting citizen. Please try again."}, 400 - active_service_state = SRState.get_state_by_name("Active") - active_service_request.sr_state_id = active_service_state.sr_state_id + active_service_state = find_active_ss() + #print("DATETIME:", datetime.now(), "==>Key : ", key, "===>AFTER CALL TO find_active_ss:", active_service_state) + active_service_request.sr_state_id = active_service_state.sr_state_id db.session.add(citizen) db.session.commit() @@ -88,5 +167,15 @@ def post(self): result = self.citizen_schema.dump(citizen) socketio.emit('update_active_citizen', result.data, room=csr.office_id) + #print("DATETIME:", datetime.now(), "end loop: ", y , "==>Key : ", key) + return {'citizen': result.data, 'errors': result.errors}, 200 + +try: + citizen_state = CitizenState.query.filter_by(cs_state_name="Active").first() + active_id = citizen_state.cs_id +except: + active_id = 1 + print("==> In citizen_generic_invite.py") + print(" --> NOTE!! You should only see this if doing a 'python3 manage.py db upgrade'") diff --git a/api/app/resources/theq/citizen/citizen_generic_invite_original.py b/api/app/resources/theq/citizen/citizen_generic_invite_original.py new file mode 100644 index 000000000..7cb06f1dc --- /dev/null +++ b/api/app/resources/theq/citizen/citizen_generic_invite_original.py @@ -0,0 +1,142 @@ +'''Copyright 2018 Province of British Columbia + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License.''' + +from filelock import FileLock +from flask import g, request +from flask_restx import Resource +from qsystem import api, api_call_with_retry, db, oidc, socketio, my_print +from app.models.theq import Citizen, CSR, CitizenState, Period, PeriodState, ServiceReq, SRState +from app.schemas.theq import CitizenSchema + +@api.route("/citizens/invite/", methods=['POST']) +class CitizenGenericInvite(Resource): + + citizen_schema = CitizenSchema() + citizens_schema = CitizenSchema(many=True) + + @oidc.accept_token(require_token=True) + @api_call_with_retry + def post(self): + + line = 0 + print("==> In Python /citizens/invite") + line = line + 1 + print(" --> Line: " + str(line)) + csr = CSR.find_by_username(g.oidc_token_info['username']) + line = line + 1 + print(" --> Line: " + str(line)) + lock = FileLock("lock/invite_citizen_{}.lock".format(csr.office_id)) + line = line + 1 + print(" --> Line: " + str(line)) + + with lock: + + line = line + 1 + print(" --> Line: " + str(line)) + active_citizen_state = CitizenState.query.filter_by(cs_state_name='Active').first() + line = line + 1 + print(" --> Line: " + str(line)) + waiting_period_state = PeriodState.get_state_by_name("Waiting") + line = line + 1 + print(" --> Line: " + str(line)) + citizen = None + line = line + 1 + print(" --> Line: " + str(line)) + json_data = request.get_json() + line = line + 1 + print(" --> Line: " + str(line)) + + if json_data and 'counter_id' in json_data: + counter_id = int(json_data.get('counter_id')) + else: + counter_id = int(csr.counter_id) + + line = line + 1 + print(" --> Line: " + str(line)) + citizen = Citizen.query \ + .filter_by(counter_id=counter_id, cs_id=active_citizen_state.cs_id, office_id=csr.office_id) \ + .join(Citizen.service_reqs) \ + .join(ServiceReq.periods) \ + .filter_by(ps_id=waiting_period_state.ps_id) \ + .filter(Period.time_end.is_(None)) \ + .order_by(Citizen.priority, Citizen.citizen_id) \ + .first() + + # If no matching citizen with the same counter type, get next one + line = line + 1 + print(" --> Line: " + str(line)) + if citizen is None: + citizen = Citizen.query \ + .filter_by(cs_id=active_citizen_state.cs_id, office_id=csr.office_id) \ + .join(Citizen.service_reqs) \ + .join(ServiceReq.periods) \ + .filter_by(ps_id=waiting_period_state.ps_id) \ + .filter(Period.time_end.is_(None)) \ + .order_by(Citizen.priority, Citizen.citizen_id) \ + .first() + + line = line + 1 + print(" --> Line: " + str(line)) + if citizen is None: + return {"message": "There is no citizen to invite"}, 400 + + line = line + 1 + print(" --> Line: " + str(line)) + my_print("==> POST /citizens/invite/ Citizen: " + str(citizen.citizen_id) + ', Ticket: ' + citizen.ticket_number) + + line = line + 1 + print(" --> Line: " + str(line)) + db.session.refresh(citizen) + line = line + 1 + print(" --> Line: " + str(line)) + active_service_request = citizen.get_active_service_request() + + line = line + 1 + print(" --> Line: " + str(line)) + try: + active_service_request.invite(csr, invite_type="generic", sr_count = len(citizen.service_reqs)) + except TypeError: + return {"message": "Error inviting citizen. Please try again."}, 400 + + line = line + 1 + print(" --> Line: " + str(line)) + active_service_state = SRState.get_state_by_name("Active") + line = line + 1 + print(" --> Line: " + str(line)) + active_service_request.sr_state_id = active_service_state.sr_state_id + + line = line + 1 + print(" --> Line: " + str(line)) + db.session.add(citizen) + line = line + 1 + print(" --> Line: " + str(line)) + db.session.commit() + + line = line + 1 + print(" --> Line: " + str(line)) + socketio.emit('update_customer_list', {}, room=csr.office_id) + line = line + 1 + print(" --> Line: " + str(line)) + socketio.emit('citizen_invited', {}, room='sb-%s' % csr.office.office_number) + line = line + 1 + print(" --> Line: " + str(line)) + result = self.citizen_schema.dump(citizen) + line = line + 1 + print(" --> Line: " + str(line)) + socketio.emit('update_active_citizen', result.data, room=csr.office_id) + + line = line + 1 + print(" --> Line: " + str(line)) + return {'citizen': result.data, + 'errors': result.errors}, 200 diff --git a/api/app/resources/theq/csrs.py b/api/app/resources/theq/csrs.py index 9a85d2930..3fbd39f9c 100644 --- a/api/app/resources/theq/csrs.py +++ b/api/app/resources/theq/csrs.py @@ -101,7 +101,7 @@ def get(self): if exam.booking is not None: attention_needed = attention_needed or exam.booking.start_time < start_date if exam.expiry_date is not None: - attention_needed = attention_needed or exam.expiry_date < start_date + attention_needed = attention_needed or self.timezone.localize(exam.expiry_date) < start_date if exam.exam_returned_date is not None: attention_needed = False if attention_needed: @@ -113,9 +113,12 @@ def get(self): if not attention_needed: for exam in office_exams: if exam.exam_type.group_exam_ind == 1: - attention_needed = attention_needed or exam.booking.start_time < start_date + if exam.booking is not None: + attention_needed = attention_needed or exam.booking.start_time < start_date + else: + attention_needed = True if exam.expiry_date is not None: - attention_needed = attention_needed or exam.expiry_date < start_date + attention_needed = attention_needed or self.timezone.localize(exam.expiry_date) < start_date if exam.booking is not None and exam.number_of_students is not None: attention_needed = attention_needed or exam.booking.start_time < start_date attention_needed = attention_needed or (len(exam.booking.invigilators) < 1 diff --git a/api/app/resources/theq/websocket.py b/api/app/resources/theq/websocket.py index f6ceea605..2bbc6f0fe 100644 --- a/api/app/resources/theq/websocket.py +++ b/api/app/resources/theq/websocket.py @@ -37,6 +37,7 @@ def on_join(message): csr = CSR.find_by_username(claims["preferred_username"]) if csr: join_room(csr.office_id) + print("==> In websocket.py, CSR joinroom, CSR: " + csr.username + "; request sid: " + str(request.sid)) emit('joinRoomSuccess', {"sucess": True}) emit('get_Csr_State_IDs', {"success": True}) emit('update_customer_list', {"success": True}) @@ -56,6 +57,7 @@ def on_join_smartboard(message): my_print("Joining room: %s" % room) join_room(room) + print("==> In websocket.py, Smartboard joinroom, Office id: " + str(office_id) + "; request sid: " + str(request.sid)) emit('joinSmartboardRoomSuccess') except KeyError as e: print(e) diff --git a/api/app/schemas/bookings/__init__.py b/api/app/schemas/bookings/__init__.py index 0c4d93d49..92f28d0bb 100644 --- a/api/app/schemas/bookings/__init__.py +++ b/api/app/schemas/bookings/__init__.py @@ -16,7 +16,7 @@ from app.schemas.bookings.exam_type_schema import ExamTypeSchema from app.schemas.bookings.room_schema import RoomSchema from app.schemas.bookings.booking_schema import BookingSchema -from app.schemas.bookings.exam_schema import ExamSchema +from app.schemas.bookings.exam_schema import ExamSchema, CandidateSchema from app.schemas.bookings.appointment_schema import AppointmentSchema diff --git a/api/app/schemas/bookings/exam_schema.py b/api/app/schemas/bookings/exam_schema.py index 1c6fd6e82..20ac55fe4 100644 --- a/api/app/schemas/bookings/exam_schema.py +++ b/api/app/schemas/bookings/exam_schema.py @@ -15,11 +15,23 @@ from marshmallow import fields import toastedmarshmallow from app.models.bookings import Exam -from app.schemas.bookings import BookingSchema, ExamTypeSchema +from app.schemas.bookings import BookingSchema, ExamTypeSchema, InvigilatorSchema from app.schemas.theq import OfficeSchema from qsystem import ma +class CandidateSchema(ma.Schema): + examinee_name = fields.String() + examinee_email = fields.String() + exam_type_id = fields.String() + fees = fields.String() + payee_ind = fields.String() + receipt = fields.String() + receipt_number = fields.String() + payee_name = fields.String() + payee_email = fields.String() + + class ExamSchema(ma.ModelSchema): class Meta: @@ -36,25 +48,36 @@ class Meta: exam_received_date = fields.DateTime(allow_none=True) exam_type_id = fields.Int() examinee_name = fields.Str() + examinee_phone = fields.Str() + examinee_email = fields.Str() expiry_date = fields.DateTime() notes = fields.Str(allow_none=True) number_of_students = fields.Int() office_id = fields.Int() + invigilator_id = fields.Int() session_number = fields.Int() + exam_returned_ind = fields.Int() exam_returned_date = fields.Str(allow_none=True) - exam_returned_tracking_number = fields.String(allow_none=True) + exam_returned_tracking_number = fields.Str(allow_none=True) exam_written_ind = fields.Int() - offsite_location = fields.String() + upload_received_ind = fields.Int() + offsite_location = fields.Str() sbc_managed_ind = fields.Int() - receipt = fields.String() + receipt = fields.Str() + receipt_number = fields.Str() + fees = fields.Str() payee_ind = fields.Int() receipt_sent_ind = fields.Int() - payee_name = fields.String() - payee_email = fields.String() - payee_phone = fields.String() + payee_name = fields.Str() + payee_email = fields.Str() + payee_phone = fields.Str() + bcmp_job_id = fields.Str(allow_none=True) + is_pesticide = fields.Int(allow_none=True) + candidates_list = fields.Nested(CandidateSchema) booking = fields.Nested(BookingSchema()) exam_type = fields.Nested(ExamTypeSchema()) + invigilator = fields.Nested(InvigilatorSchema()) office = fields.Nested(OfficeSchema(only=('appointments_enabled_ind', 'exams_enabled_ind', 'office_id', 'office_name', 'office_number', 'timezone'))) diff --git a/api/app/utilities/bcmp_service.py b/api/app/utilities/bcmp_service.py index 683d6fc66..85cbb8e8d 100644 --- a/api/app/utilities/bcmp_service.py +++ b/api/app/utilities/bcmp_service.py @@ -3,6 +3,7 @@ import urllib from qsystem import application from app.utilities.document_service import DocumentService +from datetime import datetime class BCMPService: @@ -11,6 +12,9 @@ class BCMPService: def __init__(self): return + + def __exam_time_format(self, date_value): + return date_value.strftime("%a %b %d, %Y at %-I:%M %p") def send_request(self, path, method, data): if method == 'POST': @@ -28,15 +32,13 @@ def send_request(self, path, method, data): print(req) response = urllib.request.urlopen(req) + - print('response') - print(response.status) - - response_data = response.read().decode('utf-8') + response_data = response.read().decode('utf8') print(response_data) try: - return json.loads(response.read().decode('utf-8')) + return json.loads(response_data) except json.decoder.JSONDecodeError: logging.warning("Error decoding JSON response data. Response data: %s" % response_data) return False @@ -49,13 +51,12 @@ def check_exam_status(self, exam): ] } response = self.send_request(url, 'POST', data) - print(response) - if response: + if response and response['jobs']: for job in response['jobs']: print(job) if job['jobId'] == exam.bcmp_job_id: - return job['jobStatus'] + return job return False @@ -73,18 +74,91 @@ def bulk_check_exam_status(self, exams): return response - def create_individual_exam(self, exam, exam_type): + def create_individual_exam(self, exam, exam_fees, invigilator, pesticide_office, oidc_token_info): url = "%s/auth=env_exam;%s/JSON/create:ENV-IPM-EXAM" % (self.base_url, self.auth_token) + + office_name = None + if pesticide_office: + office_name = pesticide_office.office_name + + receipt_number = "%s fees" % exam_fees + if exam.receipt: + receipt_number = exam.receipt + + exam_type_name = None + if exam.exam_type: + exam_type_name = exam.exam_type.exam_type_name + + invigilator_name = None + if invigilator: + invigilator_name = invigilator.invigilator_name + bcmp_exam = { - "category": exam_type.exam_type_name, + "EXAM_SESSION_LOCATION" : office_name, + "SESSION_DATE_TIME" : self.__exam_time_format(exam.expiry_date), + "REGISTRAR_name" : oidc_token_info['preferred_username'], + "RECIPIENT_EMAIL_ADDRESS" : oidc_token_info['email'], + "REGISTRAR_phoneNumber" : "", "students": [ - {"name": exam.examinee_name} + { + "REGISTRAR_name": invigilator_name, + "EXAM_CATEGORY": exam_type_name, + "STUDENT_LEGAL_NAME_first": exam.examinee_name, + "STUDENT_LEGAL_NAME_last": exam.examinee_name, + "STUDENT_emailAddress": exam.examinee_email, + "STUDENT_phoneNumber": exam.examinee_phone, + "REGISTRATION_NOTES": exam.notes, + "RECEIPT_RMS_NUMBER": receipt_number + } ] } response = self.send_request(url, 'POST', bcmp_exam) return response + def create_group_exam_bcmp(self, exam, candiate_list, invigilator, pesticide_office, oidc_token_info): + url = "%s/auth=env_exam;%s/JSON/create:ENV-IPM-EXAM-GROUP" % (self.base_url, self.auth_token) + + invigilator_name = None + if invigilator: + invigilator_name = invigilator.invigilator_name + + office_name = None + if pesticide_office: + office_name = pesticide_office.office_name + + print(exam.expiry_date.strftime("%a %b %d, %Y at %-I:%M %p")) + + bcmp_exam = { + "EXAM_SESSION_LOCATION": office_name, + "SESSION_DATE_TIME" : self.__exam_time_format(exam.expiry_date), + "REGISTRAR_name" : oidc_token_info['preferred_username'], + "RECIPIENT_EMAIL_ADDRESS" : oidc_token_info['email'], + "REGISTRAR_phoneNumber": "", + "students": [] + } + + for candiate in candiate_list: + bcmp_exam["students"].append({ + "EXAM_CATEGORY": candiate["exam_type"], + "STUDENT_LEGAL_NAME_first": candiate["examinee_name"], + "STUDENT_LEGAL_NAME_last": candiate["examinee_name"], + "STUDENT_emailAddress": candiate["examinee_email"], + "STUDENT_phoneNumber": "", + "STUDENT_ADDRESS_line1": "", + "STUDENT_ADDRESS_line2": "", + "STUDENT_ADDRESS_city": "", + "STUDENT_ADDRESS_province": "", + "STUDENT_ADDRESS_postalCode": "", + "REGISTRATION_NOTES": "", + "RECEIPT_RMS_NUMBER": candiate["receipt"], + "PAYMENT_METHOD": candiate["fees"], + "FEE_PAYMENT_NOTES": "" + }) + + response = self.send_request(url, 'POST', bcmp_exam) + return response + def create_group_exam(self, exam): url = "%s/auth=env_exam;%s/JSON/create:ENV-IPM-EXAM" % (self.base_url, self.auth_token) @@ -120,7 +194,8 @@ def send_exam_to_bcmp(self, exam): } } - self.send_request(url, "POST", json_data) + response = self.send_request(url, 'POST', json_data) + return response def email_exam_invigilator(self, exam, invigilator_name, invigilator_email, invigilator_phone): url = "%s/auth=env_exam;%s/JSON/create:ENV-IPM-EXAM-API-ACTION" % (self.base_url, self.auth_token) diff --git a/api/app/utilities/document_service.py b/api/app/utilities/document_service.py index e2e9064d6..c885741b2 100644 --- a/api/app/utilities/document_service.py +++ b/api/app/utilities/document_service.py @@ -47,12 +47,12 @@ def __init__(self, host, bucket, access_key, secret_key, use_secure): def get_presigned_put_url(self, object_name): """Retrieves the a presigned URL for putting objects into an S3 source""" - url = self.client.presigned_put_object(self.bucket, object_name, expires=timedelta(minutes=30)) + url = self.client.presigned_put_object(self.bucket, object_name, expires=timedelta(days=7)) return url def get_presigned_get_url(self, object_name): """Retrieves the presigned URL for GETting objects out of an S3 source""" - url = self.client.presigned_get_object(self.bucket, object_name, expires=timedelta(minutes=30)) + url = self.client.presigned_get_object(self.bucket, object_name, expires=timedelta(days=7)) return url diff --git a/api/config.py b/api/config.py index 3a567e2ab..d2a37540b 100644 --- a/api/config.py +++ b/api/config.py @@ -1,6 +1,7 @@ import logging import os import dotenv +from pprint import pprint # Load all the environment variables from a .env file located in some directory above. dotenv.load_dotenv(dotenv.find_dotenv()) @@ -64,37 +65,87 @@ class BaseConfig(object): DB_NAME = os.getenv('DATABASE_NAME','') DB_HOST = os.getenv('DATABASE_HOST','') DB_PORT = os.getenv('DATABASE_PORT','') - DB_TIMEOUT_STRING = os.getenv('DATABASE_TIMEOUT_STRING', '') - SQLALCHEMY_DATABASE_URI = '{engine}://{user}:{password}@{host}:{port}/{name}{timeout}'.format( + DB_POOL_TIMEOUT = os.getenv('DATABASE_TIMEOUT_STRING', '') + DB_CONNECT_TIMEOUT = os.getenv('DATABASE_CONNECT_TIMEOUT_STRING', '') + + SQLALCHEMY_DATABASE_URI = '{engine}://{user}:{password}@{host}:{port}/{name}'.format( engine=DB_ENGINE, user=DB_USER, password=DB_PASSWORD, host=DB_HOST, port=DB_PORT, name=DB_NAME, - timeout=DB_TIMEOUT_STRING ) - SQLALCHEMY_DATABASE_URI_DISPLAY = '{engine}://{user}:@{host}:{port}/{name}{timeout}'.format( + SQLALCHEMY_DATABASE_URI_DISPLAY = '{engine}://{user}:@{host}:{port}/{name}'.format( engine=DB_ENGINE, user=DB_USER, host=DB_HOST, port=DB_PORT, name=DB_NAME, - timeout=DB_TIMEOUT_STRING ) # Get SQLAlchemy environment variables. pool_size = int(os.getenv('SQLALCHEMY_POOL_SIZE', '9')) max_overflow = int(os.getenv('SQLALCHEMY_MAX_OVERFLOW', '18')) + # db_timeout = int(os.getenv('SQLALCHEMY_TIMEOUT', '10')) + + # Karims settings + # SQLALCHEMY_ENGINE_OPTIONS = { + # 'pool_size': pool_size, + # 'max_overflow': max_overflow, + # 'pool_pre_ping': True, + # 'pool_timeout': 5, + # 'pool_recycle': 3600, + # 'connect_args': { + # 'connect_timeout': 3 + # } + # } + + # Try to set some options to avoid long delays. + # SQLALCHEMY_ENGINE_OPTIONS = { + # 'pool_size' : pool_size, + # 'max_overflow' : max_overflow, + # 'pool_pre_ping' : True, + # 'pool_timeout': DB_POOL_TIMEOUT, + # 'pool_recycle': 3600, + # 'connect_args': { + # 'connect_timeout': DB_CONNECT_TIMEOUT, + # 'options' : '-c statement_timeout=1000' + # } + # } + + # Get SQLAlchemy environment variables. + pool_size = int(os.getenv('SQLALCHEMY_POOL_SIZE', '9')) + pool_timeout = os.getenv('SQLALCHEMY_POOL_TIMEOUT', '') + connect_timeout_string = os.getenv('SQLALCHEMY_CONNECT_TIMEOUT', '') + max_overflow = int(os.getenv('SQLALCHEMY_MAX_OVERFLOW', '18')) + pool_pre_ping = (os.getenv('SQLALCHEMY_POOL_PRE_PING', 'False')).upper() == "TRUE" # Try to set some options to avoid long delays. SQLALCHEMY_ENGINE_OPTIONS = { - 'pool_size' : pool_size, - 'max_overflow' : max_overflow, - 'pool_pre_ping' : False + 'pool_size': pool_size, + 'max_overflow': max_overflow, + 'pool_pre_ping': pool_pre_ping } + if pool_timeout != "": + SQLALCHEMY_ENGINE_OPTIONS['pool_timeout'] = pool_timeout + + if connect_timeout_string != "": + connect_timeout = int(connect_timeout_string) + + # Determine which database engine being used, to use correct syntax. + if "PG8000" in DB_ENGINE.upper(): + SQLALCHEMY_ENGINE_OPTIONS['connect_args'] = {'timeout': connect_timeout} + # SQLALCHEMY_ENGINE_OPTIONS['connect_args'] = {'timeout': connect_timeout, 'tcp_user_timeout': 500 } + else: + # SQLALCHEMY_ENGINE_OPTIONS['connect_args'] = { 'connect_timeout': connect_timeout, 'tcp_user_timeout': 500 } + SQLALCHEMY_ENGINE_OPTIONS['connect_args'] = {'connect_timeout': connect_timeout} + + print("==> SQLALCHEMY_ENGINE_OPTIONS (Engine: " + DB_ENGINE) + pprint(SQLALCHEMY_ENGINE_OPTIONS) + # Set echo appropriately. if (os.getenv('SQLALCHEMY_ECHO', "False")).upper() == "TRUE": SQLALCHEMY_ECHO=True @@ -115,6 +166,17 @@ class BaseConfig(object): BACK_OFFICE_DISPLAY = os.getenv("BACK_OFFICE_DISPLAY", "BackOffice") RECURRING_FEATURE_FLAG = os.getenv("RECURRING_FEATURE_FLAG", "On") + MINIO_HOST = os.getenv('MINIO_HOST', 'localhost:9000') + MINIO_BUCKET = os.getenv('MINIO_BUCKET', 'exams') + MINIO_ACCESS_KEY = os.getenv('MINIO_ACCESS_KEY', 'minio') + MINIO_SECRET_KEY = os.getenv('MINIO_SECRET_KEY', 'minio1234') + MINIO_USE_SECURE = os.getenv('MINIO_USE_SECURE', 0) + + #print(parse_dsn(("postgresql://localhost:5000?connect_timeout=10"))) + #quote_ident("connect_timeout", scope) + + + class LocalConfig(BaseConfig): DEBUG = True TESTING = False @@ -147,13 +209,14 @@ class LocalConfig(BaseConfig): port=DB_PORT, name=DB_NAME ) - BCMP_BASE_URL = 'https://bcmaildirect.gov.bc.ca/JOB_TEST' - BCMP_AUTH_TOKEN = 'f697697a090c4f349545a09d21b3eb08' - MINIO_HOST = 'localhost:9000' - MINIO_BUCKET = 'exams' - MINIO_ACCESS_KEY = 'minio' - MINIO_SECRET_KEY = 'minio1234' - MINIO_USE_SECURE = 0 + BCMP_BASE_URL = os.getenv('BCMP_BASE_URL') + BCMP_AUTH_TOKEN = os.getenv('BCMP_AUTH_TOKEN') + + MINIO_HOST = os.getenv('MINIO_HOST', 'localhost:9000') + MINIO_BUCKET = os.getenv('MINIO_BUCKET', 'exams') + MINIO_ACCESS_KEY = os.getenv('MINIO_ACCESS_KEY', 'minio') + MINIO_SECRET_KEY = os.getenv('MINIO_SECRET_KEY', 'minio1234') + MINIO_USE_SECURE = os.getenv('MINIO_USE_SECURE', 0) class DevelopmentConfig(BaseConfig): @@ -164,8 +227,8 @@ class DevelopmentConfig(BaseConfig): USE_HTTPS = True PREFERRED_URL_SCHEME = 'https' - BCMP_BASE_URL = 'https://bcmaildirect.gov.bc.ca/JOB_TEST' - BCMP_AUTH_TOKEN = 'f697697a090c4f349545a09d21b3eb08' + BCMP_BASE_URL = os.getenv('BCMP_BASE_URL') + BCMP_AUTH_TOKEN = os.getenv('BCMP_AUTH_TOKEN') class TestConfig(BaseConfig): @@ -176,8 +239,8 @@ class TestConfig(BaseConfig): USE_HTTPS = True PREFERRED_URL_SCHEME = 'https' - BCMP_BASE_URL = 'https://bcmaildirect.gov.bc.ca/JOB_TEST' - BCMP_AUTH_TOKEN = 'f697697a090c4f349545a09d21b3eb08' + BCMP_BASE_URL = os.getenv('BCMP_BASE_URL') + BCMP_AUTH_TOKEN = os.getenv('BCMP_AUTH_TOKEN') class ProductionConfig(BaseConfig): @@ -188,8 +251,8 @@ class ProductionConfig(BaseConfig): USE_HTTPS = True PREFERRED_URL_SCHEME = 'https' - BCMP_BASE_URL = 'https://bcmaildirect.gov.bc.ca/JOB' - BCMP_AUTH_TOKEN = 'f697697a090c4f349545a09d21b3eb08' + BCMP_BASE_URL = os.getenv('BCMP_BASE_URL') + BCMP_AUTH_TOKEN = os.getenv('BCMP_AUTH_TOKEN') def configure_app(app): diff --git a/api/gunicorn_config.py b/api/gunicorn_config.py index c69f54257..1535f858c 100644 --- a/api/gunicorn_config.py +++ b/api/gunicorn_config.py @@ -16,7 +16,7 @@ worker_class = 'eventlet' worker_connections = 500 -timeout = 60 +timeout = 10 keepalive = 20 def profiler_enable(worker, req): diff --git a/api/manage.py b/api/manage.py index d1714b79d..1186adf68 100644 --- a/api/manage.py +++ b/api/manage.py @@ -847,6 +847,16 @@ def run(self): group_exam_ind=0, pesticide_exam_ind=1, ) + + exam_type_twenty_four = bookings.ExamType( + exam_type_name="Group Pesticide Exam", + exam_color="#FFFFFF", + number_of_hours=0, + method_type="written", + ita_ind=0, + group_exam_ind=1, + pesticide_exam_ind=0, + ) db.session.add(exam_type_one) db.session.add(exam_type_two) @@ -869,6 +879,7 @@ def run(self): db.session.add(exam_type_twenty_one) db.session.add(exam_type_twenty_two) db.session.add(exam_type_twenty_three) + db.session.add(exam_type_twenty_four) db.session.commit() print("--> Bookings: Exam - No exams added") diff --git a/api/migrations/versions/89294ac623e8_.py b/api/migrations/versions/89294ac623e8_.py new file mode 100644 index 000000000..b330631d4 --- /dev/null +++ b/api/migrations/versions/89294ac623e8_.py @@ -0,0 +1,27 @@ +"""empty message + +Revision ID: 89294ac623e8 +Revises: 05301cda7812 +Create Date: 2020-04-01 19:42:56.383186 + +""" +from alembic import op +import sqlalchemy as sa +import sqlalchemy_utc + + +# revision identifiers, used by Alembic. +revision = '89294ac623e8' +down_revision = '05301cda7812' +branch_labels = None +depends_on = None + + +def upgrade(): + op.add_column('exam', sa.Column('candidates_list', sa.JSON(), nullable=True)) + op.add_column('exam', sa.Column('is_pesticide', sa.Integer(), nullable=True, default=0)) + + +def downgrade(): + op.drop_column('exam', 'candidates_list') + op.drop_column('exam', 'is_pesticide') diff --git a/api/qsystem.py b/api/qsystem.py index 725550d87..2023e3191 100644 --- a/api/qsystem.py +++ b/api/qsystem.py @@ -22,10 +22,15 @@ from sqlalchemy import event from sqlalchemy.engine import Engine - def my_print(string): if print_flag: - print(string) + print(time_string() + string) + +def time_string(): + now = datetime.datetime.now() + ms = now.strftime("%f")[:3] + now_string = now.strftime("%Y-%m-%d %H:%M:%S,") + return "[" + now_string + ms + "] " application = Flask(__name__, instance_relative_config=True) @@ -49,7 +54,7 @@ def my_print(string): ma = Marshmallow(application) # Set up socket io and rabbit mq. -socketio = SocketIO(logger=socket_flag, engineio_logger=engine_flag, +socketio = SocketIO(logger=socket_flag, engineio_logger=engine_flag,ping_timeout=6,ping_interval=3, cors_allowed_origins=application.config['CORS_ALLOWED_ORIGINS']) if application.config['ACTIVE_MQ_URL'] is not None: @@ -102,13 +107,14 @@ def my_print(string): # Code to determine all db.engine properties and sub-properties, as necessary. if False: print("==> All DB Engine options") - for attr in dir(db.engine): - print(" --> db.engine." + attr + " = " + str(getattr(db.engine, attr))) + for attr in dir(db._engine_options.keys): + print(" --> db._engine_options.keys." + attr + " = " + str(getattr(db._engine_options.keys, attr))) # print("db.engine.%s = %s") % (attr, getattr(db.engine, attr)) # See whether options took. if print_flag: print("==> DB Engine options") + print(" --> db options: " + str(db.engine)) print(" --> pool size: " + str(db.engine.pool.size())) print(" --> max overflow: " + str(db.engine.pool._max_overflow)) print(" --> echo: " + str(db.engine.echo)) @@ -135,7 +141,8 @@ def my_print(string): if h.__class__.__name__ != "NullHandler": print(" --> name: " + name + "; handler type: " + h.__class__.__name__) -def api_call_with_retry(f, max_time=15000, max_tries=12, delay_first=100, delay_start=200, delay_mult=1.5): +# def api_call_with_retry(f, max_time=15000, max_tries=12, delay_first=100, delay_start=200, delay_mult=1.5): +def api_call_with_retry(f, max_time=15000, max_tries=12, delay_first=175, delay_start=175, delay_mult=1.0): @wraps(f) def decorated_function(*args, **kwargs): @@ -190,13 +197,13 @@ def print_retry_info(print_debug, parameters, f, kwargs): if print_debug: msg = "==> RT K:" + parameters['key'] + "; T:" + str(parameters['current_try']) \ + "; F:" + str(f) + "; KW:" + str(kwargs) if kwargs is not None else "None" - print(msg) + print(time_string() + msg) def print_error_info(print_debug, parameters, err): if print_debug: msg = "==> AE K:" + parameters['key'] + "; T:" + str(parameters['current_try']) \ + "; E:" + str(err).replace("\n", ">").replace("\r", ">") - print(msg) + print(time_string() + msg) # Print more info, if the builtins.dict error. # if "builtins.dict" in str(err): @@ -266,6 +273,7 @@ def get_key(): import app.resources.bookings.booking.booking_put import app.resources.bookings.booking.booking_recurring_delete import app.resources.bookings.booking.booking_recurring_put +import app.resources.bookings.exam.exam_bcmp import app.resources.bookings.exam.exam_bulk_status import app.resources.bookings.exam.exam_delete import app.resources.bookings.exam.exam_detail @@ -279,6 +287,7 @@ def get_key(): import app.resources.bookings.exam.exam_transfer import app.resources.bookings.exam.exam_upload import app.resources.bookings.invigilator.invigilator_list +import app.resources.bookings.invigilator.invigilator_list_offsite import app.resources.bookings.invigilator.invigilator_put import app.resources.bookings.room.room_list import app.resources.bookings.exam_type.exam_type_list diff --git a/api/requirements.txt b/api/requirements.txt index e158d0d78..00bb36d97 100755 --- a/api/requirements.txt +++ b/api/requirements.txt @@ -1,6 +1,5 @@ # Moved to the top of the requirements as another package uses the latest Werkzeug. Werkzeug==0.16.1 is required for flask-restx until later release flask-restx 0.2.0. flask-restx -flask-restplus==0.12.1 # SQLAlchemy needs to point to version 1.3.12 because there is a bug with # with inviting Citizens expected to be fixed with Release 1.3.14 @@ -22,7 +21,7 @@ filelock flask-caching flask-compress flask-cors -flask_marshmallow +flask_marshmallow==0.11.0 flask-socketio gunicorn marshmallow-sqlalchemy diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 737695d00..f207f1624 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -13461,4 +13461,4 @@ "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=" } } -} +} \ No newline at end of file diff --git a/frontend/src/Socket.vue b/frontend/src/Socket.vue index 8e7495647..0aac5f53e 100644 --- a/frontend/src/Socket.vue +++ b/frontend/src/Socket.vue @@ -48,6 +48,8 @@ limitations under the License.*/ connect() { socket = io(process.env.SOCKET_URL, { + timeout: '3000', + reconnectionDelayMax: '100', path: '/api/v1/socket.io', transports: ['websocket'] }) @@ -58,10 +60,13 @@ limitations under the License.*/ }, addListeners() { + console.log("Begin Add Listener"); socket.on('reconnecting',()=>{this.onReconnecting()}) socket.on('joinRoomSuccess',()=>{this.onJoinRoom(true)}) socket.on('joinRoomFail',()=>{this.onJoinRoom(false)}) + console.log("Call get csr State IDS"); socket.on('get_Csr_State_IDs',()=>{this.getCsrStateIDs()}) + console.log("DO WE SEE THIS MESSAGE BEFORE POP UP") socket.on('update_customer_list',()=>{this.onUpdateCustomerList()}) socket.on('update_active_citizen', (citizen) => { this.onUpdateActiveCitizen(citizen) } ) socket.on('csr_update', (data)=>{this.onCSRUpdate(data)}) @@ -69,6 +74,7 @@ limitations under the License.*/ }, join() { + console.log("==> In Socket.vue.join, socket.io.engine.id is: " + socket.io.engine.id.toString()) socket.emit('joinRoom',{count:0}, ()=>{console.log('socket emit: "joinRoom"')} ) }, diff --git a/frontend/src/add-citizen/add-citizen.vue b/frontend/src/add-citizen/add-citizen.vue index b5f295773..8ba2c0233 100644 --- a/frontend/src/add-citizen/add-citizen.vue +++ b/frontend/src/add-citizen/add-citizen.vue @@ -27,7 +27,7 @@ @dismiss-count-down="countDownChanged">{{this.$store.state.alertMessage}}
-
+
@@ -230,6 +230,19 @@ export default { + diff --git a/frontend/src/appointments/checkin-modal.vue b/frontend/src/appointments/checkin-modal.vue index d4872c28a..b4d05e8dd 100644 --- a/frontend/src/appointments/checkin-modal.vue +++ b/frontend/src/appointments/checkin-modal.vue @@ -7,6 +7,11 @@ no-close-on-esc hide-header size="sm"> + - @@ -469,6 +473,9 @@ {{ row.item.office.office_name }} +
+ +
@@ -504,6 +511,14 @@ SelectInvigilatorModal }, mounted() { + if(this.is_pesticide_designate) { + const pestFilterOptions = [ + {text: 'Awaiting Upload', value: 'awaiting_upload'}, + {text: 'Awaiting Receipt', value: 'awaiting_receipt'}, + {text: 'All', value: 'all'}, + ] + this.newQuickActionOptions = this.newQuickActionOptions.concat(pestFilterOptions) + } this.getExams().then( () => { this.getBookings() }) this.getOffices() this.getInvigilators() @@ -535,10 +550,6 @@ {text: 'Office Exam Manager Action Items', value:'oemai'}, {text: 'Expired', value: 'expired'}, {text: 'Returned', value: 'returned'}, - {text: 'Saved Drafts', value: 'saved_drafts'}, - {text: 'Awaiting Upload', value: 'awaiting_upload'}, - {text: 'Awaiting Receipt', value: 'awaiting_receipt'}, - {text: 'All', value: 'all'}, ], newQuickActionOptionsNoOEM: [ {text: 'Ready', value: 'ready'}, @@ -547,6 +558,7 @@ {text: 'Returned', value: 'returned'}, {text: 'All', value: 'all'}, ], + isLoading: false, } }, computed: { @@ -554,6 +566,7 @@ 'exam_inventory', 'role_code', 'is_liaison_designate', + 'is_pesticide_designate', 'is_ita_designate']), ...mapState([ 'bookings', @@ -662,7 +675,10 @@ officeNumber() { if (this.inventoryFilters && this.inventoryFilters.office_number) { let { office_number } = this.inventoryFilters - if (office_number !== 'default') { + if (this.inventoryFilters.office_number === 'pesticide_offsite') { + let office = (this.offices.find(office => office.office_name == 'Pesticide Offsite')) + return office.office_number + } else if (office_number !== 'default') { return office_number } } @@ -715,10 +731,7 @@ this.toggleExamInventoryModal(false) }, viewAllOfficePesticideExams() { - if (!this.showAllPesticide) { - this.$store.dispatch('getAllPesticideExams') - return - } + this.$store.dispatch('getAllPesticideExams') this.$store.commit('toggleShowAllPesticideExams', false) }, checkChallenger(item) { @@ -987,6 +1000,10 @@ let examInventory = this.exam_inventory let office_number = this.inventoryFilters.office_number === 'default' ? this.user.office.office_number : this.inventoryFilters.office_number + if (this.inventoryFilters.office_number === 'pesticide_offsite') { + office_number = (this.offices.find(office => office.office_name == 'Pesticide Offsite')).office_number + this.inventoryFilters.office_number = office_number + } let filtered = [] if (examInventory.length > 0) { @@ -1065,19 +1082,35 @@ evenMoreFiltered = moreFiltered break } + let uploadFiltered = [] + switch(this.inventoryFilters.uploadFilter) { + case 'notuploaded': + uploadFiltered = evenMoreFiltered.filter(exam => !exam.upload_received_ind) + break + default: + uploadFiltered = evenMoreFiltered + } + let receptSentFiltered = [] + switch(this.inventoryFilters.receptSentFilter) { + case 'notsent': + receptSentFiltered = uploadFiltered.filter(exam => !exam.receipt_sent_ind) + break + default: + receptSentFiltered = uploadFiltered + } let finalFiltered = [] switch (this.inventoryFilters.returnedFilter) { case 'both': - finalFiltered = evenMoreFiltered + finalFiltered = receptSentFiltered break case 'returned': - finalFiltered = evenMoreFiltered.filter(ex => ex.exam_returned_date) + finalFiltered = receptSentFiltered.filter(ex => ex.exam_returned_date) break case 'unreturned': - finalFiltered = evenMoreFiltered.filter(ex => !ex.exam_returned_date) + finalFiltered = receptSentFiltered.filter(ex => !ex.exam_returned_date) break default: - finalFiltered = evenMoreFiltered + finalFiltered = receptSentFiltered break } return finalFiltered @@ -1154,7 +1187,7 @@ }, returnExam(item) { this.actionedExam = item - if (item.exam_type.pesticide_exam_ind) { + if (item.is_pesticide) { this.toggleUploadExamModal(true) } else { this.toggleReturnExamModal(true) @@ -1173,6 +1206,8 @@ this.setInventoryFilters({type:'returnedFilter', value:'both'}) this.setInventoryFilters({type:'scheduledFilter', value:'both'}) this.setInventoryFilters({type:'requireAttentionFilter', value:'default'}) + this.setInventoryFilters({type:'receptSentFilter', value:'default'}) + this.setInventoryFilters({type:'uploadFilter', value:'default'}) }else if (option.value === 'group'){ this.setSelectedQuickAction('') this.setSelectedQuickActionFilter('') @@ -1181,6 +1216,8 @@ this.setInventoryFilters({type:'returnedFilter', value:'both'}) this.setInventoryFilters({type:'scheduledFilter', value:'both'}) this.setInventoryFilters({type:'requireAttentionFilter', value:'default'}) + this.setInventoryFilters({type:'receptSentFilter', value:'default'}) + this.setInventoryFilters({type:'uploadFilter', value:'default'}) }else if (option.value === 'all'){ this.setSelectedQuickAction('') this.setSelectedQuickActionFilter('') @@ -1188,6 +1225,8 @@ this.setInventoryFilters({type:'returnedFilter', value:'both'}) this.setInventoryFilters({type:'scheduledFilter', value:'both'}) this.setInventoryFilters({type:'requireAttentionFilter', value:'default'}) + this.setInventoryFilters({type:'receptSentFilter', value:'default'}) + this.setInventoryFilters({type:'uploadFilter', value:'default'}) this.setInventoryFilters({type:'groupFilter', value:'both'}) } }, @@ -1202,6 +1241,8 @@ this.setInventoryFilters({type:'scheduledFilter', value:'both'}) this.setInventoryFilters({type:'requireAttentionFilter', value:'default'}) this.setInventoryFilters({type:'requireOEMAttentionFilter', value: 'default'}) + this.setInventoryFilters({type:'receptSentFilter', value: 'default'}) + this.setInventoryFilters({type:'uploadFilter', value: 'default'}) }else if(option.value === 'require_attention') { if (this.selectedExamType === 'individual') { this.setInventoryFilters({type: 'returnedFilter', value: 'both'}) @@ -1209,15 +1250,21 @@ this.setInventoryFilters({type: 'scheduledFilter', value: 'both'}) this.setInventoryFilters({type: 'requireAttentionFilter', value: 'individual'}) this.setInventoryFilters({type:'requireOEMAttentionFilter', value: 'default'}) + this.setInventoryFilters({type:'receptSentFilter', value: 'default'}) + this.setInventoryFilters({type:'uploadFilter', value: 'default'}) } else if (this.selectedExamType === 'group') { this.setInventoryFilters({type: 'returnedFilter', value: 'unreturned'}) this.setInventoryFilters({type: 'expiryFilter', value: 'all'}) this.setInventoryFilters({type: 'scheduledFilter', value: 'unscheduled'}) this.setInventoryFilters({type: 'requireAttentionFilter', value: 'group'}) this.setInventoryFilters({type:'requireOEMAttentionFilter', value: 'default'}) + this.setInventoryFilters({type:'receptSentFilter', value: 'default'}) + this.setInventoryFilters({type:'uploadFilter', value: 'default'}) } else if (this.selectedExamType === 'all') { this.setInventoryFilters({type: 'requireAttentionFilter', value: 'both'}) this.setInventoryFilters({type:'requireOEMAttentionFilter', value: 'default'}) + this.setInventoryFilters({type:'receptSentFilter', value: 'default'}) + this.setInventoryFilters({type:'uploadFilter', value: 'default'}) this.setInventoryFilters({type: 'expiryFilter', value: 'all'}) } } @@ -1227,12 +1274,16 @@ this.setInventoryFilters({type:'scheduledFilter', value:'scheduled'}) this.setInventoryFilters({type:'requireAttentionFilter', value:'default'}) this.setInventoryFilters({type:'requireOEMAttentionFilter', value: 'default'}) + this.setInventoryFilters({type:'receptSentFilter', value: 'default'}) + this.setInventoryFilters({type:'uploadFilter', value: 'default'}) }else if(option.value === 'expired'){ this.setInventoryFilters({type:'expiryFilter', value:'expired'}) this.setInventoryFilters({type:'returnedFilter', value:'unreturned'}) this.setInventoryFilters({type:'scheduledFilter', value:'both'}) this.setInventoryFilters({type:'requireAttentionFilter', value:'default'}) this.setInventoryFilters({type:'requireOEMAttentionFilter', value: 'default'}) + this.setInventoryFilters({type:'receptSentFilter', value:'default'}) + this.setInventoryFilters({type:'uploadFilter', value:'default'}) }else if(option.value === 'oemai'){ if(this.selectedExamType === 'individual'){ this.setInventoryFilters({type:'returnedFilter', value:'both'}) @@ -1240,36 +1291,65 @@ this.setInventoryFilters({type:'scheduledFilter', value:'both'}) this.setInventoryFilters({type:'requireAttentionFilter', value:'default'}) this.setInventoryFilters({type:'requireOEMAttentionFilter', value: 'individual'}) + this.setInventoryFilters({type:'receptSentFilter', value:'default'}) + this.setInventoryFilters({type:'uploadFilter', value:'default'}) }else if(this.selectedExamType === 'group'){ this.setInventoryFilters({type:'returnedFilter', value:'both'}) this.setInventoryFilters({type:'expiryFilter', value:'all'}) this.setInventoryFilters({type:'scheduledFilter', value:'both'}) this.setInventoryFilters({type:'requireAttentionFilter', value:'default'}) this.setInventoryFilters({type:'requireOEMAttentionFilter', value: 'group'}) + this.setInventoryFilters({type:'receptSentFilter', value:'default'}) + this.setInventoryFilters({type:'uploadFilter', value:'default'}) }else if(this.selectedExamType === 'all'){ this.setInventoryFilters({type:'returnedFilter', value:'both'}) this.setInventoryFilters({type:'expiryFilter', value:'all'}) this.setInventoryFilters({type:'scheduledFilter', value:'both'}) this.setInventoryFilters({type:'requireAttentionFilter', value:'default'}) this.setInventoryFilters({type:'requireOEMAttentionFilter', value: 'both'}) + this.setInventoryFilters({type:'receptSentFilter', value:'default'}) + this.setInventoryFilters({type:'uploadFilter', value:'default'}) } - }else if(option.value === 'saved_drafts'){ - this.setInventoryFilters({type: 'expiryFilter', value: 'all'}) - }else if(option.value === 'awaiting_upload'){ - this.setInventoryFilters({type: 'expiryFilter', value: 'current'}) - this.setInventoryFilters({type: 'groupFilter', value: 'both'}) - this.setInventoryFilters({type: 'office_number', value: 'default'}) - this.setInventoryFilters({type: 'returnedFilter', value: 'unreturned'}) - this.setInventoryFilters({type: 'scheduledFilter', value: 'both'}) - this.setInventoryFilters({type: 'uploaded', value: 'all'}) + } else if(option.value === 'awaiting_upload'){ + this.isLoading = true; + this.setInventoryFilters({type: 'office_number', value: 'pesticide_offsite'}) + this.updateExamStatus().then(success => { + console.log(success) + this.$store.commit('toggleShowAllPesticideExams', false) + this.setInventoryFilters({type: 'expiryFilter', value: 'current'}) + this.setInventoryFilters({type: 'groupFilter', value: 'both'}) + this.setInventoryFilters({type: 'returnedFilter', value: 'unreturned'}) + this.setInventoryFilters({type: 'scheduledFilter', value: 'both'}) + this.setInventoryFilters({type: 'receptSentFilter', value: 'default'}) + this.setInventoryFilters({type: 'uploadFilter', value: 'notuploaded'}) + this.isLoading = false; + }, err => { + console.error(err) + this.isLoading = false; + }).catch(err => { + this.isLoading = false; + }) + } else if(option.value === 'awaiting_receipt'){ + + // this.$store.commit('toggleShowAllPesticideExams', false) + this.viewAllOfficePesticideExams() - this.updateExamStatus() - }else if(option.value === 'all'){ + this.setInventoryFilters({type: 'expiryFilter', value: 'current'}) + this.setInventoryFilters({type:'scheduledFilter', value:'both'}) + this.setInventoryFilters({type:'returnedFilter', value:'unreturned'}) + this.setInventoryFilters({type:'requireAttentionFilter', value:'default'}) + this.setInventoryFilters({type:'requireOEMAttentionFilter', value: 'default'}) + this.setInventoryFilters({type:'uploadFilter', value: 'default'}) + this.setInventoryFilters({type:'receptSentFilter', value: 'notsent'}) + } else if(option.value === 'all'){ this.setInventoryFilters({type: 'expiryFilter', value: 'all'}) this.setInventoryFilters({type:'scheduledFilter', value:'both'}) + this.setInventoryFilters({type:'groupFilter', value:'both'}) this.setInventoryFilters({type:'returnedFilter', value:'both'}) this.setInventoryFilters({type:'requireAttentionFilter', value:'default'}) this.setInventoryFilters({type:'requireOEMAttentionFilter', value: 'default'}) + this.setInventoryFilters({type:'uploadFilter', value: 'default'}) + this.setInventoryFilters({type:'receptSentFilter', value: 'default'}) } }, setFilter(e) { @@ -1312,7 +1392,7 @@ if (!value) { return '' } else if (value instanceof Object) { - return keys(value) + return Object.keys(value) .sort() .map(key => toString(value[key])) .join(' ') @@ -1349,6 +1429,11 @@ rank: 1, style: {fontSize: '1rem', color: '#4e9de0'} } + let feePending = { + icon: 'dollar-sign', + rank: 2, + style: {fontSize: '1rem', color: 'green'} + } if (item.exam_returned_date) { return envelopeOpenText } @@ -1421,6 +1506,9 @@ if (!item.exam_received_date) { return exclamationTriangle } + if (item.is_pesticide && item.exam_received_date && !item.receipt) { + return feePending + } return clipboardCheck }, stillRequires(item) { @@ -1450,6 +1538,7 @@ output.push('Event ID') } } + console.log(item) if (item.booking) { if(item.exam_type.group_exam_ind == 1){ if(length_of_invigilator_array == 0 && number_of_invigilators == 1){ diff --git a/frontend/src/exams/form-components/group-pesticide-modal.vue b/frontend/src/exams/form-components/group-pesticide-modal.vue index 3d0d3b62e..13f29aaf4 100644 --- a/frontend/src/exams/form-components/group-pesticide-modal.vue +++ b/frontend/src/exams/form-components/group-pesticide-modal.vue @@ -12,8 +12,7 @@ autocomplete="off" class="my-0 pb-0" size="sm" - @click.prevent - @input.prevent="clickSelectItem" + @click="clickSelectItem" :select-size="6" /> @@ -176,7 +175,7 @@ class="mr-2" style="font-size: 1rem;" />
-
{{ row.item.name }}
+
{{ row.item.exam_type_name }}
{{ currentlyEditing === 'exam' && row.item.exam_type_id === highlightedTableRow.exam_type_id ? row.item._rowVariant = 'primary' : row.item._rowVariant = 'secondary' }} @@ -348,6 +347,7 @@ output.push(candidate) } } + this.$store.commit('setCandidateTableData', output) return output }, }, diff --git a/frontend/src/exams/select-invigilator-modal.vue b/frontend/src/exams/select-invigilator-modal.vue index 9739ab6e7..5d6890bfc 100644 --- a/frontend/src/exams/select-invigilator-modal.vue +++ b/frontend/src/exams/select-invigilator-modal.vue @@ -45,6 +45,7 @@ components: { }, mounted() { this.getPesticideOfficeInvigilators() + this.getPesticideOffsiteInvigilators() }, props: [], data () { @@ -61,6 +62,7 @@ ...mapState({ showSelectInvigilatorModal: state => state.showSelectInvigilatorModal, pesticide_invigilators: state => state.pesticide_invigilators, + selectedExam: 'selectedExam', }), modal: { get() { @@ -76,7 +78,7 @@ }, }, methods: { - ...mapActions(['emailInvigilator', 'getPesticideOfficeInvigilators']), + ...mapActions(['emailInvigilator', 'getPesticideOfficeInvigilators', 'getPesticideOffsiteInvigilators']), ...mapMutations([ 'setSelectedExam', 'toggleSelectInvigilatorModal', @@ -98,15 +100,18 @@ }, submit() { this.loading = true - this.emailInvigilator(this.selected_invigilator) - .then(() => { - this.toggleSelectInvigilatorModal(false); - this.loading = false - }) - .catch(() => { - this.alertMessage = "An error occurred emailing the invigilator" - this.loading = false - }) + this.emailInvigilator({ + 'invigilator': this.selected_invigilator, + 'exam': selectedExam, + }) + .then(() => { + this.toggleSelectInvigilatorModal(false); + this.loading = false + }) + .catch(() => { + this.alertMessage = "An error occurred emailing the invigilator" + this.loading = false + }) }, }, } diff --git a/frontend/src/exams/upload-pesticide-exam.vue b/frontend/src/exams/upload-pesticide-exam.vue index d8103ada1..862727953 100644 --- a/frontend/src/exams/upload-pesticide-exam.vue +++ b/frontend/src/exams/upload-pesticide-exam.vue @@ -1,18 +1,17 @@