def get(self, office_id: int): try: office = Office.find_by_id(office_id) appointments_days_limit = office.appointments_days_limit # Dictionary to store the available slots per day tz = pytz.timezone(office.timezone.timezone_name) # today's date and time today = datetime.datetime.now().astimezone(tz) # Get all the dates from today until booking is allowed days = [today + datetime.timedelta(days=x) for x in range(appointments_days_limit)] service = None service_id = request.args.get('service_id') if (service_id): service = Service.query.get(int(service_id)) return AvailabilityService.get_available_slots(office=office, days=days, service=service) except exc.SQLAlchemyError as e: print(e) return {'message': 'API is down'}, 500
def post(self): my_print("==> In AppointmentDraftPost, POST /appointments/draft") json_data = request.get_json() office_id = json_data.get('office_id') service_id = json_data.get('service_id') start_time = parse(json_data.get('start_time')) end_time = parse(json_data.get('end_time')) office = Office.find_by_id(office_id) service = Service.query.get(int(service_id)) if service_id else None # end_time can be null for CSRs when they click; whereas citizens know end-time. if not end_time: end_time = add_delta_to_time(start_time, minutes=office.appointment_duration, timezone=office.timezone.timezone_name) # Unauthenticated requests from citizens won't have name, so we set a fallback if (hasattr(g, 'oidc_token_info') and hasattr(g.oidc_token_info, 'username')): user = PublicUser.find_by_username(g.oidc_token_info['username']) citizen_name = user.display_name else: citizen_name = 'Draft' # Delete all expired drafts before checking availability Appointment.delete_expired_drafts() csr = None if (hasattr(g, 'oidc_token_info')): csr = CSR.find_by_username(g.oidc_token_info['username']) # CSRs are not limited by drafts, can always see other CSRs drafts # This mitigates two CSRs in office creating at same time for same meeting # Ensure there's no race condition when submitting a draft if not csr and not AvailabilityService.has_available_slots( office=office, start_time=start_time, end_time=end_time, service=service): return {"code": "CONFLICT_APPOINTMENT", "message": "Cannot create appointment due to scheduling conflict. Please pick another time."}, 400 # Set draft specific data json_data['is_draft'] = True json_data['citizen_name'] = citizen_name appointment, warning = self.appointment_schema.load(json_data) if warning: logging.warning("WARNING: %s", warning) return {"message": warning}, 422 db.session.add(appointment) db.session.commit() result = self.appointment_schema.dump(appointment) socketio.emit('appointment_create', result.data) return {"appointment": result.data, "warning" : warning}, 201
def get(self): try: result = Office.get_all_active_offices() return {'offices': result.data, 'errors': result.errors} except exc.SQLAlchemyError as e: print(e) return {'message': 'API is down'}, 500
def post(self): try: result = [] json_data = request.get_json() previous_citizen_id = json_data.get('previous_citizen_id', False) if previous_citizen_id: previous_citizen = Citizen.query.filter_by( citizen_id=previous_citizen_id).first() # get nth line nth_line = self.get_nth_line(previous_citizen) # get all in Q + Agenda panel res_list = [] # result= all citizen in q result = self.walkinObj.get_all_citizen_in_q( citizen=previous_citizen) # process result # am_on_true= means get all citizen in Q booked_check_app, walkin_app = self.process_all_citizen_in_q( result) # sorting-maintaing the order group res_list = tuple(booked_check_app + walkin_app) # get the nth object in checkedin and walkin list # bool checks for both False and 0 nth_app = False if nth_line: if len(res_list) >= int(nth_line) and (int(nth_line) > 0): nth_app = res_list[int(nth_line) - 1] if nth_app['citizen_id']: citizen = Citizen.query.filter_by( citizen_id=nth_app['citizen_id']).first() if (not (citizen.automatic_reminder_flag) or (citizen.automatic_reminder_flag == 0)): office_obj = Office.find_by_id( citizen.office_id) if citizen.notification_phone: citizen = self.send_sms_reminder( citizen, office_obj) citizen.automatic_reminder_flag = 1 if citizen.notification_email: citizen = self.send_email_reminder( citizen, office_obj) citizen.automatic_reminder_flag = 1 db.session.add(citizen) db.session.commit() result = self.citizen_schema.dump(previous_citizen) return { 'citizen': result, 'errors': self.citizen_schema.validate(previous_citizen) }, 200 except ValidationError as err: return {'message': err.messages}, 422
def delete(self, id): appointment = Appointment.query.filter_by(appointment_id=id) \ .first_or_404() csr = None if is_public_user() else CSR.find_by_username( g.oidc_token_info['username']) user: PublicUser = PublicUser.find_by_username( g.oidc_token_info['username']) if is_public_user() else None if is_public_user(): # Check if it's a public user citizen = Citizen.find_citizen_by_id(appointment.citizen_id) if not citizen or citizen.citizen_id != appointment.citizen_id: abort(403) # Must call this prior to deleting from DB, so cannot # combine with repeated is_draft check below if not appointment.is_draft: SnowPlow.snowplow_appointment(None, csr, appointment, 'appointment_delete') db.session.delete(appointment) db.session.commit() if not application.config['DISABLE_AUTO_REFRESH']: socketio.emit('appointment_delete', id) # Do not log snowplow events or send emails if it's a draft. if not appointment.is_draft: # If the appointment is public user's and if staff deletes it send email if csr: office = Office.find_by_id(appointment.office_id) # Send blackout email try: pprint('Sending email for appointment cancellation') send_email( generate_ches_token(), *get_cancel_email_contents(appointment, user, office, office.timezone)) except Exception as exc: pprint(f'Error on token generation - {exc}') return {}, 204
def delete(self, id): appointment = Appointment.query.filter_by(appointment_id=id)\ .first_or_404() csr = None if is_public_user() else CSR.find_by_username( g.oidc_token_info['username']) user: PublicUser = PublicUser.find_by_username( g.oidc_token_info['username']) if is_public_user() else None if is_public_user(): # Check if it's a public user citizen = Citizen.find_citizen_by_id(appointment.citizen_id) if not citizen or citizen.citizen_id != appointment.citizen_id: abort(403) SnowPlow.snowplow_appointment(None, csr, appointment, 'appointment_delete') db.session.delete(appointment) db.session.commit() # If the appointment is public user's and if staff deletes it send email if csr: office = Office.find_by_id(appointment.office_id) # Send blackout email @copy_current_request_context def async_email(subject, email, sender, body): send_email(subject, email, sender, body) thread = Thread(target=async_email, args=get_cancel_email_contents( appointment, user, office, office.timezone)) thread.daemon = True thread.start() return {}, 204
def sync_offices_cache(): Office.clear_offices_cache()
def post(self, id): csr = CSR.find_by_username(g.jwt_oidc_token_info['username']) citizen = Citizen.query.filter_by(citizen_id=id).first() active_service_request = citizen.get_active_service_request() my_print("==> POST /citizens/" + str(citizen.citizen_id) + '/add_to_queue, Ticket: ' + citizen.ticket_number) if active_service_request is None: return {"message": "Citizen has no active service requests"} # Figure out what Snowplow call to make. Default is addtoqueue snowplow_call = "addtoqueue" if len(citizen.service_reqs) != 1 or len( active_service_request.periods) != 1: active_period = active_service_request.get_active_period() if active_period.ps.ps_name == "Invited": snowplow_call = "queuefromprep" elif active_period.ps.ps_name == "Being Served": snowplow_call = "returntoqueue" else: # TODO: Put in a Feedback Slack/Service now call here. return {"message": "Invalid citizen/period state. "} active_service_request.add_to_queue(csr, snowplow_call) pending_service_state = SRState.get_state_by_name("Pending") active_service_request.sr_state_id = pending_service_state.sr_state_id # send walkin spot confirmation try: if (citizen.notification_phone or citizen.notification_email ) and not (citizen.reminder_flag) and not ( citizen.notification_sent_time): update_table = False try: appointment_portal_url = application.config.get( 'APPOINTMENT_PORTAL_URL', '') # Dynamic URL creations url = '' if appointment_portal_url and citizen.walkin_unique_id: if appointment_portal_url.endswith('/'): appointment_portal_url = appointment_portal_url[: -1] url = "{}/{}/{}".format(appointment_portal_url, 'walk-in-Q', citizen.walkin_unique_id) # email email_sent = False if citizen.notification_email: officeObj = Office.find_by_id(citizen.office_id) print( 'Sending email for walk in spot confirmations to') email_sent = get_walkin_spot_confirmation_email_contents( citizen, url, officeObj) # SMS sms_sent = False if citizen.notification_phone: sms_sent = send_walkin_spot_confirmation_sms( citizen, url, request.headers['Authorization'].replace( 'Bearer ', '')) if email_sent: status = send_email( request.headers['Authorization'].replace( 'Bearer ', ''), *email_sent) update_table = True if sms_sent: update_table = True except Exception as exc: pprint(f'Error on token generation - {exc}') update_table = False if update_table: citizen.reminder_flag = 0 citizen.notification_sent_time = datetime.utcnow() except Exception as err: logging.error('{}'.format(str(err))) pprint(err) db.session.add(citizen) db.session.commit() socketio.emit('update_customer_list', {}, room=csr.office.office_name) socketio.emit('citizen_invited', {}, room='sb-%s' % csr.office.office_number) result = self.citizen_schema.dump(citizen) socketio.emit('update_active_citizen', result, room=csr.office.office_name) return { 'citizen': result, 'errors': self.citizen_schema.validate(citizen) }, 200
def post(self): my_print("==> In AppointmentPost, POST /appointments/") json_data = request.get_json() if not json_data: return { "message": "No input data received for creating an appointment" }, 400 # Should delete draft appointment, and free up slot, before booking. # Clear up a draft if one was previously created by user reserving this time. if json_data.get('appointment_draft_id'): draft_id_to_delete = int(json_data['appointment_draft_id']) Appointment.delete_draft([draft_id_to_delete]) if not application.config['DISABLE_AUTO_REFRESH']: socketio.emit('appointment_delete', draft_id_to_delete) is_blackout_appt = json_data.get('blackout_flag', 'N') == 'Y' csr = None user = None office = None # Create a citizen for later use. citizen = self.citizen_schema.load({}).data # Check if the appointment is created by public user. Can't depend on the IDP as BCeID is used by other users as well is_public_user_appointment = is_public_user() if is_public_user_appointment: office_id = json_data.get('office_id') service_id = json_data.get('service_id') user = PublicUser.find_by_username(g.oidc_token_info['username']) # Add values for contact info and notes json_data['contact_information'] = user.email telephone = f'. Phone: {user.telephone}' if user.telephone else '' json_data['comments'] = json_data.get('comments', '') + telephone citizen.user_id = user.user_id citizen.citizen_name = user.display_name office = Office.find_by_id(office_id) service = Service.query.get(int(service_id)) # Validate if the same user has other appointments for same day at same office appointments = Appointment.find_by_username_and_office_id( office_id=office_id, user_name=g.oidc_token_info['username'], start_time=json_data.get('start_time'), timezone=office.timezone.timezone_name) if appointments and len( appointments) >= office.max_person_appointment_per_day: return { "code": "MAX_NO_OF_APPOINTMENTS_REACHED", "message": "Maximum number of appointments reached" }, 400 # Check for race condition start_time = parse(json_data.get('start_time')) end_time = parse(json_data.get('end_time')) if not AvailabilityService.has_available_slots( office=office, start_time=start_time, end_time=end_time, service=service): return { "code": "CONFLICT_APPOINTMENT", "message": "Cannot create appointment due to scheduling conflict. Please pick another time." }, 400 else: csr = CSR.find_by_username(g.oidc_token_info['username']) office_id = csr.office_id office = Office.find_by_id(office_id) citizen.office_id = office_id citizen.qt_xn_citizen_ind = 0 citizen_state = CitizenState.query.filter_by( cs_state_name="Appointment booked").first() citizen.cs_id = citizen_state.cs_id citizen.start_time = datetime.now() citizen.service_count = 1 db.session.add(citizen) db.session.commit() appointment, warning = self.appointment_schema.load(json_data) if is_public_user_appointment: appointment.citizen_name = user.display_name appointment.online_flag = True if warning: logging.warning("WARNING: %s", warning) return {"message": warning}, 422 if appointment.office_id == office_id: appointment.citizen_id = citizen.citizen_id db.session.add(appointment) db.session.commit() # Generate CHES token try: ches_token = generate_ches_token() except Exception as exc: pprint(f'Error on token generation - {exc}') # If staff user is creating a blackout event then send email to all of the citizens with appointments for that period if is_blackout_appt: appointment_ids_to_delete = [] appointments_for_the_day = Appointment.get_appointment_conflicts( office_id, json_data.get('start_time'), json_data.get('end_time')) for (cancelled_appointment, office, timezone, user) in appointments_for_the_day: if cancelled_appointment.appointment_id != appointment.appointment_id and not cancelled_appointment.checked_in_time: appointment_ids_to_delete.append( cancelled_appointment.appointment_id) # Send blackout email try: pprint( 'Sending email for appointment cancellation due to blackout' ) send_email( ches_token, *get_blackout_email_contents( appointment, cancelled_appointment, office, timezone, user)) except Exception as exc: pprint(f'Error on email sending - {exc}') # Delete appointments if len(appointment_ids_to_delete) > 0: Appointment.delete_appointments(appointment_ids_to_delete) else: # Send confirmation email try: pprint('Sending email for appointment confirmation') send_email( ches_token, *get_confirmation_email_contents( appointment, office, office.timezone, user)) except Exception as exc: pprint(f'Error on email sending - {exc}') SnowPlow.snowplow_appointment(citizen, csr, appointment, 'appointment_create') result = self.appointment_schema.dump(appointment) if not application.config['DISABLE_AUTO_REFRESH']: socketio.emit('appointment_create', result.data) return {"appointment": result.data, "errors": result.errors}, 201 else: return { "The Appointment Office ID and CSR Office ID do not match!" }, 403
def put(self, id): json_data = request.get_json() if 'counter_id' not in json_data: json_data['counter_id'] = counter_id if not json_data: return {'message': 'No input data received for updating citizen'}, 400 csr = CSR.find_by_username(g.jwt_oidc_token_info['username']) citizen = Citizen.query.filter_by(citizen_id=id).first() my_print("==> PUT /citizens/" + str(citizen.citizen_id) + '/, Ticket: ' + str(citizen.ticket_number)) if not ((json_data.get('is_first_reminder', False) or json_data.get('is_second_reminder', False))): try: citizen = self.citizen_schema.load(json_data, instance=citizen, partial=True) except ValidationError as err: return {'message': err.messages}, 422 else: try: office_obj = Office.find_by_id(citizen.office_id) if (citizen.notification_phone): sms_sent = False # code/function call to send sms notification, sms_sent = send_walkin_reminder_sms(citizen, office_obj, request.headers['Authorization'].replace('Bearer ', '')) if (json_data.get('is_first_reminder', False)): if (sms_sent): citizen.reminder_flag = 1 citizen.notification_sent_time = datetime.utcnow() if (json_data.get('is_second_reminder', False)): if (sms_sent): citizen.reminder_flag = 2 citizen.notification_sent_time = datetime.utcnow() if (citizen.notification_email): # code/function call to send first email notification, email_sent = False email_sent = get_walkin_reminder_email_contents(citizen, office_obj) if email_sent: send_email(request.headers['Authorization'].replace('Bearer ', ''), *email_sent) if (json_data.get('is_first_reminder', False)): if email_sent: citizen.reminder_flag = 1 citizen.notification_sent_time = datetime.utcnow() if (json_data.get('is_second_reminder', False)): if email_sent: citizen.reminder_flag = 2 citizen.notification_sent_time = datetime.utcnow() except ValidationError as err: return {'message': err.messages}, 422 db.session.add(citizen) db.session.commit() # If this put request is the result of an appointment checkin, make a Snowplow call. if ('snowplow_addcitizen' in json_data) and (json_data['snowplow_addcitizen'] == True): SnowPlow.add_citizen(citizen, csr) result = self.citizen_schema.dump(citizen) citizen = Citizen.query.filter_by(citizen_id=citizen.citizen_id).first() socketio.emit('update_active_citizen', result, room=csr.office.office_name) return {'citizen': result, 'errors': self.citizen_schema.validate(citizen)}, 200
def put(self, id): json_data = request.get_json() csr = None user = None if not json_data: return { "message": "No input data received for updating an appointment" } is_public_user_appt = is_public_user() # Should delete draft appointment, and free up slot, before booking. # Clear up a draft if one was previously created by user reserving this time. if json_data.get('appointment_draft_id'): draft_id_to_delete = int(json_data['appointment_draft_id']) Appointment.delete_draft([draft_id_to_delete]) if not application.config['DISABLE_AUTO_REFRESH']: socketio.emit('appointment_delete', draft_id_to_delete) if is_public_user_appt: office_id = json_data.get('office_id') office = Office.find_by_id(office_id) # user = PublicUser.find_by_username(g.oidc_token_info['username']) # citizen = Citizen.find_citizen_by_username(g.oidc_token_info['username'], office_id) # Validate if the same user has other appointments for same day at same office appointments = Appointment.find_by_username_and_office_id( office_id=office_id, user_name=g.oidc_token_info['username'], start_time=json_data.get('start_time'), timezone=office.timezone.timezone_name, appointment_id=id) if appointments and len( appointments) >= office.max_person_appointment_per_day: return { "code": "MAX_NO_OF_APPOINTMENTS_REACHED", "message": "Maximum number of appoinments reached" }, 400 # Check for race condition start_time = parse(json_data.get('start_time')) end_time = parse(json_data.get('end_time')) service_id = json_data.get('service_id') service = Service.query.get(int(service_id)) if not AvailabilityService.has_available_slots( office=office, start_time=start_time, end_time=end_time, service=service): return { "code": "CONFLICT_APPOINTMENT", "message": "Cannot create appointment due to scheduling conflict. Please pick another time." }, 400 else: csr = CSR.find_by_username(g.oidc_token_info['username']) office_id = csr.office_id office = Office.find_by_id(office_id) appointment = Appointment.query.filter_by(appointment_id=id) \ .filter_by(office_id=office_id) \ .first_or_404() # If appointment is not made by same user, throw error if is_public_user_appt: citizen = Citizen.find_citizen_by_id(appointment.citizen_id) user = PublicUser.find_by_username(g.oidc_token_info['username']) # Should just match based on appointment_id and other info. Can't have proper auth yet. if citizen.user_id != user.user_id: abort(403) appointment, warning = self.appointment_schema.load( json_data, instance=appointment, partial=True) if warning: logging.warning("WARNING: %s", warning) return {"message": warning}, 422 db.session.add(appointment) db.session.commit() # Send confirmation email try: pprint('Sending email for appointment update') send_email( generate_ches_token(), *get_confirmation_email_contents(appointment, office, office.timezone, user)) except Exception as exc: pprint(f'Error on token generation - {exc}') # Make Snowplow call. schema = 'appointment_update' if "checked_in_time" in json_data: schema = 'appointment_checkin' if not appointment.is_draft: SnowPlow.snowplow_appointment(None, csr, appointment, schema) result = self.appointment_schema.dump(appointment) if not application.config['DISABLE_AUTO_REFRESH']: socketio.emit('appointment_update', result.data) return {"appointment": result.data, "errors": result.errors}, 200
def put(self, id): json_data = request.get_json() csr = None user = None if not json_data: return {"message": "No input data received for updating an appointment"} is_public_user_appt = is_public_user() if is_public_user_appt: office_id = json_data.get('office_id') office = Office.find_by_id(office_id) # user = PublicUser.find_by_username(g.oidc_token_info['username']) # citizen = Citizen.find_citizen_by_username(g.oidc_token_info['username'], office_id) # Validate if the same user has other appointments for same day at same office appointments = Appointment.find_by_username_and_office_id(office_id=office_id, user_name=g.oidc_token_info['username'], start_time=json_data.get('start_time'), timezone=office.timezone.timezone_name, appointment_id=id) if appointments and len(appointments) >= office.max_person_appointment_per_day: return {"code": "MAX_NO_OF_APPOINTMENTS_REACHED", "message": "Maximum number of appoinments reached"}, 400 # Check for race condition start_time = parse(json_data.get('start_time')) end_time = parse(json_data.get('end_time')) if not AvailabilityService.has_available_slots(office=office, start_time=start_time, end_time=end_time): return {"code": "CONFLICT_APPOINTMENT", "message": "Cannot create appointment due to conflict in time"}, 400 else: csr = CSR.find_by_username(g.oidc_token_info['username']) office_id = csr.office_id office = Office.find_by_id(office_id) appointment = Appointment.query.filter_by(appointment_id=id) \ .filter_by(office_id=office_id) \ .first_or_404() # If appointment is not made by same user, throw error if is_public_user_appt: citizen = Citizen.find_citizen_by_id(appointment.citizen_id) user = PublicUser.find_by_username(g.oidc_token_info['username']) if citizen.user_id != user.user_id: abort(403) appointment, warning = self.appointment_schema.load(json_data, instance=appointment, partial=True) if warning: logging.warning("WARNING: %s", warning) return {"message": warning}, 422 db.session.add(appointment) db.session.commit() # Send confirmation email @copy_current_request_context def async_email(subject, email, sender, body): send_email(subject, email, sender, body) thread = Thread(target=async_email, args=get_confirmation_email_contents(appointment, office, office.timezone, user)) thread.daemon = True thread.start() # Make Snowplow call. schema = 'appointment_update' if "checked_in_time" in json_data: schema = 'appointment_checkin' SnowPlow.snowplow_appointment(None, csr, appointment, schema) result = self.appointment_schema.dump(appointment) return {"appointment": result.data, "errors": result.errors}, 200
def post(self): json_data = request.get_json() if not json_data: return {"message": "No input data received for creating an appointment"}, 400 is_blackout_appt = json_data.get('blackout_flag', 'N') == 'Y' csr = None user = None office = None # Create a citizen for later use. citizen = self.citizen_schema.load({}).data # Check if the appointment is created by public user. Can't depend on the IDP as BCeID is used by other users as well is_public_user_appointment = is_public_user() if is_public_user_appointment: office_id = json_data.get('office_id') user = PublicUser.find_by_username(g.oidc_token_info['username']) # Add values for contact info and notes json_data['contact_information'] = user.email telephone = f'. Phone: {user.telephone}' if user.telephone else '' json_data['comments'] = json_data.get('comments', '') + telephone citizen.user_id = user.user_id citizen.citizen_name = user.display_name office = Office.find_by_id(office_id) # Validate if the same user has other appointments for same day at same office appointments = Appointment.find_by_username_and_office_id(office_id=office_id, user_name=g.oidc_token_info['username'], start_time=json_data.get('start_time'), timezone=office.timezone.timezone_name) if appointments and len(appointments) >= office.max_person_appointment_per_day: return {"code": "MAX_NO_OF_APPOINTMENTS_REACHED", "message": "Maximum number of appointments reached"}, 400 # Check for race condition start_time = parse(json_data.get('start_time')) end_time = parse(json_data.get('end_time')) if not AvailabilityService.has_available_slots(office=office, start_time=start_time, end_time=end_time): return {"code": "CONFLICT_APPOINTMENT", "message": "Cannot create appointment due to conflict in time"}, 400 else: csr = CSR.find_by_username(g.oidc_token_info['username']) office_id = csr.office_id office = Office.find_by_id(office_id) citizen.office_id = office_id citizen.qt_xn_citizen_ind = 0 citizen_state = CitizenState.query.filter_by(cs_state_name="Appointment booked").first() citizen.cs_id = citizen_state.cs_id citizen.start_time = datetime.now() citizen.service_count = 1 db.session.add(citizen) db.session.commit() appointment, warning = self.appointment_schema.load(json_data) if is_public_user_appointment: appointment.citizen_name = user.display_name appointment.online_flag = True if warning: logging.warning("WARNING: %s", warning) return {"message": warning}, 422 if appointment.office_id == office_id: appointment.citizen_id = citizen.citizen_id db.session.add(appointment) db.session.commit() # If staff user is creating a blackout event then send email to all of the citizens with appointments for that period if is_blackout_appt: appointment_ids_to_delete = [] appointments_for_the_day = Appointment.get_appointment_conflicts(office_id, json_data.get('start_time'), json_data.get('end_time')) for (cancelled_appointment, office, timezone, user) in appointments_for_the_day: if cancelled_appointment.appointment_id != appointment.appointment_id and not cancelled_appointment.checked_in_time: appointment_ids_to_delete.append(cancelled_appointment.appointment_id) # Send blackout email @copy_current_request_context def async_email(subject, email, sender, body): return send_email(subject, email, sender, body) thread = Thread(target=async_email, args=get_blackout_email_contents(appointment, cancelled_appointment, office, timezone, user)) thread.daemon = True thread.start() # Delete appointments if len(appointment_ids_to_delete) > 0: Appointment.delete_appointments(appointment_ids_to_delete) else: # Send confirmation email @copy_current_request_context def async_email(subject, email, sender, body): send_email(subject, email, sender, body) thread = Thread(target=async_email, args=get_confirmation_email_contents(appointment, office, office.timezone, user)) thread.daemon = True thread.start() SnowPlow.snowplow_appointment(citizen, csr, appointment, 'appointment_create') result = self.appointment_schema.dump(appointment) return {"appointment": result.data, "errors": result.errors}, 201 else: return {"The Appointment Office ID and CSR Office ID do not match!"}, 403