From 522ee48d1e50657d8e9e2364cd2e327ab0b2bd1f Mon Sep 17 00:00:00 2001 From: Jonathan Yu Date: Sat, 15 Oct 2016 14:57:04 -0400 Subject: [PATCH] merged develop into OP-841 and resolved conflicts, replaced visibility with privacy in confirmation view function, updated confirmation.html page # Conflicts: # app/lib/email_utils.py # app/models.py # app/request/utils.py --- .gitignore | 3 +- app/__init__.py | 9 +- app/{constants.py => constants/__init__.py} | 64 +--- app/constants/event_type.py | 25 ++ app/constants/permission.py | 37 +++ app/constants/response_type.py | 10 + app/constants/role_name.py | 6 + app/constants/user_type.py | 3 + app/db_utils.py | 16 +- app/lib/email_utils.py | 6 +- app/lib/file_utils.py | 30 ++ app/lib/utils.py | 18 +- app/models.py | 182 +++++------ app/request/api/views.py | 27 +- app/request/utils.py | 20 +- app/request/views.py | 8 +- app/response/utils.py | 213 +++++++++++-- app/response/views.py | 53 ++- app/responses/__init__.py | 5 - app/responses/utils.py | 154 --------- app/responses/views.py | 65 ---- app/static/styles/request_info.css | 7 +- .../email_templates/email_confirmation.html | 3 +- .../email_templates/email_file_upload.html | 9 + .../_view_request_edit_visibility.html | 8 +- app/templates/request/_view_request_info.html | 20 +- .../_view_request_info_x-editable.html | 2 +- app/templates/request/confirmation.html | 13 +- app/templates/request/fileupload.js.html | 23 +- app/templates/request/view_note.html | 2 +- app/templates/request/view_request.html | 14 +- app/templates/upload/uploads.html | 4 +- .../{constants.py => constants/__init__.py} | 9 - app/upload/constants/upload_status.py | 4 + app/upload/utils.py | 54 +++- app/upload/views.py | 149 +++++---- celery_worker.py | 4 +- config.py | 2 + lib.py | 8 - manage.py | 68 +++- migrations/env.py | 1 + ...f4e_added_agency_description_column_to_.py | 26 -- .../fa0cc904ac83_initial_migration.py | 14 +- requirements.txt | 4 + tests/base.py | 39 +++ tests/test_basics.py | 22 -- tests/tools.py | 82 +++++ tests/upload.py | 301 ++++++++++++++++++ 48 files changed, 1205 insertions(+), 641 deletions(-) rename app/{constants.py => constants/__init__.py} (55%) create mode 100644 app/constants/event_type.py create mode 100644 app/constants/permission.py create mode 100644 app/constants/response_type.py create mode 100644 app/constants/role_name.py create mode 100644 app/constants/user_type.py create mode 100644 app/lib/file_utils.py delete mode 100644 app/responses/__init__.py delete mode 100644 app/responses/utils.py delete mode 100644 app/responses/views.py rename app/upload/{constants.py => constants/__init__.py} (87%) create mode 100644 app/upload/constants/upload_status.py delete mode 100644 lib.py delete mode 100644 migrations/versions/3d3b2c178f4e_added_agency_description_column_to_.py create mode 100644 tests/base.py delete mode 100644 tests/test_basics.py create mode 100644 tests/tools.py create mode 100644 tests/upload.py diff --git a/.gitignore b/.gitignore index 8646c2156..72c0c92d2 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,8 @@ saml/ redis-stable/ python-sudo.sh quarantine/ - +.vagrant/ +tmp/ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/app/__init__.py b/app/__init__.py index 9349f38cf..9b3371795 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,18 +1,15 @@ import redis from business_calendar import Calendar, MO, TU, WE, TH, FR +from celery import Celery from flask import Flask from flask_bootstrap import Bootstrap from flask_kvsession import KVSessionExtension from flask_login import LoginManager from flask_mail import Mail from flask_recaptcha import ReCaptcha -from business_calendar import Calendar, MO, TU, WE, TH, FR from flask_sqlalchemy import SQLAlchemy from simplekv.decorator import PrefixDecorator from simplekv.memory.redisstore import RedisStore -from celery import Celery - - from config import config, Config @@ -27,7 +24,6 @@ upload_redis = redis.StrictRedis(db=2) mail = Mail() -app = Flask(__name__) calendar = Calendar( workdays=[MO, TU, WE, TH, FR], @@ -55,6 +51,8 @@ def create_app(config_name): :return: Flask application """ + + app = Flask(__name__) app.config.from_object(config[config_name]) config[config_name].init_app(app) @@ -67,7 +65,6 @@ def create_app(config_name): with app.app_context(): from app.models import Anonymous - login_manager.login_view = 'auth.login' login_manager.anonymous_user = Anonymous KVSessionExtension(prefixed_store, app) diff --git a/app/constants.py b/app/constants/__init__.py similarity index 55% rename from app/constants.py rename to app/constants/__init__.py index e3c1a2d80..7f0b161f2 100644 --- a/app/constants.py +++ b/app/constants/__init__.py @@ -1,52 +1,5 @@ -from app.lib.utils import mapping - ACKNOWLEDGEMENT_DAYS_DUE = 5 -ROLE_NAME = mapping( - ANONYMOUS='Anonymous User', - PUBLIC_NON_REQUESTER='Public User - None Requester', - PUBLIC_REQUESTER='Public User - Requester', - AGENCY_HELPER='Agency Helper', - AGENCY_OFFICER='Agency FOIL Officer', - AGENCY_ADMIN='Agency Administrator' -) - -# TODO: apply the above mapping convention where appropriate - -EVENT_TYPE = { - "user_added": "user_added", - "user_permissions_changed": "user_permissions_changed", - "user_information_edited": "user_information_edited", - "request_created": "request_created", - "request_acknowledged": "request_acknowledged", - "request_status_changed": "request_status_changed", - "request_extended": "request_extended", - "request_closed": "request_closed", - "request_title_edited": "request_title_edited", - "request_agency_description_edited": "request_agency_description_edited", - "request_title_privacy_edited": "request_title_privacy_edited", - "request_agency_description_privacy_edited": "request_agency_description_privacy_edited", - "email_notification_sent": "email_notification_sent", - "file_added": "file_added", - "file_edited": "file_edited", - "file_removed": "file_removed", - "link_added": "link_added", - "link_edited": "link_edited", - "link_removed": "link_removed", - "instructions_added": "instructions_added", - "instructions_edited": "instructions_edited", - "instructions_removed": "instructions_removed", - "note_added": "note_added", - "note_edited": "note_edited", - "note_deleted": "note_deleted", -} - -USER_TYPE = { - "anonymous_user": "anonymous_user", - "agency_user": "agency_user", - "public_user": "public_user" -} - CATEGORIES = [ ('', ''), ('Business', 'Business'), @@ -138,24 +91,11 @@ PUBLIC_USER_MICROSOFT = 'MSLiveSSO' ANONYMOUS_USER = 'AnonymousUser' -PUBLIC_USER = [ +PUBLIC_USER = frozenset(( PUBLIC_USER_NYC_ID, PUBLIC_USER_FACEBOOK, PUBLIC_USER_LINKEDIN, PUBLIC_USER_GOOGLE, PUBLIC_USER_YAHOO, PUBLIC_USER_MICROSOFT -] - -RESPONSE_TYPE = { - "note": "note", - "record types": "record types", - "file": "file", - "link": "link", - "offline instructions": "offline instructions", - "email": "email", - "sms": "sms", - "push": "push", - "extension": "extension", - "status": "status" -} +)) diff --git a/app/constants/event_type.py b/app/constants/event_type.py new file mode 100644 index 000000000..d4c2eaac9 --- /dev/null +++ b/app/constants/event_type.py @@ -0,0 +1,25 @@ +USER_ADDED = "user_added" +USER_PERM_CHANGED = "user_permissions_changed" +USER_INFO_EDITED = "user_information_edited" +REQ_CREATED = "request_created" +REQ_ACKNOWLEDGED = "request_acknowledged" +REQ_STATUS_CHANGED = "request_status_changed" +REQ_EXTENDED = "request_extended" +REQ_CLOSED = "request_closed" +REQ_TITLE_EDITED = "request_title_edited" +REQ_AGENCY_DESC_EDITED = "request_agency_description_edited" +REQ_TITLE_PRIVACY_EDITED = "request_title_privacy_edited" +REQ_AGENCY_DESC_PRIVACY_EDITED = "request_agency_description_privacy_edited" +EMAIL_NOTIFICATION_SENT = "email_notification_sent" +FILE_ADDED = "file_added" +FILE_EDITED = "file_edited" +FILE_REMOVED = "file_removed" +LINK_ADDED = "link_added" +LINK_EDITED = "link_edited" +LINK_REMOVED = "link_removed" +INSTRUCTIONS_ADDED = "instructions_added" +INSTRUCTIONS_EDITED = "instructions_edited" +INSTRUCTIONS_REMOVED = "instructions_removed" +NOTE_ADDED = "note_added" +NOTE_EDITED = "note_edited" +NOTE_DELETED = "note_deleted" diff --git a/app/constants/permission.py b/app/constants/permission.py new file mode 100644 index 000000000..edb184e95 --- /dev/null +++ b/app/constants/permission.py @@ -0,0 +1,37 @@ +# Duplicate Request (New Request based on same criteria) +DUPLICATE_REQUEST = 0x00001 +# View detailed request status (Open, In Progress, Closed) +VIEW_REQUEST_STATUS_PUBLIC = 0x00002 +# View detailed request status (Open, In Progress, Due Soon, Overdue, Closed) +VIEW_REQUEST_STATUS_ALL = 0x00004 +# View all public request information +VIEW_REQUEST_INFO_PUBLIC = 0x00008 +# View all request information +VIEW_REQUEST_INFO_ALL = 0x00010 +# Add Note (Agency Only) or (Agency Only & Requester Only) or (Agency Only, Requester / Agency) +ADD_NOTE = 0x00020 +# Upload Documents (Agency Only & Requester Only) or (Agency Only / Private) or +# (Agency Only / Private, Agency / Requester, All Users) +UPLOAD_DOCUMENTS = 0x00040 +# View Documents Immediately - Public or 'Released and Private' +VIEW_DOCUMENTS_IMMEDIATELY = 0x00080 +# View requests where they are assigned +VIEW_REQUESTS_HELPER = 0x00100 +# View all requests for their agency +VIEW_REQUESTS_AGENCY = 0x00200 +# View all requests for all agencies +VIEW_REQUESTS_ALL = 0x00400 +# Extend Request +EXTEND_REQUESTS = 0x00800 +# Close Request (Denial/Fulfill) +CLOSE_REQUESTS = 0x01000 +# Add Helper (Helper permissions must be specified on a per request basis) +ADD_HELPERS = 0x02000 +# Remove Helper +REMOVE_HELPERS = 0x04000 +# Acknowledge +ACKNOWLEDGE = 0x08000 +# Change Request POC +CHANGE_REQUEST_POC = 0x10000 +# All permissions +ADMINISTER = 0x20000 diff --git a/app/constants/response_type.py b/app/constants/response_type.py new file mode 100644 index 000000000..3e8c984b0 --- /dev/null +++ b/app/constants/response_type.py @@ -0,0 +1,10 @@ +NOTE = "note" +RECORD_TYPES = "record types" +FILE = "file" +LINK = "link" +OFFLINE_INSTRUCTIONS = "offline instructions" +EMAIL = "email" +SMS = "sms" +PUSH = "push" +EXTENSION = "extension" +STATUS = "status" diff --git a/app/constants/role_name.py b/app/constants/role_name.py new file mode 100644 index 000000000..dfbfb388d --- /dev/null +++ b/app/constants/role_name.py @@ -0,0 +1,6 @@ +ANONYMOUS = 'Anonymous User' +PUBLIC_NON_REQUESTER = 'Public User - None Requester' +PUBLIC_REQUESTER = 'Public User - Requester' +AGENCY_HELPER = 'Agency Helper' +AGENCY_OFFICER = 'Agency FOIL Officer' +AGENCY_ADMIN = 'Agency Administrator' diff --git a/app/constants/user_type.py b/app/constants/user_type.py new file mode 100644 index 000000000..375c5c746 --- /dev/null +++ b/app/constants/user_type.py @@ -0,0 +1,3 @@ +ANONYMOUS = "anonymous_user" +AGENCY = "agency_user" +PUBLIC = "public_user" diff --git a/app/db_utils.py b/app/db_utils.py index 90e381d09..6b0bb60c5 100644 --- a/app/db_utils.py +++ b/app/db_utils.py @@ -4,13 +4,27 @@ synopsis: Handles the functions for database control """ import json - +from contextlib import contextmanager from app import db # TODO: Add comment explaining why this is needed from app.models import Agencies, Users +@contextmanager +def db_session(): + """ + Provide a transactional scope around a series of operations. + Flask-SQLAlchemy handles closing the session after an HTTP request. + """ + try: + yield db.session + db.session.commit() + except: + db.session.rollback() + raise + + def create_object(obj): """ :param obj: Object class being created in database diff --git a/app/lib/email_utils.py b/app/lib/email_utils.py index 66488b789..3d8fb1be1 100644 --- a/app/lib/email_utils.py +++ b/app/lib/email_utils.py @@ -1,9 +1,9 @@ - #!/usr/bin/python # -*- coding: utf-8 -*- """ app.email_utils ~~~~~~~~~~~~~~~~ + Implements e-mail notifications for OpenRecords. Flask-mail is a dependency, and the following environment variables need to be set in order for this to work: (Currently using Fake SMTP for testing) MAIL_SERVER: 'localhost' @@ -12,6 +12,7 @@ MAIL_USERNAME: os.environ.get('MAIL_USERNAME') MAIL_PASSWORD: os.environ.get('MAIL_PASSWORD') DEFAULT_MAIL_SENDER: 'Records Admin ' + """ from flask import current_app, render_template @@ -28,6 +29,7 @@ def send_email(subject, template, to=list(), cc=list(), bcc=list(), **kwargs): """ Function that sends asynchronous emails for the application. Takes in arguments from the frontend. + :param to: Person(s) email is being sent to :param cc: Person(s) being CC'ed on the email :param bcc: Person(s) being BCC'ed on the email @@ -43,4 +45,4 @@ def send_email(subject, template, to=list(), cc=list(), bcc=list(), **kwargs): # Renders email template from .txt file commented out and not currently used in development # msg.body = render_template(template + '.txt', **kwargs) msg.html = render_template(template + '.html', **kwargs) - send_async_email.delay(msg) \ No newline at end of file + send_async_email.delay(msg) diff --git a/app/lib/file_utils.py b/app/lib/file_utils.py new file mode 100644 index 000000000..766b5af83 --- /dev/null +++ b/app/lib/file_utils.py @@ -0,0 +1,30 @@ +""" + app.file.utils + ~~~~~~~~~~~~~~~~ + + synopsis: Handles the functions for files + +""" +from flask import current_app +import magic +import os + + +def get_mime_type(request_id, filename): + """ + Gets the mime_type of a file in the uploaded directory using python magic. + :param request_id: Request ID for the specific file. + :param filename: the name of the uploaded file. + + :return: mime_type of the file as determined by python magic. + """ + + upload_file = os.path.join(current_app.config['UPLOAD_DIRECTORY'], request_id, filename) + mime_type = magic.from_file(upload_file, mime=True) + if current_app.config['MAGIC_FILE'] != '': + # Check using custom mime database file + m = magic.Magic( + magic_file=current_app.config['MAGIC_FILE'], + mime=True) + m.from_file(upload_file) + return mime_type diff --git a/app/lib/utils.py b/app/lib/utils.py index a620f68e6..b8a58a82c 100644 --- a/app/lib/utils.py +++ b/app/lib/utils.py @@ -3,10 +3,7 @@ """ - -def mapping(**named_values): - return type('Mapping', (), named_values) - +from base64 import b64decode class InvalidUserException(Exception): @@ -16,3 +13,16 @@ def __init__(self, user): """ super(InvalidUserException, self).__init__( "Invalid user: {}".format(user)) + + +def b64decode_lenient(data): + """ + Decodes base64 (bytes or str), padding being optional + + :param data: a string or bytes-like object of base64 data + :return: a decoded string + """ + if type(data) is str: + data = data.encode() + data += b'=' * (4 - (len(data) % 4)) + return b64decode(data).decode() diff --git a/app/models.py b/app/models.py index 666efb9d4..2c7eaaf27 100644 --- a/app/models.py +++ b/app/models.py @@ -1,63 +1,23 @@ """ -Models for open records database +Models for OpenRecords database """ - import csv import json from datetime import datetime +from flask import current_app from flask_login import UserMixin, AnonymousUserMixin from flask_login import current_user from sqlalchemy import ForeignKeyConstraint -from sqlalchemy.dialects.postgresql import JSON - -from app import app, db -from app.constants import PUBLIC_USER, AGENCY_USER -from sqlalchemy.dialects.postgresql import ARRAY - +from sqlalchemy.dialects.postgresql import ARRAY, JSON -class Permissions: - """ - Define the permission codes for certain actions: - - DUPLICATE_REQUEST: Duplicate Request (New Request based on same criteria) - VIEW_REQUEST_STATUS_PUBLIC: View detailed request status (Open, In Progress, Closed) - VIEW_REQUEST_STATUS_ALL: View detailed request status (Open, In Progress, Due Soon, Overdue, Closed) - VIEW_REQUEST_INFO_PUBLIC: View all public request information - VIEW_REQUEST_INFO_ALL: View all request information - ADD_NOTE: Add Note (Agency Only) or (Agency Only & Requester Only) or (Agency Only, Requester / Agency) - UPLOAD_DOCUMENTS: Upload Documents (Agency Only & Requester Only) or (Agency Only / Private) or - (Agency Only / Private, Agency / Requester, All Users) - VIEW_DOCUMENTS_IMMEDIATELY: View Documents Immediately - Public or 'Released and Private' - VIEW_REQUESTS_HELPER: View requests where they are assigned - VIEW_REQUESTS_AGENCY: View all requests for their agency - VIEW_REQUESTS_ALL: View all requests for all agencies - EXTEND_REQUESTS: Extend Request - CLOSE_REQUESTS: Close Request (Denial/Fulfill) - ADD_HELPERS: Add Helper (Helper permissions must be specified on a per request basis) - REMOVE_HELPERS: Remove Helper - ACKNOWLEDGE: Acknowledge - CHANGE_REQUEST_POC: Change Request POC - ADMINISTER: All permissions - """ - DUPLICATE_REQUEST = 0x00001 - VIEW_REQUEST_STATUS_PUBLIC = 0x00002 - VIEW_REQUEST_STATUS_ALL = 0x00004 - VIEW_REQUEST_INFO_PUBLIC = 0x00008 - VIEW_REQUEST_INFO_ALL = 0x00010 - ADD_NOTE = 0x00020 - UPLOAD_DOCUMENTS = 0x00040 - VIEW_DOCUMENTS_IMMEDIATELY = 0x00080 - VIEW_REQUESTS_HELPER = 0x00100 - VIEW_REQUESTS_AGENCY = 0x00200 - VIEW_REQUESTS_ALL = 0x00400 - EXTEND_REQUESTS = 0x00800 - CLOSE_REQUESTS = 0x01000 - ADD_HELPERS = 0x02000 - REMOVE_HELPERS = 0x04000 - ACKNOWLEDGE = 0x08000 - CHANGE_REQUEST_POC = 0x10000 - ADMINISTER = 0x20000 +from app import db +from app.constants import ( + PUBLIC_USER, + AGENCY_USER, + permission, + role_name, +) class Roles(db.Model): @@ -75,40 +35,68 @@ class Roles(db.Model): name = db.Column(db.String(64), unique=True) permissions = db.Column(db.Integer) - @staticmethod - def insert_roles(): + @classmethod + def populate(cls): """ - Insert permissions for each role: Anonymous User, Public User - Non Requester, Public User - Requester, - Agency Helper, Agency FOIL Officer, Agency Administrator. + Insert permissions for each role. """ roles = { - 'Anonymous User': (Permissions.DUPLICATE_REQUEST | Permissions.VIEW_REQUEST_STATUS_PUBLIC | - Permissions.VIEW_REQUEST_INFO_PUBLIC), - 'Public User - Non Requester': (Permissions.ADD_NOTE | Permissions.DUPLICATE_REQUEST | - Permissions.VIEW_REQUEST_STATUS_PUBLIC | - Permissions.VIEW_REQUEST_INFO_PUBLIC - ), - 'Public User - Requester': (Permissions.ADD_NOTE | Permissions.UPLOAD_DOCUMENTS | - Permissions.VIEW_DOCUMENTS_IMMEDIATELY | Permissions.VIEW_REQUEST_INFO_ALL | - Permissions.VIEW_REQUEST_STATUS_PUBLIC), - 'Agency Helper': (Permissions.ADD_NOTE | Permissions.UPLOAD_DOCUMENTS | Permissions.VIEW_REQUESTS_HELPER | - Permissions.VIEW_REQUEST_INFO_ALL | Permissions.VIEW_REQUEST_STATUS_ALL), - 'Agency FOIL Officer': (Permissions.ADD_NOTE | Permissions.UPLOAD_DOCUMENTS | Permissions.EXTEND_REQUESTS | - Permissions.CLOSE_REQUESTS | Permissions.ADD_HELPERS | Permissions.REMOVE_HELPERS | - Permissions.ACKNOWLEDGE | Permissions.VIEW_REQUESTS_AGENCY | - Permissions.VIEW_REQUEST_INFO_ALL | Permissions.VIEW_REQUEST_STATUS_ALL), - 'Agency Administrator': (Permissions.ADD_NOTE | Permissions.UPLOAD_DOCUMENTS | Permissions.EXTEND_REQUESTS | - Permissions.CLOSE_REQUESTS | Permissions.ADD_HELPERS | Permissions.REMOVE_HELPERS | - Permissions.ACKNOWLEDGE | Permissions.CHANGE_REQUEST_POC | - Permissions.VIEW_REQUESTS_ALL | Permissions.VIEW_REQUEST_INFO_ALL | - Permissions.VIEW_REQUEST_STATUS_ALL) + role_name.ANONYMOUS: ( + permission.DUPLICATE_REQUEST | + permission.VIEW_REQUEST_STATUS_PUBLIC | + permission.VIEW_REQUEST_INFO_PUBLIC + ), + role_name.PUBLIC_NON_REQUESTER: ( + permission.ADD_NOTE | + permission.DUPLICATE_REQUEST | + permission.VIEW_REQUEST_STATUS_PUBLIC | + permission.VIEW_REQUEST_INFO_PUBLIC + ), + role_name.PUBLIC_REQUESTER: ( + permission.ADD_NOTE | + permission.UPLOAD_DOCUMENTS | + permission.VIEW_DOCUMENTS_IMMEDIATELY | + permission.VIEW_REQUEST_INFO_ALL | + permission.VIEW_REQUEST_STATUS_PUBLIC + ), + role_name.AGENCY_HELPER: ( + permission.ADD_NOTE | + permission.UPLOAD_DOCUMENTS | + permission.VIEW_REQUESTS_HELPER | + permission.VIEW_REQUEST_INFO_ALL | + permission.VIEW_REQUEST_STATUS_ALL + ), + role_name.AGENCY_OFFICER: ( + permission.ADD_NOTE | + permission.UPLOAD_DOCUMENTS | + permission.EXTEND_REQUESTS | + permission.CLOSE_REQUESTS | + permission.ADD_HELPERS | + permission.REMOVE_HELPERS | + permission.ACKNOWLEDGE | + permission.VIEW_REQUESTS_AGENCY | + permission.VIEW_REQUEST_INFO_ALL | + permission.VIEW_REQUEST_STATUS_ALL + ), + role_name.AGENCY_ADMIN: ( + permission.ADD_NOTE | + permission.UPLOAD_DOCUMENTS | + permission.EXTEND_REQUESTS | + permission.CLOSE_REQUESTS | + permission.ADD_HELPERS | + permission.REMOVE_HELPERS | + permission.ACKNOWLEDGE | + permission.CHANGE_REQUEST_POC | + permission.VIEW_REQUESTS_ALL | + permission.VIEW_REQUEST_INFO_ALL | + permission.VIEW_REQUEST_STATUS_ALL + ) } - # import pdb; pdb.set_trace() for name, value in roles.items(): role = Roles.query.filter_by(name=name).first() if role is None: - role = Roles(name=name) + role = cls(name=name) role.permissions = value db.session.add(role) db.session.commit() @@ -138,16 +126,16 @@ class Agencies(db.Model): default_email = db.Column(db.String(254)) appeals_email = db.Column(db.String(254)) - @staticmethod - def insert_agencies(): + @classmethod + def populate(cls): """ Automatically populate the agencies table for the OpenRecords application. """ - data = open(app.config['AGENCY_DATA'], 'r') + data = open(current_app.config['AGENCY_DATA'], 'r') dictreader = csv.DictReader(data) for row in dictreader: - agency = Agencies( + agency = cls( ein=row['ein'], category=row['category'], name=row['name'], @@ -183,7 +171,7 @@ class Users(UserMixin, db.Model): mailing_address - a JSON object containing the user's address """ __tablename__ = 'users' - guid = db.Column(db.String(64), primary_key=True, unique=True) # guid + user type + guid = db.Column(db.String(64), primary_key=True) # guid + user type user_type = db.Column(db.String(64), primary_key=True) agency = db.Column(db.Integer, db.ForeignKey('agencies.ein')) email = db.Column(db.String(254)) @@ -294,7 +282,8 @@ class Requests(db.Model): due_date - the date that is set five days after date_submitted, the agency has to acknowledge the request by the due date submission - a Enum that selects from a list of submission methods current_status - a Enum that selects from a list of different statuses a request can have - visibility - a JSON object that contains the visbility settings of a request + privacy - a JSON object that contains the boolean privacy options of a request's title and agency description + (True = Private, False = Public) """ __tablename__ = 'requests' @@ -309,7 +298,7 @@ class Requests(db.Model): db.String(30)) # direct input/mail/fax/email/phone/311/text method of answering request default is direct input current_status = db.Column(db.Enum('Open', 'In Progress', 'Due Soon', 'Overdue', 'Closed', 'Re-Opened', name='statuses')) # due soon is within the next "5" business days - visibility = db.Column(JSON) + privacy = db.Column(JSON) agency_description = db.Column(db.String(5000)) def __init__( @@ -319,20 +308,20 @@ def __init__( description, agency, date_created, - visibility=None, + privacy=None, date_submitted=None, due_date=None, submission=None, current_status=None, agency_description=None ): - visibility_default = {'title': 'private', 'agency_description': 'private'} + privacy_default = {'title': 'false', 'agency_description': 'true'} self.id = id self.title = title self.description = description self.agency = agency self.date_created = date_created - self.visibility = visibility or json.dumps(visibility_default) + self.privacy = privacy or json.dumps(privacy_default) self.date_submitted = date_submitted self.due_date = due_date self.submission = submission @@ -366,8 +355,8 @@ class Events(db.Model): response_id = db.Column(db.Integer, db.ForeignKey('responses.id')) type = db.Column(db.String(30)) timestamp = db.Column(db.DateTime, default=datetime.utcnow()) - previous_response_value = db.Column(db.String) - new_response_value = db.Column(db.String) + previous_response_value = db.Column(JSON) + new_response_value = db.Column(JSON) __table_args__ = (ForeignKeyConstraint([user_id, user_type], [Users.guid, Users.user_type]), @@ -386,7 +375,7 @@ class Responses(db.Model): type - a string containing the type of response that was given for a request date_modified - a datetime object that keeps track of when a request was changed content - a JSON object that contains the content for all the possible responses a request can have - privacy - a string containing the privacy option for a response + privacy - an Enum containing the privacy options for a response """ __tablename__ = 'responses' @@ -395,7 +384,7 @@ class Responses(db.Model): type = db.Column(db.String(30)) date_modified = db.Column(db.DateTime) metadata_id = db.Column(db.Integer) - privacy = db.Column(db.Enum("private", "public", name="privacy")) + privacy = db.Column(db.Enum("private", "release_private", "release_public", name="privacy")) def __repr__(self): return '' % self.id @@ -436,8 +425,12 @@ class UserRequests(db.Model): [Users.guid, Users.user_type]), {}) - def __repr__(self): - return '' % self.user_guid + def has_permission(self, permission): + """ + Ex: + has_permission(permission.ADD_NOTE) + """ + return bool(self.permissions & permission) class Notes(db.Model): @@ -520,8 +513,7 @@ class Emails(db.Model): bcc - a string containing who is bcc'd in an email subject - a string containing the subject of an email email_content - a string containing the content of an email - attachments - an array of integers containing that links to the files metadata_id - + linked_files - an array of strings containing the links to the files """ __tablename__ = 'emails' metadata_id = db.Column(db.Integer, primary_key=True) @@ -530,4 +522,4 @@ class Emails(db.Model): bcc = db.Column(db.String) subject = db.Column(db.String(5000)) email_content = db.Column(db.String) - attachments = db.Column(ARRAY(db.Integer)) + linked_files = db.Column(ARRAY(db.String)) diff --git a/app/request/api/views.py b/app/request/api/views.py index 63ca48a77..bc31299b7 100644 --- a/app/request/api/views.py +++ b/app/request/api/views.py @@ -14,29 +14,29 @@ import json -@request_api_blueprint.route('/edit_visibility', methods=['GET', 'POST']) -def edit_visibility(): +@request_api_blueprint.route('/edit_privacy', methods=['GET', 'POST']) +def edit_privacy(): """ - Edits the visibility privacy options of a request's title and agency description. + Edits the privacy privacy options of a request's title and agency description. Retrieves updated privacy options from AJAX call on view_request page and stores changes into database. - :return: JSON Response with updated title and agency description visibility options + :return: JSON Response with updated title and agency description privacy options """ title = flask_request.form.get('title') agency_desc = flask_request.form.get('desc') request_id = flask_request.form.get('id') current_request = Requests.query.filter_by(id=request_id).first() - # Gets request's current visibility and loads it as a string - visibility = json.loads(current_request.visibility) - # Stores title visibility if changed or uses current visibility if exists - visibility['title'] = title or visibility['title'] - # Stores agency description visibility if changed or uses current visibility - visibility['agency_description'] = agency_desc or visibility['agency_description'] - update_object(attribute='visibility', - value=json.dumps(visibility), + # Gets request's current privacy and loads it as a string + privacy = json.loads(current_request.privacy) + # Stores title privacy if changed or uses current privacy if exists + privacy['title'] = title or privacy['title'] + # Stores agency description privacy if changed or uses current privacy + privacy['agency_description'] = agency_desc or privacy['agency_description'] + update_object(attribute='privacy', + value=json.dumps(privacy), obj_type='Requests', obj_id=current_request.id) - return jsonify(visibility), 200 + return jsonify(privacy), 200 @request_api_blueprint.route('/view/edit', methods=['PUT']) @@ -44,6 +44,7 @@ def edit_request_info(): """ Edits the title and agency description of a FOIL request through an API PUT method. Retrieves updated edited content from AJAX call on view_request page and stores changes into database. + :return: JSON Response with updated content: either request title or agency description) """ edit_request = flask_request.form diff --git a/app/request/utils.py b/app/request/utils.py index a866ec5d7..e6fc08930 100644 --- a/app/request/utils.py +++ b/app/request/utils.py @@ -21,15 +21,15 @@ from app import calendar, upload_redis from app.constants import ( + event_type, + role_name as role, ACKNOWLEDGEMENT_DAYS_DUE, - EVENT_TYPE, ANONYMOUS_USER, - ROLE_NAME ) from app.lib.db_utils import create_object, update_object from app.lib.user_information import create_mailing_address from app.models import Requests, Agencies, Events, Users, UserRequests, Roles, Emails -from app.upload.constants import UPLOAD_STATUS +from app.upload.constants import upload_status from app.upload.utils import ( is_valid_file_type, scan_file, @@ -133,14 +133,14 @@ def create_request(title, upload_event = Events(user_id=user.guid, user_type=user.user_type, request_id=request_id, - type=EVENT_TYPE['file_added'], + type=event_type.FILE_ADDED, timestamp=datetime.utcnow()) create_object(upload_event) role_to_user = { - ROLE_NAME.PUBLIC_REQUESTER : current_user.is_public, - ROLE_NAME.ANONYMOUS: current_user.is_anonymous, - ROLE_NAME.AGENCY_OFFICER: current_user.is_agency + role.PUBLIC_REQUESTER : current_user.is_public, + role.ANONYMOUS: current_user.is_anonymous, + role.AGENCY_OFFICER: current_user.is_agency } role_name = [k for (k, v) in role_to_user.items() if v][0] # (key for "truthy" value) @@ -150,14 +150,14 @@ def create_request(title, event = Events(user_id=user.guid, user_type=user.user_type, request_id=request_id, - type=EVENT_TYPE['request_created'], + type=event_type.REQ_CREATED, timestamp=timestamp) create_object(event) if current_user.is_agency: agency_event = Events(user_id=current_user.guid, user_type=current_user.user_type, request_id=request.id, - type=EVENT_TYPE['request_created'], + type=event_type.REQ_CREATED, timestamp=timestamp) create_object(agency_event) @@ -258,7 +258,7 @@ def _move_validated_upload(request_id, tmp_path): os.rename(tmp_path, valid_path) upload_redis.set( get_upload_key(request_id, valid_name), - UPLOAD_STATUS.READY) + upload_status.READY) def generate_request_id(agency): diff --git a/app/request/views.py b/app/request/views.py index 8022dbe8e..ee5732d85 100644 --- a/app/request/views.py +++ b/app/request/views.py @@ -137,11 +137,11 @@ def confirmation(request_id): """ current_request = Requests.query.filter_by(id=request_id).first() - visibility = json.loads(current_request.visibility) + privacy = json.loads(current_request.privacy) creation_event = Events.query.filter_by(request_id=request_id, type='request_created').first() user = Users.query.filter_by(guid=creation_event.user_id).first() - return render_template('request/confirmation.html', request=current_request, visibility=visibility, user=user, + return render_template('request/confirmation.html', request=current_request, privacy=privacy, user=user, current_user=current_user) @request.route('/view_all', methods=['GET']) @@ -157,5 +157,5 @@ def view(request_id): :return: redirects to view_request.html which is the frame of the view a request page """ current_request = Requests.query.filter_by(id=request_id).first() - visibility = json.loads(current_request.visibility) - return render_template('request/view_request.html', request=current_request, visibility=visibility) + privacy = json.loads(current_request.privacy) + return render_template('request/view_request.html', request=current_request, privacy=privacy) diff --git a/app/response/utils.py b/app/response/utils.py index d09980258..38fb0d207 100644 --- a/app/response/utils.py +++ b/app/response/utils.py @@ -5,22 +5,42 @@ synopsis: Handles the functions for responses """ -from flask_login import current_user -from app.models import Responses, Events, Notes +from flask import current_app +from app.models import Responses, Events, Notes, Files, Emails, Users, UserRequests from app.lib.db_utils import create_object +from app.lib.email_utils import send_email +from app.lib.file_utils import get_mime_type from datetime import datetime -from app.constants import EVENT_TYPE, RESPONSE_TYPE +from app.constants import event_type, response_type, AGENCY_USER, PUBLIC_USER_NYC_ID, ANONYMOUS_USER +import os +import re +import json -def add_file(): +def add_file(request_id, filename, title, privacy): """ - Will add a file to the database for the specified request. - :return: - """ - # TODO: Implement adding a file - print("add_file function") + Creates and stores the file object for the specified request. + Gets the file mimetype from a helper function in lib.file_utils - return None + :param request_id: Request ID that the file is being added to + :param filename: The secured_filename of the file. + :param title: The title of the file which is entered by the uploader. + :param privacy: The privacy option of the file. + + :return: Stores the file metadata into the Files table. + Provides parameters for the process_response function to create and store responses and events object. + """ + size = os.path.getsize(os.path.join(current_app.config['UPLOAD_DIRECTORY'] + request_id, filename)) + mime_type = get_mime_type(request_id, filename) + files = Files(name=filename, mime_type=mime_type, title=title, size=size) + files_metadata = json.dumps({'name': filename, + 'mime_type': mime_type, + 'title': title, + 'size': size}) + files_metadata = files_metadata.replace('{', '').replace('}', '') + create_object(obj=files) + _process_response(request_id, response_type.FILE, event_type.FILE_ADDED, files.metadata_id, privacy, + new_response_value=files_metadata) def delete_file(): @@ -45,15 +65,19 @@ def edit_file(): def add_note(request_id, content): """ + Creates and stores the note object for the specified request. :param request_id: takes in FOIL request ID as an argument for the process_response function :param content: content of the note to be created and stored as a note object + :return: Stores the note content into the Notes table. Provides parameters for the process_response function to create and store responses and events object. """ note = Notes(content=content) create_object(obj=note) - process_response(request_id, RESPONSE_TYPE['note'], EVENT_TYPE['note_added'], note.metadata_id) + content = json.dumps({'content': content}) + _process_response(request_id, response_type.NOTE, event_type.NOTE_ADDED, note.metadata_id, + new_response_value=content) def delete_note(): @@ -92,13 +116,27 @@ def edit_extension(): print("edit_extension function") -def add_email(): +def add_email(request_id, subject, email_content, to=None, cc=None, bcc=None): """ - Will add an email to the database for the specified request. - :return: + Creates and stores the note object for the specified request. + + :param request_id: takes in FOIL request ID as an argument for the process_response function + :param subject: subject of the email to be created and stored as a email object + :param email_content: email body content of the email to be created and stored as a email object + :param to: list of person(s) email is being sent to + :param cc: list of person(s) email is being cc'ed to + :param bcc: list of person(s) email is being bcc'ed + :return: Stores the email metadata into the Emails table. + Provides parameters for the process_response function to create and store responses and events object. """ - # TODO: Implement adding an email - print("add_email function") + to = ','.join([email.replace('{', '').replace('}', '') for email in to]) if to else None + cc = ','.join([email.replace('{', '').replace('}', '') for email in cc]) if cc else None + bcc = ','.join([email.replace('{', '').replace('}', '') for email in bcc]) if bcc else None + email = Emails(to=to, cc=cc, bcc=bcc, subject=subject, email_content=email_content) + create_object(obj=email) + email_content = json.dumps({'email_content': email_content}) + _process_response(request_id, response_type.EMAIL, event_type.EMAIL_NOTIFICATION_SENT, email.metadata_id, + new_response_value=email_content) def add_sms(): @@ -119,21 +157,150 @@ def add_push(): print("add_push function") -def process_response(request_id, response_type, event_type, metadata_id, privacy='private'): +def process_upload_data(form): + """ + Helper function that processes the uploaded file form data. + A files dictionary is first created and then populated with keys and their respective values of the form data. + + :return: A files dictionary that contains the uploaded file(s)'s metadata that will be passed as arguments to be + stored in the database. + """ + files = {} + # re_obj is a regular expression that specifies a set of strings and allows you to check if a particular string + # matches the regular expression. In this case, we are specifying 'filename_' and checking for it. + re_obj = re.compile('filename_') + for key in form.keys(): + if re_obj.match(key): + files[key.split('filename_')[1]] = {} + for key in files: + re_obj = re.compile(key) + for form_key in form.keys(): + if re_obj.match(form_key): + files[key][form_key.split(key + '::')[1]] = form[form_key] + return files + + +def send_response_email(request_id, privacy, filenames, email_content): + """ + Function that sends email detailing a file response has been uploaded to a request. + If the file privacy is private, only agency users are emailed. + If the file privacy is release, the requester is emailed and the agency users are bcced. + + :param request_id: FOIL request ID + :param privacy: privacy option of the uploaded file + :param filenames: list of filenames + :param email_content: content body of the email notification being sent + :return: Sends email notification detailing a file response has been uploaded to a request. + + """ + # TODO: make subject constants + subject = 'Response Added' + # Get list of agency users on the request + agency_user_guids = UserRequests.query.filter_by(request_id=request_id, user_type=AGENCY_USER) + + # Query for the agency email information + agency_emails = [] + for user_guid in agency_user_guids: + agency_user_email = Users.query.filter_by(guid=user_guid, user_type=AGENCY_USER).first().email + agency_emails.append(agency_user_email) + + bcc = agency_emails or ['agency@email.com'] + + file_to_link = {} + for filename in filenames: + file_to_link[filename] = "http://127.0.0.1:5000/request/view/{}".format(filename) + + if privacy == 'release': + # Query for the requester's email information + # Query for the requester's guid from UserRequests using first because there can only be one unique requester + requester_guid = UserRequests.query.filter_by(request_id=request_id).filter( + UserRequests.user_type.in_([ANONYMOUS_USER, PUBLIC_USER_NYC_ID])).first().user_guid + requester_email = Users.query.filter_by(guid=requester_guid).first().email + + # Send email with files to requester and bcc agency users as privacy option is release + to = [requester_email] + _safely_send_and_add_email(request_id, email_content, subject, "email_templates/email_file_upload", + "Department of Records and Information Services", file_to_link, to=to, bcc=bcc) + + if privacy == 'private': + # Send email with files to agency users only as privacy option is private + _safely_send_and_add_email(request_id, email_content, subject, "email_templates/email_file_upload", + "Department of Records and Information Services", file_to_link, bcc=bcc) + + +def process_privacy_options(files): + """ + Creates a dictionary, files_privacy_options, containing lists of 'release' and 'private', with values of filenames. + + :param files: list of filenames + :return: Dictionary with 'release' and 'private' lists + """ + private_files = [] + release_files = [] + for filename in files: + if files[filename]['privacy'] == 'private': + private_files.append(filename) + else: + release_files.append(filename) + + files_privacy_options = dict() + + if release_files: + files_privacy_options['release'] = release_files + + if private_files: + files_privacy_options['private'] = private_files + return files_privacy_options + + +def _safely_send_and_add_email(request_id, + email_content, + subject, + template, + department, + files_links, + to=None, + bcc=None): + """ + Sends email and creates and stores the email object into the Emails table. + + :param request_id: FOIL request ID + :param email_content: body of the email + :param subject: subject of the email (current is for TESTING purposes) + :param template: html template of the email body being rendered + :param department: department of the request (current is for TESTING purposes) + :param files_links: url link of files placed in email body to be downloaded (current link is for TESTING purposes) + :param to: list of person(s) email is being sent to + :param bcc: list of person(s) email is being bcc'ed + :return: + """ + try: + send_email(subject, template, to=to, bcc=bcc, department=department, files_links=files_links) + add_email(request_id, subject, email_content, to=to, bcc=bcc) + except AssertionError: + print('Must include: To, CC, or BCC') + except Exception as e: + print("Error:", e) + + +def _process_response(request_id, responses_type, events_type, metadata_id, privacy='private', new_response_value='', + previous_response_value=''): """ Creates and stores responses and events objects to the database :param request_id: FOIL request ID to be stored into the responses and events tables - :param response_type: type of response to be stored in the responses table - :param event_type: type of event to be stored in the events table + :param responses_type: type of response to be stored in the responses table + :param events_type: type of event to be stored in the events table :param metadata_id: metadata_id of the specific response to be stored in the responses table :param privacy: privacy of the response (default is 'private') to be stored in the responses table + :param new_response_value: string content of the new response, to be stored in the responses table + :param previous_response_value: string content of the previous response, to be stored in the responses table :return: Creates and stores response object with given arguments from separate response type functions. Creates and stores events object to the database. """ # create response object response = Responses(request_id=request_id, - type=response_type, + type=responses_type, date_modified=datetime.utcnow(), metadata_id=metadata_id, privacy=privacy) @@ -147,8 +314,10 @@ def process_response(request_id, response_type, event_type, metadata_id, privacy # will this be called for anonymous user? # user_id=current_user.id, # user_type=current_user.type, - type=event_type, + type=events_type, timestamp=datetime.utcnow(), - response_id=response.id) + response_id=response.id, + previous_response_value=previous_response_value, + new_response_value=new_response_value) # store event object create_object(obj=event) diff --git a/app/response/views.py b/app/response/views.py index efaa2537f..7cf4f45f1 100644 --- a/app/response/views.py +++ b/app/response/views.py @@ -6,13 +6,13 @@ import json -from flask import render_template, flash, request as flask_request +from flask import render_template, flash, request as flask_request, url_for, redirect from flask_wtf import Form from wtforms import StringField, SubmitField from app.models import Requests from app.response import response -from app.response.utils import add_note +from app.response.utils import add_note, add_file, process_upload_data, send_response_email, process_privacy_options # simple form used to test functionality of storing a note to responses table @@ -23,19 +23,46 @@ class NoteForm(Form): @response.route('/note/', methods=['GET', 'POST']) def response_note(request_id): - request = Requests.query.filter_by(id=request_id).first().id + """ + Note response endpoint that takes in the content of a note for a specific request from the frontend. + Passes data into helper function in response.utils to update changes into database. + + :param request_id: Specific FOIL request ID for the note + :return: Message indicating note has been submitted + """ + current_request = Requests.query.filter_by(id=request_id).first() + privacy = json.loads(current_request.privacy) form = NoteForm() if flask_request.method == 'POST': - add_note(request_id=request, + add_note(request_id=current_request.id, content=form.note.data) flash('Note has been submitted') - return render_template('request/view_note.html', request=request, form=form) + return render_template('request/view_note.html', request=current_request, form=form, privacy=privacy) -# TODO: Implement response route for file -@response.route('/file/', methods=['GET', 'POST']) -def response_file(): - pass +@response.route('/file/', methods=['POST']) +def response_file(request_id): + """ + File response endpoint that takes in the metadata of a file for a specific request from the frontend. + Calls process_upload_data to process the uploaded file form data. + Passes data into helper function in response.utils to update changes into database. + + :param request_id: Specific FOIL request ID for the file + :return: redirects to view request page as of right now (IN DEVELOPMENT) + """ + current_request = Requests.query.filter_by(id=request_id).first() + if flask_request.method == 'POST': + files = process_upload_data(flask_request.form) + for file_data in files: + add_file(current_request.id, + file_data, + files[file_data]['title'], + files[file_data]['privacy']) + file_options = process_privacy_options(files) + email_content = flask_request.form['email-content'] + for privacy, files in file_options.items(): + send_response_email(request_id, privacy, files, email_content) + return redirect(url_for('request.view', request_id=request_id)) # TODO: Implement response route for extension @@ -47,11 +74,17 @@ def response_extension(): # TODO: Implement response route for email @response.route('/email', methods=['GET', 'POST']) def response_email(): + """ + Currently renders the template of the email onto the add file form. + + :return: Render email template to add file form + """ data = json.loads(flask_request.data.decode()) request_id = data['request_id'] return render_template('email_templates/email_file_upload.html', department="Department of Records and Information Services", - page="http://127.0.0.1:5000/request/view/{}".format(request_id)) + page=flask_request.host_url.strip('/') + url_for('request.view', request_id=request_id), + files_links={}) # TODO: Implement response route for sms diff --git a/app/responses/__init__.py b/app/responses/__init__.py deleted file mode 100644 index ef3ec566b..000000000 --- a/app/responses/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from flask import Blueprint - -response = Blueprint('response', __name__) - -from . import views diff --git a/app/responses/utils.py b/app/responses/utils.py deleted file mode 100644 index dd5d7d0ac..000000000 --- a/app/responses/utils.py +++ /dev/null @@ -1,154 +0,0 @@ -""" - app.response.utils - ~~~~~~~~~~~~~~~~ - - synopsis: Handles the functions for responses - -""" -from flask_login import current_user -from app.models import Responses, Events, Notes -from app.db_utils import create_object -from datetime import datetime -from app.constants import EVENT_TYPE, RESPONSE_TYPE - - -def add_file(): - """ - Will add a file to the database for the specified request. - :return: - """ - # TODO: Implement adding a file - print("add_file function") - - return None - - -def delete_file(): - """ - Will delete a file in the database for the specified request. - :return: - """ - # TODO: Implement deleting a file - print("delete_file function") - - return None - - -def edit_file(): - """ - Will edit a file to the database for the specified request. - :return: - """ - # TODO: Implement editing a file - print("edit_file function") - - -def add_note(request_id, content): - """ - - :param request_id: takes in FOIL request ID as an argument for the process_response function - :param content: content of the note to be created and stored as a note object - :return: Stores the note content into the Notes table. - Provides parameters for the process_response function to create and store responses and events object. - """ - note = Notes(content=content) - create_object(obj=note) - process_response(request_id, RESPONSE_TYPE['note'], EVENT_TYPE['note_added'], note.metadata_id) - - -def delete_note(): - """ - Will delete a note in the database for the specified request. - :return: - """ - # TODO: Implement deleting a note - print("delete_note function") - - -def edit_note(): - """ - Will edit a note in the database for the specified request. - :return: - """ - # TODO: Implement deleting a note - print("edit_note function") - - -def add_extension(): - """ - Will add an extension to the database for the specified request. - :return: - """ - # TODO: Implement adding an extension - print("add_extension function") - - -def edit_extension(): - """ - Will edit an extension to the database for the specified request. - :return: - """ - # TODO: Implement editing an extension - print("edit_extension function") - - -def add_email(): - """ - Will add an email to the database for the specified request. - :return: - """ - # TODO: Implement adding an email - print("add_email function") - - -def add_sms(): - """ - Will add an SMS to the database for the specified request. - :return: - """ - # TODO: Implement adding an SMS - print("add_sms function") - - -def add_push(): - """ - Will add a push to the database for the specified request. - :return: - """ - # TODO: Implement adding a push - print("add_push function") - - -def process_response(request_id, response_type, event_type, metadata_id, privacy='private'): - """ - Creates and stores responses and events objects to the database - - :param request_id: FOIL request ID to be stored into the responses and events tables - :param response_type: type of response to be stored in the responses table - :param event_type: type of event to be stored in the events table - :param metadata_id: metadata_id of the specific response to be stored in the responses table - :param privacy: privacy of the response (default is 'private') to be stored in the responses table - :return: Creates and stores response object with given arguments from separate response type functions. - Creates and stores events object to the database. - """ - # create response object - response = Responses(request_id=request_id, - type=response_type, - date_modified=datetime.utcnow(), - metadata_id=metadata_id, - privacy=privacy) - # store response object - create_object(obj=response) - - # create event object - event = Events(request_id=request_id, - # user_id and user_type currently commented out for testing - # will need in production to store user information in events table - # will this be called for anonymous user? - # user_id=current_user.id, - # user_type=current_user.type, - type=event_type, - timestamp=datetime.utcnow(), - response_id=response.id) - # store event object - create_object(obj=event) diff --git a/app/responses/views.py b/app/responses/views.py deleted file mode 100644 index dfed781e3..000000000 --- a/app/responses/views.py +++ /dev/null @@ -1,65 +0,0 @@ -""" -.. module:: response.views. - - :synopsis: Handles the response URL endpoints for the OpenRecords application -""" - -from app.responses import response -from app.models import Requests -from flask import render_template, flash, request as flask_request -from flask_wtf import Form -from wtforms import StringField, SubmitField -from app.responses.utils import add_note - - -# simple form used to test functionality of storing a note to responses table -class NoteForm(Form): - note = StringField('Add Note') - submit = SubmitField('Submit') - - -@response.route('/note/', methods=['GET', 'POST']) -def response_note(request_id): - request = Requests.query.filter_by(id=request_id).first().id - form = NoteForm() - if flask_request.method == 'POST': - add_note(request_id=request, - content=form.note.data) - flash('Note has been submitted') - return render_template('request/view_request.html', request=request, form=form) - - -# TODO: Implement response route for file -@response.route('/file/', methods=['GET', 'POST']) -def response_file(): - pass - - -# TODO: Implement response route for extension -@response.route('/extension/', methods=['GET', 'POST']) -def response_extension(): - pass - - -# TODO: Implement response route for email -@response.route('/email/', methods=['GET', 'POST']) -def response_email(): - pass - - -# TODO: Implement response route for sms -@response.route('/sms/', methods=['GET', 'POST']) -def response_sms(): - pass - - -# TODO: Implement response route for push -@response.route('/push/', methods=['GET', 'POST']) -def response_push(): - pass - - -# TODO: Implement response route for visiblity -@response.route('/visiblity/', methods=['GET', 'POST']) -def response_visiblity(): - pass diff --git a/app/static/styles/request_info.css b/app/static/styles/request_info.css index 8786b24eb..446ae9c8d 100644 --- a/app/static/styles/request_info.css +++ b/app/static/styles/request_info.css @@ -66,8 +66,13 @@ h3.control-widget { font-weight: 400; } -/* sets the div of the agency description to a max height and enables scrolling if overflow*/ +/* sets the div of the agency description to a max height and enables scrolling if overflow */ .request-info .agency-desc-text { max-height: 20rem; overflow-y: scroll; +} + +/* disables clear button in xeditable textarea */ +.request-info .editable-clear-x { + display: none !important; } \ No newline at end of file diff --git a/app/templates/email_templates/email_confirmation.html b/app/templates/email_templates/email_confirmation.html index ad75d0d2d..e4dfb2d1e 100644 --- a/app/templates/email_templates/email_confirmation.html +++ b/app/templates/email_templates/email_confirmation.html @@ -13,11 +13,12 @@

Request Title: {{ current_request.title }}
+
Request Description: {{ current_request.description }}

-

Contact Information:

+

Contact Information

diff --git a/app/templates/email_templates/email_file_upload.html b/app/templates/email_templates/email_file_upload.html index 19ca2e811..05af79e40 100644 --- a/app/templates/email_templates/email_file_upload.html +++ b/app/templates/email_templates/email_file_upload.html @@ -5,4 +5,13 @@

You can view the request and take any necessary action at the following webpage: {{ page }}. +

+

+ Download the request files at: +
+

    + {% for filename, link in files_links.items() %} +
  • {{ filename }}
  • + {% endfor %} +

\ No newline at end of file diff --git a/app/templates/request/_view_request_edit_visibility.html b/app/templates/request/_view_request_edit_visibility.html index cea2f2b35..0110f878a 100644 --- a/app/templates/request/_view_request_edit_visibility.html +++ b/app/templates/request/_view_request_edit_visibility.html @@ -1,11 +1,11 @@ - {% include 'request/_view_request_edit_visibility.html' %} {% include 'request/_view_request_info_x-editable.html' %} {% endblock %} \ No newline at end of file diff --git a/app/templates/upload/uploads.html b/app/templates/upload/uploads.html index 6defb5fa9..109d5ead4 100644 --- a/app/templates/upload/uploads.html +++ b/app/templates/upload/uploads.html @@ -4,7 +4,7 @@ -