def status(): """ Check the status of an upload. Request Parameters: - request_id - filename - for_update (bool, optional) :returns: { "status": upload status } """ try: status = redis.get( get_upload_key(request.args['request_id'], secure_filename(request.args['filename']), eval_request_bool(request.args.get('for_update')))) if status is not None: response = {"status": status.decode("utf-8")} else: response = {"error": "Upload status not found."} status_code = 200 except KeyError: response = {} status_code = 422 return jsonify(response), status_code
def status(): """ Check the status of an upload. Request Parameters: - request_id - filename - for_update (bool, optional) :returns: { "status": upload status } """ try: status = redis.get( get_upload_key( request.args['request_id'], secure_filename(request.args['filename']), eval_request_bool(request.args.get('for_update')) ) ) if status is not None: response = {"status": status.decode("utf-8")} else: response = {"error": "Upload status not found."} status_code = 200 except KeyError: sentry.captureException() response = {} status_code = 422 return jsonify(response), status_code
def oauth_logout(): timed_out = eval_request_bool(request.args.get('timeout')) forced_logout = eval_request_bool(request.args.get('forced_logout')) if forced_logout: user_session = redis_get_user_session(current_user.session_id) if user_session is not None: user_session.destroy() if timed_out: flash("Your session timed out. Please login again", category='info') if 'token' in session: revoke_and_remove_access_token() if current_user.is_anonymous: return redirect(url_for("main.index")) update_object({'session_id': None}, Users, (current_user.guid, current_user.auth_user_type)) logout_user() session.destroy() if forced_logout: return redirect(url_for("auth.login")) return redirect(url_for("main.index"))
def patch(agency_ein): """ Only accessible by Super Users Currently only changes: is_active """ if not current_user.is_anonymous and current_user.is_super: is_active = request.form.get('is_active') if is_active is not None and Agencies.query.filter_by( ein=agency_ein).first() is not None: update_object({'is_active': eval_request_bool(is_active)}, Agencies, agency_ein) create_object( Events(request_id=None, user_guid=current_user.guid, auth_user_type=current_user.auth_user_type, type_=AGENCY_ACTIVATED, new_value={"ein": agency_ein}, timestamp=datetime.utcnow())) return '', 200 return '', 400 return '', 403
def new(): """ Create a new FOIL request sends a confirmation email after the Requests object is created. title: request title description: request description agency: agency selected for the request submission: submission method for the request :return: redirect to homepage on successful form validation if form fields are missing or has improper values, backend error messages (WTForms) will appear """ site_key = current_app.config["RECAPTCHA_SITE_KEY"] kiosk_mode = eval_request_bool( escape(flask_request.args.get("kiosk_mode", False))) category = str(escape(flask_request.args.get("category", None))) agency = str(escape(flask_request.args.get("agency", None))) title = str(escape(flask_request.args.get("title", None))) if current_user.is_public: form = PublicUserRequestForm() template_suffix = "user.html" elif current_user.is_anonymous: form = AnonymousRequestForm() template_suffix = "anon.html" elif current_user.is_agency: form = AgencyUserRequestForm() template_suffix = "agency.html" else: raise InvalidUserException(current_user) new_request_template = "request/new_request_" + template_suffix if flask_request.method == "POST": # validate upload with no request id available upload_path = None if form.request_file.data: form.request_file.validate(form) upload_path = handle_upload_no_id(form.request_file) if form.request_file.errors: return render_template(new_request_template, form=form, site_key=site_key) custom_metadata = json.loads( flask_request.form.get("custom-request-forms-data", {})) tz_name = (flask_request.form["tz-name"] if flask_request.form["tz-name"] else current_app.config["APP_TIMEZONE"]) if current_user.is_public: request_id = create_request( form.request_title.data, form.request_description.data, form.request_category.data, agency_ein=form.request_agency.data, upload_path=upload_path, tz_name=tz_name, custom_metadata=custom_metadata, ) elif current_user.is_agency: request_id = create_request( form.request_title.data, form.request_description.data, category=None, agency_ein=(form.request_agency.data if form.request_agency.data != "None" else current_user.default_agency_ein), submission=form.method_received.data, agency_date_submitted_local=form.request_date.data, email=form.email.data, first_name=form.first_name.data, last_name=form.last_name.data, user_title=form.user_title.data, organization=form.user_organization.data, phone=form.phone.data, fax=form.fax.data, address=get_address(form), upload_path=upload_path, tz_name=tz_name, custom_metadata=custom_metadata, ) else: # Anonymous User request_id = create_request( form.request_title.data, form.request_description.data, form.request_category.data, agency_ein=form.request_agency.data, email=form.email.data, first_name=form.first_name.data, last_name=form.last_name.data, user_title=form.user_title.data, organization=form.user_organization.data, phone=form.phone.data, fax=form.fax.data, address=get_address(form), upload_path=upload_path, tz_name=tz_name, custom_metadata=custom_metadata, ) current_request = Requests.query.filter_by(id=request_id).first() requester = current_request.requester send_confirmation_email(request=current_request, agency=current_request.agency, user=requester) if current_request.agency.is_active: if requester.email: flashed_message_html = render_template( "request/confirmation_email.html") flash(Markup(flashed_message_html), category="success") else: flashed_message_html = render_template( "request/confirmation_non_email.html") flash(Markup(flashed_message_html), category="warning") return redirect(url_for("request.view", request_id=request_id)) else: flashed_message_html = render_template( "request/non_portal_agency_message.html", agency=current_request.agency) flash(Markup(flashed_message_html), category="warning") return redirect( url_for("request.non_portal_agency", agency_name=current_request.agency.name)) return render_template( new_request_template, form=form, site_key=site_key, kiosk_mode=kiosk_mode, category=category, agency=agency, title=title, )
def update_agency_active_status(agency_ein, is_active): """ Update the active status of an agency. :param agency_ein: String identifier for agency (4 characters) :param is_active: Boolean value for agency active status (True = Active) :return: Boolean value (True if successfully changed active status) """ agency = Agencies.query.filter_by(ein=agency_ein).first() is_valid_agency = agency is not None activate_agency = eval_request_bool(is_active) if is_active is not None and is_valid_agency: update_object( {'is_active': activate_agency}, Agencies, agency_ein ) if activate_agency: create_object( Events( request_id=None, user_guid=current_user.guid, type_=AGENCY_ACTIVATED, previous_value={"ein": agency_ein, "is_active": "False"}, new_value={"ein": agency_ein, "is_active": "True"}, timestamp=datetime.utcnow() ) ) # create request documents for request in agency.requests: request.es_create() return True else: create_object( Events( request_id=None, user_guid=current_user.guid, type_=AGENCY_DEACTIVATED, previous_value={"ein": agency_ein, "is_active": "True"}, new_value={"ein": agency_ein, "is_active": "False"}, timestamp=datetime.utcnow() ) ) # remove requests from index for request in agency.requests: request.es_delete() # deactivate agency users for user in agency.active_users: update_object( {"is_agency_active": "False", "is_agency_admin": "False"}, AgencyUsers, (user.guid, agency_ein) ) create_object( Events( request_id=None, user_guid=current_user.guid, type_=AGENCY_USER_DEACTIVATED, previous_value={"user_guid": user.guid, "ein": agency_ein, "is_active": "True"}, new_value={"user_guid": user.guid, "ein": agency_ein, "is_active": "False"}, timestamp=datetime.utcnow() ) ) return True return False
def get_request_responses(): """ Returns a set of responses (id, type, and template), ordered by date descending, and starting from a specified index. Request parameters: - start: (int) starting index - request_id: FOIL request id - with_template: (default: False) include html (rows and modals) for each response """ start = int(flask_request.args['start']) current_request = Requests.query.filter_by( id=flask_request.args['request_id']).one() responses = Responses.query.filter( Responses.request_id == current_request.id, Responses.type != response_type.EMAIL, Responses.deleted == False).order_by(desc( Responses.date_modified)).all()[start:start + RESPONSES_INCREMENT] template_path = 'request/responses/' response_jsons = [] row_count = 0 for response in responses: # If a user is anonymous or a public user who is not the requester AND the date for Release and Public is in # the future, do not generate response row if (current_user == response.request.requester or current_user in response.request.agency_users) or ( response.privacy != response_privacy.PRIVATE and response.release_date and response.release_date < datetime.utcnow()): json = {'id': response.id, 'type': response.type} if eval_request_bool(flask_request.args.get('with_template')): row_count += 1 row = render_template( template_path + 'row.html', response=response, row_num=start + row_count, response_type=response_type, determination_type=determination_type, show_preview=not ( response.type == response_type.DETERMINATION and (response.dtype == determination_type.ACKNOWLEDGMENT or response.dtype == determination_type.REOPENING))) modal = render_template( template_path + 'modal.html', response=response, requires_workflow=response.type in response_type.EMAIL_WORKFLOW_TYPES, modal_body=render_template( "{}modal_body/{}.html".format(template_path, response.type), response=response, privacies=[ response_privacy.RELEASE_AND_PUBLIC, response_privacy.RELEASE_AND_PRIVATE, response_privacy.PRIVATE ], determination_type=determination_type, request_status=request_status, edit_response_privacy_permission=is_allowed( user=current_user, request_id=response.request_id, permission=get_permission( permission_type='privacy', response_type=type(response))), edit_response_permission=is_allowed( user=current_user, request_id=response.request_id, permission=get_permission( permission_type='edit', response_type=type(response))), delete_response_permission=is_allowed( user=current_user, request_id=response.request_id, permission=get_permission( permission_type='delete', response_type=type(response))), is_editable=response.is_editable, current_request=current_request), response_type=response_type, determination_type=determination_type, request_status=request_status, edit_response_permission=is_allowed( user=current_user, request_id=response.request_id, permission=get_permission( permission_type='edit', response_type=type(response))), delete_response_permission=is_allowed( user=current_user, request_id=response.request_id, permission=get_permission( permission_type='delete', response_type=type(response))), edit_response_privacy_permission=is_allowed( user=current_user, request_id=response.request_id, permission=get_permission( permission_type='privacy', response_type=type(response))), is_editable=response.is_editable, current_request=current_request) json['template'] = row + modal response_jsons.append(json) return jsonify(responses=response_jsons)
def requests_doc(doc_type): """ Converts and sends the a search result-set as a file of the specified document type. - Filtering on set size is ignored; all results are returned. - Currently only supports CSVs. Document name format: "FOIL_requests_results_<timestamp:MM_DD_YYYY_at_HH_mm_pp>" Request parameters are identical to those of /search/requests. :param doc_type: document type ('csv' only) """ if current_user.is_agency and doc_type.lower() == 'csv': try: agency_ein = request.args.get('agency_ein', '') except ValueError: sentry.captureException() agency_ein = None tz_name = request.args.get('tz_name', current_app.config['APP_TIMEZONE']) start = 0 buffer = StringIO() # csvwriter cannot accept BytesIO writer = csv.writer(buffer) writer.writerow(["FOIL ID", "Agency", "Title", "Description", "Agency Description", "Current Status", "Date Created", "Date Received", "Date Due", "Date Closed", "Requester Name", "Requester Email", "Requester Title", "Requester Organization", "Requester Phone Number", "Requester Fax Number", "Requester Address 1", "Requester Address 2", "Requester City", "Requester State", "Requester Zipcode", "Assigned User Emails"]) results = search_requests( query=request.args.get('query'), foil_id=eval_request_bool(request.args.get('foil_id')), title=eval_request_bool(request.args.get('title')), agency_request_summary=eval_request_bool(request.args.get('agency_request_summary')), description=eval_request_bool(request.args.get('description')) if not current_user.is_anonymous else False, requester_name=eval_request_bool(request.args.get('requester_name')) if current_user.is_agency else False, date_rec_from=request.args.get('date_rec_from'), date_rec_to=request.args.get('date_rec_to'), date_due_from=request.args.get('date_due_from'), date_due_to=request.args.get('date_due_to'), date_closed_from=request.args.get('date_closed_from'), date_closed_to=request.args.get('date_closed_to'), agency_ein=agency_ein, agency_user_guid=request.args.get('agency_user'), open_=eval_request_bool(request.args.get('open')), closed=eval_request_bool(request.args.get('closed')), in_progress=eval_request_bool(request.args.get('in_progress')) if current_user.is_agency else False, due_soon=eval_request_bool(request.args.get('due_soon')) if current_user.is_agency else False, overdue=eval_request_bool(request.args.get('overdue')) if current_user.is_agency else False, start=start, sort_date_received=request.args.get('sort_date_submitted'), sort_date_due=request.args.get('sort_date_due'), sort_title=request.args.get('sort_title'), tz_name=request.args.get('tz_name', current_app.config['APP_TIMEZONE']), for_csv=True ) ids = [result["_id"] for result in results] all_requests = Requests.query.filter(Requests.id.in_(ids)).options( joinedload(Requests.agency_users)).options(joinedload(Requests.requester)).options( joinedload(Requests.agency)).all() for req in all_requests: writer.writerow([ req.id, req.agency.name, req.title, req.description, req.agency_request_summary, req.status, req.date_created, req.date_submitted, req.due_date, req.date_closed, req.requester.name, req.requester.email, req.requester.title, req.requester.organization, req.requester.phone_number, req.requester.fax_number, req.requester.mailing_address.get('address_one'), req.requester.mailing_address.get('address_two'), req.requester.mailing_address.get('city'), req.requester.mailing_address.get('state'), req.requester.mailing_address.get('zip'), ", ".join(u.email for u in req.agency_users)]) dt = datetime.utcnow() timestamp = utc_to_local(dt, tz_name) if tz_name is not None else dt return send_file( BytesIO(buffer.getvalue().encode('UTF-8')), # convert to bytes attachment_filename="FOIL_requests_results_{}.csv".format( timestamp.strftime("%m_%d_%Y_at_%I_%M_%p")), as_attachment=True ) return '', 400
def get_request_events(): """ Returns a set of events (id, type, and template), ordered by date descending, and starting from a specific index. Request parameters: - start: (int) starting index - request_id: FOIL request id - with_template: (default: False) include html rows for each event """ start = int(flask_request.args['start']) current_request = Requests.query.filter_by(id=flask_request.args['request_id']).one() events = Events.query.filter( Events.request_id == current_request.id, Events.type.in_(event_type.FOR_REQUEST_HISTORY) ).order_by( desc(Events.timestamp) ).all() total = len(events) events = events[start: start + EVENTS_INCREMENT] template_path = 'request/events/' event_jsons = [] types_with_modal = [ event_type.FILE_EDITED, event_type.INSTRUCTIONS_EDITED, event_type.LINK_EDITED, event_type.NOTE_EDITED, event_type.REQ_AGENCY_REQ_SUM_EDITED, event_type.REQ_AGENCY_REQ_SUM_PRIVACY_EDITED, event_type.REQ_TITLE_EDITED, event_type.REQ_TITLE_PRIVACY_EDITED, event_type.REQUESTER_INFO_EDITED, event_type.USER_PERM_CHANGED, ] for i, event in enumerate(events): json = { 'id': event.id, 'type': event.type } if eval_request_bool(flask_request.args.get('with_template')): has_modal = event.type in types_with_modal row = render_template( template_path + 'row.html', event=event, row_num=start + i + 1, has_modal=has_modal ) if has_modal: if event.type == event_type.USER_PERM_CHANGED: previous_permissions = set([ p.label for p in get_permissions_as_list(event.previous_value['permissions']) ]) new_permissions = set([ p.label for p in get_permissions_as_list(event.new_value['permissions']) ]) modal = render_template( template_path + 'modal.html', event=event, modal_body=render_template( "{}modal_body/{}.html".format( template_path, event.type.lower() ), event=event, permissions_granted=list(new_permissions - previous_permissions), permissions_revoked=list(previous_permissions - new_permissions), ), ) else: modal = render_template( template_path + 'modal.html', modal_body=render_template( "{}modal_body/{}.html".format( template_path, event.type.lower() ), event=event ), event=event, ) else: modal = "" json['template'] = row + modal event_jsons.append(json) return jsonify(events=event_jsons, total=total)
def get_request_responses(): """ Returns a set of responses (id, type, and template), ordered by date descending, and starting from a specified index. Request parameters: - start: (int) starting index - request_id: FOIL request id - with_template: (default: False) include html (rows and modals) for each response """ start = int(flask_request.args['start']) current_request = Requests.query.filter_by(id=flask_request.args['request_id']).one() if current_user in current_request.agency_users: # If the user is an agency user assigned to the request, all responses can be retrieved. responses = Responses.query.filter( Responses.request_id == current_request.id, ~Responses.id.in_([cm.method_id for cm in CommunicationMethods.query.all()]), Responses.type != response_type.EMAIL, Responses.deleted == False ).order_by( desc(Responses.date_modified) ).all() elif current_user == current_request.requester: # If the user is the requester, then only responses that are "Release and Private" or "Release and Public" # can be retrieved. responses = Responses.query.filter( Responses.request_id == current_request.id, ~Responses.id.in_([cm.method_id for cm in CommunicationMethods.query.all()]), Responses.type != response_type.EMAIL, Responses.deleted == False, Responses.privacy.in_([response_privacy.RELEASE_AND_PRIVATE, response_privacy.RELEASE_AND_PUBLIC]) ).order_by( desc(Responses.date_modified) ).all() else: # If the user is not an agency user assigned to the request or the requester, then only responses that are # "Release and Public" whose release date is not in the future can be retrieved. responses = Responses.query.filter( Responses.request_id == current_request.id, ~Responses.id.in_([cm.method_id for cm in CommunicationMethods.query.all()]), Responses.type != response_type.EMAIL, Responses.deleted == False, Responses.privacy.in_([response_privacy.RELEASE_AND_PUBLIC]), Responses.release_date.isnot(None), Responses.release_date < datetime.utcnow() ).order_by( desc(Responses.date_modified) ).all() total = len(responses) responses = responses[start: start + RESPONSES_INCREMENT] template_path = 'request/responses/' response_jsons = [] row_count = 0 for response in responses: json = { 'id': response.id, 'type': response.type } if eval_request_bool(flask_request.args.get('with_template')): row_count += 1 row = render_template( template_path + 'row.html', response=response, row_num=start + row_count, response_type=response_type, determination_type=determination_type, show_preview=not (response.type == response_type.DETERMINATION and (response.dtype == determination_type.ACKNOWLEDGMENT or response.dtype == determination_type.REOPENING)) ) modal = render_template( template_path + 'modal.html', response=response, requires_workflow=response.type in response_type.EMAIL_WORKFLOW_TYPES, modal_body=render_template( "{}modal_body/{}.html".format( template_path, response.type ), response=response, privacies=[response_privacy.RELEASE_AND_PUBLIC, response_privacy.RELEASE_AND_PRIVATE, response_privacy.PRIVATE], determination_type=determination_type, request_status=request_status, edit_response_privacy_permission=is_allowed(user=current_user, request_id=response.request_id, permission=get_permission( permission_type='privacy', response_type=type( response))), edit_response_permission=is_allowed(user=current_user, request_id=response.request_id, permission=get_permission(permission_type='edit', response_type=type( response))), delete_response_permission=is_allowed(user=current_user, request_id=response.request_id, permission=get_permission(permission_type='delete', response_type=type(response))), is_editable=response.is_editable, current_request=current_request ), response_type=response_type, determination_type=determination_type, request_status=request_status, edit_response_permission=is_allowed(user=current_user, request_id=response.request_id, permission=get_permission(permission_type='edit', response_type=type(response))), delete_response_permission=is_allowed(user=current_user, request_id=response.request_id, permission=get_permission(permission_type='delete', response_type=type(response))), edit_response_privacy_permission=is_allowed(user=current_user, request_id=response.request_id, permission=get_permission( permission_type='privacy', response_type=type( response))), is_editable=response.is_editable, current_request=current_request ) json['template'] = row + modal response_jsons.append(json) return jsonify(responses=response_jsons, total=total)
def get_request_events(): """ Returns a set of events (id, type, and template), ordered by date descending, and starting from a specific index. Request parameters: - start: (int) starting index - request_id: FOIL request id - with_template: (default: False) include html rows for each event """ start = int(flask_request.args['start']) current_request = Requests.query.filter_by( id=flask_request.args['request_id']).one() events = Events.query.filter( Events.request_id == current_request.id, Events.type.in_(event_type.FOR_REQUEST_HISTORY)).order_by( desc(Events.timestamp)).all()[start:start + EVENTS_INCREMENT] template_path = 'request/events/' event_jsons = [] types_with_modal = [ event_type.FILE_EDITED, event_type.INSTRUCTIONS_EDITED, event_type.LINK_EDITED, event_type.NOTE_EDITED, event_type.REQ_AGENCY_REQ_SUM_EDITED, event_type.REQ_AGENCY_REQ_SUM_PRIVACY_EDITED, event_type.REQ_TITLE_EDITED, event_type.REQ_TITLE_PRIVACY_EDITED, event_type.REQUESTER_INFO_EDITED, event_type.USER_PERM_CHANGED, ] for i, event in enumerate(events): json = {'id': event.id, 'type': event.type} if eval_request_bool(flask_request.args.get('with_template')): has_modal = event.type in types_with_modal row = render_template(template_path + 'row.html', event=event, row_num=start + i + 1, has_modal=has_modal) if has_modal: if event.type == event_type.USER_PERM_CHANGED: previous_permissions = set([ p.label for p in get_permissions_as_list( event.previous_value['permissions']) ]) new_permissions = set([ p.label for p in get_permissions_as_list( event.new_value['permissions']) ]) modal = render_template( template_path + 'modal.html', event=event, modal_body=render_template( "{}modal_body/{}.html".format( template_path, event.type.lower()), event=event, permissions_granted=list(new_permissions - previous_permissions), permissions_revoked=list(previous_permissions - new_permissions), ), ) else: modal = render_template( template_path + 'modal.html', modal_body=render_template( "{}modal_body/{}.html".format( template_path, event.type.lower()), event=event), event=event, ) else: modal = "" json['template'] = row + modal event_jsons.append(json) return jsonify(events=event_jsons)
def delete(r_id_type, r_id, filecode): """ Removes an uploaded file. :param r_id_type: "response" or "request" :param r_id: the Response or Request identifier :param filecode: the encoded name of the uploaded file (base64 without padding) Optional request body parameters: - quarantined_only (bool) only delete the file if it is quarantined (beware: takes precedence over 'updated_only') - updated_only (bool) only delete the file if it is in the 'updated' directory :returns: On success: { "deleted": filename } On failure: { "error": error message } """ filename = secure_filename(b64decode_lenient(filecode)) if r_id_type not in ["request", "response"]: response = {"error": "Invalid ID type."} else: try: if r_id_type == "response": response = Responses.query.filter_by(id=r_id, deleted=False) r_id = response.request_id path = '' quarantined_only = eval_request_bool(request.form.get('quarantined_only')) has_add_edit = (is_allowed(user=current_user, request_id=r_id, permission=permission.ADD_FILE) or is_allowed(user=current_user, request_id=r_id, permission=permission.EDIT_FILE)) if quarantined_only and has_add_edit: path = os.path.join( current_app.config['UPLOAD_QUARANTINE_DIRECTORY'], r_id ) elif eval_request_bool(request.form.get('updated_only')) and \ is_allowed(user=current_user, request_id=r_id, permission=permission.EDIT_FILE): path = os.path.join( current_app.config['UPLOAD_DIRECTORY'], r_id, UPDATED_FILE_DIRNAME ) else: path_for_status = { upload_status.PROCESSING: current_app.config['UPLOAD_QUARANTINE_DIRECTORY'], upload_status.SCANNING: current_app.config['UPLOAD_QUARANTINE_DIRECTORY'], upload_status.READY: current_app.config['UPLOAD_DIRECTORY'] } status = redis.get(get_upload_key(r_id, filename)) if status is not None: dest_path = path_for_status[status.decode("utf-8")] if (dest_path == current_app.config['UPLOAD_QUARANTINE_DIRECTORY'] and has_add_edit) or ( dest_path == current_app.config['UPLOAD_DIRECTORY'] and is_allowed(user=current_user, request_id=r_id, permission=permission.ADD_FILE) ): path = os.path.join( dest_path, r_id ) filepath = os.path.join(path, filename) found = False if path != '': if quarantined_only: if os.path.exists(filepath): os.remove(filepath) found = True else: if fu.exists(filepath): fu.remove(filepath) found = True if found: response = {"deleted": filename} else: response = {"error": "Upload not found."} except Exception as e: sentry.captureException() current_app.logger.exception("Error on DELETE /upload/: {}".format(e)) response = {"error": "Failed to delete '{}'".format(filename)} return jsonify(response), 200
def requests_doc(doc_type): """ Converts and sends the a search result-set as a file of the specified document type. - Filtering on set size is ignored; all results are returned. - Currently only supports CSVs. Document name format: "FOIL_requests_results_<timestamp:MM_DD_YYYY_at_HH_mm_pp>" Request parameters are identical to those of /search/requests. :param doc_type: document type ('csv' only) """ if current_user.is_agency and doc_type.lower() == 'csv': try: agency_ein = request.args.get('agency_ein', '') except ValueError: agency_ein = None tz_name = request.args.get('tz_name') start = 0 buffer = StringIO() # csvwriter cannot accept BytesIO writer = csv.writer(buffer) writer.writerow([ "FOIL ID", "Agency", "Title", "Description", "Agency Description", "Current Status", "Date Created", "Date Received", "Date Due", "Date Closed", "Requester Name", "Requester Email", "Requester Title", "Requester Organization", "Requester Phone Number", "Requester Fax Number", "Requester Address 1", "Requester Address 2", "Requester City", "Requester State", "Requester Zipcode", "Assigned User Emails" ]) while True: results = search_requests( request.args.get('query'), eval_request_bool(request.args.get('foil_id')), eval_request_bool(request.args.get('title')), eval_request_bool(request.args.get('agency_request_summary')), eval_request_bool(request.args.get('description')), eval_request_bool(request.args.get('requester_name')), request.args.get('date_rec_from'), request.args.get('date_rec_to'), request.args.get('date_due_from'), request.args.get('date_due_to'), request.args.get('date_closed_from'), request.args.get('date_closed_to'), agency_ein, eval_request_bool(request.args.get('open')), eval_request_bool(request.args.get('closed')), eval_request_bool(request.args.get('in_progress')), eval_request_bool(request.args.get('due_soon')), eval_request_bool(request.args.get('overdue')), ALL_RESULTS_CHUNKSIZE, start, request.args.get('sort_date_submitted'), request.args.get('sort_date_due'), request.args.get('sort_title'), tz_name, for_csv=True) total = results["hits"]["total"] if total != 0: convert_dates(results, tz_name=tz_name) for result in results["hits"]["hits"]: r = Requests.query.filter_by(id=result["_id"]).one() mailing_address = (r.requester.mailing_address if r.requester.mailing_address is not None else {}) date_closed = result['_source'].get('date_closed', '') date_closed = date_closed if str(date_closed) != str( list()) else '' writer.writerow([ result["_id"], result["_source"]["agency_name"], result["_source"]["title"], result["_source"]["description"], result["_source"]["agency_request_summary"], r.status, result["_source"]["date_created"], result["_source"]["date_submitted"], result["_source"]["date_due"], date_closed, result["_source"]["requester_name"], r.requester.email, r.requester.title, r.requester.organization, r.requester.phone_number, r.requester.fax_number, mailing_address.get('address_one'), mailing_address.get('address_two'), mailing_address.get('city'), mailing_address.get('state'), mailing_address.get('zip'), ", ".join(u.email for u in r.agency_users) ]) start += ALL_RESULTS_CHUNKSIZE if start > total: break if total != 0: dt = datetime.utcnow() timestamp = utc_to_local(dt, tz_name) if tz_name is not None else dt return send_file( BytesIO(buffer.getvalue().encode('UTF-8')), # convert to bytes attachment_filename="FOIL_requests_results_{}.csv".format( timestamp.strftime("%m_%d_%Y_at_%I_%M_%p")), as_attachment=True) return '', 400
def patch(user_id): """ Request Parameters: - title - organization - email - phone_number - fax_number - mailing_address - is_super - is_agency_active - is_agency_admin (Mailing Address) - zip - city - state - address_one - address_two Restrictions: - Anonymous Users - cannot access this endpoint - Agency Administrators - cannot change their agency status - can only update the agency status of users within their agency - cannot change any super user status - Super Users - cannot change their super user status - Agency Users - cannot change any user except for themselves or *anonymous* requesters for requests they are assigned to - cannot change super user or agency status - Public Users - can only update themselves - cannot change super user or agency status """ # Anonymous users cannot access endpoint if current_user.is_anonymous: return jsonify({'error': 'Anonymous users cannot access this endpoint'}), 403 # Public users cannot access endpoint if current_user.is_public: return jsonify({'error': 'Public users cannot access this endpoint'}), 403 # Unauthenticated users cannot access endpoint if not current_user.is_authenticated: return jsonify({'error': 'User must be authenticated to access endpoint'}), 403 # Retrieve the user user_ = Users.query.filter_by(guid=user_id).one_or_none() # If the user does not exist, return 404 - Not Found if not user_: return jsonify({'error': 'Specified user does not exist.'}), 404 # Gather Form details is_agency_admin = eval_request_bool(request.form.get('is_agency_admin')) if request.form.get('is_agency_admin', None) else None is_agency_active = eval_request_bool(request.form.get('is_agency_active')) if request.form.get('is_agency_active', None) else None is_super = eval_request_bool(request.form.get('is_super')) if request.form.get('is_super', None) else None agency_ein = request.form.get('agency_ein', None) # Checks that apply if user is changing their own profile changing_self = current_user == user_ # Agency User Restrictions (applies to Admins and Regular Users) if user_.is_agency: # Endpoint can only be used for a specific agency if not agency_ein: return jsonify({'error': 'agency_ein must be provided to modify an agency user'}), 400 # Agency must exist and be active to modify users agency = Agencies.query.filter_by(ein=agency_ein).one_or_none() if not agency and agency.is_active: return jsonify({'error': 'Agency must exist in the database and be active'}), 400 if not current_user.is_super: # Current user must belong to agency specified by agency_ein current_user_is_agency_admin = current_user.is_agency_admin(agency.ein) if not current_user_is_agency_admin: return jsonify({'error': 'Current user must belong to agency specified by agency_ein'}), 400 user_in_agency = AgencyUsers.query.filter(AgencyUsers.user_guid == user_.guid, AgencyUsers.agency_ein == agency_ein).one_or_none() if user_in_agency is None: return jsonify({'error': 'User to be modified must belong to agency specified by agency_ein'}), 400 # Non-Agency Admins cannot access endpoint to modify other agency_users if not current_user.is_super and not current_user.is_agency_admin(agency_ein): return jsonify({'error': 'User must be agency admin to modify users'}), 403 # Cannot modify super status when changing agency active or agency admin status if (is_agency_admin or is_agency_active) and is_super: return jsonify({ 'error': 'Cannot modify super status when changing agency active or agency admin status'}), 400 if changing_self: # Super users cannot change their own is_super value if current_user.is_super and is_super: return jsonify({'error': 'Super users cannot change their own `super` status'}), 400 if is_agency_admin: return jsonify({'error': 'Agency Administrators cannot change their administrator permissions'}), 400 elif user_.is_public: if current_user != user_: return jsonify({'error': 'Public user attributes cannot be modified by agency users.'}), 400 elif user_.is_anonymous_requester: ur = current_user.user_requests.filter_by(request_id=user_.anonymous_request.id).one_or_none() if not ur: return jsonify({ 'error': 'Agency users can only modify anonymous requesters for requests where they are assigned.'}), 403 if not ur.has_permission(permission.EDIT_REQUESTER_INFO): return jsonify({'error': 'Current user does not have EDIT_REQUESTER_INFO permission'}), 403 # Gather User Fields user_editable_fields = user_attrs.UserEditableFieldsDict( email=request.form.get('email'), phone_number=request.form.get('phone'), fax_number=request.form.get('fax'), title=request.form.get('title'), organization=request.form.get('organization'), address_one=request.form.get('address_one'), address_two=request.form.get('address_two'), zip=request.form.get('zipcode'), city=request.form.get('city'), ) status_field_val = user_attrs.UserStatusDict( is_agency_admin=request.form.get('is_agency_admin'), is_agency_active=request.form.get('is_agency_active'), is_super=request.form.get('is_super') ) if changing_self: if not user_editable_fields.is_valid: return jsonify({"error": "Missing contact information."}), 400 else: if user_.is_agency and not status_field_val.is_valid: return jsonify({"error": "User status values invalid"}), 400 # Status Values for Events old_status = {} new_status = {} # User Attributes for Events old_user_attrs = {'_mailing_address': {}} new_user_attrs = {'_mailing_address': {}} for key, value in status_field_val.items(): if value is not None: if key == 'is_agency_admin': cur_val = user_.is_agency_admin(agency_ein) elif key == 'is_agency_active': cur_val = user_.is_agency_active(agency_ein) else: cur_val = getattr(user_, key) new_val = eval_request_bool(status_field_val[key]) if cur_val != new_val: old_status[key] = cur_val new_status[key] = new_val for key, value in user_editable_fields.items(): # Address is a dictionary and needs to be handled separately if key == 'address': for address_key, address_value in value.items(): cur_val = (user_.mailing_address.get(address_key) if user_.mailing_address else None) new_val = address_value if cur_val != new_val: old_user_attrs['_mailing_address'][address_key] = cur_val new_user_attrs['_mailing_address'][address_key] = new_val continue if value is not None: cur_val = getattr(user_, key) new_val = user_editable_fields[key] if cur_val != new_val: old_user_attrs[key] = cur_val new_user_attrs[key] = new_val # Update the Users object if new_user_attrs is not None (empty dict) if new_user_attrs and new_user_attrs.get('_mailing_address') and old_user_attrs: update_object( new_user_attrs, Users, user_id ) # GUID is added to the 'new' value in events to identify the user that was changed new_user_attrs['user_guid'] = user_.guid # create event(s) event_kwargs = { 'request_id': user_.anonymous_request.id if user_.is_anonymous_requester else None, 'response_id': None, 'user_guid': current_user.guid, 'timestamp': datetime.utcnow() } create_object(Events( type_=(event_type.REQUESTER_INFO_EDITED if user_.is_anonymous_requester else event_type.USER_INFO_EDITED), previous_value=old_user_attrs, new_value=new_user_attrs, **event_kwargs )) if new_status: redis_key = "{current_user_guid}-{update_user_guid}-{agency_ein}-{timestamp}".format( current_user_guid=current_user.guid, update_user_guid=user_.guid, agency_ein=agency_ein, timestamp=datetime.now()) old_status['user_guid'] = user_.guid new_status['user_guid'] = user_.guid old_status['agency_ein'] = agency_ein new_status['agency_ein'] = agency_ein # Update agency active status and create associated event. Occurs first because a user can be # activated / deactivated with admin status set to True. if is_agency_active is not None: update_object( new_status, AgencyUsers, (user_.guid, agency_ein) ) event_kwargs = { 'request_id': user_.anonymous_request.id if user_.is_anonymous_requester else None, 'response_id': None, 'user_guid': current_user.guid, 'timestamp': datetime.utcnow() } if is_agency_active: create_object(Events( type_=event_type.AGENCY_USER_ACTIVATED, previous_value=old_status, new_value=new_status, **event_kwargs )) if is_agency_admin is not None and is_agency_admin: make_user_admin.apply_async(args=(user_.guid, current_user.guid, agency_ein), task_id=redis_key) return jsonify({'status': 'success', 'message': 'Update task has been scheduled.'}), 200 else: create_object(Events( type_=event_type.AGENCY_USER_DEACTIVATED, previous_value=old_status, new_value=new_status, **event_kwargs )) remove_user_permissions.apply_async( args=(user_.guid, current_user.guid, agency_ein, event_type.AGENCY_USER_DEACTIVATED), task_id=redis_key) return jsonify({'status': 'success', 'message': 'Update task has been scheduled.'}), 200 return jsonify({'status': 'success', 'message': 'Agency user successfully updated'}), 200 # Update agency admin status and create associated event. elif is_agency_admin is not None: new_status['agency_ein'] = agency_ein update_object( new_status, AgencyUsers, (user_.guid, agency_ein) ) event_kwargs = { 'request_id': user_.anonymous_request.id if user_.is_anonymous_requester else None, 'response_id': None, 'user_guid': current_user.guid, 'timestamp': datetime.utcnow() } if is_agency_admin: create_object(Events( type_=event_type.USER_MADE_AGENCY_ADMIN, previous_value=old_status, new_value=new_status, **event_kwargs )) make_user_admin.apply_async(args=(user_.guid, current_user.guid, agency_ein), task_id=redis_key) return jsonify({'status': 'success', 'message': 'Update task has been scheduled.'}), 200 else: create_object(Events( type_=event_type.USER_MADE_AGENCY_USER, previous_value=old_status, new_value=new_status, **event_kwargs )) remove_user_permissions.apply_async( args=(user_.guid, current_user.guid, agency_ein, event_type.USER_MADE_AGENCY_USER), task_id=redis_key) return jsonify({'status': 'success', 'message': 'Update task has been scheduled.'}), 200 # Update user super status and create associated event. elif is_super is not None: new_status['agency_ein'] = agency_ein update_object( new_status, AgencyUsers, (user_.guid, agency_ein) ) event_kwargs = { 'request_id': user_.anonymous_request.id if user_.is_anonymous_requester else None, 'response_id': None, 'user_guid': current_user.guid, 'timestamp': datetime.utcnow() } if is_super: create_object(Events( type_=event_type.USER_MADE_SUPER_USER, previous_value=old_status, new_value=new_status, **event_kwargs )) else: create_object(Events( type_=event_type.USER_REMOVED_FROM_SUPER, previous_value=old_status, new_value=new_status, **event_kwargs )) return jsonify({'status': 'success', 'message': 'Agency user successfully updated'}), 200 # Always returns 200 so that we can use the data from the response in the client side javascript return jsonify({'status': 'Not Modified', 'message': 'No changes detected'}), 200
def post(request_id): """ Create a new upload. Handles chunked files through the Content-Range header. For filesize validation and more upload logic, see: /static/js/upload/fileupload.js Optional request body parameters: - update (bool) save the uploaded file to the 'updated' directory (this indicates the file is meant to replace a previously uploaded file) - response_id (int) the id of a response associated with the file this upload is replacing - REQUIRED if 'update' is 'true' - ignored if 'update' is 'false' :returns: { "name": file name, "size": file size } """ files = request.files file_ = files[next(files.keys())] filename = secure_filename(file_.filename) is_update = eval_request_bool(request.form.get('update')) agency_ein = Requests.query.filter_by(id=request_id).one().agency.ein if is_allowed(user=current_user, request_id=request_id, permission=permission.ADD_FILE) or \ is_allowed(user=current_user, request_id=request_id, permission=permission.EDIT_FILE): response_id = request.form.get('response_id') if is_update else None if upload_exists(request_id, filename, response_id): response = { "files": [{ "name": filename, "error": "A file with this name has already " "been uploaded for this request." # TODO: "link": <link-to-existing-file> ? would be nice }] } else: upload_path = os.path.join( current_app.config['UPLOAD_QUARANTINE_DIRECTORY'], request_id) if not os.path.exists(upload_path): os.mkdir(upload_path) filepath = os.path.join(upload_path, filename) key = get_upload_key(request_id, filename, is_update) try: if CONTENT_RANGE_HEADER in request.headers: start, size = parse_content_range( request.headers[CONTENT_RANGE_HEADER]) # Only validate mime type on first chunk valid_file_type = True file_type = None if start == 0: valid_file_type, file_type = is_valid_file_type(file_) if current_user.is_agency_active(agency_ein): valid_file_type = True if os.path.exists(filepath): # remove existing file (upload 'restarted' for same file) os.remove(filepath) if valid_file_type: redis.set(key, upload_status.PROCESSING) with open(filepath, 'ab') as fp: fp.seek(start) fp.write(file_.stream.read()) # scan if last chunk written if os.path.getsize(filepath) == size: scan_and_complete_upload.delay( request_id, filepath, is_update, response_id) else: valid_file_type, file_type = is_valid_file_type(file_) if current_user.is_agency_active(agency_ein): valid_file_type = True if valid_file_type: redis.set(key, upload_status.PROCESSING) file_.save(filepath) scan_and_complete_upload.delay(request_id, filepath, is_update, response_id) if not valid_file_type: response = { "files": [{ "name": filename, "error": "The file type '{}' is not allowed.".format( file_type) }] } else: response = { "files": [{ "name": filename, "original_name": file_.filename, "size": os.path.getsize(filepath), }] } except Exception as e: redis.set(key, upload_status.ERROR) current_app.logger.exception( "Upload for file '{}' failed: {}".format(filename, e)) response = { "files": [{ "name": filename, "error": "There was a problem uploading this file." }] } return jsonify(response), 200
def post(request_id): """ Create a new upload. Handles chunked files through the Content-Range header. For filesize validation and more upload logic, see: /static/js/upload/fileupload.js Optional request body parameters: - update (bool) save the uploaded file to the 'updated' directory (this indicates the file is meant to replace a previously uploaded file) - response_id (int) the id of a response associated with the file this upload is replacing - REQUIRED if 'update' is 'true' - ignored if 'update' is 'false' :returns: { "name": file name, "size": file size } """ files = request.files file_ = files[next(files.keys())] filename = secure_filename(file_.filename) is_update = eval_request_bool(request.form.get('update')) agency_ein = Requests.query.filter_by(id=request_id).one().agency.ein if is_allowed(user=current_user, request_id=request_id, permission=permission.ADD_FILE) or \ is_allowed(user=current_user, request_id=request_id, permission=permission.EDIT_FILE): response_id = request.form.get('response_id') if is_update else None if upload_exists(request_id, filename, response_id): response = { "files": [{ "name": filename, "error": "A file with this name has already " "been uploaded for this request." # TODO: "link": <link-to-existing-file> ? would be nice }] } else: upload_path = os.path.join( current_app.config['UPLOAD_QUARANTINE_DIRECTORY'], request_id) if not os.path.exists(upload_path): os.mkdir(upload_path) filepath = os.path.join(upload_path, filename) key = get_upload_key(request_id, filename, is_update) try: if CONTENT_RANGE_HEADER in request.headers: start, size = parse_content_range( request.headers[CONTENT_RANGE_HEADER]) # Only validate mime type on first chunk valid_file_type = True file_type = None if start == 0: valid_file_type, file_type = is_valid_file_type(file_) if current_user.is_agency_active(agency_ein): valid_file_type = True if os.path.exists(filepath): # remove existing file (upload 'restarted' for same file) os.remove(filepath) if valid_file_type: redis.set(key, upload_status.PROCESSING) with open(filepath, 'ab') as fp: fp.seek(start) fp.write(file_.stream.read()) # scan if last chunk written if os.path.getsize(filepath) == size: scan_and_complete_upload.delay(request_id, filepath, is_update, response_id) else: valid_file_type, file_type = is_valid_file_type(file_) if current_user.is_agency_active(agency_ein): valid_file_type = True if valid_file_type: redis.set(key, upload_status.PROCESSING) file_.save(filepath) scan_and_complete_upload.delay(request_id, filepath, is_update, response_id) if not valid_file_type: response = { "files": [{ "name": filename, "error": "The file type '{}' is not allowed.".format( file_type) }] } else: response = { "files": [{ "name": filename, "original_name": file_.filename, "size": os.path.getsize(filepath), }] } except Exception as e: sentry.captureException() redis.set(key, upload_status.ERROR) current_app.logger.exception("Upload for file '{}' failed: {}".format(filename, e)) response = { "files": [{ "name": filename, "error": "There was a problem uploading this file." }] } return jsonify(response), 200
def delete(r_id_type, r_id, filecode): """ Removes an uploaded file. :param r_id_type: "response" or "request" :param r_id: the Response or Request identifier :param filecode: the encoded name of the uploaded file (base64 without padding) Optional request body parameters: - quarantined_only (bool) only delete the file if it is quarantined (beware: takes precedence over 'updated_only') - updated_only (bool) only delete the file if it is in the 'updated' directory :returns: On success: { "deleted": filename } On failure: { "error": error message } """ filename = secure_filename(b64decode_lenient(filecode)) if r_id_type not in ["request", "response"]: response = {"error": "Invalid ID type."} else: try: if r_id_type == "response": response = Responses.query.filter_by(id=r_id, deleted=False) r_id = response.request_id path = '' quarantined_only = eval_request_bool( request.form.get('quarantined_only')) has_add_edit = (is_allowed(user=current_user, request_id=r_id, permission=permission.ADD_FILE) or is_allowed(user=current_user, request_id=r_id, permission=permission.EDIT_FILE)) if quarantined_only and has_add_edit: path = os.path.join( current_app.config['UPLOAD_QUARANTINE_DIRECTORY'], r_id) elif eval_request_bool(request.form.get('updated_only')) and \ is_allowed(user=current_user, request_id=r_id, permission=permission.EDIT_FILE): path = os.path.join(current_app.config['UPLOAD_DIRECTORY'], r_id, UPDATED_FILE_DIRNAME) else: path_for_status = { upload_status.PROCESSING: current_app.config['UPLOAD_QUARANTINE_DIRECTORY'], upload_status.SCANNING: current_app.config['UPLOAD_QUARANTINE_DIRECTORY'], upload_status.READY: current_app.config['UPLOAD_DIRECTORY'] } status = redis.get(get_upload_key(r_id, filename)) if status is not None: dest_path = path_for_status[status.decode("utf-8")] if (dest_path == current_app.config['UPLOAD_QUARANTINE_DIRECTORY'] and has_add_edit ) or (dest_path == current_app.config['UPLOAD_DIRECTORY'] and is_allowed(user=current_user, request_id=r_id, permission=permission.ADD_FILE)): path = os.path.join(dest_path, r_id) filepath = os.path.join(path, filename) found = False if path != '': if quarantined_only: if os.path.exists(filepath): os.remove(filepath) found = True else: if fu.exists(filepath): fu.remove(filepath) found = True if found: response = {"deleted": filename} else: response = {"error": "Upload not found."} except Exception as e: current_app.logger.exception( "Error on DELETE /upload/: {}".format(e)) response = {"error": "Failed to delete '{}'".format(filename)} return jsonify(response), 200
def update_agency_active_status(agency_ein, is_active): """ Update the active status of an agency. :param agency_ein: String identifier for agency (4 characters) :param is_active: Boolean value for agency active status (True = Active) :return: Boolean value (True if successfully changed active status) """ agency = Agencies.query.filter_by(ein=agency_ein).first() is_valid_agency = agency is not None activate_agency = eval_request_bool(is_active) if is_active is not None and is_valid_agency: update_object({'is_active': activate_agency}, Agencies, agency_ein) if activate_agency: create_object( Events(request_id=None, user_guid=current_user.guid, type_=AGENCY_ACTIVATED, previous_value={ "ein": agency_ein, "is_active": "False" }, new_value={ "ein": agency_ein, "is_active": "True" }, timestamp=datetime.utcnow())) # create request documents for request in agency.requests: request.es_create() return True else: create_object( Events(request_id=None, user_guid=current_user.guid, type_=AGENCY_DEACTIVATED, previous_value={ "ein": agency_ein, "is_active": "True" }, new_value={ "ein": agency_ein, "is_active": "False" }, timestamp=datetime.utcnow())) # remove requests from index for request in agency.requests: request.es_delete() # deactivate agency users for user in agency.active_users: update_object( { "is_agency_active": "False", "is_agency_admin": "False" }, AgencyUsers, (user.guid, agency_ein)) create_object( Events(request_id=None, user_guid=current_user.guid, type_=AGENCY_USER_DEACTIVATED, previous_value={ "user_guid": user.guid, "ein": agency_ein, "is_active": "True" }, new_value={ "user_guid": user.guid, "ein": agency_ein, "is_active": "False" }, timestamp=datetime.utcnow())) return True return False
def patch(user_id): """ Request Parameters: - title - organization - email - phone_number - fax_number - mailing_address - is_super - is_agency_active - is_agency_admin (Mailing Address) - zip - city - state - address_one - address_two Restrictions: - Anonymous Users - cannot access this endpoint - Agency Administrators - cannot change their agency status - can only update the agency status of users within their agency - cannot change any super user status - Super Users - cannot change their super user status - Agency Users - cannot change any user except for themselves or *anonymous* requesters for requests they are assigned to - cannot change super user or agency status - Public Users - can only update themselves - cannot change super user or agency status """ if not current_user.is_anonymous: # attempt to parse user_id and find user try: guid, auth_type = user_id.split(USER_ID_DELIMITER) user_ = Users.query.filter_by(guid=guid, auth_user_type=auth_type).one() except (ValueError, NoResultFound, MultipleResultsFound): return jsonify({}), 404 agency_ein = request.form.get('agency_ein', None) if agency_ein is None and not (auth_type == user_type_auth.ANONYMOUS_USER or 'is_super' in request.form): return jsonify({}), 404 updating_self = current_user == user_ current_user_is_agency_user = ( current_user.is_agency and not current_user.is_super and not current_user.is_agency_admin(agency_ein) and current_user.is_agency_active(agency_ein)) current_user_is_agency_admin = ( current_user.is_agency and not current_user.is_super and current_user.is_agency_admin(agency_ein) and current_user.is_agency_active(agency_ein)) same_agency = agency_ein in [ agency.ein for agency in current_user.agencies.all() ] associated_anonymous_requester = ( user_.is_anonymous_requester and current_user.user_requests.filter_by( request_id=user_.anonymous_request.id).first() is None) is_agency_admin = request.form.get('is_agency_admin') is_agency_active = request.form.get('is_agency_active') is_super = request.form.get('is_super') changing_status = any((is_agency_active, is_agency_admin, is_super)) rform_copy = dict(request.form) try: rform_copy.pop('is_agency_admin') rform_copy.pop('is_agency_active') rform_copy.pop('agency_ein') changing_more_than_agency_status = len(rform_copy) != 0 except KeyError: changing_more_than_agency_status = False # VALIDATE if (( updating_self and ( # super user attempting to change their own super status (current_user.is_super and is_super is not None) or # agency admin or public user attempting to change their own agency/super status (changing_status and (current_user_is_agency_admin or current_user.is_public)))) or (not updating_self and ( # public user attempting to change another user current_user.is_public or # agency user attempting to change a agency/super status (current_user_is_agency_user and changing_status) or # agency user attempting to change a user that is not an anonymous requester # for a request they are assigned to (current_user_is_agency_user and (not user_.is_anonymous_requester or not associated_anonymous_requester)) or # agency admin attempting to change another user that is not in the same agency or # attempting to change more than just the agency status of a user (current_user_is_agency_admin and not (associated_anonymous_requester or user_.is_anonymous_requester) and (not same_agency or changing_more_than_agency_status)) or # agency admin attempting to change an anonymous requester for a request # they are not assigned to (current_user_is_agency_admin and associated_anonymous_requester)))): return jsonify({}), 403 # UPDATE user_fields = [ 'email', 'phone_number', 'fax_number', 'title', 'organization' ] status_fields = ['is_agency_admin', 'is_agency_active', 'is_super'] address_fields = ['zip', 'city', 'state', 'address_one', 'address_two'] user_field_val = { 'email': request.form.get('email'), 'phone_number': request.form.get('phone'), 'fax_number': request.form.get('fax'), 'title': request.form.get('title'), 'organization': request.form.get('organization'), } status_field_val = { 'is_agency_admin': request.form.get('is_agency_admin'), 'is_agency_active': request.form.get('is_agency_active'), 'is_super': request.form.get('is_super') } address_field_val = { 'address_one': request.form.get('address_one'), 'address_two': request.form.get('address_two'), 'zip': request.form.get('zipcode'), 'city': request.form.get('city'), 'state': request.form.get('state') } # check if missing contact information if (user_field_val['email'] == '' and user_field_val['phone_number'] == '' and user_field_val['fax_number'] == '' and (address_field_val['city'] == '' or address_field_val['zip'] == '' or address_field_val['state'] == '' or address_field_val['address_one'] == '')): return jsonify({"error": "Missing contact information."}), 400 old = {} old_address = {} new = {} new_address = {} for field in status_fields: if status_field_val[field] is not None: if field == 'is_agency_admin': cur_val = user_.is_agency_admin(agency_ein) elif field == 'is_agency_active': cur_val = user_.is_agency_active(agency_ein) else: cur_val = getattr(user_, field) new_val = eval_request_bool(status_field_val[field]) if cur_val != new_val: old[field] = cur_val new[field] = new_val for field in user_fields: val = user_field_val[field] if val is not None: if val == '': user_field_val[ field] = None # null in db, not empty string cur_val = getattr(user_, field) new_val = user_field_val[field] if cur_val != new_val: old[field] = cur_val new[field] = new_val for field in address_fields: val = address_field_val[field] if val is not None: if val == '': address_field_val[field] = None cur_val = (user_.mailing_address.get(field) if user_.mailing_address else None) new_val = address_field_val[field] if cur_val != new_val: old_address[field] = cur_val new_address[field] = new_val if new or new_address: # in spite of not changing, the guid and auth type of # the user being updated is added to Events.new_value # in order to identify this user new['user_guid'] = user_.guid new['auth_user_type'] = user_.auth_user_type if new_address: new['mailing_address'] = new_address if old_address: old['mailing_address'] = old_address if ('is_agency_admin' in new) or ('is_agency_active' in new): new['agency_ein'] = agency_ein update_object(new, AgencyUsers, (guid, auth_type, agency_ein)) else: update_object(new, Users, (guid, auth_type)) # create event(s) event_kwargs = { 'request_id': user_.anonymous_request.id if user_.is_anonymous_requester else None, 'response_id': None, 'user_guid': current_user.guid, 'auth_user_type': current_user.auth_user_type, 'timestamp': datetime.utcnow() } if changing_status: new_statuses = {} old_statuses = {} for field in status_fields: if new.get(field) is not None: new_statuses[field] = new.pop(field) old_statuses[field] = old.pop(field) # TODO: a better way to store user identifiers (than in the value columns) new_statuses['user_guid'] = user_.guid new_statuses['auth_user_type'] = user_.auth_user_type new_statuses['agency_ein'] = agency_ein is_agency_active = new_statuses.get('is_agency_active') is_agency_admin = new_statuses.get('is_agency_admin') if is_agency_active is not None and not is_agency_active: # remove ALL UserRequests for user_request in user_.user_requests.all(): create_user_request_event(event_type.USER_REMOVED, user_request) delete_object(user_request) elif is_agency_admin is not None: def set_permissions_and_create_event(user_req, perms): """ Set permissions for a user request and create a 'user_permissions_changed' Event. :param user_req: user request :param perms: permissions to set for user request """ old_permissions = user_req.permissions user_request.set_permissions(perms) create_user_request_event(event_type.USER_PERM_CHANGED, user_req, old_permissions) if is_agency_admin: permissions = Roles.query.filter_by( name=role_name.AGENCY_ADMIN).one().permissions # create UserRequests for ALL existing requests under user's agency where user is not assigned # for where the user *is* assigned, only change the permissions for req in user_.agencies.filter_by( ein=agency_ein).one().requests: user_request = UserRequests.query.filter_by( request_id=req.id, user_guid=user_.guid, auth_user_type=user_.auth_user_type).first() if user_request is None: user_request = UserRequests( user_guid=user_.guid, auth_user_type=user_.auth_user_type, request_id=req.id, request_user_type=user_type_request.AGENCY, permissions=permissions) create_object(user_request) create_user_request_event( event_type.USER_ADDED, user_request) else: set_permissions_and_create_event( user_request, permissions) else: # update ALL UserRequests (strip user of permissions) for user_request in user_.user_requests.all(): set_permissions_and_create_event( user_request, permission.NONE) # TODO: single email detailing user changes? create_object( Events(type_=event_type.USER_STATUS_CHANGED, previous_value=old_statuses, new_value=new_statuses, **event_kwargs)) if old: # something besides status changed ('new' holds user guid and auth type) create_object( Events(type_=(event_type.REQUESTER_INFO_EDITED if user_.is_anonymous_requester else event_type.USER_INFO_EDITED), previous_value=old, new_value=new, **event_kwargs)) return jsonify({}), 200 else: return jsonify({"message": "No changes detected."}), 200 return jsonify({}), 403
def requests(): """ For request parameters, see app.search.utils.search_requests All Users can search by: - FOIL ID Anonymous Users can search by: - Title (public only) - Agency Description (public only) Public Users can search by: - Title (public only OR public and private if user is requester) - Agency Description (public only) - Description (if user is requester) Agency Users can search by: - Title - Agency Description - Description - Requester Name All Users can filter by: - Status, Open (anything not Closed if not agency user) - Status, Closed - Date Submitted - Agency Only Agency Users can filter by: - Status, In Progress - Status, Due Soon - Status, Overdue - Date Due """ try: agency_ein = request.args.get('agency_ein', '') except ValueError: agency_ein = None try: size = int(request.args.get('size', DEFAULT_HITS_SIZE)) except ValueError: size = DEFAULT_HITS_SIZE try: start = int(request.args.get('start'), 0) except ValueError: start = 0 query = request.args.get('query') # Determine if searching for FOIL ID foil_id = eval_request_bool(request.args.get('foil_id')) or re.match( r'^(FOIL-|foil-|)\d{4}-\d{3}-\d{5}$', query) results = search_requests( query, foil_id, eval_request_bool(request.args.get('title')), eval_request_bool(request.args.get('agency_request_summary')), eval_request_bool(request.args.get('description')) if not current_user.is_anonymous else False, eval_request_bool(request.args.get('requester_name')) if current_user.is_agency else False, request.args.get('date_rec_from'), request.args.get('date_rec_to'), request.args.get('date_due_from'), request.args.get('date_due_to'), request.args.get('date_closed_from'), request.args.get('date_closed_to'), agency_ein, eval_request_bool(request.args.get('open')), eval_request_bool(request.args.get('closed')), eval_request_bool(request.args.get('in_progress')) if current_user.is_agency else False, eval_request_bool(request.args.get('due_soon')) if current_user.is_agency else False, eval_request_bool(request.args.get('overdue')) if current_user.is_agency else False, size, start, request.args.get('sort_date_submitted'), request.args.get('sort_date_due'), request.args.get('sort_title'), request.args.get('tz_name') # eval_request_bool(request.args.get('by_phrase')), # eval_request_bool(request.args.get('highlight')), ) # format results total = results["hits"]["total"] formatted_results = None if total != 0: convert_dates(results) formatted_results = render_template("request/result_row.html", requests=results["hits"]["hits"]) # query=query) # only for testing return jsonify({ "count": len(results["hits"]["hits"]), "total": total, "results": formatted_results }), 200
def requests(): """ For request parameters, see app.search.utils.search_requests All Users can search by: - FOIL ID Anonymous Users can search by: - Title (public only) - Agency Request Summary (public only) Public Users can search by: - Title (public only OR public and private if user is requester) - Agency Request Summary (public only) - Description (if user is requester) Agency Users can search by: - Title - Agency Request Summary - Description - Requester Name All Users can filter by: - Status, Open (anything not Closed if not agency user) - Status, Closed - Date Submitted - Agency Only Agency Users can filter by: - Status, In Progress - Status, Due Soon - Status, Overdue - Date Due """ try: agency_ein = request.args.get("agency_ein", "") except ValueError: sentry.captureException() agency_ein = None try: size = int(request.args.get("size", DEFAULT_HITS_SIZE)) except ValueError: sentry.captureException() size = DEFAULT_HITS_SIZE try: start = int(request.args.get("start"), 0) except ValueError: sentry.captureException() start = 0 query = request.args.get("query") # Determine if searching for FOIL ID foil_id = eval_request_bool(request.args.get("foil_id")) or re.match( r"^(FOIL-|foil-|)\d{4}-\d{3}-\d{5}$", query ) results = search_requests( query=query, foil_id=foil_id, title=eval_request_bool(request.args.get("title")), agency_request_summary=eval_request_bool( request.args.get("agency_request_summary") ), description=eval_request_bool(request.args.get("description")) if not current_user.is_anonymous else False, requester_name=eval_request_bool(request.args.get("requester_name")) if current_user.is_agency else False, date_rec_from=request.args.get("date_rec_from"), date_rec_to=request.args.get("date_rec_to"), date_due_from=request.args.get("date_due_from"), date_due_to=request.args.get("date_due_to"), date_closed_from=request.args.get("date_closed_from"), date_closed_to=request.args.get("date_closed_to"), agency_ein=agency_ein, agency_user_guid=request.args.get("agency_user"), request_type=request.args.get("request_type"), open_=eval_request_bool(request.args.get("open")), closed=eval_request_bool(request.args.get("closed")), in_progress=eval_request_bool(request.args.get("in_progress")) if current_user.is_agency else False, due_soon=eval_request_bool(request.args.get("due_soon")) if current_user.is_agency else False, overdue=eval_request_bool(request.args.get("overdue")) if current_user.is_agency else False, size=size, start=start, sort_date_received=request.args.get("sort_date_submitted"), sort_date_due=request.args.get("sort_date_due"), sort_title=request.args.get("sort_title"), tz_name=request.args.get("tz_name", current_app.config["APP_TIMEZONE"]), ) # format results total = results["hits"]["total"] formatted_results = None if total != 0: convert_dates(results) formatted_results = render_template( "request/result_row.html", requests=results["hits"]["hits"], today=datetime.utcnow(), ) # query=query) # only for testing return ( jsonify( { "count": len(results["hits"]["hits"]), "total": total, "results": formatted_results, } ), 200, )
def get_request_responses(): """ Returns a set of responses (id, type, and template), ordered by date descending, and starting from a specified index. Request parameters: - start: (int) starting index - request_id: FOIL request id - with_template: (default: False) include html (rows and modals) for each response """ start = int(flask_request.args['start']) current_request = Requests.query.filter_by( id=flask_request.args['request_id']).one() if current_user in current_request.agency_users: # If the user is an agency user assigned to the request, all responses can be retrieved. responses = Responses.query.filter( Responses.request_id == current_request.id, ~Responses.id.in_( [cm.method_id for cm in CommunicationMethods.query.all()]), Responses.type != response_type.EMAIL, Responses.deleted == False).order_by(desc( Responses.date_modified)).all()[start:start + RESPONSES_INCREMENT] elif current_user == current_request.requester: # If the user is the requester, then only responses that are "Release and Private" or "Release and Public" # can be retrieved. responses = Responses.query.filter( Responses.request_id == current_request.id, ~Responses.id.in_( [cm.method_id for cm in CommunicationMethods.query.all()]), Responses.type != response_type.EMAIL, Responses.deleted == False, Responses.privacy.in_([ response_privacy.RELEASE_AND_PRIVATE, response_privacy.RELEASE_AND_PUBLIC ])).order_by(desc( Responses.date_modified)).all()[start:start + RESPONSES_INCREMENT] else: # If the user is not an agency user assigned to the request or the requester, then only responses that are # "Release and Public" whose release date is not in the future can be retrieved. responses = Responses.query.filter( Responses.request_id == current_request.id, ~Responses.id.in_( [cm.method_id for cm in CommunicationMethods.query.all()]), Responses.type != response_type.EMAIL, Responses.deleted == False, Responses.privacy.in_([response_privacy.RELEASE_AND_PUBLIC]), Responses.release_date.isnot(None), Responses.release_date < datetime.utcnow()).order_by( desc(Responses.date_modified)).all()[start:start + RESPONSES_INCREMENT] template_path = 'request/responses/' response_jsons = [] row_count = 0 for response in responses: json = {'id': response.id, 'type': response.type} if eval_request_bool(flask_request.args.get('with_template')): row_count += 1 row = render_template( template_path + 'row.html', response=response, row_num=start + row_count, response_type=response_type, determination_type=determination_type, show_preview=not ( response.type == response_type.DETERMINATION and (response.dtype == determination_type.ACKNOWLEDGMENT or response.dtype == determination_type.REOPENING))) modal = render_template( template_path + 'modal.html', response=response, requires_workflow=response.type in response_type.EMAIL_WORKFLOW_TYPES, modal_body=render_template( "{}modal_body/{}.html".format(template_path, response.type), response=response, privacies=[ response_privacy.RELEASE_AND_PUBLIC, response_privacy.RELEASE_AND_PRIVATE, response_privacy.PRIVATE ], determination_type=determination_type, request_status=request_status, edit_response_privacy_permission=is_allowed( user=current_user, request_id=response.request_id, permission=get_permission( permission_type='privacy', response_type=type(response))), edit_response_permission=is_allowed( user=current_user, request_id=response.request_id, permission=get_permission( permission_type='edit', response_type=type(response))), delete_response_permission=is_allowed( user=current_user, request_id=response.request_id, permission=get_permission( permission_type='delete', response_type=type(response))), is_editable=response.is_editable, current_request=current_request), response_type=response_type, determination_type=determination_type, request_status=request_status, edit_response_permission=is_allowed( user=current_user, request_id=response.request_id, permission=get_permission(permission_type='edit', response_type=type(response))), delete_response_permission=is_allowed( user=current_user, request_id=response.request_id, permission=get_permission(permission_type='delete', response_type=type(response))), edit_response_privacy_permission=is_allowed( user=current_user, request_id=response.request_id, permission=get_permission(permission_type='privacy', response_type=type(response))), is_editable=response.is_editable, current_request=current_request) json['template'] = row + modal response_jsons.append(json) return jsonify(responses=response_jsons)
def requests_doc(doc_type): """ Converts and sends the a search result-set as a file of the specified document type. - Filtering on set size is ignored; all results are returned. - Currently only supports CSVs. - CSV only includes requests belonging to that user's agency Document name format: "FOIL_requests_results_<timestamp:MM_DD_YYYY_at_HH_mm_pp>" Request parameters are identical to those of /search/requests. :param doc_type: document type ('csv' only) """ if current_user.is_agency and doc_type.lower() == 'csv': try: agency_ein = request.args.get('agency_ein', '') except ValueError: sentry.captureException() agency_ein = None tz_name = request.args.get('tz_name', current_app.config['APP_TIMEZONE']) start = 0 buffer = StringIO() # csvwriter cannot accept BytesIO writer = csv.writer(buffer) writer.writerow(["FOIL ID", "Agency", "Title", "Description", "Agency Request Summary", "Current Status", "Date Created", "Date Received", "Date Due", "Date Closed", "Requester Name", "Requester Email", "Requester Title", "Requester Organization", "Requester Phone Number", "Requester Fax Number", "Requester Address 1", "Requester Address 2", "Requester City", "Requester State", "Requester Zipcode", "Assigned User Emails"]) results = search_requests( query=request.args.get('query'), foil_id=eval_request_bool(request.args.get('foil_id')), title=eval_request_bool(request.args.get('title')), agency_request_summary=eval_request_bool(request.args.get('agency_request_summary')), description=eval_request_bool(request.args.get('description')) if not current_user.is_anonymous else False, requester_name=eval_request_bool(request.args.get('requester_name')) if current_user.is_agency else False, date_rec_from=request.args.get('date_rec_from'), date_rec_to=request.args.get('date_rec_to'), date_due_from=request.args.get('date_due_from'), date_due_to=request.args.get('date_due_to'), date_closed_from=request.args.get('date_closed_from'), date_closed_to=request.args.get('date_closed_to'), agency_ein=agency_ein, agency_user_guid=request.args.get('agency_user'), open_=eval_request_bool(request.args.get('open')), closed=eval_request_bool(request.args.get('closed')), in_progress=eval_request_bool(request.args.get('in_progress')) if current_user.is_agency else False, due_soon=eval_request_bool(request.args.get('due_soon')) if current_user.is_agency else False, overdue=eval_request_bool(request.args.get('overdue')) if current_user.is_agency else False, start=start, sort_date_received=request.args.get('sort_date_submitted'), sort_date_due=request.args.get('sort_date_due'), sort_title=request.args.get('sort_title'), tz_name=request.args.get('tz_name', current_app.config['APP_TIMEZONE']), for_csv=True ) ids = [result["_id"] for result in results] all_requests = Requests.query.filter(Requests.id.in_(ids)).options( joinedload(Requests.agency_users)).options(joinedload(Requests.requester)).options( joinedload(Requests.agency)).all() user_agencies = current_user.get_agencies for req in all_requests: if req.agency_ein in user_agencies: writer.writerow([ req.id, req.agency.name, req.title, req.description, req.agency_request_summary, req.status, req.date_created, req.date_submitted, req.due_date, req.date_closed, req.requester.name, req.requester.email, req.requester.title, req.requester.organization, req.requester.phone_number, req.requester.fax_number, req.requester.mailing_address.get('address_one'), req.requester.mailing_address.get('address_two'), req.requester.mailing_address.get('city'), req.requester.mailing_address.get('state'), req.requester.mailing_address.get('zip'), ", ".join(u.email for u in req.agency_users)]) dt = datetime.utcnow() timestamp = utc_to_local(dt, tz_name) if tz_name is not None else dt return send_file( BytesIO(buffer.getvalue().encode('UTF-8')), # convert to bytes attachment_filename="FOIL_requests_results_{}.csv".format( timestamp.strftime("%m_%d_%Y_at_%I_%M_%p")), as_attachment=True ) return '', 400
def requests(): """ For request parameters, see app.search.utils.search_requests All Users can search by: - FOIL ID Anonymous Users can search by: - Title (public only) - Agency Request Summary (public only) Public Users can search by: - Title (public only OR public and private if user is requester) - Agency Request Summary (public only) - Description (if user is requester) Agency Users can search by: - Title - Agency Request Summary - Description - Requester Name All Users can filter by: - Status, Open (anything not Closed if not agency user) - Status, Closed - Date Submitted - Agency Only Agency Users can filter by: - Status, In Progress - Status, Due Soon - Status, Overdue - Date Due """ try: agency_ein = request.args.get('agency_ein', '') except ValueError: sentry.captureException() agency_ein = None try: size = int(request.args.get('size', DEFAULT_HITS_SIZE)) except ValueError: sentry.captureException() size = DEFAULT_HITS_SIZE try: start = int(request.args.get('start'), 0) except ValueError: sentry.captureException() start = 0 query = request.args.get('query') # Determine if searching for FOIL ID foil_id = eval_request_bool(request.args.get('foil_id')) or re.match(r'^(FOIL-|foil-|)\d{4}-\d{3}-\d{5}$', query) results = search_requests( query=query, foil_id=foil_id, title=eval_request_bool(request.args.get('title')), agency_request_summary=eval_request_bool(request.args.get('agency_request_summary')), description=eval_request_bool(request.args.get('description')) if not current_user.is_anonymous else False, requester_name=eval_request_bool(request.args.get('requester_name')) if current_user.is_agency else False, date_rec_from=request.args.get('date_rec_from'), date_rec_to=request.args.get('date_rec_to'), date_due_from=request.args.get('date_due_from'), date_due_to=request.args.get('date_due_to'), date_closed_from=request.args.get('date_closed_from'), date_closed_to=request.args.get('date_closed_to'), agency_ein=agency_ein, agency_user_guid=request.args.get('agency_user'), open_=eval_request_bool(request.args.get('open')), closed=eval_request_bool(request.args.get('closed')), in_progress=eval_request_bool(request.args.get('in_progress')) if current_user.is_agency else False, due_soon=eval_request_bool(request.args.get('due_soon')) if current_user.is_agency else False, overdue=eval_request_bool(request.args.get('overdue')) if current_user.is_agency else False, size=size, start=start, sort_date_received=request.args.get('sort_date_submitted'), sort_date_due=request.args.get('sort_date_due'), sort_title=request.args.get('sort_title'), tz_name=request.args.get('tz_name', current_app.config['APP_TIMEZONE']) ) # format results total = results["hits"]["total"] formatted_results = None if total != 0: convert_dates(results) formatted_results = render_template("request/result_row.html", requests=results["hits"]["hits"]) # query=query) # only for testing return jsonify({ "count": len(results["hits"]["hits"]), "total": total, "results": formatted_results }), 200
def patch(user_id): """ Request Parameters: - title - organization - email - phone_number - fax_number - mailing_address - is_super - is_agency_active - is_agency_admin (Mailing Address) - zip - city - state - address_one - address_two Restrictions: - Anonymous Users - cannot access this endpoint - Agency Administrators - cannot change their agency status - can only update the agency status of users within their agency - cannot change any super user status - Super Users - cannot change their super user status - Agency Users - cannot change any user except for themselves or *anonymous* requesters for requests they are assigned to - cannot change super user or agency status - Public Users - can only update themselves - cannot change super user or agency status """ # Anonymous users cannot access endpoint if current_user.is_anonymous: return jsonify( {'error': 'Anonymous users cannot access this endpoint'}), 403 # Public users cannot access endpoint if current_user.is_public: return jsonify({'error': 'Public users cannot access this endpoint'}), 403 # Unauthenticated users cannot access endpoint if not current_user.is_authenticated: return jsonify( {'error': 'User must be authenticated to access endpoint'}), 403 # Retrieve the user user_ = Users.query.filter_by(guid=user_id).one_or_none() # If the user does not exist, return 404 - Not Found if not user_: return jsonify({'error': 'Specified user does not exist.'}), 404 # Gather Form details is_agency_admin = eval_request_bool( request.form.get('is_agency_admin')) if request.form.get( 'is_agency_admin', None) else None is_agency_active = eval_request_bool( request.form.get('is_agency_active')) if request.form.get( 'is_agency_active', None) else None is_super = eval_request_bool( request.form.get('is_super')) if request.form.get('is_super', None) else None agency_ein = request.form.get('agency_ein', None) # Checks that apply if user is changing their own profile changing_self = current_user == user_ # Agency User Restrictions (applies to Admins and Regular Users) if user_.is_agency: # Endpoint can only be used for a specific agency if not agency_ein: return jsonify({ 'error': 'agency_ein must be provided to modify an agency user' }), 400 # Agency must exist and be active to modify users agency = Agencies.query.filter_by(ein=agency_ein).one_or_none() if not agency and agency.is_active: return jsonify( {'error': 'Agency must exist in the database and be active'}), 400 if not current_user.is_super: # Current user must belong to agency specified by agency_ein current_user_is_agency_admin = current_user.is_agency_admin( agency.ein) if not current_user_is_agency_admin: return jsonify({ 'error': 'Current user must belong to agency specified by agency_ein' }), 400 user_in_agency = AgencyUsers.query.filter( AgencyUsers.user_guid == user_.guid, AgencyUsers.agency_ein == agency_ein).one_or_none() if user_in_agency is None: return jsonify({ 'error': 'User to be modified must belong to agency specified by agency_ein' }), 400 # Non-Agency Admins cannot access endpoint to modify other agency_users if not current_user.is_super and not current_user.is_agency_admin( agency_ein): return jsonify( {'error': 'User must be agency admin to modify users'}), 403 # Cannot modify super status when changing agency active or agency admin status if (is_agency_admin or is_agency_active) and is_super: return jsonify({ 'error': 'Cannot modify super status when changing agency active or agency admin status' }), 400 if changing_self: # Super users cannot change their own is_super value if current_user.is_super and is_super: return jsonify({ 'error': 'Super users cannot change their own `super` status' }), 400 if is_agency_admin: return jsonify({ 'error': 'Agency Administrators cannot change their administrator permissions' }), 400 elif user_.is_public: if current_user != user_: return jsonify({ 'error': 'Public user attributes cannot be modified by agency users.' }), 400 elif user_.is_anonymous_requester: ur = current_user.user_requests.filter_by( request_id=user_.anonymous_request.id).one_or_none() if not ur: return jsonify({ 'error': 'Agency users can only modify anonymous requesters for requests where they are assigned.' }), 403 if not ur.has_permission(permission.EDIT_REQUESTER_INFO): return jsonify({ 'error': 'Current user does not have EDIT_REQUESTER_INFO permission' }), 403 # Gather User Fields user_editable_fields = user_attrs.UserEditableFieldsDict( email=request.form.get('email'), phone_number=request.form.get('phone'), fax_number=request.form.get('fax'), title=request.form.get('title'), organization=request.form.get('organization'), address_one=request.form.get('address_one'), address_two=request.form.get('address_two'), zip=request.form.get('zipcode'), city=request.form.get('city'), state=request.form.get('state')) status_field_val = user_attrs.UserStatusDict( is_agency_admin=request.form.get('is_agency_admin'), is_agency_active=request.form.get('is_agency_active'), is_super=request.form.get('is_super')) if changing_self: if not user_editable_fields.is_valid: return jsonify({"error": "Missing contact information."}), 400 else: if user_.is_agency and not status_field_val.is_valid: return jsonify({"error": "User status values invalid"}), 400 # Status Values for Events old_status = {} new_status = {} # User Attributes for Events old_user_attrs = defaultdict(dict) new_user_attrs = defaultdict(dict) for key, value in status_field_val.items(): if value is not None: if key == 'is_agency_admin': cur_val = user_.is_agency_admin(agency_ein) elif key == 'is_agency_active': cur_val = user_.is_agency_active(agency_ein) else: cur_val = getattr(user_, key) new_val = eval_request_bool(status_field_val[key]) if cur_val != new_val: old_status[key] = cur_val new_status[key] = new_val for key, value in user_editable_fields.items(): # Address is a dictionary and needs to be handled separately if key == 'address': for address_key, address_value in value.items(): cur_val = (user_.mailing_address.get(address_key) if user_.mailing_address else None) new_val = address_value if cur_val != new_val: old_user_attrs['_mailing_address'][address_key] = cur_val new_user_attrs['_mailing_address'][address_key] = new_val continue if value is not None: cur_val = getattr(user_, key) new_val = user_editable_fields[key] if cur_val != new_val: old_user_attrs[key] = cur_val new_user_attrs[key] = new_val # Update the Users object if new_user_attrs is not None (empty dict) if new_user_attrs and old_user_attrs: update_object(new_user_attrs, Users, user_id) # GUID is added to the 'new' value in events to identify the user that was changed new_user_attrs['user_guid'] = user_.guid # create event(s) event_kwargs = { 'request_id': user_.anonymous_request.id if user_.is_anonymous_requester else None, 'response_id': None, 'user_guid': current_user.guid, 'timestamp': datetime.utcnow() } create_object( Events(type_=(event_type.REQUESTER_INFO_EDITED if user_.is_anonymous_requester else event_type.USER_INFO_EDITED), previous_value=old_user_attrs, new_value=new_user_attrs, **event_kwargs)) if new_status: redis_key = "{current_user_guid}-{update_user_guid}-{agency_ein}-{timestamp}".format( current_user_guid=current_user.guid, update_user_guid=user_.guid, agency_ein=agency_ein, timestamp=datetime.now()) old_status['user_guid'] = user_.guid new_status['user_guid'] = user_.guid old_status['agency_ein'] = agency_ein new_status['agency_ein'] = agency_ein # Update agency active status and create associated event. Occurs first because a user can be # activated / deactivated with admin status set to True. if is_agency_active is not None: update_object(new_status, AgencyUsers, (user_.guid, agency_ein)) event_kwargs = { 'request_id': user_.anonymous_request.id if user_.is_anonymous_requester else None, 'response_id': None, 'user_guid': current_user.guid, 'timestamp': datetime.utcnow() } if is_agency_active: create_object( Events(type_=event_type.AGENCY_USER_ACTIVATED, previous_value=old_status, new_value=new_status, **event_kwargs)) if is_agency_admin is not None and is_agency_admin: make_user_admin.apply_async(args=(user_.guid, current_user.guid, agency_ein), task_id=redis_key) return jsonify({ 'status': 'success', 'message': 'Update task has been scheduled.' }), 200 else: create_object( Events(type_=event_type.AGENCY_USER_DEACTIVATED, previous_value=old_status, new_value=new_status, **event_kwargs)) remove_user_permissions.apply_async( args=(user_.guid, current_user.guid, agency_ein, event_type.AGENCY_USER_DEACTIVATED), task_id=redis_key) return jsonify({ 'status': 'success', 'message': 'Update task has been scheduled.' }), 200 return jsonify({ 'status': 'success', 'message': 'Agency user successfully updated' }), 200 # Update agency admin status and create associated event. elif is_agency_admin is not None: new_status['agency_ein'] = agency_ein update_object(new_status, AgencyUsers, (user_.guid, agency_ein)) event_kwargs = { 'request_id': user_.anonymous_request.id if user_.is_anonymous_requester else None, 'response_id': None, 'user_guid': current_user.guid, 'timestamp': datetime.utcnow() } if is_agency_admin: create_object( Events(type_=event_type.USER_MADE_AGENCY_ADMIN, previous_value=old_status, new_value=new_status, **event_kwargs)) make_user_admin.apply_async(args=(user_.guid, current_user.guid, agency_ein), task_id=redis_key) return jsonify({ 'status': 'success', 'message': 'Update task has been scheduled.' }), 200 else: create_object( Events(type_=event_type.USER_MADE_AGENCY_USER, previous_value=old_status, new_value=new_status, **event_kwargs)) remove_user_permissions.apply_async( args=(user_.guid, current_user.guid, agency_ein, event_type.USER_MADE_AGENCY_USER), task_id=redis_key) return jsonify({ 'status': 'success', 'message': 'Update task has been scheduled.' }), 200 # Update user super status and create associated event. elif is_super is not None: new_status['agency_ein'] = agency_ein update_object(new_status, AgencyUsers, (user_.guid, agency_ein)) event_kwargs = { 'request_id': user_.anonymous_request.id if user_.is_anonymous_requester else None, 'response_id': None, 'user_guid': current_user.guid, 'timestamp': datetime.utcnow() } if is_super: create_object( Events(type_=event_type.USER_MADE_SUPER_USER, previous_value=old_status, new_value=new_status, **event_kwargs)) else: create_object( Events(type_=event_type.USER_REMOVED_FROM_SUPER, previous_value=old_status, new_value=new_status, **event_kwargs)) return jsonify({ 'status': 'success', 'message': 'Agency user successfully updated' }), 200 # Always returns 200 so that we can use the data from the response in the client side javascript return jsonify({ 'status': 'Not Modified', 'message': 'No changes detected' }), 200