diff --git a/.env.example b/.env.example index b75d50192..f4ad428f1 100644 --- a/.env.example +++ b/.env.example @@ -13,6 +13,7 @@ APP_VERSION_STRING= LOGFILE_DIRECTORY= # Data +APP_LAUNCH_DATE= AGENCY_DATA= CUSTOM_REQUEST_FORMS= LETTER_TEMPLATES_DATA= diff --git a/.gitignore b/.gitignore index a18a5ae4d..a85a62151 100644 --- a/.gitignore +++ b/.gitignore @@ -98,3 +98,6 @@ data/FOIL-* data_test/* executables/* openrecords.code-workspace +celerybeat-schedule +*.pid +*.pgdump diff --git a/.startup/fakesmtp_startup.sh b/.startup/fakesmtp_startup.sh old mode 100644 new mode 100755 diff --git a/.startup/flask_startup.sh b/.startup/flask_startup.sh old mode 100644 new mode 100755 diff --git a/.startup/tmux_setup.sh b/.startup/tmux_setup.sh old mode 100644 new mode 100755 diff --git a/app/__init__.py b/app/__init__.py index 4ff7e55e6..0158a8cfd 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -47,8 +47,7 @@ email_redis = redis.StrictRedis( db=Config.EMAIL_REDIS_DB, host=Config.REDIS_HOST, port=Config.REDIS_PORT) -holidays = NYCHolidays(years=[year for year in range( - date.today().year, date.today().year + 5)]) +holidays = NYCHolidays(years=[year for year in range(Config.APP_LAUNCH_DATE.year, date.today().year + 5)]) calendar = Calendar( workdays=[MO, TU, WE, TH, FR], holidays=[str(key) for key in holidays.keys()] diff --git a/app/auth/forms.py b/app/auth/forms.py index bb6cf4e54..2ac0fd4dd 100644 --- a/app/auth/forms.py +++ b/app/auth/forms.py @@ -6,18 +6,8 @@ """ from flask_wtf import Form -from wtforms import ( - StringField, - SelectField, - PasswordField, - SubmitField, -) -from wtforms.validators import ( - Length, - Email, - Optional, - DataRequired -) +from wtforms import StringField, SelectField, PasswordField, SubmitField +from wtforms.validators import Length, Email, Optional, DataRequired from app.constants import STATES from app.models import Agencies @@ -31,13 +21,13 @@ class StripFieldsForm(Form): class Meta: def bind_field(self, form, unbound_field, options): - filters = unbound_field.kwargs.get('filters', []) + filters = unbound_field.kwargs.get("filters", []) filters.append(strip_filter) return unbound_field.bind(form=form, filters=filters, **options) def strip_filter(value): - if value is not None and hasattr(value, 'strip'): + if value is not None and hasattr(value, "strip"): return value.strip() return value @@ -53,18 +43,25 @@ class ManageUserAccountForm(StripFieldsForm): Mailing Address: The user's mailing address; Optional; Format: Address One, Address Two, City, State, Zip (5 Digits) """ - title = StringField('Title', validators=[Length(max=64), Optional()]) - organization = StringField('Organization', validators=[Length(max=254), Optional()]) - notification_email = StringField('Notification Email', validators=[Email(), Length(max=254), Optional()]) - phone_number = StringField('Phone', validators=[Length(min=10, max=25), Optional()]) - fax_number = StringField('Fax', validators=[Length(min=10, max=25), Optional()]) - address_one = StringField('Line 1', validators=[Optional()]) - address_two = StringField('Line 2', validators=[Optional()]) - city = StringField('City', validators=[Optional()]) - state = SelectField('State', choices=STATES, default='NY', validators=[Optional()]) - zipcode = StringField('Zip Code (5 Digits)', validators=[Length(min=5, max=5), Optional()]) - - submit = SubmitField('Update OpenRecords Account') + + title = StringField("Title", validators=[Length(max=64), Optional()]) + organization = StringField("Organization", validators=[Length(max=254), Optional()]) + notification_email = StringField( + "Notification Email", validators=[Email(), Length(max=254), Optional()] + ) + phone_number = StringField("Phone", validators=[Length(min=10, max=25), Optional()]) + fax_number = StringField("Fax", validators=[Length(min=10, max=25), Optional()]) + address_one = StringField("Line 1", validators=[Optional()]) + address_two = StringField("Line 2", validators=[Optional()]) + city = StringField("City", validators=[Optional()]) + state = SelectField( + "State / U.S. Territory", choices=STATES, default="NY", validators=[Optional()] + ) + zipcode = StringField( + "Zip Code (5 Digits)", validators=[Length(min=5, max=5), Optional()] + ) + + submit = SubmitField("Update OpenRecords Account") def __init__(self, user=None): """ @@ -77,29 +74,32 @@ def autofill(self): if self.user is not None: self.title.data = self.user.title self.organization.data = self.user.organization - self.notification_email.data = self.user.notification_email or self.user.email + self.notification_email.data = ( + self.user.notification_email or self.user.email + ) self.phone_number.data = self.user.phone_number self.fax_number.data = self.user.fax_number if self.user.mailing_address is not None: - self.address_one.data = self.user.mailing_address.get('address_one') - self.address_two.data = self.user.mailing_address.get('address_two') - self.city.data = self.user.mailing_address.get('city') - self.state.data = self.user.mailing_address.get('state') - self.zipcode.data = self.user.mailing_address.get('zip') + self.address_one.data = self.user.mailing_address.get("address_one") + self.address_two.data = self.user.mailing_address.get("address_two") + self.city.data = self.user.mailing_address.get("city") + self.state.data = self.user.mailing_address.get("state") + self.zipcode.data = self.user.mailing_address.get("zip") def validate(self): """ One mthod of contact must be provided. """ base_is_valid = super(ManageUserAccountForm, self).validate() return base_is_valid and bool( - self.notification_email.data or - self.phone_number.data or - self.fax_number.data or - ( # mailing address - self.address_one.data and - self.city.data and - self.state.data and - self.zipcode.data - )) + self.notification_email.data + or self.phone_number.data + or self.fax_number.data + or ( # mailing address + self.address_one.data + and self.city.data + and self.state.data + and self.zipcode.data + ) + ) class ManageAgencyUserAccountForm(StripFieldsForm): @@ -113,19 +113,26 @@ class ManageAgencyUserAccountForm(StripFieldsForm): Mailing Address: The user's mailing address; Optional; Format: Address One, Address Two, City, State, Zip (5 Digits) """ - default_agency = SelectField('Primary Agency', validators=[DataRequired()]) - title = StringField('Title', validators=[Length(max=64), Optional()]) - organization = StringField('Organization', validators=[Length(max=254), Optional()]) - notification_email = StringField('Notification Email', validators=[Email(), Length(max=254), Optional()]) - phone_number = StringField('Phone', validators=[Length(min=10, max=25), Optional()]) - fax_number = StringField('Fax', validators=[Length(min=10, max=25), Optional()]) - address_one = StringField('Line 1', validators=[Optional()]) - address_two = StringField('Line 2', validators=[Optional()]) - city = StringField('City', validators=[Optional()]) - state = SelectField('State', choices=STATES, default='NY', validators=[Optional()]) - zipcode = StringField('Zip Code (5 Digits)', validators=[Length(min=5, max=5), Optional()]) - - submit = SubmitField('Update OpenRecords Account') + + default_agency = SelectField("Primary Agency", validators=[DataRequired()]) + title = StringField("Title", validators=[Length(max=64), Optional()]) + organization = StringField("Organization", validators=[Length(max=254), Optional()]) + notification_email = StringField( + "Notification Email", validators=[Email(), Length(max=254), Optional()] + ) + phone_number = StringField("Phone", validators=[Length(min=10, max=25), Optional()]) + fax_number = StringField("Fax", validators=[Length(min=10, max=25), Optional()]) + address_one = StringField("Line 1", validators=[Optional()]) + address_two = StringField("Line 2", validators=[Optional()]) + city = StringField("City", validators=[Optional()]) + state = SelectField( + "State / U.S. Territory", choices=STATES, default="NY", validators=[Optional()] + ) + zipcode = StringField( + "Zip Code (5 Digits)", validators=[Length(min=5, max=5), Optional()] + ) + + submit = SubmitField("Update OpenRecords Account") def __init__(self, user=None): """ @@ -139,33 +146,36 @@ def autofill(self): if self.user is not None: self.title.data = self.user.title self.organization.data = self.user.organization - self.notification_email.data = self.user.notification_email or self.user.email + self.notification_email.data = ( + self.user.notification_email or self.user.email + ) self.phone_number.data = self.user.phone_number self.fax_number.data = self.user.fax_number if self.user.mailing_address is not None: - self.address_one.data = self.user.mailing_address.get('address_one') - self.address_two.data = self.user.mailing_address.get('address_two') - self.city.data = self.user.mailing_address.get('city') - self.state.data = self.user.mailing_address.get('state') - self.zipcode.data = self.user.mailing_address.get('zip') + self.address_one.data = self.user.mailing_address.get("address_one") + self.address_two.data = self.user.mailing_address.get("address_two") + self.city.data = self.user.mailing_address.get("city") + self.state.data = self.user.mailing_address.get("state") + self.zipcode.data = self.user.mailing_address.get("zip") def validate(self): """ One mthod of contact must be provided. """ base_is_valid = super(ManageAgencyUserAccountForm, self).validate() return base_is_valid and bool( - self.notification_email.data or - self.phone_number.data or - self.fax_number.data or - ( # mailing address - self.address_one.data and - self.city.data and - self.state.data and - self.zipcode.data - )) + self.notification_email.data + or self.phone_number.data + or self.fax_number.data + or ( # mailing address + self.address_one.data + and self.city.data + and self.state.data + and self.zipcode.data + ) + ) class BasicLoginForm(Form): - email = StringField('Email') - password = PasswordField('Password') + email = StringField("Email") + password = PasswordField("Password") - login = SubmitField('Login') + login = SubmitField("Login") diff --git a/app/commands.py b/app/commands.py new file mode 100644 index 000000000..1e65f71e2 --- /dev/null +++ b/app/commands.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +"""Click commands.""" +import os +import sys +from glob import glob +from subprocess import call + +import click +from flask import current_app +from flask.cli import with_appcontext +from werkzeug.exceptions import MethodNotAllowed, NotFound diff --git a/app/constants/__init__.py b/app/constants/__init__.py index efcdfe312..159a6d7d9 100644 --- a/app/constants/__init__.py +++ b/app/constants/__init__.py @@ -57,6 +57,7 @@ ('OK', 'Oklahoma'), ('OR', 'Oregon'), ('PA', 'Pennsylvania'), + ('PR', 'Puerto Rico'), ('RI', 'Rhode Island'), ('SC', 'South Carolina'), ('SD', 'South Dakota'), diff --git a/app/models.py b/app/models.py index 4ded338eb..20a78469d 100644 --- a/app/models.py +++ b/app/models.py @@ -9,27 +9,16 @@ from elasticsearch.helpers import bulk from flask import current_app, session -from flask_login import ( - UserMixin, - AnonymousUserMixin -) +from flask_login import UserMixin, AnonymousUserMixin, current_user from functools import reduce from operator import ior from sqlalchemy import desc -from sqlalchemy.dialects.postgresql import ( - ARRAY, - JSONB -) +from sqlalchemy.dialects.postgresql import ARRAY, JSONB from sqlalchemy.orm import column_property from sqlalchemy.orm.exc import MultipleResultsFound from warnings import warn -from app import ( - db, - es, - calendar, - sentry -) +from app import db, es, calendar, sentry from app.constants import ( ES_DATETIME_FORMAT, permission, @@ -40,7 +29,7 @@ determination_type, response_privacy, submission_methods, - event_type + event_type, ) from app.constants.request_date import RELEASE_PUBLIC_DAYS from app.constants.schemas import AGENCIES_SCHEMA @@ -48,7 +37,7 @@ from app.lib.utils import ( eval_request_bool, DuplicateFileException, - InvalidDeterminationException + InvalidDeterminationException, ) @@ -64,7 +53,8 @@ class Roles(db.Model): permissions -- Column: Integer users -- Relationship: 'User', 'role' """ - __tablename__ = 'roles' + + __tablename__ = "roles" id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(64), unique=True) permissions = db.Column(db.BigInteger) @@ -75,85 +65,81 @@ def populate(cls): Insert permissions for each role. """ roles = { - role_name.ANONYMOUS: ( - permission.NONE - ), - role_name.PUBLIC_REQUESTER: ( - permission.ADD_NOTE - ), + role_name.ANONYMOUS: (permission.NONE), + role_name.PUBLIC_REQUESTER: (permission.ADD_NOTE), role_name.AGENCY_HELPER: ( - permission.ADD_NOTE | - permission.ADD_FILE | - permission.ADD_LINK | - permission.ADD_OFFLINE_INSTRUCTIONS + permission.ADD_NOTE + | permission.ADD_FILE + | permission.ADD_LINK + | permission.ADD_OFFLINE_INSTRUCTIONS ), role_name.AGENCY_OFFICER: ( - permission.ACKNOWLEDGE | - permission.DENY | - permission.EXTEND | - permission.CLOSE | - permission.RE_OPEN | - permission.ADD_NOTE | - permission.ADD_FILE | - permission.ADD_LINK | - permission.ADD_OFFLINE_INSTRUCTIONS | - permission.GENERATE_LETTER | - permission.EDIT_NOTE | - permission.EDIT_NOTE_PRIVACY | - permission.EDIT_FILE | - permission.EDIT_FILE_PRIVACY | - permission.EDIT_LINK | - permission.EDIT_LINK_PRIVACY | - permission.EDIT_OFFLINE_INSTRUCTIONS | - permission.EDIT_OFFLINE_INSTRUCTIONS_PRIVACY | - permission.EDIT_OFFLINE_INSTRUCTIONS | - permission.EDIT_FILE_PRIVACY | - permission.DELETE_NOTE | - permission.DELETE_FILE | - permission.DELETE_LINK | - permission.DELETE_OFFLINE_INSTRUCTIONS | - permission.EDIT_TITLE | - permission.CHANGE_PRIVACY_TITLE | - permission.EDIT_AGENCY_REQUEST_SUMMARY | - permission.CHANGE_PRIVACY_AGENCY_REQUEST_SUMMARY | - permission.EDIT_REQUESTER_INFO + permission.ACKNOWLEDGE + | permission.DENY + | permission.EXTEND + | permission.CLOSE + | permission.RE_OPEN + | permission.ADD_NOTE + | permission.ADD_FILE + | permission.ADD_LINK + | permission.ADD_OFFLINE_INSTRUCTIONS + | permission.GENERATE_LETTER + | permission.EDIT_NOTE + | permission.EDIT_NOTE_PRIVACY + | permission.EDIT_FILE + | permission.EDIT_FILE_PRIVACY + | permission.EDIT_LINK + | permission.EDIT_LINK_PRIVACY + | permission.EDIT_OFFLINE_INSTRUCTIONS + | permission.EDIT_OFFLINE_INSTRUCTIONS_PRIVACY + | permission.EDIT_OFFLINE_INSTRUCTIONS + | permission.EDIT_FILE_PRIVACY + | permission.DELETE_NOTE + | permission.DELETE_FILE + | permission.DELETE_LINK + | permission.DELETE_OFFLINE_INSTRUCTIONS + | permission.EDIT_TITLE + | permission.CHANGE_PRIVACY_TITLE + | permission.EDIT_AGENCY_REQUEST_SUMMARY + | permission.CHANGE_PRIVACY_AGENCY_REQUEST_SUMMARY + | permission.EDIT_REQUESTER_INFO ), role_name.AGENCY_ADMIN: ( - permission.ACKNOWLEDGE | - permission.DENY | - permission.EXTEND | - permission.CLOSE | - permission.RE_OPEN | - permission.ADD_NOTE | - permission.ADD_FILE | - permission.ADD_LINK | - permission.ADD_OFFLINE_INSTRUCTIONS | - permission.GENERATE_LETTER | - permission.EDIT_NOTE | - permission.EDIT_NOTE_PRIVACY | - permission.EDIT_FILE | - permission.EDIT_FILE_PRIVACY | - permission.EDIT_LINK | - permission.EDIT_LINK_PRIVACY | - permission.EDIT_OFFLINE_INSTRUCTIONS | - permission.EDIT_OFFLINE_INSTRUCTIONS_PRIVACY | - permission.EDIT_FILE_PRIVACY | - permission.EDIT_TITLE | - permission.DELETE_NOTE | - permission.DELETE_FILE | - permission.DELETE_LINK | - permission.DELETE_OFFLINE_INSTRUCTIONS | - permission.CHANGE_PRIVACY_TITLE | - permission.EDIT_AGENCY_REQUEST_SUMMARY | - permission.CHANGE_PRIVACY_AGENCY_REQUEST_SUMMARY | - permission.ADD_USER_TO_REQUEST | - permission.REMOVE_USER_FROM_REQUEST | - permission.EDIT_USER_REQUEST_PERMISSIONS | - permission.ADD_USER_TO_AGENCY | - permission.REMOVE_USER_FROM_AGENCY | - permission.CHANGE_USER_ADMIN_PRIVILEGE | - permission.EDIT_REQUESTER_INFO - ) + permission.ACKNOWLEDGE + | permission.DENY + | permission.EXTEND + | permission.CLOSE + | permission.RE_OPEN + | permission.ADD_NOTE + | permission.ADD_FILE + | permission.ADD_LINK + | permission.ADD_OFFLINE_INSTRUCTIONS + | permission.GENERATE_LETTER + | permission.EDIT_NOTE + | permission.EDIT_NOTE_PRIVACY + | permission.EDIT_FILE + | permission.EDIT_FILE_PRIVACY + | permission.EDIT_LINK + | permission.EDIT_LINK_PRIVACY + | permission.EDIT_OFFLINE_INSTRUCTIONS + | permission.EDIT_OFFLINE_INSTRUCTIONS_PRIVACY + | permission.EDIT_FILE_PRIVACY + | permission.EDIT_TITLE + | permission.DELETE_NOTE + | permission.DELETE_FILE + | permission.DELETE_LINK + | permission.DELETE_OFFLINE_INSTRUCTIONS + | permission.CHANGE_PRIVACY_TITLE + | permission.EDIT_AGENCY_REQUEST_SUMMARY + | permission.CHANGE_PRIVACY_AGENCY_REQUEST_SUMMARY + | permission.ADD_USER_TO_REQUEST + | permission.REMOVE_USER_FROM_REQUEST + | permission.EDIT_USER_REQUEST_PERMISSIONS + | permission.ADD_USER_TO_AGENCY + | permission.REMOVE_USER_FROM_AGENCY + | permission.CHANGE_USER_ADMIN_PRIVILEGE + | permission.EDIT_REQUESTER_INFO + ), } for name, value in roles.items(): @@ -165,7 +151,7 @@ def populate(cls): db.session.commit() def __repr__(self): - return '' % self.name + return "" % self.name class Agencies(db.Model): @@ -193,13 +179,16 @@ class Agencies(db.Model): OpenRecords """ - __tablename__ = 'agencies' + + __tablename__ = "agencies" ein = db.Column(db.String(4), primary_key=True) parent_ein = db.Column(db.String(3)) categories = db.Column(ARRAY(db.String(256))) - _name = db.Column(db.String(256), nullable=False, name='name') + _name = db.Column(db.String(256), nullable=False, name="name") acronym = db.Column(db.String(64), nullable=True) - _next_request_number = db.Column(db.Integer(), db.Sequence('request_seq'), name='next_request_number') + _next_request_number = db.Column( + db.Integer(), db.Sequence("request_seq"), name="next_request_number" + ) default_email = db.Column(db.String(254)) appeals_email = db.Column(db.String(254)) is_active = db.Column(db.Boolean(), default=False) @@ -208,34 +197,34 @@ class Agencies(db.Model): # TODO: Use validation on agency_features column administrators = db.relationship( - 'Users', + "Users", secondary="agency_users", primaryjoin="and_(Agencies.ein == AgencyUsers.agency_ein, " - "AgencyUsers.is_agency_active == True, " - "AgencyUsers.is_agency_admin == True)", - secondaryjoin="AgencyUsers.user_guid == Users.guid" + "AgencyUsers.is_agency_active == True, " + "AgencyUsers.is_agency_admin == True)", + secondaryjoin="AgencyUsers.user_guid == Users.guid", ) standard_users = db.relationship( - 'Users', + "Users", secondary="agency_users", primaryjoin="and_(Agencies.ein == AgencyUsers.agency_ein, " - "AgencyUsers.is_agency_active == True, " - "AgencyUsers.is_agency_admin == False)", - secondaryjoin="AgencyUsers.user_guid == Users.guid" + "AgencyUsers.is_agency_active == True, " + "AgencyUsers.is_agency_admin == False)", + secondaryjoin="AgencyUsers.user_guid == Users.guid", ) active_users = db.relationship( - 'Users', + "Users", secondary="agency_users", primaryjoin="and_(Agencies.ein == AgencyUsers.agency_ein, " - "AgencyUsers.is_agency_active == True)", - secondaryjoin="AgencyUsers.user_guid == Users.guid" + "AgencyUsers.is_agency_active == True)", + secondaryjoin="AgencyUsers.user_guid == Users.guid", ) inactive_users = db.relationship( - 'Users', + "Users", secondary="agency_users", primaryjoin="and_(Agencies.ein == AgencyUsers.agency_ein, " - "AgencyUsers.is_agency_active == False)", - secondaryjoin="AgencyUsers.user_guid == Users.guid" + "AgencyUsers.is_agency_active == False)", + secondaryjoin="AgencyUsers.user_guid == Users.guid", ) @property @@ -255,11 +244,12 @@ def parent(self): @property def next_request_number(self): from app.lib.db_utils import update_object + num = self._next_request_number update_object( - {'_next_request_number': self._next_request_number + 1}, + {"_next_request_number": self._next_request_number + 1}, Agencies, - self.formatted_parent_ein + self.formatted_parent_ein, ) return num @@ -269,8 +259,11 @@ def next_request_number(self, value): @property def name(self): - return '{name} ({acronym})'.format(name=self._name, acronym=self.acronym) if self.acronym else '{name}'.format( - name=self._name) + return ( + "{name} ({acronym})".format(name=self._name, acronym=self.acronym) + if self.acronym + else "{name}".format(name=self._name) + ) @name.setter def name(self, value): @@ -281,35 +274,43 @@ def populate(cls, json_name=None): """ Automatically populate the agencies table for the OpenRecords application. """ - filename = json_name or current_app.config['AGENCY_DATA'] - with open(filename, 'r') as data: + filename = json_name or current_app.config["AGENCY_DATA"] + with open(filename, "r") as data: data = json.load(data) if not validate_schema(data, AGENCIES_SCHEMA): - warn("Invalid JSON Data. Not importing any agencies.", category=UserWarning) + warn( + "Invalid JSON Data. Not importing any agencies.", + category=UserWarning, + ) return False - for agency in data['agencies']: - if Agencies.query.filter_by(ein=agency['ein']).first() is not None: - warn("Duplicate EIN ({ein}); Row not imported".format(ein=agency['ein']), category=UserWarning) + for agency in data["agencies"]: + if Agencies.query.filter_by(ein=agency["ein"]).first() is not None: + warn( + "Duplicate EIN ({ein}); Row not imported".format( + ein=agency["ein"] + ), + category=UserWarning, + ) continue a = cls( - ein=agency['ein'], - parent_ein=agency['parent_ein'], - categories=agency['categories'], - name=agency['name'], - acronym=agency.get('acronym', None), - next_request_number=agency['next_request_number'], - default_email=agency['default_email'], - appeals_email=agency['appeals_email'], - is_active=agency['is_active'], - agency_features=agency['agency_features'] + ein=agency["ein"], + parent_ein=agency["parent_ein"], + categories=agency["categories"], + name=agency["name"], + acronym=agency.get("acronym", None), + next_request_number=agency["next_request_number"], + default_email=agency["default_email"], + appeals_email=agency["appeals_email"], + is_active=agency["is_active"], + agency_features=agency["agency_features"], ) db.session.add(a) db.session.commit() def __repr__(self): - return '' % self.name + return "" % self.name class Users(UserMixin, db.Model): @@ -334,7 +335,8 @@ class Users(UserMixin, db.Model): fax_number - string containing the user's fax number mailing_address - a JSON object containing the user's address """ - __tablename__ = 'users' + + __tablename__ = "users" guid = db.Column(db.String(64), unique=True, primary_key=True) is_nyc_employee = db.Column(db.Boolean, default=False) has_nyc_account = db.Column(db.Boolean, default=False) @@ -352,23 +354,24 @@ class Users(UserMixin, db.Model): organization = db.Column(db.String(128)) # Outside organization phone_number = db.Column(db.String(25)) fax_number = db.Column(db.String(25)) - _mailing_address = db.Column(JSONB, - name='mailing_address') # TODO: define validation for minimum acceptable mailing address + _mailing_address = db.Column( + JSONB, name="mailing_address" + ) # TODO: define validation for minimum acceptable mailing address session_id = db.Column(db.String(254), nullable=True, default=None) signature = db.Column(db.String(), nullable=True, default=None) fullname = column_property(first_name + " " + last_name) # Relationships - user_requests = db.relationship("UserRequests", backref="user", lazy='dynamic') + user_requests = db.relationship("UserRequests", backref="user", lazy="dynamic") agencies = db.relationship( - 'Agencies', + "Agencies", secondary="agency_users", primaryjoin="AgencyUsers.user_guid == Users.guid", secondaryjoin="and_(AgencyUsers.agency_ein == Agencies.ein, " - "AgencyUsers.is_agency_active == True)", - lazy='dynamic' + "AgencyUsers.is_agency_active == True)", + lazy="dynamic", ) - agency_users = db.relationship("AgencyUsers", backref="user", lazy='dynamic') + agency_users = db.relationship("AgencyUsers", backref="user", lazy="dynamic") @property def is_authenticated(self): @@ -376,12 +379,12 @@ def is_authenticated(self): Verifies the access token currently stored in the user's session by invoking the OAuth User Web Service and checking the response. """ - if current_app.config['USE_LDAP']: + if current_app.config["USE_LDAP"]: return True - if current_app.config['USE_SAML']: - if session.get('samlUserdata', None): + if current_app.config["USE_SAML"]: + if session.get("samlUserdata", None): return True - if current_app.config['USE_LOCAL_AUTH']: + if current_app.config["USE_LOCAL_AUTH"]: return True return False @@ -420,8 +423,14 @@ def default_agency_ein(self): Return the Users default agency ein. :return: String """ - agency = AgencyUsers.query.join(Users).filter(AgencyUsers.is_primary_agency == True, - AgencyUsers.user_guid == self.guid).one_or_none() + agency = ( + AgencyUsers.query.join(Users) + .filter( + AgencyUsers.is_primary_agency == True, + AgencyUsers.user_guid == self.guid, + ) + .one_or_none() + ) if agency is not None: return agency.agency_ein return None @@ -462,7 +471,9 @@ def anonymous_request(self): if this user is an anonymous requester. """ if self.is_anonymous_requester: - return Requests.query.filter_by(id=self.user_requests.one().request_id).one() + return Requests.query.filter_by( + id=self.user_requests.one().request_id + ).one() return None @property @@ -515,12 +526,17 @@ def is_agency_active(self, ein=None): def agencies_for_forms(self): agencies = self.agencies.with_entities(Agencies.ein, Agencies._name).all() - agencies.insert(0, agencies.pop(agencies.index((self.default_agency.ein, self.default_agency._name)))) + agencies.insert( + 0, + agencies.pop( + agencies.index((self.default_agency.ein, self.default_agency._name)) + ), + ) return agencies @property def name(self): - return ' '.join((self.first_name.title(), self.last_name.title())) + return " ".join((self.first_name.title(), self.last_name.title())) @property def mailing_address(self): @@ -534,8 +550,8 @@ def mailing_address(self, mailing_address): def formatted_point_of_contact_number(self): if self.phone_number: formatted_phone_number = self.phone_number - formatted_phone_number = formatted_phone_number.strip('(') - formatted_phone_number = formatted_phone_number.replace(') ', '-') + formatted_phone_number = formatted_phone_number.strip("(") + formatted_phone_number = formatted_phone_number.replace(") ", "-") return formatted_phone_number def get_id(self): @@ -546,23 +562,23 @@ def es_update(self): Call es_update for any request where this user is the requester since the request es doc relies on the requester's name. """ - if current_app.config['ELASTICSEARCH_ENABLED']: + if current_app.config["ELASTICSEARCH_ENABLED"]: requests = [request.id for request in self.requests] - actions = [{ - '_op_type': 'update', - '_id': request_id, - 'doc': { - 'requester_id': self.guid, - 'requester_name': self.name + actions = [ + { + "_op_type": "update", + "_id": request_id, + "doc": {"requester_id": self.guid, "requester_name": self.name}, } - } for request_id in requests] + for request_id in requests + ] bulk( es, actions, - index=current_app.config['ELASTICSEARCH_INDEX'], - doc_type='request', - chunk_size=current_app.config['ELASTICSEARCH_CHUNK_SIZE'] + index=current_app.config["ELASTICSEARCH_INDEX"], + doc_type="request", + chunk_size=current_app.config["ELASTICSEARCH_CHUNK_SIZE"], ) @property @@ -591,35 +607,35 @@ def val_for_events(self): @classmethod def populate(cls, csv_name=None): - filename = csv_name or current_app.config['STAFF_DATA'] - with open(filename, 'r') as data: + filename = csv_name or current_app.config["STAFF_DATA"] + with open(filename, "r") as data: dictreader = csv.DictReader(data) for row in dictreader: - if Users.query.filter_by(email=row['email']).first() is None: + if Users.query.filter_by(email=row["email"]).first() is None: user = cls( guid=str(uuid4()), - is_super=eval(row['is_super']), - first_name=row['first_name'], - middle_initial=row['middle_initial'], - last_name=row['last_name'], - email=row['email'], - email_validated=eval(row['email_validated']), - terms_of_use_accepted=eval(row['terms_of_use_accepted']), - phone_number=row['phone_number'], - fax_number=row['fax_number'] + is_super=eval(row["is_super"]), + first_name=row["first_name"], + middle_initial=row["middle_initial"], + last_name=row["last_name"], + email=row["email"], + email_validated=eval(row["email_validated"]), + terms_of_use_accepted=eval(row["terms_of_use_accepted"]), + phone_number=row["phone_number"], + fax_number=row["fax_number"], ) db.session.add(user) db.session.commit() - agency_eins = row['agencies'].split('|') + agency_eins = row["agencies"].split("|") for agency in agency_eins: - ein, is_active, is_admin, is_primary_agency = agency.split('#') + ein, is_active, is_admin, is_primary_agency = agency.split("#") agency_user = AgencyUsers( user_guid=user.guid, agency_ein=ein, is_agency_active=eval_request_bool(is_active), is_agency_admin=eval_request_bool(is_admin), - is_primary_agency=eval_request_bool(is_primary_agency) + is_primary_agency=eval_request_bool(is_primary_agency), ) db.session.add(agency_user) if agency_eins: @@ -631,7 +647,7 @@ def __init__(self, **kwargs): super(Users, self).__init__(**kwargs) def __repr__(self): - return ''.format(self.get_id()) + return "".format(self.get_id()) class Anonymous(AnonymousUserMixin): @@ -669,7 +685,7 @@ def is_agency(self): return False def __repr__(self): - return '' + return "" class AgencyUsers(db.Model): @@ -685,19 +701,18 @@ class AgencyUsers(db.Model): primary_agency - a boolean value that determines whether the agency identified by agency_ein is the users default agency """ - __tablename__ = 'agency_users' + + __tablename__ = "agency_users" user_guid = db.Column(db.String(64), db.ForeignKey("users.guid"), primary_key=True) - agency_ein = db.Column(db.String(4), db.ForeignKey("agencies.ein"), primary_key=True) + agency_ein = db.Column( + db.String(4), db.ForeignKey("agencies.ein"), primary_key=True + ) is_agency_active = db.Column(db.Boolean, default=False, nullable=False) is_agency_admin = db.Column(db.Boolean, default=False, nullable=False) is_primary_agency = db.Column(db.Boolean, default=False, nullable=False) __table_args__ = ( - db.ForeignKeyConstraint( - [user_guid], - [Users.guid], - onupdate="CASCADE" - ), + db.ForeignKeyConstraint([user_guid], [Users.guid], onupdate="CASCADE"), ) @@ -723,79 +738,92 @@ class Requests(db.Model): agency_request_summary_release_date - a datetime of when the agency_request_summary will be made public custom_metadata - a JSON that contains the metadata from an agency's custom request forms """ - __tablename__ = 'requests' + + __tablename__ = "requests" id = db.Column(db.String(19), primary_key=True) - agency_ein = db.Column(db.String(4), db.ForeignKey('agencies.ein')) - category = db.Column(db.String, default='All', - nullable=False) # FIXME: should be nullable, 'All' shouldn't be used + agency_ein = db.Column(db.String(4), db.ForeignKey("agencies.ein")) + category = db.Column( + db.String, default="All", nullable=False + ) # FIXME: should be nullable, 'All' shouldn't be used title = db.Column(db.String(90)) description = db.Column(db.String(5000)) date_created = db.Column(db.DateTime, default=datetime.utcnow()) - date_submitted = db.Column(db.DateTime) # used to calculate due date, rounded off to next business day + date_submitted = db.Column( + db.DateTime + ) # used to calculate due date, rounded off to next business day date_closed = db.Column(db.DateTime, default=None, nullable=True) due_date = db.Column(db.DateTime) submission = db.Column( - db.Enum(submission_methods.DIRECT_INPUT, - submission_methods.FAX, - submission_methods.PHONE, - submission_methods.EMAIL, - submission_methods.MAIL, - submission_methods.IN_PERSON, - submission_methods.THREE_ONE_ONE, - name='submission')) + db.Enum( + submission_methods.DIRECT_INPUT, + submission_methods.FAX, + submission_methods.PHONE, + submission_methods.EMAIL, + submission_methods.MAIL, + submission_methods.IN_PERSON, + submission_methods.THREE_ONE_ONE, + name="submission", + ) + ) status = db.Column( - db.Enum(request_status.OPEN, - request_status.IN_PROGRESS, - request_status.DUE_SOON, # within the next 5 business days - request_status.OVERDUE, - request_status.CLOSED, - name='status'), - nullable=False + db.Enum( + request_status.OPEN, + request_status.IN_PROGRESS, + request_status.DUE_SOON, # within the next 5 business days + request_status.OVERDUE, + request_status.CLOSED, + name="status", + ), + nullable=False, ) privacy = db.Column(JSONB) agency_request_summary = db.Column(db.String(5000)) agency_request_summary_release_date = db.Column(db.DateTime) custom_metadata = db.Column(JSONB) - user_requests = db.relationship('UserRequests', backref=db.backref('request', uselist=False), lazy='dynamic') - agency = db.relationship('Agencies', backref='requests', uselist=False) - responses = db.relationship('Responses', backref=db.backref('request', uselist=False), lazy='dynamic') + user_requests = db.relationship( + "UserRequests", backref=db.backref("request", uselist=False), lazy="dynamic" + ) + agency = db.relationship("Agencies", backref="requests", uselist=False) + responses = db.relationship( + "Responses", backref=db.backref("request", uselist=False), lazy="dynamic" + ) requester = db.relationship( - 'Users', - secondary='user_requests', # expects table name + "Users", + secondary="user_requests", # expects table name primaryjoin=lambda: Requests.id == UserRequests.request_id, secondaryjoin="and_(Users.guid == UserRequests.user_guid, " - "UserRequests.request_user_type == '{}')".format(user_type_request.REQUESTER), + "UserRequests.request_user_type == '{}')".format(user_type_request.REQUESTER), backref="requests", viewonly=True, - uselist=False + uselist=False, ) # any agency user associated with a request is considered an assigned user agency_users = db.relationship( - 'Users', - secondary='user_requests', + "Users", + secondary="user_requests", primaryjoin=lambda: Requests.id == UserRequests.request_id, secondaryjoin="and_(Users.guid == UserRequests.user_guid, " - "UserRequests.request_user_type == '{}')".format(user_type_request.AGENCY), - viewonly=True + "UserRequests.request_user_type == '{}')".format(user_type_request.AGENCY), + viewonly=True, ) - PRIVACY_DEFAULT = {'title': False, 'agency_request_summary': True} + PRIVACY_DEFAULT = {"title": False, "agency_request_summary": True} def __init__( - self, - id, - title, - description, - agency_ein, - date_created, - category=None, - privacy=None, - date_submitted=None, # FIXME: are some of these really nullable? - due_date=None, - submission=None, - status=request_status.OPEN, - custom_metadata=None + self, + id, + title, + description, + agency_ein, + date_created, + category=None, + privacy=None, + date_submitted=None, # FIXME: are some of these really nullable? + due_date=None, + submission=None, + status=request_status.OPEN, + custom_metadata=None, ): self.id = id self.title = title @@ -819,34 +847,79 @@ def val_for_events(self): be the same on Request creation are not included. """ return { - 'title': self.title, - 'description': self.description, - 'due_date': self.due_date.isoformat(), + "title": self.title, + "description": self.description, + "due_date": self.due_date.isoformat(), } @property def was_acknowledged(self): - if self.responses.join(Determinations).filter( - Determinations.dtype == determination_type.ACKNOWLEDGMENT).one_or_none() is not None: + if ( + self.responses.join(Determinations) + .filter(Determinations.dtype == determination_type.ACKNOWLEDGMENT) + .one_or_none() + is not None + ): return True return False @property def was_reopened(self): - return self.responses.join(Determinations).filter( - Determinations.dtype == determination_type.REOPENING).first() is not None + return ( + self.responses.join(Determinations) + .filter(Determinations.dtype == determination_type.REOPENING) + .first() + is not None + ) @property def last_date_closed(self): if self.status == request_status.CLOSED: - return self.responses.join(Determinations).filter( - Determinations.dtype.in_([determination_type.CLOSING, determination_type.DENIAL]) - ).order_by(desc(Determinations.date_modified)).limit(1).one().date_modified + return ( + self.responses.join(Determinations) + .filter( + Determinations.dtype.in_( + [determination_type.CLOSING, determination_type.DENIAL] + ) + ) + .order_by(desc(Determinations.date_modified)) + .limit(1) + .one() + .date_modified + ) return None @property def days_until_due(self): - return calendar.busdaycount(datetime.utcnow(), self.due_date.replace(hour=23, minute=59, second=59)) + return calendar.busdaycount( + datetime.utcnow(), self.due_date.replace(hour=23, minute=59, second=59) + ) + + @property + def show_title(self) -> bool: + """Determine whether the title should be displayed on the front-end. + The title may be displayed in the following circumstances: + - The currently logged in user is in the requests assigned agency users OR + - The current User is the requester OR + - The title is not private AND + - The request was acknowledged OR + - The request was denied OR + - The request is overdue for an acknowledgment + Returns: + bool: True if the title should be shown, False otherwise. + """ + return ( + current_user in self.agency_users + or current_user == self.requester + or ( + not self.privacy["title"] + and ( + self.was_acknowledged + or self.status == request_status.CLOSED + or self.days_until_due < 0 + ) + ) + ) @property def url(self): @@ -856,42 +929,58 @@ def url(self): Since we cannot use SERVER_NAME in config (and, by extension, 'url_for'), BASE_URL and VIEW_REQUEST_ENDPOINT will have to do. """ - return urljoin(current_app.config['BASE_URL'], - "{view_request_endpoint}/{request_id}".format( - view_request_endpoint=current_app.config['VIEW_REQUEST_ENDPOINT'], - request_id=self.id - )) + return urljoin( + current_app.config["BASE_URL"], + "{view_request_endpoint}/{request_id}".format( + view_request_endpoint=current_app.config["VIEW_REQUEST_ENDPOINT"], + request_id=self.id, + ), + ) @property def agency_request_summary_released(self): """ Determine whether the agency_request_summary has been made public and has passed its release date """ - return self.status == request_status.CLOSED and not self.privacy['agency_request_summary'] and \ - self.agency_request_summary and self.agency_request_summary_release_date is not None and \ - self.agency_request_summary_release_date < datetime.utcnow() + return ( + self.status == request_status.CLOSED + and not self.privacy["agency_request_summary"] + and self.agency_request_summary + and self.agency_request_summary_release_date is not None + and self.agency_request_summary_release_date < datetime.utcnow() + ) def es_update(self): - if current_app.config['ELASTICSEARCH_ENABLED']: + if current_app.config["ELASTICSEARCH_ENABLED"]: if self.agency.is_active: es.update( index=current_app.config["ELASTICSEARCH_INDEX"], - doc_type='request', + doc_type="request", id=self.id, body={ - 'doc': { - 'title': self.title, - 'description': self.description, - 'agency_request_summary': self.agency_request_summary, - 'assigned_users': [user.get_id() for user in self.agency_users], - 'title_private': self.privacy['title'], - 'agency_request_summary_private': not self.agency_request_summary_released, - 'date_due': self.due_date.strftime(ES_DATETIME_FORMAT), - 'date_closed': self.date_closed.strftime( - ES_DATETIME_FORMAT) if self.date_closed is not None else [], - 'status': self.status, - 'requester_name': self.requester.name, - 'public_title': 'Private' if self.privacy['title'] else self.title + "doc": { + "title": self.title, + "description": self.description, + "agency_request_summary": self.agency_request_summary, + "assigned_users": [ + user.get_id() for user in self.agency_users + ], + "title_private": self.privacy["title"], + "agency_request_summary_private": not self.agency_request_summary_released, + "date_due": self.due_date.strftime(ES_DATETIME_FORMAT), + "date_closed": self.date_closed.strftime(ES_DATETIME_FORMAT) + if self.date_closed is not None + else [], + "status": self.status, + "requester_name": self.requester.name, + "requester_id": ( + self.requester.get_id() + if not self.requester.is_anonymous_requester + else "" + ), + "public_title": "Private" + if self.privacy["title"] + else self.title, } }, # refresh='wait_for' @@ -899,48 +988,50 @@ def es_update(self): def es_create(self): """ Must be called AFTER UserRequest has been created. """ - if current_app.config['ELASTICSEARCH_ENABLED']: + if current_app.config["ELASTICSEARCH_ENABLED"]: es.create( index=current_app.config["ELASTICSEARCH_INDEX"], - doc_type='request', + doc_type="request", id=self.id, body={ - 'title': self.title, - 'description': self.description, - 'agency_request_summary': self.agency_request_summary, - 'agency_ein': self.agency_ein, - 'agency_name': self.agency.name, - 'assigned_users': [user.get_id() for user in self.agency_users], - 'agency_acronym': self.agency.acronym, - 'title_private': self.privacy['title'], - 'agency_request_summary_private': not self.agency_request_summary_released, - 'date_created': self.date_created.strftime(ES_DATETIME_FORMAT), - 'date_submitted': self.date_submitted.strftime(ES_DATETIME_FORMAT), - 'date_received': self.date_created.strftime( - ES_DATETIME_FORMAT) if self.date_created < self.date_submitted else self.date_submitted.strftime( - ES_DATETIME_FORMAT), - 'date_due': self.due_date.strftime(ES_DATETIME_FORMAT), - 'submission': self.submission, - 'status': self.status, - 'requester_id': (self.requester.get_id() - if not self.requester.is_anonymous_requester - else ''), - 'requester_name': self.requester.name, - 'public_title': 'Private' if self.privacy['title'] else self.title, - } + "title": self.title, + "description": self.description, + "agency_request_summary": self.agency_request_summary, + "agency_ein": self.agency_ein, + "agency_name": self.agency.name, + "assigned_users": [user.get_id() for user in self.agency_users], + "agency_acronym": self.agency.acronym, + "title_private": self.privacy["title"], + "agency_request_summary_private": not self.agency_request_summary_released, + "date_created": self.date_created.strftime(ES_DATETIME_FORMAT), + "date_submitted": self.date_submitted.strftime(ES_DATETIME_FORMAT), + "date_received": self.date_created.strftime(ES_DATETIME_FORMAT) + if self.date_created < self.date_submitted + else self.date_submitted.strftime(ES_DATETIME_FORMAT), + "date_due": self.due_date.strftime(ES_DATETIME_FORMAT), + "submission": self.submission, + "status": self.status, + "requester_id": ( + self.requester.get_id() + if not self.requester.is_anonymous_requester + else "" + ), + "requester_name": self.requester.name, + "public_title": "Private" if self.privacy["title"] else self.title, + }, ) def es_delete(self): """ Delete a document from the elastic search index """ - if current_app.config['ELASTICSEARCH_ENABLED']: + if current_app.config["ELASTICSEARCH_ENABLED"]: es.delete( index=current_app.config["ELASTICSEARCH_INDEX"], - doc_type='request', - id=self.id + doc_type="request", + id=self.id, ) def __repr__(self): - return '' % self.id + return "" % self.id class Events(db.Model): @@ -957,40 +1048,37 @@ class Events(db.Model): previous_value - a string containing the old value of the event new_value - a string containing the new value of the event """ - __tablename__ = 'events' + + __tablename__ = "events" id = db.Column(db.Integer, primary_key=True) - request_id = db.Column(db.String(19), db.ForeignKey('requests.id')) + request_id = db.Column(db.String(19), db.ForeignKey("requests.id")) user_guid = db.Column(db.String(64)) - response_id = db.Column(db.Integer, db.ForeignKey('responses.id')) + response_id = db.Column(db.Integer, db.ForeignKey("responses.id")) type = db.Column(db.String(64)) timestamp = db.Column(db.DateTime, default=datetime.utcnow()) previous_value = db.Column(JSONB) new_value = db.Column(JSONB) __table_args__ = ( - db.ForeignKeyConstraint( - [user_guid], - [Users.guid], - onupdate="CASCADE" - ), + db.ForeignKeyConstraint([user_guid], [Users.guid], onupdate="CASCADE"), ) response = db.relationship("Responses", backref="events") request = db.relationship("Requests", backref="events") user = db.relationship( - "Users", - primaryjoin="Events.user_guid == Users.guid", - backref="events" + "Users", primaryjoin="Events.user_guid == Users.guid", backref="events" ) - def __init__(self, - request_id, - user_guid, - type_, - previous_value=None, - new_value=None, - response_id=None, - timestamp=None): + def __init__( + self, + request_id, + user_guid, + type_, + previous_value=None, + new_value=None, + response_id=None, + timestamp=None, + ): self.request_id = request_id self.user_guid = user_guid self.response_id = response_id @@ -1000,18 +1088,17 @@ def __init__(self, self.timestamp = timestamp or datetime.utcnow() def __repr__(self): - return '' % self.id + return "" % self.id @property def affected_user(self): if self.new_value is not None and "user_guid" in self.new_value: - return Users.query.filter_by( - guid=self.new_value["user_guid"] - ).one() + return Users.query.filter_by(guid=self.new_value["user_guid"]).one() class RowContent(object): - - def __init__(self, event, verb, string, affected_user=None, no_user_string=None): + def __init__( + self, event, verb, string, affected_user=None, no_user_string=None + ): """ :param verb: action describing event :param string: format()-ready string where first field is verb and @@ -1032,7 +1119,7 @@ def __str__(self): if self.no_user_string is not None and self.event.user is None: string = self.no_user_string else: - string = ' '.join((self.event.user.name, self.string)) + string = " ".join((self.event.user.name, self.string)) return string.format(*format_args) @staticmethod @@ -1047,67 +1134,96 @@ def history_row_content(self): """ if self.type == event_type.REQ_STATUS_CHANGED: return "This request's status was changed to:
{}".format( - self.new_value['status']) + self.new_value["status"] + ) valid_types = { - event_type.USER_ADDED: - self.RowContent(self, "added", "{} user: {}.", self.affected_user, "User {}: {}."), - event_type.USER_REMOVED: - self.RowContent(self, "removed", "{} user: {}.", self.affected_user), - event_type.USER_PERM_CHANGED: - self.RowContent(self, "changed", "{} permssions for user: {}.", self.affected_user), - event_type.REQUESTER_INFO_EDITED: - self.RowContent(self, "changed", "{} the requester's information."), - event_type.REQ_CREATED: - self.RowContent(self, "created", "{} this request."), - event_type.AGENCY_REQ_CREATED: - self.RowContent(self, "created", "{} this request on behalf of {}.", self.request.requester), - event_type.REQ_ACKNOWLEDGED: - self.RowContent(self, "acknowledged", "{} this request."), - event_type.REQ_EXTENDED: - self.RowContent(self, "extended", "{} this request."), - event_type.REQ_CLOSED: - self.RowContent(self, "closed", "{} this request."), - event_type.REQ_DENIED: - self.RowContent(self, "denied", "{} this request."), - event_type.REQ_REOPENED: - self.RowContent(self, "re-opened", "{} this request."), - event_type.REQ_TITLE_EDITED: - self.RowContent(self, "changed", "{} the title."), - event_type.REQ_AGENCY_REQ_SUM_EDITED: - self.RowContent(self, "changed", "{} the agency request summary."), - event_type.REQ_TITLE_PRIVACY_EDITED: - self.RowContent(self, "changed", "{} the title privacy."), - event_type.REQ_AGENCY_REQ_SUM_PRIVACY_EDITED: - self.RowContent(self, "changed", "{} the agency request summary privacy."), - event_type.FILE_ADDED: - self.RowContent(self, "added", "{} a file response."), - event_type.FILE_EDITED: - self.RowContent(self, "changed", "{} a file response."), - event_type.FILE_REMOVED: - self.RowContent(self, "deleted", "{} a file response."), - event_type.LINK_ADDED: - self.RowContent(self, "added", "{} a link response."), - event_type.LINK_EDITED: - self.RowContent(self, "changed", "{} a link response."), - event_type.LINK_REMOVED: - self.RowContent(self, "deleted", "{} a link response."), - event_type.INSTRUCTIONS_ADDED: - self.RowContent(self, "added", "{} an offline instructions response."), - event_type.INSTRUCTIONS_EDITED: - self.RowContent(self, "changed", "{} an offline instructions response."), - event_type.INSTRUCTIONS_REMOVED: - self.RowContent(self, "deleted", "{} an offline instructions response."), - event_type.NOTE_ADDED: - self.RowContent(self, "added", "{} a note response."), - event_type.NOTE_EDITED: - self.RowContent(self, "changed", "{} a note response."), - event_type.NOTE_REMOVED: - self.RowContent(self, "deleted", "{} a note response."), - event_type.ENVELOPE_CREATED: - self.RowContent(self, "created", "{} an envelope."), - event_type.RESPONSE_LETTER_CREATED: - self.RowContent(self, "added", "{} a letter.") + event_type.USER_ADDED: self.RowContent( + self, "added", "{} user: {}.", self.affected_user, "User {}: {}." + ), + event_type.USER_REMOVED: self.RowContent( + self, "removed", "{} user: {}.", self.affected_user + ), + event_type.USER_PERM_CHANGED: self.RowContent( + self, "changed", "{} permssions for user: {}.", self.affected_user + ), + event_type.REQUESTER_INFO_EDITED: self.RowContent( + self, "changed", "{} the requester's information." + ), + event_type.REQ_CREATED: self.RowContent( + self, "created", "{} this request." + ), + event_type.AGENCY_REQ_CREATED: self.RowContent( + self, + "created", + "{} this request on behalf of {}.", + self.request.requester, + ), + event_type.REQ_ACKNOWLEDGED: self.RowContent( + self, "acknowledged", "{} this request." + ), + event_type.REQ_EXTENDED: self.RowContent( + self, "extended", "{} this request." + ), + event_type.REQ_CLOSED: self.RowContent(self, "closed", "{} this request."), + event_type.REQ_DENIED: self.RowContent(self, "denied", "{} this request."), + event_type.REQ_REOPENED: self.RowContent( + self, "re-opened", "{} this request." + ), + event_type.REQ_TITLE_EDITED: self.RowContent( + self, "changed", "{} the title." + ), + event_type.REQ_AGENCY_REQ_SUM_EDITED: self.RowContent( + self, "changed", "{} the agency request summary." + ), + event_type.REQ_TITLE_PRIVACY_EDITED: self.RowContent( + self, "changed", "{} the title privacy." + ), + event_type.REQ_AGENCY_REQ_SUM_PRIVACY_EDITED: self.RowContent( + self, "changed", "{} the agency request summary privacy." + ), + event_type.FILE_ADDED: self.RowContent( + self, "added", "{} a file response." + ), + event_type.FILE_EDITED: self.RowContent( + self, "changed", "{} a file response." + ), + event_type.FILE_REMOVED: self.RowContent( + self, "deleted", "{} a file response." + ), + event_type.LINK_ADDED: self.RowContent( + self, "added", "{} a link response." + ), + event_type.LINK_EDITED: self.RowContent( + self, "changed", "{} a link response." + ), + event_type.LINK_REMOVED: self.RowContent( + self, "deleted", "{} a link response." + ), + event_type.INSTRUCTIONS_ADDED: self.RowContent( + self, "added", "{} an offline instructions response." + ), + event_type.INSTRUCTIONS_EDITED: self.RowContent( + self, "changed", "{} an offline instructions response." + ), + event_type.INSTRUCTIONS_REMOVED: self.RowContent( + self, "deleted", "{} an offline instructions response." + ), + event_type.NOTE_ADDED: self.RowContent( + self, "added", "{} a note response." + ), + event_type.NOTE_EDITED: self.RowContent( + self, "changed", "{} a note response." + ), + event_type.NOTE_REMOVED: self.RowContent( + self, "deleted", "{} a note response." + ), + event_type.ENVELOPE_CREATED: self.RowContent( + self, "created", "{} an envelope." + ), + event_type.RESPONSE_LETTER_CREATED: self.RowContent( + self, "added", "{} a letter." + ), } if self.type in valid_types: @@ -1125,45 +1241,49 @@ class Responses(db.Model): content - a JSON object that contains the content for all the possible responses a request can have privacy - an Enum containing the privacy options for a response """ - __tablename__ = 'responses' + + __tablename__ = "responses" id = db.Column(db.Integer, primary_key=True) - request_id = db.Column(db.String(19), db.ForeignKey('requests.id')) - privacy = db.Column(db.Enum( - response_privacy.PRIVATE, - response_privacy.RELEASE_AND_PRIVATE, - response_privacy.RELEASE_AND_PUBLIC, - name="privacy")) + request_id = db.Column(db.String(19), db.ForeignKey("requests.id")) + privacy = db.Column( + db.Enum( + response_privacy.PRIVATE, + response_privacy.RELEASE_AND_PRIVATE, + response_privacy.RELEASE_AND_PUBLIC, + name="privacy", + ) + ) date_modified = db.Column(db.DateTime) release_date = db.Column(db.DateTime) deleted = db.Column(db.Boolean, default=False, nullable=False) is_editable = db.Column(db.Boolean, default=False, nullable=False) - type = db.Column(db.Enum( - response_type.NOTE, - response_type.LINK, - response_type.FILE, - response_type.INSTRUCTIONS, - response_type.DETERMINATION, - response_type.EMAIL, - response_type.LETTER, - response_type.ENVELOPE, - name='type' - )) - - __mapper_args__ = {'polymorphic_on': type} + type = db.Column( + db.Enum( + response_type.NOTE, + response_type.LINK, + response_type.FILE, + response_type.INSTRUCTIONS, + response_type.DETERMINATION, + response_type.EMAIL, + response_type.LETTER, + response_type.ENVELOPE, + name="type", + ) + ) + + __mapper_args__ = {"polymorphic_on": type} # TODO: overwrite filter to automatically check if deleted=False - def __init__(self, - request_id, - privacy, - date_modified=None, - is_editable=False): + def __init__(self, request_id, privacy, date_modified=None, is_editable=False): self.request_id = request_id self.privacy = privacy self.date_modified = date_modified or datetime.utcnow() - self.release_date = (calendar.addbusdays(datetime.utcnow(), RELEASE_PUBLIC_DAYS) - if privacy == response_privacy.RELEASE_AND_PUBLIC - else None) + self.release_date = ( + calendar.addbusdays(datetime.utcnow(), RELEASE_PUBLIC_DAYS) + if privacy == response_privacy.RELEASE_AND_PUBLIC + else None + ) self.is_editable = is_editable # NOTE: If you can find a way to make this class work with abc, @@ -1176,24 +1296,29 @@ def preview(self): @property def val_for_events(self): """ JSON to store in Events 'new_value' field. """ - val = { - c.name: getattr(self, c.name) - for c in self.__table__.columns - } - val.pop('id') - val['privacy'] = self.privacy + val = {c.name: getattr(self, c.name) for c in self.__table__.columns} + val.pop("id") + val["privacy"] = self.privacy return val @property def is_public(self): - return (self.privacy == response_privacy.RELEASE_AND_PUBLIC and - self.release_date is not None and - datetime.utcnow() > self.release_date) + return ( + self.privacy == response_privacy.RELEASE_AND_PUBLIC + and self.release_date is not None + and datetime.utcnow() > self.release_date + ) @property def creator(self): - return Events.query.filter(Events.response_id == self.id, - Events.type.in_(event_type.RESPONSE_ADDED_TYPES)).one().user + return ( + Events.query.filter( + Events.response_id == self.id, + Events.type.in_(event_type.RESPONSE_ADDED_TYPES), + ) + .one() + .user + ) @property def communication_method_type(self): @@ -1203,9 +1328,14 @@ def communication_method_type(self): :return: response_type.LETTER or response_type.EMAIL :rtype: str """ - communication_methods = CommunicationMethods.query.filter_by(response_id=self.id).all() - return response_type.LETTER if response_type.LETTER in [cm.method_type for cm in - communication_methods] else response_type.EMAIL + communication_methods = CommunicationMethods.query.filter_by( + response_id=self.id + ).all() + return ( + response_type.LETTER + if response_type.LETTER in [cm.method_type for cm in communication_methods] + else response_type.EMAIL + ) @property def event_timestamp(self): @@ -1214,7 +1344,11 @@ def event_timestamp(self): the newest timestamp of the newest event which will be displayed on the frontend. :return: timestamp of the newest event row associated with a response """ - timestamps = Events.query.filter_by(response_id=self.id).order_by(desc(Events.timestamp)).all() + timestamps = ( + Events.query.filter_by(response_id=self.id) + .order_by(desc(Events.timestamp)) + .all() + ) if timestamps: return timestamps[0].timestamp return self.date_modified @@ -1225,7 +1359,7 @@ def make_public(self): db.session.commit() def __repr__(self): - return '' % self.id + return "" % self.id class Reasons(db.Model): @@ -1241,35 +1375,40 @@ class Reasons(db.Model): Reason are based off the Law Department's responses. """ - __tablename__ = 'reasons' + + __tablename__ = "reasons" id = db.Column(db.Integer, primary_key=True) - type = db.Column(db.Enum( - determination_type.CLOSING, - determination_type.DENIAL, - determination_type.REOPENING, - name="reason_type" - ), nullable=False) - agency_ein = db.Column(db.String(4), db.ForeignKey('agencies.ein')) + type = db.Column( + db.Enum( + determination_type.CLOSING, + determination_type.DENIAL, + determination_type.REOPENING, + name="reason_type", + ), + nullable=False, + ) + agency_ein = db.Column(db.String(4), db.ForeignKey("agencies.ein")) title = db.Column(db.String, nullable=False) content = db.Column(db.String, nullable=False) has_appeals_language = db.Column(db.Boolean, default=True) @classmethod def populate(cls): - with open(current_app.config['REASON_DATA'], 'r') as data: + with open(current_app.config["REASON_DATA"], "r") as data: dictreader = csv.DictReader(data) for row in dictreader: - agency_ein = row['agency_ein'] if row['agency_ein'] else None + agency_ein = row["agency_ein"] if row["agency_ein"] else None reason = cls( - type=row['type'], - title=row['title'], - content=row['content'], - has_appeals_language=eval_request_bool(row['has_appeals_language']), - agency_ein=agency_ein + type=row["type"], + title=row["title"], + content=row["content"], + has_appeals_language=eval_request_bool(row["has_appeals_language"]), + agency_ein=agency_ein, ) - if not Reasons.query.filter_by(title=row['title'], content=row['content'], - agency_ein=agency_ein).first(): + if not Reasons.query.filter_by( + title=row["title"], content=row["content"], agency_ein=agency_ein + ).first(): db.session.add(reason) db.session.commit() @@ -1289,13 +1428,19 @@ class UserRequests(db.Model): for public information. point_of_contact = a boolean to determine the point of contact of a request """ - __tablename__ = 'user_requests' + + __tablename__ = "user_requests" user_guid = db.Column(db.String(64), primary_key=True) - request_id = db.Column(db.String(19), db.ForeignKey("requests.id"), primary_key=True) + request_id = db.Column( + db.String(19), db.ForeignKey("requests.id"), primary_key=True + ) request_user_type = db.Column( - db.Enum(user_type_request.REQUESTER, - user_type_request.AGENCY, - name='request_user_type')) + db.Enum( + user_type_request.REQUESTER, + user_type_request.AGENCY, + name="request_user_type", + ) + ) permissions = db.Column(db.BigInteger) point_of_contact = db.Column(db.Boolean, default=False) # Note: If an anonymous user creates a request, they will be listed in the UserRequests table, but will have the @@ -1303,11 +1448,7 @@ class UserRequests(db.Model): # current anonymous user is in fact the requester. __table_args__ = ( - db.ForeignKeyConstraint( - [user_guid], - [Users.guid], - onupdate="CASCADE" - ), + db.ForeignKeyConstraint([user_guid], [Users.guid], onupdate="CASCADE"), ) @property @@ -1319,7 +1460,7 @@ def val_for_events(self): "user_guid": self.user_guid, "request_user_type": self.request_user_type, "permissions": self.permissions, - "point_of_contact": self.point_of_contact + "point_of_contact": self.point_of_contact, } def has_permission(self, perm): @@ -1355,7 +1496,9 @@ def set_permissions(self, permissions): db.session.commit() def get_permission_choice_indices(self): - return [i for i, p in enumerate(permission.ALL) if bool(self.permissions & p.value)] + return [ + i for i, p in enumerate(permission.ALL) if bool(self.permissions & p.value) + ] class ResponseTokens(db.Model): @@ -1367,15 +1510,15 @@ class ResponseTokens(db.Model): response_id - a foreign key that links to a response's primary key expiration_date - a datetime object containing the date at which this token becomes invalid """ - __tablename__ = 'response_tokens' + + __tablename__ = "response_tokens" id = db.Column(db.Integer, primary_key=True) token = db.Column(db.String, nullable=False) response_id = db.Column(db.Integer, db.ForeignKey("responses.id"), nullable=False) response = db.relationship("Responses", backref=db.backref("token", uselist=False)) - def __init__(self, - response_id): + def __init__(self, response_id): self.token = self.generate_token() self.response_id = response_id @@ -1391,21 +1534,16 @@ class Notes(Responses): id - an integer that is the primary key of Notes content - a string that contains the content of a note """ + __tablename__ = response_type.NOTE - __mapper_args__ = {'polymorphic_identity': response_type.NOTE} + __mapper_args__ = {"polymorphic_identity": response_type.NOTE} id = db.Column(db.Integer, db.ForeignKey(Responses.id), primary_key=True) content = db.Column(db.String(5000)) - def __init__(self, - request_id, - privacy, - content, - date_modified=None, - is_editable=False): - super(Notes, self).__init__(request_id, - privacy, - date_modified, - is_editable) + def __init__( + self, request_id, privacy, content, date_modified=None, is_editable=False + ): + super(Notes, self).__init__(request_id, privacy, date_modified, is_editable) self.content = content @property @@ -1424,8 +1562,9 @@ class Files(Responses): size - a string containing the size of a file hash - a string containing the sha1 hash of a file """ + __tablename__ = response_type.FILE - __mapper_args__ = {'polymorphic_identity': response_type.FILE} + __mapper_args__ = {"polymorphic_identity": response_type.FILE} id = db.Column(db.Integer, db.ForeignKey(Responses.id), primary_key=True) title = db.Column(db.String(250)) name = db.Column(db.String) @@ -1433,41 +1572,31 @@ class Files(Responses): size = db.Column(db.Integer) hash = db.Column(db.String) - def __init__(self, - request_id, - privacy, - title, - name, - mime_type, - size, - hash_, - date_modified=None, - is_editable=False): + def __init__( + self, + request_id, + privacy, + title, + name, + mime_type, + size, + hash_, + date_modified=None, + is_editable=False, + ): try: file_exists = Files.query.filter_by(request_id=request_id, hash=hash_).all() for file in file_exists: if not file.deleted: - raise DuplicateFileException( - file_name=name, - request_id=request_id - ) + raise DuplicateFileException(file_name=name, request_id=request_id) except DuplicateFileException: sentry.captureException() - raise DuplicateFileException( - file_name=name, - request_id=request_id - ) + raise DuplicateFileException(file_name=name, request_id=request_id) except MultipleResultsFound: sentry.captureException() - raise DuplicateFileException( - file_name=name, - request_id=request_id - ) + raise DuplicateFileException(file_name=name, request_id=request_id) - super(Files, self).__init__(request_id, - privacy, - date_modified, - is_editable) + super(Files, self).__init__(request_id, privacy, date_modified, is_editable) self.name = name self.mime_type = mime_type self.title = title @@ -1487,23 +1616,17 @@ class Links(Responses): title - a string containing the title of a link url - a string containing the url link """ + __tablename__ = response_type.LINK - __mapper_args__ = {'polymorphic_identity': response_type.LINK} + __mapper_args__ = {"polymorphic_identity": response_type.LINK} id = db.Column(db.Integer, db.ForeignKey(Responses.id), primary_key=True) title = db.Column(db.String) url = db.Column(db.String) - def __init__(self, - request_id, - privacy, - title, - url, - date_modified=None, - is_editable=False): - super(Links, self).__init__(request_id, - privacy, - date_modified, - is_editable) + def __init__( + self, request_id, privacy, title, url, date_modified=None, is_editable=False + ): + super(Links, self).__init__(request_id, privacy, date_modified, is_editable) self.title = title self.url = url @@ -1519,21 +1642,18 @@ class Instructions(Responses): id - an integer that is the primary key of Instructions content - a string containing the content of an instruction """ + __tablename__ = response_type.INSTRUCTIONS - __mapper_args__ = {'polymorphic_identity': response_type.INSTRUCTIONS} + __mapper_args__ = {"polymorphic_identity": response_type.INSTRUCTIONS} id = db.Column(db.Integer, db.ForeignKey(Responses.id), primary_key=True) content = db.Column(db.String) - def __init__(self, - request_id, - privacy, - content, - date_modified=None, - is_editable=False): - super(Instructions, self).__init__(request_id, - privacy, - date_modified, - is_editable) + def __init__( + self, request_id, privacy, content, date_modified=None, is_editable=False + ): + super(Instructions, self).__init__( + request_id, privacy, date_modified, is_editable + ) self.content = content @property @@ -1552,8 +1672,9 @@ class Emails(Responses): subject - a string containing the subject of an email email_content - a string containing the content of an email """ + __tablename__ = response_type.EMAIL - __mapper_args__ = {'polymorphic_identity': response_type.EMAIL} + __mapper_args__ = {"polymorphic_identity": response_type.EMAIL} id = db.Column(db.Integer, db.ForeignKey(Responses.id), primary_key=True) to = db.Column(db.String) cc = db.Column(db.String) @@ -1561,20 +1682,19 @@ class Emails(Responses): subject = db.Column(db.String(5000)) body = db.Column(db.String) - def __init__(self, - request_id, - privacy, - to, - cc, - bcc, - subject, - body, - date_modified=None, - is_editable=False): - super(Emails, self).__init__(request_id, - privacy, - date_modified, - is_editable) + def __init__( + self, + request_id, + privacy, + to, + cc, + bcc, + subject, + body, + date_modified=None, + is_editable=False, + ): + super(Emails, self).__init__(request_id, privacy, date_modified, is_editable) self.to = to self.cc = cc self.bcc = bcc @@ -1587,10 +1707,7 @@ def preview(self): @property def val_for_events(self): - return { - 'privacy': self.privacy, - 'body': self.body, - } + return {"privacy": self.privacy, "body": self.body} class Envelopes(Responses): @@ -1600,23 +1717,16 @@ class Envelopes(Responses): id - an integer that is the primary key of Envelopes (FK to Responses) latex - the latex used to generate the envelope PDF """ + __tablename__ = response_type.ENVELOPE - __mapper_args__ = {'polymorphic_identity': response_type.ENVELOPE} + __mapper_args__ = {"polymorphic_identity": response_type.ENVELOPE} id = db.Column(db.Integer, db.ForeignKey(Responses.id), primary_key=True) latex = db.Column(db.String) - def __init__(self, - request_id, - privacy, - latex, - date_modified=None, - is_editable=False): - super(Envelopes, self).__init__( - request_id, - privacy, - date_modified, - is_editable - ) + def __init__( + self, request_id, privacy, latex, date_modified=None, is_editable=False + ): + super(Envelopes, self).__init__(request_id, privacy, date_modified, is_editable) self.latex = latex @property @@ -1634,26 +1744,32 @@ class EnvelopeTemplates(db.Model): title - a short descriptor for the envelope template template_name - the name of the template to be loaded from the filesystem """ - __tablename__ = 'envelope_templates' + + __tablename__ = "envelope_templates" id = db.Column(db.Integer, primary_key=True) - agency_ein = db.Column(db.String(4), db.ForeignKey('agencies.ein')) + agency_ein = db.Column(db.String(4), db.ForeignKey("agencies.ein")) title = db.Column(db.String, nullable=False) template_name = db.Column(db.String, nullable=False) @classmethod def populate(cls, csv_name=None): - filename = csv_name or current_app.config['ENVELOPE_TEMPLATES_DATA'] + filename = csv_name or current_app.config["ENVELOPE_TEMPLATES_DATA"] print(filename) - with open(filename, 'r') as data: + with open(filename, "r") as data: dictreader = csv.DictReader(data) for row in dictreader: - if EnvelopeTemplates.query.filter_by(agency_ein=row['agency_ein'], - title=row['title'], - template_name=row['template_name']).one_or_none() is None: + if ( + EnvelopeTemplates.query.filter_by( + agency_ein=row["agency_ein"], + title=row["title"], + template_name=row["template_name"], + ).one_or_none() + is None + ): template = EnvelopeTemplates( - agency_ein=row['agency_ein'], - title=row['title'], - template_name=row['template_name'] + agency_ein=row["agency_ein"], + title=row["title"], + template_name=row["template_name"], ) db.session.add(template) @@ -1667,23 +1783,17 @@ class Letters(Responses): id - an integer that is the primary key of Letters (FK to Responses) content - A string containing the content of a letter (HTML Formatted) """ + __tablename__ = response_type.LETTER - __mapper_args__ = {'polymorphic_identity': response_type.LETTER} + __mapper_args__ = {"polymorphic_identity": response_type.LETTER} id = db.Column(db.Integer, db.ForeignKey(Responses.id), primary_key=True) title = db.Column(db.String, nullable=False) content = db.Column(db.String) - def __init__(self, - request_id, - privacy, - title, - content, - date_modified=None, - is_editable=False): - super(Letters, self).__init__(request_id, - privacy, - date_modified, - is_editable) + def __init__( + self, request_id, privacy, title, content, date_modified=None, is_editable=False + ): + super(Letters, self).__init__(request_id, privacy, date_modified, is_editable) self.title = title self.content = content @@ -1705,36 +1815,46 @@ class LetterTemplates(db.Model): Reason are based off the Law Department's responses. """ - __tablename__ = 'letter_templates' + + __tablename__ = "letter_templates" id = db.Column(db.Integer, primary_key=True) - type_ = db.Column(db.Enum( - determination_type.ACKNOWLEDGMENT, - determination_type.EXTENSION, - determination_type.CLOSING, - determination_type.DENIAL, - determination_type.REOPENING, - response_type.LETTER, - name="letter_type" - ), nullable=False, name='type') - agency_ein = db.Column(db.String(4), db.ForeignKey('agencies.ein')) + type_ = db.Column( + db.Enum( + determination_type.ACKNOWLEDGMENT, + determination_type.EXTENSION, + determination_type.CLOSING, + determination_type.DENIAL, + determination_type.REOPENING, + response_type.LETTER, + name="letter_type", + ), + nullable=False, + name="type", + ) + agency_ein = db.Column(db.String(4), db.ForeignKey("agencies.ein")) title = db.Column(db.String, nullable=False) content = db.Column(db.String, nullable=False) @classmethod def populate(cls, csv_name=None): - filename = csv_name or current_app.config['LETTER_TEMPLATES_DATA'] - with open(filename, 'r') as data: + filename = csv_name or current_app.config["LETTER_TEMPLATES_DATA"] + with open(filename, "r") as data: dictreader = csv.DictReader(data) for row in dictreader: - if LetterTemplates.query.filter_by(type_=row['type'], - agency_ein=row['agency_ein'], - title=row['title'], - content=row['content']).one_or_none() is None: + if ( + LetterTemplates.query.filter_by( + type_=row["type"], + agency_ein=row["agency_ein"], + title=row["title"], + content=row["content"], + ).one_or_none() + is None + ): template = LetterTemplates( - type_=row['type'], - agency_ein=row['agency_ein'], - title=row['title'], - content=row['content'] + type_=row["type"], + agency_ein=row["agency_ein"], + title=row["title"], + content=row["content"], ) db.session.add(template) @@ -1759,42 +1879,56 @@ class Determinations(Responses): reopening | new estimated date of completion | N/A """ + __tablename__ = response_type.DETERMINATION - __mapper_args__ = {'polymorphic_identity': response_type.DETERMINATION} + __mapper_args__ = {"polymorphic_identity": response_type.DETERMINATION} id = db.Column(db.Integer, db.ForeignKey(Responses.id), primary_key=True) - dtype = db.Column(db.Enum( - determination_type.DENIAL, - determination_type.ACKNOWLEDGMENT, - determination_type.EXTENSION, - determination_type.CLOSING, - determination_type.REOPENING, - name="determination_type" - ), nullable=False) + dtype = db.Column( + db.Enum( + determination_type.DENIAL, + determination_type.ACKNOWLEDGMENT, + determination_type.EXTENSION, + determination_type.CLOSING, + determination_type.REOPENING, + name="determination_type", + ), + nullable=False, + ) reason = db.Column(db.String) # nullable only for acknowledge and re-opening date = db.Column(db.DateTime) # nullable only for denial, closing - def __init__(self, - request_id, - privacy, # TODO: always RELEASE_AND_PUBLIC? - dtype, - reason, - date=None, - date_modified=None, - is_editable=False): - super(Determinations, self).__init__(request_id, - privacy, - date_modified, - is_editable) + def __init__( + self, + request_id, + privacy, # TODO: always RELEASE_AND_PUBLIC? + dtype, + reason, + date=None, + date_modified=None, + is_editable=False, + ): + super(Determinations, self).__init__( + request_id, privacy, date_modified, is_editable + ) self.dtype = dtype - if dtype not in (determination_type.ACKNOWLEDGMENT, - determination_type.REOPENING) and reason is None: - raise InvalidDeterminationException(request_id=request_id, dtype=dtype, missing_field='reason') + if ( + dtype + not in (determination_type.ACKNOWLEDGMENT, determination_type.REOPENING) + and reason is None + ): + raise InvalidDeterminationException( + request_id=request_id, dtype=dtype, missing_field="reason" + ) self.reason = reason - if dtype not in (determination_type.DENIAL, - determination_type.CLOSING) and date is None: - raise InvalidDeterminationException(request_id=request_id, dtype=dtype, missing_field='date') + if ( + dtype not in (determination_type.DENIAL, determination_type.CLOSING) + and date is None + ): + raise InvalidDeterminationException( + request_id=request_id, dtype=dtype, missing_field="date" + ) self.date = date @property @@ -1803,13 +1937,13 @@ def preview(self): @property def val_for_events(self): - val = { - 'reason': self.reason - } - if self.dtype in (determination_type.ACKNOWLEDGMENT, - determination_type.EXTENSION, - determination_type.REOPENING): - val['due_date'] = self.date.isoformat() + val = {"reason": self.reason} + if self.dtype in ( + determination_type.ACKNOWLEDGMENT, + determination_type.EXTENSION, + determination_type.REOPENING, + ): + val["due_date"] = self.date.isoformat() return val @@ -1825,19 +1959,18 @@ class CommunicationMethods(db.Model): method_id - an integer that is a primary key of CommunicationMethods (FK to Responses) method_type - enum ('letters', 'emails') method associated with the response """ - __tablename__ = 'communication_methods' + + __tablename__ = "communication_methods" response_id = db.Column(db.Integer, db.ForeignKey(Responses.id), primary_key=True) method_id = db.Column(db.Integer, db.ForeignKey(Responses.id), primary_key=True) - method_type = db.Column(db.Enum( - response_type.LETTER, - response_type.EMAIL, - name='communication_method_type' - ), nullable=True) - - def __init__(self, - response_id, - method_id, - method_type): + method_type = db.Column( + db.Enum( + response_type.LETTER, response_type.EMAIL, name="communication_method_type" + ), + nullable=True, + ) + + def __init__(self, response_id, method_id, method_type): self.response_id = response_id self.method_id = method_id self.method_type = method_type @@ -1856,9 +1989,10 @@ class CustomRequestForms(db.Model): category - an integer to separate different types of custom forms for an agency minimum_required - an integer to dictates the minimum amount of fields required for a successful submission """ - __tablename__ = 'custom_request_forms' + + __tablename__ = "custom_request_forms" id = db.Column(db.Integer, primary_key=True) - agency_ein = db.Column(db.String(4), db.ForeignKey('agencies.ein'), nullable=False) + agency_ein = db.Column(db.String(4), db.ForeignKey("agencies.ein"), nullable=False) form_name = db.Column(db.String, nullable=False) form_description = db.Column(db.String, nullable=False) field_definitions = db.Column(JSONB, nullable=False) @@ -1871,24 +2005,32 @@ def populate(cls, json_name=None): """ Automatically populate the custom_request_forms table for the OpenRecords application. """ - filename = json_name or current_app.config['CUSTOM_REQUEST_FORMS_DATA'] - with open(filename, 'r') as data: + filename = json_name or current_app.config["CUSTOM_REQUEST_FORMS_DATA"] + with open(filename, "r") as data: data = json.load(data) - for form in data['custom_request_forms']: - if CustomRequestForms.query.filter_by(agency_ein=form['agency_ein'], - form_name=form['form_name']).first() is not None: - warn("Duplicate custom_request_form ({}); Row not imported".format(form['agency_ein']), - category=UserWarning) + for form in data["custom_request_forms"]: + if ( + CustomRequestForms.query.filter_by( + agency_ein=form["agency_ein"], form_name=form["form_name"] + ).first() + is not None + ): + warn( + "Duplicate custom_request_form ({}); Row not imported".format( + form["agency_ein"] + ), + category=UserWarning, + ) continue custom_request_form = cls( - agency_ein=form['agency_ein'], - form_name=form['form_name'], - form_description=form['form_description'], - field_definitions=form['field_definitions'], - repeatable=form['repeatable'], - category=form['category'], - minimum_required=form['minimum_required'] + agency_ein=form["agency_ein"], + form_name=form["form_name"], + form_description=form["form_description"], + field_definitions=form["field_definitions"], + repeatable=form["repeatable"], + category=form["category"], + minimum_required=form["minimum_required"], ) db.session.add(custom_request_form) db.session.commit() diff --git a/app/request/forms.py b/app/request/forms.py index 630bfe1dc..0b53f0aab 100644 --- a/app/request/forms.py +++ b/app/request/forms.py @@ -17,29 +17,18 @@ DateTimeField, SelectMultipleField, ) -from wtforms.validators import ( - Email, - Length, - InputRequired -) -from sqlalchemy import ( - or_, - asc -) +from wtforms.validators import Email, Length, InputRequired +from sqlalchemy import or_, asc from app.agency.api.utils import get_active_users_as_choices from app.constants import ( CATEGORIES, STATES, submission_methods, determination_type, - response_type + response_type, ) from app.lib.db_utils import get_agency_choices -from app.models import ( - Reasons, - LetterTemplates, - EnvelopeTemplates -) +from app.models import Reasons, LetterTemplates, EnvelopeTemplates class PublicUserRequestForm(Form): @@ -55,22 +44,22 @@ class PublicUserRequestForm(Form): """ # Request Information - request_category = SelectField('Category (optional)', choices=CATEGORIES) - request_agency = SelectField('Agency (required)', choices=None) - request_title = StringField('Request Title (required)') - request_type = SelectField('Request Type (required)', choices=[]) - request_description = TextAreaField('Request Description (required)') + request_category = SelectField("Category (optional)", choices=CATEGORIES) + request_agency = SelectField("Agency (required)", choices=None) + request_title = StringField("Request Title (required)") + request_type = SelectField("Request Type (required)", choices=[]) + request_description = TextAreaField("Request Description (required)") # File Upload - request_file = FileField('Upload File (optional, must be less than 20 Mb)') + request_file = FileField("Upload File (optional, must be less than 20 Mb)") # Submit Button - submit = SubmitField('Submit Request') + submit = SubmitField("Submit Request") def __init__(self): super(PublicUserRequestForm, self).__init__() self.request_agency.choices = get_agency_choices() - self.request_agency.choices.insert(0, ('', '')) + self.request_agency.choices.insert(0, ("", "")) class AgencyUserRequestForm(Form): @@ -97,38 +86,41 @@ class AgencyUserRequestForm(Form): """ # Request Information - request_agency = SelectField('Agency (required)', choices=None) - request_type = SelectField('Request Type (required)', choices=[]) - request_title = StringField('Request Title (required)') - request_description = TextAreaField('Request Description (required)') - request_date = DateTimeField("Date (required)", format="%m/%d/%Y", default=datetime.today) + request_agency = SelectField("Agency (required)", choices=None) + request_type = SelectField("Request Type (required)", choices=[]) + request_title = StringField("Request Title (required)") + request_description = TextAreaField("Request Description (required)") + request_date = DateTimeField( + "Date (required)", format="%m/%d/%Y", default=datetime.today + ) # Personal Information # TODO: when refactoring these classes, include length and other validators - first_name = StringField('First Name (required)') - last_name = StringField('Last Name (required)') - user_title = StringField('Title') - user_organization = StringField('Organization') + first_name = StringField("First Name (required)") + last_name = StringField("Last Name (required)") + user_title = StringField("Title") + user_organization = StringField("Organization") # Contact Information - email = StringField('Email') - phone = StringField('Phone') - fax = StringField('Fax') - address = StringField('Address Line 1') - address_two = StringField('Address Line 2') - city = StringField('City') - state = SelectField('State', choices=STATES, default='NY') - zipcode = StringField('Zip') + email = StringField("Email") + phone = StringField("Phone") + fax = StringField("Fax") + address = StringField("Address Line 1") + address_two = StringField("Address Line 2") + city = StringField("City") + state = SelectField("State / U.S. Territory", choices=STATES, default="NY") + zipcode = StringField("Zip") # Method Received - method_received = SelectField('Format Received (required)', - choices=submission_methods.AS_CHOICES) + method_received = SelectField( + "Format Received (required)", choices=submission_methods.AS_CHOICES + ) # File Upload - request_file = FileField('Upload File (optional, must be less than 20 Mb)') + request_file = FileField("Upload File (optional, must be less than 20 Mb)") # Submit Button - submit = SubmitField('Submit Request') + submit = SubmitField("Submit Request") def __init__(self): super(AgencyUserRequestForm, self).__init__() @@ -156,52 +148,53 @@ class AnonymousRequestForm(Form): fax: requester's fax number address, city, state, zip: requester's address """ + # Request Information - request_category = SelectField('Category (optional)', choices=CATEGORIES) - request_agency = SelectField('Agency (required)', choices=None) - request_type = SelectField('Request Type (required)', choices=[]) - request_title = StringField('Request Title (required)') - request_description = TextAreaField('Request Description (required)') + request_category = SelectField("Category (optional)", choices=CATEGORIES) + request_agency = SelectField("Agency (required)", choices=None) + request_type = SelectField("Request Type (required)", choices=[]) + request_title = StringField("Request Title (required)") + request_description = TextAreaField("Request Description (required)") # Personal Information - first_name = StringField('First Name (required)') - last_name = StringField('Last Name (required)') - user_title = StringField('Title') - user_organization = StringField('Organization') + first_name = StringField("First Name (required)") + last_name = StringField("Last Name (required)") + user_title = StringField("Title") + user_organization = StringField("Organization") # Contact Information - email = StringField('Email') - phone = StringField('Phone') - fax = StringField('Fax') - address = StringField('Address Line 1') - address_two = StringField('Address Line 2') - city = StringField('City') - state = SelectField('State', choices=STATES, default='NY') - zipcode = StringField('Zip') + email = StringField("Email") + phone = StringField("Phone") + fax = StringField("Fax") + address = StringField("Address Line 1") + address_two = StringField("Address Line 2") + city = StringField("City") + state = SelectField("State / U.S. Territory", choices=STATES, default="NY") + zipcode = StringField("Zip") # File Upload - request_file = FileField('Upload File (optional, must be less than 20 Mb)') + request_file = FileField("Upload File (optional, must be less than 20 Mb)") - submit = SubmitField('Submit Request') + submit = SubmitField("Submit Request") def __init__(self): super(AnonymousRequestForm, self).__init__() self.request_agency.choices = get_agency_choices() - self.request_agency.choices.insert(0, ('', '')) + self.request_agency.choices.insert(0, ("", "")) class EditRequesterForm(Form): # TODO: Add class docstring - email = StringField('Email') - phone = StringField('Phone Number') - fax = StringField('Fax Number') - address_one = StringField('Address Line 1') - address_two = StringField('Address Line 2') - city = StringField('City') - state = SelectField('State', choices=STATES) - zipcode = StringField('Zip Code') - title = StringField('Title') - organization = StringField('Organization') + email = StringField("Email") + phone = StringField("Phone Number") + fax = StringField("Fax Number") + address_one = StringField("Address Line 1") + address_two = StringField("Address Line 2") + city = StringField("City") + state = SelectField("State / U.S. Territory", choices=STATES) + zipcode = StringField("Zip Code") + title = StringField("Title") + organization = StringField("Organization") def __init__(self, requester): """ @@ -230,43 +223,49 @@ def __init__(self, agency_ein): (reason.id, reason.title) for reason in Reasons.query.filter( Reasons.type == determination_type.CLOSING, - Reasons.agency_ein == agency_ein - ).order_by(asc(Reasons.id))] + Reasons.agency_ein == agency_ein, + ).order_by(asc(Reasons.id)) + ] agency_denials = [ (reason.id, reason.title) for reason in Reasons.query.filter( Reasons.type == determination_type.DENIAL, - Reasons.agency_ein == agency_ein - ).order_by(asc(Reasons.id))] + Reasons.agency_ein == agency_ein, + ).order_by(asc(Reasons.id)) + ] agency_reopenings = [ (reason.id, reason.title) for reason in Reasons.query.filter( Reasons.type == determination_type.REOPENING, - Reasons.agency_ein == agency_ein + Reasons.agency_ein == agency_ein, ).order_by(asc(Reasons.id)) ] default_closings = [ (reason.id, reason.title) for reason in Reasons.query.filter( - Reasons.type == determination_type.CLOSING, - Reasons.agency_ein == None - ).order_by(asc(Reasons.id))] + Reasons.type == determination_type.CLOSING, Reasons.agency_ein == None + ).order_by(asc(Reasons.id)) + ] default_denials = [ (reason.id, reason.title) for reason in Reasons.query.filter( - Reasons.type == determination_type.DENIAL, - Reasons.agency_ein == None - ).order_by(asc(Reasons.id))] + Reasons.type == determination_type.DENIAL, Reasons.agency_ein == None + ).order_by(asc(Reasons.id)) + ] default_reopenings = [ (reason.id, reason.title) for reason in Reasons.query.filter( - Reasons.type == determination_type.REOPENING, - Reasons.agency_ein == None - ).order_by(asc(Reasons.id))] + Reasons.type == determination_type.REOPENING, Reasons.agency_ein == None + ).order_by(asc(Reasons.id)) + ] - if determination_type.CLOSING in self.ultimate_determination_type and \ - determination_type.DENIAL in self.ultimate_determination_type: - self.reasons.choices = agency_closings + agency_denials + default_closings + default_denials + if ( + determination_type.CLOSING in self.ultimate_determination_type + and determination_type.DENIAL in self.ultimate_determination_type + ): + self.reasons.choices = ( + agency_closings + agency_denials + default_closings + default_denials + ) elif determination_type.DENIAL in self.ultimate_determination_type: self.reasons.choices = agency_denials + default_denials elif determination_type.REOPENING in self.ultimate_determination_type: @@ -285,19 +284,22 @@ def ultimate_determination_type(self): class DenyRequestForm(DeterminationForm): # TODO: Add class docstring - reasons = SelectMultipleField('Reasons for Denial (Choose 1 or more)') + reasons = SelectMultipleField("Reasons for Denial (Choose 1 or more)") ultimate_determination_type = [determination_type.DENIAL] class CloseRequestForm(DeterminationForm): # TODO: Add class docstring - reasons = SelectMultipleField('Reasons for Closing (Choose 1 or more)') - ultimate_determination_type = [determination_type.CLOSING, determination_type.DENIAL] + reasons = SelectMultipleField("Reasons for Closing (Choose 1 or more)") + ultimate_determination_type = [ + determination_type.CLOSING, + determination_type.DENIAL, + ] class ReopenRequestForm(DeterminationForm): # TODO: Add class docstring - reasons = SelectField('Reason for Re-Opening') + reasons = SelectField("Reason for Re-Opening") ultimate_determination_type = [determination_type.REOPENING] @@ -343,10 +345,11 @@ def __init__(self, agency_ein): LetterTemplates.type_.in_(self.letter_type), or_( LetterTemplates.agency_ein == agency_ein, - LetterTemplates.agency_ein == None - ) - )] - self.letter_templates.choices.insert(0, ('', '')) + LetterTemplates.agency_ein == None, + ), + ) + ] + self.letter_templates.choices.insert(0, ("", "")) @property def letter_templates(self): @@ -361,19 +364,19 @@ def letter_type(self): class GenerateAcknowledgmentLetterForm(GenerateLetterForm): # TODO: Add class docstring - letter_templates = SelectField('Letter Templates') + letter_templates = SelectField("Letter Templates") letter_type = [determination_type.ACKNOWLEDGMENT] class GenerateDenialLetterForm(GenerateLetterForm): # TODO: Add class docstring - letter_templates = SelectField('Letter Templates') + letter_templates = SelectField("Letter Templates") letter_type = [determination_type.DENIAL] class GenerateClosingLetterForm(GenerateLetterForm): # TODO: Add class docstring - letter_templates = SelectField('Letter Templates') + letter_templates = SelectField("Letter Templates") letter_type = [determination_type.CLOSING, determination_type.DENIAL] def __init__(self, agency_ein): @@ -382,84 +385,105 @@ def __init__(self, agency_ein): (letter.id, letter.title) for letter in LetterTemplates.query.filter( LetterTemplates.type_ == determination_type.CLOSING, - LetterTemplates.agency_ein == agency_ein - )] + LetterTemplates.agency_ein == agency_ein, + ) + ] agency_denials = [ (letter.id, letter.title) for letter in LetterTemplates.query.filter( LetterTemplates.type_ == determination_type.DENIAL, - LetterTemplates.agency_ein == agency_ein - )] + LetterTemplates.agency_ein == agency_ein, + ) + ] default_closings = [ (letter.id, letter.title) for letter in LetterTemplates.query.filter( LetterTemplates.type_ == determination_type.CLOSING, - LetterTemplates.agency_ein == None - )] + LetterTemplates.agency_ein == None, + ) + ] default_denials = [ (letter.id, letter.title) for letter in LetterTemplates.query.filter( LetterTemplates.type_ == determination_type.DENIAL, - LetterTemplates.agency_ein == None - )] - self.letter_templates.choices = agency_closings + agency_denials + default_closings + default_denials - self.letter_templates.choices.insert(0, ('', '')) + LetterTemplates.agency_ein == None, + ) + ] + self.letter_templates.choices = ( + agency_closings + agency_denials + default_closings + default_denials + ) + self.letter_templates.choices.insert(0, ("", "")) class GenerateExtensionLetterForm(GenerateLetterForm): # TODO: Add class docstring - letter_templates = SelectField('Letter Templates') + letter_templates = SelectField("Letter Templates") letter_type = [determination_type.EXTENSION] class GenerateReopeningLetterForm(GenerateLetterForm): # TODO: Add class docstring - letter_templates = SelectField('Letter Templates') + letter_templates = SelectField("Letter Templates") letter_type = [determination_type.REOPENING] class GenerateResponseLetterForm(GenerateLetterForm): # TODO: Add class docstring - letter_templates = SelectField('Letter Templates') + letter_templates = SelectField("Letter Templates") letter_type = [response_type.LETTER] class SearchRequestsForm(Form): # TODO: Add class docstring - agency_ein = SelectField('Agency') - agency_user = SelectField('User') + agency_ein = SelectField("Agency") + agency_user = SelectField("User") # category = SelectField('Category', get_categories()) def __init__(self): super(SearchRequestsForm, self).__init__() self.agency_ein.choices = get_agency_choices() - self.agency_ein.choices.insert(0, ('', 'All')) + self.agency_ein.choices.insert(0, ("", "All")) if current_user.is_agency: self.agency_ein.default = current_user.default_agency_ein - user_agencies = sorted([(agencies.ein, agencies.name) for agencies in current_user.agencies - if agencies.ein != current_user.default_agency_ein], - key=lambda x: x[1]) + user_agencies = sorted( + [ + (agencies.ein, agencies.name) + for agencies in current_user.agencies + if agencies.ein != current_user.default_agency_ein + ], + key=lambda x: x[1], + ) default_agency = current_user.default_agency # set default value of agency select field to agency user's primary agency self.agency_ein.default = default_agency.ein - self.agency_ein.choices.insert(1, self.agency_ein.choices.pop(self.agency_ein.choices.index( - (default_agency.ein, default_agency.name)) - )) + self.agency_ein.choices.insert( + 1, + self.agency_ein.choices.pop( + self.agency_ein.choices.index( + (default_agency.ein, default_agency.name) + ) + ), + ) # set secondary agencies to be below the primary for agency in user_agencies: - self.agency_ein.choices.insert(2, self.agency_ein.choices.pop(self.agency_ein.choices.index(agency))) + self.agency_ein.choices.insert( + 2, + self.agency_ein.choices.pop(self.agency_ein.choices.index(agency)), + ) # get choices for agency user select field if current_user.is_agency_admin(): - self.agency_user.choices = get_active_users_as_choices(current_user.default_agency.ein) + self.agency_user.choices = get_active_users_as_choices( + current_user.default_agency.ein + ) if current_user.is_agency_active() and not current_user.is_agency_admin(): self.agency_user.choices = [ - ('', 'All'), - (current_user.get_id(), 'My Requests') + ("", "All"), + (current_user.get_id(), "My Requests"), ] self.agency_user.default = current_user.get_id() @@ -469,17 +493,23 @@ def __init__(self): class ContactAgencyForm(Form): # TODO: Add class docstring - first_name = StringField(u'First Name', validators=[InputRequired(), Length(max=32)]) - last_name = StringField(u'Last Name', validators=[InputRequired(), Length(max=64)]) - email = StringField(u'Email', validators=[InputRequired(), Length(max=254), Email()]) - subject = StringField(u'Subject') - message = TextAreaField(u'Message', validators=[InputRequired(), Length(max=5000)]) - submit = SubmitField(u'Send') + first_name = StringField( + u"First Name", validators=[InputRequired(), Length(max=32)] + ) + last_name = StringField(u"Last Name", validators=[InputRequired(), Length(max=64)]) + email = StringField( + u"Email", validators=[InputRequired(), Length(max=254), Email()] + ) + subject = StringField(u"Subject") + message = TextAreaField(u"Message", validators=[InputRequired(), Length(max=5000)]) + submit = SubmitField(u"Send") def __init__(self, request): super(ContactAgencyForm, self).__init__() if current_user == request.requester: self.first_name.data = request.requester.first_name self.last_name.data = request.requester.last_name - self.email.data = request.requester.notification_email or request.requester.email + self.email.data = ( + request.requester.notification_email or request.requester.email + ) self.subject.data = "Inquiry about {}".format(request.id) diff --git a/app/request/views.py b/app/request/views.py index 77fd20f54..8b2076571 100644 --- a/app/request/views.py +++ b/app/request/views.py @@ -15,31 +15,18 @@ flash, Markup, jsonify, - abort + abort, ) from flask_login import current_user from sqlalchemy import any_ from sqlalchemy.orm.exc import NoResultFound from werkzeug.utils import escape -from app.constants import ( - request_status, - permission, - HIDDEN_AGENCIES -) -from app.lib.date_utils import ( - DEFAULT_YEARS_HOLIDAY_LIST, - get_holidays_date_list -) -from app.lib.permission_utils import ( - is_allowed -) +from app.constants import request_status, permission, HIDDEN_AGENCIES +from app.lib.date_utils import DEFAULT_YEARS_HOLIDAY_LIST, get_holidays_date_list +from app.lib.permission_utils import is_allowed from app.lib.utils import InvalidUserException, eval_request_bool -from app.models import ( - Requests, - Agencies, - UserRequests -) +from app.models import Requests, Agencies, UserRequests from app.request import request from app.request.forms import ( PublicUserRequestForm, @@ -56,19 +43,19 @@ SearchRequestsForm, CloseRequestForm, ContactAgencyForm, - ReopenRequestForm + ReopenRequestForm, ) from app.request.utils import ( create_request, handle_upload_no_id, get_address, send_confirmation_email, - create_contact_record + create_contact_record, ) from app.user_request.forms import ( AddUserRequestForm, EditUserRequestForm, - RemoveUserRequestForm + RemoveUserRequestForm, ) from app.user_request.utils import get_current_point_of_contact from app import sentry @@ -117,10 +104,18 @@ def new(): 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"] + 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, @@ -137,7 +132,9 @@ def new(): form.request_description.data, category=None, agency_ein=( - form.request_agency.data if form.request_agency.data != "None" else current_user.default_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, @@ -175,14 +172,20 @@ def new(): 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) + 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") + 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") + 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)) @@ -191,29 +194,41 @@ def new(): "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 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 + new_request_template, + form=form, + site_key=site_key, + kiosk_mode=kiosk_mode, + category=category, + agency=agency, + title=title, ) -@request.route('/view_all', methods=['GET']) +@request.route("/view_all", methods=["GET"]) def view_all(): - user_agencies = current_user.get_agencies if current_user.is_agency else '' + user_agencies = current_user.get_agencies if current_user.is_agency else "" return render_template( - 'request/all.html', + "request/all.html", form=SearchRequestsForm(), - holidays=sorted(get_holidays_date_list( - datetime.utcnow().year, - (datetime.utcnow() + rd(years=DEFAULT_YEARS_HOLIDAY_LIST)).year) + holidays=sorted( + get_holidays_date_list( + datetime.utcnow().year, + (datetime.utcnow() + rd(years=DEFAULT_YEARS_HOLIDAY_LIST)).year, + ) ), - user_agencies=user_agencies + user_agencies=user_agencies, ) -@request.route('/', methods=['GET']) -@request.route('/view/', methods=['GET']) +@request.route("/", methods=["GET"]) +@request.route("/view/", methods=["GET"]) def view(request_id): """ This function is for testing purposes of the view a request back until backend functionality is implemented. @@ -232,16 +247,20 @@ def view(request_id): sentry.captureException() return abort(404) - holidays = sorted(get_holidays_date_list( - datetime.utcnow().year, - (datetime.utcnow() + rd(years=DEFAULT_YEARS_HOLIDAY_LIST)).year) + holidays = sorted( + get_holidays_date_list( + datetime.utcnow().year, + (datetime.utcnow() + rd(years=DEFAULT_YEARS_HOLIDAY_LIST)).year, + ) ) active_users = [] assigned_users = [] if current_user.is_agency: for agency_user in current_request.agency.active_users: - if not agency_user in current_request.agency.administrators and (agency_user != current_user): + if not agency_user in current_request.agency.administrators and ( + agency_user != current_user + ): # populate list of assigned users that can be removed from a request if agency_user in current_request.agency_users: assigned_users.append(agency_user) @@ -250,100 +269,123 @@ def view(request_id): active_users.append(agency_user) permissions = { - 'acknowledge': permission.ACKNOWLEDGE, - 'deny': permission.DENY, - 'extend': permission.EXTEND, - 'close': permission.CLOSE, - 're_open': permission.RE_OPEN, - 'add_file': permission.ADD_FILE, - 'edit_file_privacy': permission.EDIT_FILE_PRIVACY, - 'delete_file': permission.DELETE_FILE, - 'add_note': permission.ADD_NOTE, - 'edit_note_privacy': permission.EDIT_NOTE_PRIVACY, - 'delete_note': permission.DELETE_NOTE, - 'add_link': permission.ADD_LINK, - 'edit_link_privacy': permission.EDIT_LINK_PRIVACY, - 'delete_link': permission.DELETE_LINK, - 'add_instructions': permission.ADD_OFFLINE_INSTRUCTIONS, - 'edit_instructions_privacy': permission.EDIT_OFFLINE_INSTRUCTIONS_PRIVACY, - 'delete_instructions': permission.DELETE_OFFLINE_INSTRUCTIONS, - 'generate_letter': permission.GENERATE_LETTER, - 'add_user': permission.ADD_USER_TO_REQUEST, - 'edit_user': permission.EDIT_USER_REQUEST_PERMISSIONS, - 'remove_user': permission.REMOVE_USER_FROM_REQUEST, - 'edit_title': permission.EDIT_TITLE, - 'edit_title_privacy': permission.CHANGE_PRIVACY_TITLE, - 'edit_agency_request_summary': permission.EDIT_AGENCY_REQUEST_SUMMARY, - 'edit_agency_request_summary_privacy': permission.CHANGE_PRIVACY_AGENCY_REQUEST_SUMMARY, - 'edit_requester_info': permission.EDIT_REQUESTER_INFO + "acknowledge": permission.ACKNOWLEDGE, + "deny": permission.DENY, + "extend": permission.EXTEND, + "close": permission.CLOSE, + "re_open": permission.RE_OPEN, + "add_file": permission.ADD_FILE, + "edit_file_privacy": permission.EDIT_FILE_PRIVACY, + "delete_file": permission.DELETE_FILE, + "add_note": permission.ADD_NOTE, + "edit_note_privacy": permission.EDIT_NOTE_PRIVACY, + "delete_note": permission.DELETE_NOTE, + "add_link": permission.ADD_LINK, + "edit_link_privacy": permission.EDIT_LINK_PRIVACY, + "delete_link": permission.DELETE_LINK, + "add_instructions": permission.ADD_OFFLINE_INSTRUCTIONS, + "edit_instructions_privacy": permission.EDIT_OFFLINE_INSTRUCTIONS_PRIVACY, + "delete_instructions": permission.DELETE_OFFLINE_INSTRUCTIONS, + "generate_letter": permission.GENERATE_LETTER, + "add_user": permission.ADD_USER_TO_REQUEST, + "edit_user": permission.EDIT_USER_REQUEST_PERMISSIONS, + "remove_user": permission.REMOVE_USER_FROM_REQUEST, + "edit_title": permission.EDIT_TITLE, + "edit_title_privacy": permission.CHANGE_PRIVACY_TITLE, + "edit_agency_request_summary": permission.EDIT_AGENCY_REQUEST_SUMMARY, + "edit_agency_request_summary_privacy": permission.CHANGE_PRIVACY_AGENCY_REQUEST_SUMMARY, + "edit_requester_info": permission.EDIT_REQUESTER_INFO, } # Build permissions dictionary for checking on the front-end. for key, val in permissions.items(): - if current_user.is_anonymous or not current_request.user_requests.filter_by( - user_guid=current_user.guid).first(): + if ( + current_user.is_anonymous + or not current_request.user_requests.filter_by( + user_guid=current_user.guid + ).first() + ): permissions[key] = False else: - permissions[key] = is_allowed(current_user, request_id, val) if not current_user.is_anonymous else False + permissions[key] = ( + is_allowed(current_user, request_id, val) + if not current_user.is_anonymous + else False + ) # Build dictionary of current permissions for all assigned users. assigned_user_permissions = {} for u in assigned_users: - assigned_user_permissions[u.guid] = UserRequests.query.filter_by( - request_id=request_id, user_guid=u.guid).one().get_permission_choice_indices() + assigned_user_permissions[u.guid] = ( + UserRequests.query.filter_by(request_id=request_id, user_guid=u.guid) + .one() + .get_permission_choice_indices() + ) point_of_contact = get_current_point_of_contact(request_id) if point_of_contact: - current_point_of_contact = {'user_guid': point_of_contact.user_guid} + current_point_of_contact = {"user_guid": point_of_contact.user_guid} else: - current_point_of_contact = {'user_guid': ''} + current_point_of_contact = {"user_guid": ""} # Determine if the Agency Request Summary should be shown. show_agency_request_summary = False - if current_user in current_request.agency_users \ - or current_request.agency_request_summary \ - and (current_request.requester == current_user - and current_request.status == request_status.CLOSED - and not current_request.privacy['agency_request_summary'] - or current_request.status == request_status.CLOSED - and current_request.agency_request_summary_release_date - and current_request.agency_request_summary_release_date - < datetime.utcnow() - and not current_request.privacy['agency_request_summary']): + if ( + current_user in current_request.agency_users + or current_request.agency_request_summary + and ( + current_request.requester == current_user + and current_request.status == request_status.CLOSED + and not current_request.privacy["agency_request_summary"] + or current_request.status == request_status.CLOSED + and current_request.agency_request_summary_release_date + and current_request.agency_request_summary_release_date < datetime.utcnow() + and not current_request.privacy["agency_request_summary"] + ) + ): show_agency_request_summary = True - # Determine if the title should be shown. - show_title = (current_user in current_request.agency_users or - current_request.requester == current_user or - not current_request.privacy['title']) - # Determine if "Generate Letter" functionality is enabled for the agency. - if 'letters' in current_request.agency.agency_features: - generate_letters_enabled = current_request.agency.agency_features['letters']['generate_letters'] + if "letters" in current_request.agency.agency_features: + generate_letters_enabled = current_request.agency.agency_features["letters"][ + "generate_letters" + ] else: generate_letters_enabled = False # Determine if custom request forms are enabled - if 'enabled' in current_request.agency.agency_features['custom_request_forms']: - custom_request_forms_enabled = current_request.agency.agency_features['custom_request_forms']['enabled'] + if "enabled" in current_request.agency.agency_features["custom_request_forms"]: + custom_request_forms_enabled = current_request.agency.agency_features[ + "custom_request_forms" + ]["enabled"] else: custom_request_forms_enabled = False # Determine if custom request form panels should be expanded by default - if 'expand_by_default' in current_request.agency.agency_features['custom_request_forms']: - expand_by_default = current_request.agency.agency_features['custom_request_forms']['expand_by_default'] + if ( + "expand_by_default" + in current_request.agency.agency_features["custom_request_forms"] + ): + expand_by_default = current_request.agency.agency_features[ + "custom_request_forms" + ]["expand_by_default"] else: expand_by_default = False # Determine if request description should be hidden when custom forms are enabled - if 'description_hidden_by_default' in current_request.agency.agency_features['custom_request_forms']: - description_hidden_by_default = current_request.agency.agency_features['custom_request_forms']['description_hidden_by_default'] + if ( + "description_hidden_by_default" + in current_request.agency.agency_features["custom_request_forms"] + ): + description_hidden_by_default = current_request.agency.agency_features[ + "custom_request_forms" + ]["description_hidden_by_default"] else: description_hidden_by_default = False return render_template( - 'request/view_request.html', + "request/view_request.html", request=current_request, status=request_status, agency_users=current_request.agency_users, @@ -355,12 +397,24 @@ def view(request_id): remove_user_request_form=RemoveUserRequestForm(assigned_users), add_user_request_form=AddUserRequestForm(active_users), edit_user_request_form=EditUserRequestForm(assigned_users), - generate_acknowledgment_letter_form=GenerateAcknowledgmentLetterForm(current_request.agency.ein), - generate_denial_letter_form=GenerateDenialLetterForm(current_request.agency.ein), - generate_closing_letter_form=GenerateClosingLetterForm(current_request.agency.ein), - generate_extension_letter_form=GenerateExtensionLetterForm(current_request.agency.ein), - generate_envelope_form=GenerateEnvelopeForm(current_request.agency_ein, current_request.requester), - generate_response_letter_form=GenerateResponseLetterForm(current_request.agency.ein), + generate_acknowledgment_letter_form=GenerateAcknowledgmentLetterForm( + current_request.agency.ein + ), + generate_denial_letter_form=GenerateDenialLetterForm( + current_request.agency.ein + ), + generate_closing_letter_form=GenerateClosingLetterForm( + current_request.agency.ein + ), + generate_extension_letter_form=GenerateExtensionLetterForm( + current_request.agency.ein + ), + generate_envelope_form=GenerateEnvelopeForm( + current_request.agency_ein, current_request.requester + ), + generate_response_letter_form=GenerateResponseLetterForm( + current_request.agency.ein + ), assigned_user_permissions=assigned_user_permissions, current_point_of_contact=current_point_of_contact, holidays=holidays, @@ -368,51 +422,60 @@ def view(request_id): active_users=active_users, permissions=permissions, show_agency_request_summary=show_agency_request_summary, - show_title=show_title, is_requester=(current_request.requester == current_user), permissions_length=len(permission.ALL), generate_letters_enabled=generate_letters_enabled, - custom_request_forms_enabled = custom_request_forms_enabled, + custom_request_forms_enabled=custom_request_forms_enabled, expand_by_default=expand_by_default, - description_hidden_by_default=description_hidden_by_default + description_hidden_by_default=description_hidden_by_default, ) -@request.route('/non_portal_agency/', methods=['GET']) +@request.route("/non_portal_agency/", methods=["GET"]) def non_portal_agency(agency_name): """ This function handles messaging to the requester if they submitted a request to a non-portal agency. :return: redirect to non_portal_agency page. """ - return render_template('request/non_partner_request.html', agency_name=agency_name) + return render_template("request/non_partner_request.html", agency_name=agency_name) -@request.route('/agencies', methods=['GET']) +@request.route("/agencies", methods=["GET"]) def get_agencies_as_choices(): """ Get selected category value from the request body and generate a list of sorted agencies from the category. :return: list of agency choices """ - if flask_request.args['category']: + if flask_request.args["category"]: # TODO: is sorted faster than orderby? choices = sorted( - [(agencies.ein, agencies.name) - for agencies in Agencies.query.filter( - flask_request.args['category'] == any_(Agencies.categories) - ).all() if agencies.ein not in HIDDEN_AGENCIES], - key=lambda x: x[1]) + [ + (agencies.ein, agencies.name) + for agencies in Agencies.query.filter( + flask_request.args["category"] == any_(Agencies.categories) + ).all() + if agencies.ein not in HIDDEN_AGENCIES + ], + key=lambda x: x[1], + ) else: choices = sorted( - [(agencies.ein, agencies.name) - for agencies in Agencies.query.all() if agencies.ein not in HIDDEN_AGENCIES], - key=lambda x: x[1]) - choices.insert(0, ('', '')) # Insert blank option at the beginning of choices to prevent auto selection + [ + (agencies.ein, agencies.name) + for agencies in Agencies.query.all() + if agencies.ein not in HIDDEN_AGENCIES + ], + key=lambda x: x[1], + ) + choices.insert( + 0, ("", "") + ) # Insert blank option at the beginning of choices to prevent auto selection return jsonify(choices) -@request.route('/contact/', methods=['POST']) +@request.route("/contact/", methods=["POST"]) def contact_agency(request_id): """ This function handles contacting the agency about a request as a requester. @@ -422,13 +485,18 @@ def contact_agency(request_id): form = ContactAgencyForm(current_request) del form.subject if form.validate_on_submit(): - create_contact_record(current_request, - flask_request.form['first_name'], - flask_request.form['last_name'], - flask_request.form['email'], - "Inquiry about {}".format(request_id), - flask_request.form['message']) - flash('Your message has been sent.', category='success') + create_contact_record( + current_request, + flask_request.form["first_name"], + flask_request.form["last_name"], + flask_request.form["email"], + "Inquiry about {}".format(request_id), + flask_request.form["message"], + ) + flash("Your message has been sent.", category="success") else: - flash('There was a problem sending your message. Please try again.', category='danger') - return redirect(url_for('request.view', request_id=request_id)) + flash( + "There was a problem sending your message. Please try again.", + category="danger", + ) + return redirect(url_for("request.view", request_id=request_id)) diff --git a/app/search/utils.py b/app/search/utils.py index 633ce4ecf..4ef25b6b8 100644 --- a/app/search/utils.py +++ b/app/search/utils.py @@ -6,10 +6,7 @@ from sqlalchemy.orm import joinedload from app import es -from app.constants import ( - ES_DATETIME_FORMAT, - request_status -) +from app.constants import ES_DATETIME_FORMAT, request_status from app.lib.date_utils import utc_to_local, local_to_utc from app.lib.utils import InvalidUserException from app.models import Requests, Agencies @@ -17,7 +14,7 @@ MAX_RESULT_SIZE, ES_DATE_RANGE_FORMAT, DT_DATE_RANGE_FORMAT, - MOCK_EMPTY_ELASTICSEARCH_RESULT + MOCK_EMPTY_ELASTICSEARCH_RESULT, ) @@ -41,19 +38,14 @@ def delete_index(): """ Delete all elasticsearch indices, ignoring errors. """ - es.indices.delete( - current_app.config["ELASTICSEARCH_INDEX"], - ignore=[400, 404] - ) + es.indices.delete(current_app.config["ELASTICSEARCH_INDEX"], ignore=[400, 404]) def delete_docs(): """ Delete all elasticsearch request docs. """ - es.indices.refresh( - index=current_app.config["ELASTICSEARCH_INDEX"], - ) + es.indices.refresh(index=current_app.config["ELASTICSEARCH_INDEX"]) es.delete_by_query( index=current_app.config["ELASTICSEARCH_INDEX"], doc_type="request", @@ -79,40 +71,21 @@ def create_index(): "analyzer": "english", "fields": { # for sorting by title - "keyword": { - "type": "keyword", - } - } - }, - "description": { - "type": "text", - "analyzer": "english" + "keyword": {"type": "keyword"} + }, }, + "description": {"type": "text", "analyzer": "english"}, "agency_request_summary": { "type": "text", - "analyzer": "english" - }, - "requester_id": { - "type": "keyword", - }, - "title_private": { - "type": "boolean", - }, - "agency_request_summary_private": { - "type": "boolean", - }, - "agency_ein": { - "type": "keyword", - }, - "agency_name": { - "type": "keyword", - }, - "agency_acronym": { - "type": "keyword" - }, - "status": { - "type": "keyword", + "analyzer": "english", }, + "requester_id": {"type": "keyword"}, + "title_private": {"type": "boolean"}, + "agency_request_summary_private": {"type": "boolean"}, + "agency_ein": {"type": "keyword"}, + "agency_name": {"type": "keyword"}, + "agency_acronym": {"type": "keyword"}, + "status": {"type": "keyword"}, "date_submitted": { "type": "date", "format": "strict_date_hour_minute_second", @@ -133,13 +106,11 @@ def create_index(): "type": "date", "format": "strict_date_hour_minute_second", }, - "assigned_users": { - "type": "keyword" - } + "assigned_users": {"type": "keyword"}, } } } - } + }, ) @@ -151,82 +122,94 @@ def create_docs(): agency_eins = {a.ein: a for a in Agencies.query.filter_by(is_active=True).all()} #: :type: collections.Iterable[app.models.Requests] - requests = Requests.query.filter(Requests.agency_ein.in_(agency_eins.keys())).options( - joinedload(Requests.agency_users)).options(joinedload(Requests.requester)).all() + requests = ( + Requests.query.filter(Requests.agency_ein.in_(agency_eins.keys())) + .options(joinedload(Requests.agency_users)) + .options(joinedload(Requests.requester)) + .all() + ) operations = [] for r in requests: - date_received = r.date_created.strftime( - ES_DATETIME_FORMAT) if r.date_created < r.date_submitted else r.date_submitted.strftime( - ES_DATETIME_FORMAT) + date_received = ( + r.date_created.strftime(ES_DATETIME_FORMAT) + if r.date_created < r.date_submitted + else r.date_submitted.strftime(ES_DATETIME_FORMAT) + ) operation = { - '_op_type': 'create', - '_id': r.id, - 'title': r.title, - 'description': r.description, - 'agency_request_summary': r.agency_request_summary, - 'requester_name': r.requester.name, - 'requester_id': "{guid}".format(guid=r.requester.guid), - 'title_private': r.privacy['title'], - 'agency_request_summary_private': not r.agency_request_summary_released, - 'date_created': r.date_created.strftime(ES_DATETIME_FORMAT), - 'date_submitted': r.date_submitted.strftime(ES_DATETIME_FORMAT), - 'date_received': date_received, - 'date_due': r.due_date.strftime(ES_DATETIME_FORMAT), - 'submission': r.submission, - 'status': r.status, - 'agency_ein': r.agency_ein, - 'agency_acronym': agency_eins[r.agency_ein].acronym, - 'agency_name': agency_eins[r.agency_ein].name, - 'public_title': 'Private' if r.privacy['title'] else r.title, - 'assigned_users': ["{guid}".format(guid=user.guid) for user - in r.agency_users] + "_op_type": "create", + "_id": r.id, + "title": r.title, + "description": r.description, + "agency_request_summary": r.agency_request_summary, + "requester_name": r.requester.name, + "requester_id": "{guid}".format(guid=r.requester.guid), + "title_private": r.privacy["title"], + "agency_request_summary_private": not r.agency_request_summary_released, + "date_created": r.date_created.strftime(ES_DATETIME_FORMAT), + "date_submitted": r.date_submitted.strftime(ES_DATETIME_FORMAT), + "date_received": date_received, + "date_due": r.due_date.strftime(ES_DATETIME_FORMAT), + "submission": r.submission, + "status": r.status, + "agency_ein": r.agency_ein, + "agency_acronym": agency_eins[r.agency_ein].acronym, + "agency_name": agency_eins[r.agency_ein].name, + "public_title": "Private" if not r.privacy["title"] else r.title, + "assigned_users": [ + "{guid}".format(guid=user.guid) for user in r.agency_users + ] # public_agency_request_summary } if r.date_closed is not None: - operation['date_closed'] = r.date_closed.strftime(ES_DATETIME_FORMAT) + operation["date_closed"] = r.date_closed.strftime(ES_DATETIME_FORMAT) operations.append(operation) num_success, _ = bulk( es, operations, index=current_app.config["ELASTICSEARCH_INDEX"], - doc_type='request', - chunk_size=current_app.config['ELASTICSEARCH_CHUNK_SIZE'], - raise_on_error=True + doc_type="request", + chunk_size=current_app.config["ELASTICSEARCH_CHUNK_SIZE"], + raise_on_error=True, ) - current_app.logger.info("Successfully created {num_success} of {total_num} docs.".format(num_success=num_success, - total_num=len(requests))) - - -def search_requests(query, - foil_id, - title, - agency_request_summary, - description, - requester_name, - date_rec_from, - date_rec_to, - date_due_from, - date_due_to, - date_closed_from, - date_closed_to, - agency_ein, - agency_user_guid, - open_, - closed, - in_progress, - due_soon, - overdue, - start, - sort_date_received, - sort_date_due, - sort_title, - tz_name, - size=None, - by_phrase=False, - highlight=False, - for_csv=False): + current_app.logger.info( + "Successfully created {num_success} of {total_num} docs.".format( + num_success=num_success, total_num=len(requests) + ) + ) + + +def search_requests( + query, + foil_id, + title, + agency_request_summary, + description, + requester_name, + date_rec_from, + date_rec_to, + date_due_from, + date_due_to, + date_closed_from, + date_closed_to, + agency_ein, + agency_user_guid, + open_, + closed, + in_progress, + due_soon, + overdue, + start, + sort_date_received, + sort_date_due, + sort_title, + tz_name, + size=None, + by_phrase=False, + highlight=False, + for_csv=False, +): """ The arguments of this function match the request parameters of the '/search/requests' endpoints. @@ -274,24 +257,29 @@ def search_requests(query, query = query.strip() # return no results if there is nothing to query by - if query and not any((foil_id, title, agency_request_summary, - description, requester_name)): + if query and not any( + (foil_id, title, agency_request_summary, description, requester_name) + ): return MOCK_EMPTY_ELASTICSEARCH_RESULT # if searching by foil-id, strip "FOIL-" if foil_id: - query = query.lstrip("FOIL-").lstrip('foil-') + query = query.lstrip("FOIL-").lstrip("foil-") # set sort (list of "field:direction" pairs) sort = [ - ':'.join((field, direction)) for field, direction in { - 'date_received': sort_date_received, - 'date_due': sort_date_due, - 'title.keyword': sort_title}.items() if direction in ("desc", "asc")] + ":".join((field, direction)) + for field, direction in { + "date_received": sort_date_received, + "date_due": sort_date_due, + "title.keyword": sort_title, + }.items() + if direction in ("desc", "asc") + ] # if no sort options are selected use date_received desc by default if len(sort) == 0: - sort = ['date_received:desc'] + sort = ["date_received:desc"] # set statuses (list of request statuses) if current_user.is_agency: @@ -300,24 +288,26 @@ def search_requests(query, request_status.CLOSED: closed, request_status.IN_PROGRESS: in_progress, request_status.DUE_SOON: due_soon, - request_status.OVERDUE: overdue + request_status.OVERDUE: overdue, } statuses = [s for s, b in statuses.items() if b] else: statuses = [] if open_: # Any request that isn't closed is considered open - statuses.extend([ - request_status.OPEN, - request_status.IN_PROGRESS, - request_status.DUE_SOON, - request_status.OVERDUE - ]) + statuses.extend( + [ + request_status.OPEN, + request_status.IN_PROGRESS, + request_status.DUE_SOON, + request_status.OVERDUE, + ] + ) if closed: statuses.append(request_status.CLOSED) # set matching type (full-text or phrase matching) - match_type = 'match_phrase' if by_phrase else 'match' + match_type = "match_phrase" if by_phrase else "match" # set date ranges def datestr_local_to_utc(datestr): @@ -326,41 +316,60 @@ def datestr_local_to_utc(datestr): ).strftime(DT_DATE_RANGE_FORMAT) date_ranges = [] - if any((date_rec_from, date_rec_to, date_due_from, date_due_to, date_closed_from, date_closed_to)): + if any( + ( + date_rec_from, + date_rec_to, + date_due_from, + date_due_to, + date_closed_from, + date_closed_to, + ) + ): range_filters = {} if date_rec_from or date_rec_to: - range_filters['date_received'] = {'format': ES_DATE_RANGE_FORMAT} + range_filters["date_received"] = {"format": ES_DATE_RANGE_FORMAT} if date_due_from or date_due_to: - range_filters['date_due'] = {'format': ES_DATE_RANGE_FORMAT} + range_filters["date_due"] = {"format": ES_DATE_RANGE_FORMAT} if date_closed_from or date_closed_to: - range_filters['date_closed'] = {'format': ES_DATE_RANGE_FORMAT} + range_filters["date_closed"] = {"format": ES_DATE_RANGE_FORMAT} if date_rec_from: - range_filters['date_received']['gte'] = datestr_local_to_utc(date_rec_from) + range_filters["date_received"]["gte"] = datestr_local_to_utc(date_rec_from) if date_rec_to: - range_filters['date_received']['lt'] = datestr_local_to_utc(date_rec_to) + range_filters["date_received"]["lt"] = datestr_local_to_utc(date_rec_to) if date_due_from: - range_filters['date_due']['gte'] = datestr_local_to_utc(date_due_from) + range_filters["date_due"]["gte"] = datestr_local_to_utc(date_due_from) if date_due_to: - range_filters['date_due']['lt'] = datestr_local_to_utc(date_due_to) + range_filters["date_due"]["lt"] = datestr_local_to_utc(date_due_to) if date_closed_from: - range_filters['date_closed']['gte'] = datestr_local_to_utc(date_closed_from) + range_filters["date_closed"]["gte"] = datestr_local_to_utc(date_closed_from) if date_closed_to: - range_filters['date_closed']['lte'] = datestr_local_to_utc(date_closed_to) + range_filters["date_closed"]["lte"] = datestr_local_to_utc(date_closed_to) if date_rec_from or date_rec_to: - date_ranges.append({'range': {'date_received': range_filters['date_received']}}) + date_ranges.append( + {"range": {"date_received": range_filters["date_received"]}} + ) if date_due_from or date_due_to: - date_ranges.append({'range': {'date_due': range_filters['date_due']}}) + date_ranges.append({"range": {"date_due": range_filters["date_due"]}}) if date_closed_from or date_closed_to: - date_ranges.append({'range': {'date_closed': range_filters['date_closed']}}) + date_ranges.append({"range": {"date_closed": range_filters["date_closed"]}}) # generate query dsl body query_fields = { - 'title': title, - 'description': description, - 'agency_request_summary': agency_request_summary, - 'requester_name': requester_name + "title": title, + "description": description, + "agency_request_summary": agency_request_summary, + "requester_name": requester_name, } - dsl_gen = RequestsDSLGenerator(query, query_fields, statuses, date_ranges, agency_ein, agency_user_guid, match_type) + dsl_gen = RequestsDSLGenerator( + query, + query_fields, + statuses, + date_ranges, + agency_ein, + agency_user_guid, + match_type, + ) if foil_id: dsl = dsl_gen.foil_id() else: @@ -384,10 +393,10 @@ def datestr_local_to_utc(datestr): highlight_fields[name] = {} dsl.update( { - 'highlight': { - 'pre_tags': [''], - 'post_tags': [''], - 'fields': highlight_fields + "highlight": { + "pre_tags": [''], + "post_tags": [""], + "fields": highlight_fields, } } ) @@ -399,26 +408,28 @@ def datestr_local_to_utc(datestr): if not for_csv: results = es.search( index=current_app.config["ELASTICSEARCH_INDEX"], - doc_type='request', + doc_type="request", body=dsl, - _source=['requester_id', - 'date_submitted', - 'date_due', - 'date_received', - 'date_created', - 'date_closed', - 'status', - 'agency_ein', - 'agency_name', - 'agency_acronym', - 'requester_name', - 'title_private', - 'agency_request_summary_private', - 'public_title', - 'title', - 'agency_request_summary', - 'description', - 'assigned_users'], + _source=[ + "requester_id", + "date_submitted", + "date_due", + "date_received", + "date_created", + "date_closed", + "status", + "agency_ein", + "agency_name", + "agency_acronym", + "requester_name", + "title_private", + "agency_request_summary_private", + "public_title", + "title", + "agency_request_summary", + "description", + "assigned_users", + ], size=result_set_size, from_=start, sort=sort, @@ -427,42 +438,44 @@ def datestr_local_to_utc(datestr): else: results = es.search( index=current_app.config["ELASTICSEARCH_INDEX"], - doc_type='request', - scroll='1m', + doc_type="request", + scroll="1m", body=dsl, - _source=['requester_id', - 'date_submitted', - 'date_due', - 'date_received', - 'date_created', - 'date_closed', - 'status', - 'agency_ein', - 'agency_name', - 'agency_acronym', - 'requester_name', - 'title_private', - 'agency_request_summary_private', - 'public_title', - 'title', - 'agency_request_summary', - 'description', - 'assigned_users'], + _source=[ + "requester_id", + "date_submitted", + "date_due", + "date_received", + "date_created", + "date_closed", + "status", + "agency_ein", + "agency_name", + "agency_acronym", + "requester_name", + "title_private", + "agency_request_summary_private", + "public_title", + "title", + "agency_request_summary", + "description", + "assigned_users", + ], size=result_set_size, from_=start, sort=sort, ) - sid = results['_scroll_id'] - scroll_size = results['hits']['total'] + sid = results["_scroll_id"] + scroll_size = results["hits"]["total"] - scroll_results = results['hits']['hits'] + scroll_results = results["hits"]["hits"] while scroll_size > 0: - results = es.scroll(scroll='1m', body={"scroll": "1m", "scroll_id": sid}) + results = es.scroll(scroll="1m", body={"scroll": "1m", "scroll_id": sid}) - scroll_size = len(results['hits']['hits']) + scroll_size = len(results["hits"]["hits"]) - scroll_results += results['hits']['hits'] + scroll_results += results["hits"]["hits"] return scroll_results # process highlights @@ -475,117 +488,108 @@ def datestr_local_to_utc(datestr): class RequestsDSLGenerator(object): """ Class for generating dicts representing query dsl bodies for searching request docs. """ - def __init__(self, query, query_fields, statuses, date_ranges, agency_ein, agency_user_guid, match_type): + def __init__( + self, + query, + query_fields, + statuses, + date_ranges, + agency_ein, + agency_user_guid, + match_type, + ): self.__query = query self.__query_fields = query_fields self.__statuses = statuses self.__agency_ein = agency_ein self.__match_type = match_type - self.__default_filters = [{'terms': {'status': statuses}}] + self.__default_filters = [{"terms": {"status": statuses}}] if date_ranges: self.__default_filters += date_ranges if agency_ein: - self.__default_filters.append({ - 'term': {'agency_ein': agency_ein} - }) + self.__default_filters.append({"term": {"agency_ein": agency_ein}}) if agency_user_guid: - self.__default_filters.append({ - 'term': {'assigned_users': agency_user_guid} - }) + self.__default_filters.append( + {"term": {"assigned_users": agency_user_guid}} + ) self.__filters = [] self.__conditions = [] self.requester_id = None def foil_id(self): - self.__filters = [{ - 'wildcard': { - '_uid': 'request#FOIL-*{}*'.format(self.__query) - } - }] + self.__filters = [ + {"wildcard": {"_uid": "request#FOIL-*{}*".format(self.__query)}} + ] return self.__must_query def agency_user(self): for name, use in self.__query_fields.items(): if use: - self.__filters = [ - {self.__match_type: {name: self.__query}} - ] + self.__filters = [{self.__match_type: {name: self.__query}}] self.__conditions.append(self.__must) return self.__should def anonymous_user(self): - if self.__query_fields['title']: + if self.__query_fields["title"]: self.__filters = [ - {self.__match_type: {'title': self.__query}}, - {'term': {'title_private': False}} + {self.__match_type: {"title": self.__query}}, + {"term": {"title_private": False}}, ] self.__conditions.append(self.__must) - if self.__query_fields['agency_request_summary']: + if self.__query_fields["agency_request_summary"]: self.__filters = [ - {self.__match_type: {'agency_request_summary': self.__query}}, - {'term': {'agency_request_summary_private': False}} + {self.__match_type: {"agency_request_summary": self.__query}}, + {"term": {"agency_request_summary_private": False}}, ] self.__conditions.append(self.__must) return self.__should def public_user(self): self.requester_id = current_user.get_id() - if self.__query_fields['title']: + if self.__query_fields["title"]: self.__filters = [ - {self.__match_type: {'title': self.__query}}, - {'bool': { - 'should': [ - {'term': {'requester_id': self.requester_id}}, - {'term': {'title_private': False}} - ] - }} + {self.__match_type: {"title": self.__query}}, + { + "bool": { + "should": [ + {"term": {"requester_id": self.requester_id}}, + {"term": {"title_private": False}}, + ] + } + }, ] self.__conditions.append(self.__must) - if self.__query_fields['agency_request_summary']: + if self.__query_fields["agency_request_summary"]: self.__filters = [ - {self.__match_type: {'agency_request_summary': self.__query}}, - {'term': {'agency_request_summary_private': False}} + {self.__match_type: {"agency_request_summary": self.__query}}, + {"term": {"agency_request_summary_private": False}}, ] self.__conditions.append(self.__must) - if self.__query_fields['description']: + if self.__query_fields["description"]: self.__filters = [ - {self.__match_type: {'description': self.__query}}, - {'term': {'requester_id': self.requester_id}}, + {self.__match_type: {"description": self.__query}}, + {"term": {"requester_id": self.requester_id}}, ] self.__conditions.append(self.__must) return self.__should def queryless(self): - self.__filters = [ - {'match_all': {}}, - ] + self.__filters = [{"match_all": {}}] return self.__must_query @property def __must_query(self): - return { - 'query': self.__must - } + return {"query": self.__must} @property def __must(self): - return { - 'bool': { - 'must': self.__get_filters() - } - } + return {"bool": {"must": self.__get_filters()}} @property def __should(self): - return { - 'query': { - 'bool': { - 'should': self.__conditions - } - } - } + return {"query": {"bool": {"should": self.__conditions}}} def __get_filters(self): return self.__filters + self.__default_filters @@ -610,7 +614,9 @@ def convert_dates(results, dt_format=None, tz_name=None): continue if tz_name: dt = utc_to_local(dt, tz_name) - hit["_source"][field] = dt.strftime(dt_format) if dt_format is not None else dt + hit["_source"][field] = ( + dt.strftime(dt_format) if dt_format is not None else dt + ) def _process_highlights(results, requester_id=None): @@ -625,17 +631,22 @@ def _process_highlights(results, requester_id=None): :param requester_id: id of requester as it is exists in results """ if not current_user.is_agency: - for hit in results['hits']['hits']: - is_requester = (requester_id == hit['_source']['requester_id'] - if requester_id - else False) - if ('title' in hit['highlight'] - and hit['_source']['title_private'] - and (current_user.is_anonymous or not is_requester)): - hit['highlight'].pop('title') - if ('agency_request_summary' in hit['highlight'] - and hit['_source']['agency_request_summary_private']): - hit['highlight'].pop('agency_request_summary') - if ('description' in hit['highlight'] - and not is_requester): - hit['highlight'].pop('description') + for hit in results["hits"]["hits"]: + is_requester = ( + requester_id == hit["_source"]["requester_id"] + if requester_id + else False + ) + if ( + "title" in hit["highlight"] + and hit["_source"]["title_private"] + and (current_user.is_anonymous or not is_requester) + ): + hit["highlight"].pop("title") + if ( + "agency_request_summary" in hit["highlight"] + and hit["_source"]["agency_request_summary_private"] + ): + hit["highlight"].pop("agency_request_summary") + if "description" in hit["highlight"] and not is_requester: + hit["highlight"].pop("description") diff --git a/app/search/views.py b/app/search/views.py index be4d11cc6..e74fb908c 100644 --- a/app/search/views.py +++ b/app/search/views.py @@ -3,12 +3,7 @@ from io import StringIO, BytesIO import re -from flask import ( - current_app, - request, - render_template, - jsonify, -) +from flask import current_app, request, render_template, jsonify from flask.helpers import send_file from flask_login import current_user from sqlalchemy.orm import joinedload @@ -22,7 +17,7 @@ from app import sentry -@search.route("/requests", methods=['GET']) +@search.route("/requests", methods=["GET"]) def requests(): """ For request parameters, see app.search.utils.search_requests @@ -59,54 +54,68 @@ def requests(): """ try: - agency_ein = request.args.get('agency_ein', '') + agency_ein = request.args.get("agency_ein", "") except ValueError: sentry.captureException() agency_ein = None try: - size = int(request.args.get('size', DEFAULT_HITS_SIZE)) + 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) + start = int(request.args.get("start"), 0) except ValueError: sentry.captureException() start = 0 - query = request.args.get('query') + 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) + 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'), + 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, + 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']) + 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 @@ -114,17 +123,25 @@ def requests(): formatted_results = None if total != 0: convert_dates(results) - formatted_results = render_template("request/result_row.html", - requests=results["hits"]["hits"]) + 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 + return ( + jsonify( + { + "count": len(results["hits"]["hits"]), + "total": total, + "results": formatted_results, + } + ), + 200, + ) -@search.route("/requests/", methods=['GET']) +@search.route("/requests/", methods=["GET"]) def requests_doc(doc_type): """ Converts and sends the a search result-set as a @@ -139,103 +156,127 @@ def requests_doc(doc_type): :param doc_type: document type ('csv' only) """ - if current_user.is_agency and doc_type.lower() == 'csv': + if current_user.is_agency and doc_type.lower() == "csv": try: - agency_ein = request.args.get('agency_ein', '') + 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']) + 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"]) + 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'), + 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, + 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 + 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() + 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)]) + 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 + 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 + timestamp.strftime("%m_%d_%Y_at_%I_%M_%p") + ), + as_attachment=True, ) - return '', 400 + return "", 400 diff --git a/app/static/js/request/main.js b/app/static/js/request/main.js index f5987cd47..ce91027e9 100644 --- a/app/static/js/request/main.js +++ b/app/static/js/request/main.js @@ -1,5 +1,6 @@ /* globals requiredFields: true */ "use strict"; +var showPIIWarning = true; // Don't cache ajax requests $.ajaxSetup({cache: false}); @@ -838,5 +839,6 @@ function handlePIIModalReview(){ $('#processing-submission').hide(); $('#submit').show(); $(window).scrollTop($('#request-title').offset().top - 50); + showPIIWarning = true; return; } \ No newline at end of file diff --git a/app/static/js/request/new-request-agency.js b/app/static/js/request/new-request-agency.js index 850eb930b..89443dc19 100755 --- a/app/static/js/request/new-request-agency.js +++ b/app/static/js/request/new-request-agency.js @@ -68,12 +68,10 @@ $(document).ready(function () { $("#cancel-change-category-button").off().click(function () { $(targetId).val(previousValues[target - 1]); }); - } - else{ // otherwise render the form normally + } else { // otherwise render the form normally renderCustomRequestForm(target); } - } - else { + } else { renderCustomRequestForm(target); categorySelected = true; } @@ -120,7 +118,7 @@ $(document).ready(function () { // Loop through required fields and apply a data-parsley-required attribute to them var requiredFields = ["request-title", "request-description", "first-name", "last-name", "email", - "phone", "fax", "address-line-1", "method-received", "request-date", "city", "zipcode"]; + "phone", "fax", "address-line-1", "method-received", "request-date", "city", "zipcode"]; for (var i = 0; i < requiredFields.length; i++) { $("#" + requiredFields[i]).attr("data-parsley-required", ""); @@ -245,8 +243,7 @@ $(document).ready(function () { $("#fax").removeAttr("data-parsley-required"); $("#address-line-1").removeAttr("data-parsley-required"); $("#email").removeAttr("data-parsley-required"); - } - else { + } else { // If none of the fields are valid then produce an error message and apply required fields. $(".contact-form-error-message").html(" " + "Error, contact information is required." + @@ -259,32 +256,25 @@ $(document).ready(function () { if ($("#request-file").parsley().isValid() === false) { $(".file-error").show(); - } - else { + } else { $(".file-error").hide(); } // Scroll to input label if parsley validation fails if ($("#request-title").parsley().isValid() === false) { $(window).scrollTop($(".title-label").offset().top); - } - else if ($("#request-description").parsley().isValid() === false) { + } else if ($("#request-description").parsley().isValid() === false) { $(window).scrollTop($(".description-label").offset().top); - } - else if ($("#request-file").parsley().isValid() === false) { + } else if ($("#request-file").parsley().isValid() === false) { $(".file-error").show(); $(window).scrollTop($("#upload-control").offset().top); - } - else if ($("#method-received").parsley().isValid() === false) { + } else if ($("#method-received").parsley().isValid() === false) { $(window).scrollTop($(".format-label").offset().top); - } - else if ($("#first-name").parsley().isValid() === false) { + } else if ($("#first-name").parsley().isValid() === false) { $(window).scrollTop($(".first-name-label").offset().top); - } - else if ($("#last-name").parsley().isValid() === false) { + } else if ($("#last-name").parsley().isValid() === false) { $(window).scrollTop($(".last-name-label").offset().top); - } - else if ($("#email").parsley().isValid() === false) { + } else if ($("#email").parsley().isValid() === false) { $(window).scrollTop($(".email-label").offset().top); } }); @@ -335,6 +325,14 @@ $(document).ready(function () { $(window).scrollTop($(invalidForms[0]).offset().top); return; } + + if (showPIIWarning) { + e.preventDefault(); + $('#submit').show(); + $('#pii-warning-modal').modal('show'); + showPIIWarning = false; + return; + } } // Prevent multiple submissions diff --git a/app/static/js/request/new-request-user.js b/app/static/js/request/new-request-user.js index 9d85ad349..387967e4d 100755 --- a/app/static/js/request/new-request-user.js +++ b/app/static/js/request/new-request-user.js @@ -77,12 +77,10 @@ $(document).ready(function () { $("#cancel-change-category-button").off().click(function () { $(targetId).val(previousValues[target - 1]); }); - } - else{ // otherwise render the form normally + } else { // otherwise render the form normally renderCustomRequestForm(target); } - } - else { + } else { renderCustomRequestForm(target); categorySelected = true; } @@ -176,8 +174,7 @@ $(document).ready(function () { // TODO: this or combine (see the other new-request-* js files) if ($("#request-file").parsley().isValid() === false) { $(".file-error").show(); - } - else { + } else { $(".file-error").hide(); } @@ -209,7 +206,6 @@ $(document).ready(function () { $(".upload-error").remove(); }); - var showSsnWarning = true; // variable to determine is the SSN warning modal should be shown // Disable submit button on form submission $("#request-form").submit(function (e) { $(".remove-on-resubmit").remove(); @@ -233,14 +229,11 @@ $(document).ready(function () { return; } - var ssnInTitle = checkSSN($('#request-title').val()); - var ssnInDescription = checkSSN($('#request-description').val()); - var ssnInCustomRequestForms = checkSSN($('#custom-request-forms-data').val()); - if ((ssnInTitle || ssnInDescription || ssnInCustomRequestForms) && showSsnWarning) { + if (showPIIWarning) { e.preventDefault(); $('#submit').show(); $('#pii-warning-modal').modal('show'); - showSsnWarning = false; + showPIIWarning = false; return; } } diff --git a/app/static/js/request/search.js b/app/static/js/request/search.js index 5e9420c21..ae6ff6e3c 100644 --- a/app/static/js/request/search.js +++ b/app/static/js/request/search.js @@ -1,6 +1,6 @@ "use strict"; -$(function() { +$(function () { // set time zone name $("input[name='tz_name']").val(jstz.determine().name()); @@ -28,12 +28,12 @@ $(function() { // Table head values if (isAgencyUser) { resultcol = [ - ["Status",""], - ["ID",""], - ["Date Submitted", "sort_date_submitted","desc"], - ["Title", "sort_title","none"], + ["Status", ""], + ["ID", ""], + ["Date Submitted", "sort_date_submitted", "desc"], + ["Title", "sort_title", "none"], ["Assigned Agency", ""], - ["Date Due","sort_date_due","none"], + ["Date Due", "sort_date_due", "none"], ["Date Closed", ""], ["Requester Name", ""] ]; @@ -42,8 +42,7 @@ $(function() { $("#user-agencies option").each(function () { userAgencies.push($(this).val()); }); - } - else { + } else { resultcol = [ ["Status", ""], ["ID", ""], @@ -88,8 +87,7 @@ $(function() { if (compDateElem.val().length === 10) { try { compDate = new Date(compDateElem.val()); - } - catch (err) { + } catch (err) { compDate = null; } } @@ -99,25 +97,20 @@ $(function() { (isLessThan ? checkDate <= compDate : checkDate >= compDate) : true; if (!notHolidayOrWeekend(checkDate, false)) { return dateInvalid(checkDateElem, "This date does not fall on a business day.", null, dateError); - } - else if (!validComp) { + } else if (!validComp) { return dateInvalid(checkDateElem, null, true, dateError); - } - else { + } else { return dateValid(checkDateElem); } - } - catch (err) { + } catch (err) { // failure parsing date string return dateInvalid(checkDateElem, "This is not a valid date.", null, dateError); } - } - else { + } else { // missing full date string length return dateInvalid(checkDateElem, "This is not a valid date.", null, dateError); } - } - else { + } else { // empty value is valid (no filtering) return dateValid(checkDateElem); } @@ -135,8 +128,7 @@ $(function() { searchBtn.attr("disabled", true); searchBtnAdv.attr("disabled", true); generateDocBtn.attr("disabled", true); - } - else { + } else { canSearch = true; searchBtn.attr("disabled", false); searchBtnAdv.attr("disabled", false); @@ -185,13 +177,13 @@ $(function() { }); // keypress 'Enter' = click search button - $("#search-section").keyup(function(e){ + $("#search-section").keyup(function (e) { if (canSearch && e.keyCode === 13) { searchBtn.click(); } }); // but don't submit form (it is only used to generate results document) - $("#search-form").on("keyup, keypress", function(e){ + $("#search-form").on("keyup, keypress", function (e) { var keyCode = e.keyCode || e.which; if (keyCode === 13) { e.preventDefault(); @@ -200,7 +192,7 @@ $(function() { }); // Capture form reset - $(clearSearchBtn).on('click', function(event) { + $(clearSearchBtn).on('click', function (event) { event.preventDefault(); $("#search-form")[0].reset(); @@ -239,35 +231,32 @@ $(function() { // } // }; - function buildResultsTableHead (col, sort) { + function buildResultsTableHead(col, sort) { var tableHead = ""; - for (var i=0; i< col.length; i++) { - if (col[i][1] === "") - { + for (var i = 0; i < col.length; i++) { + if (col[i][1] === "") { tableHead = tableHead + "" + col[i][0] + ""; - } - else - { - tableHead = tableHead + ''; + 'Unsorted' + + '' + + 'Sorted, Ascending' + + '' + + 'Sorted, Descending '; } } return tableHead; } - function buildResultsTable (theResults, count, total) { + function buildResultsTable(theResults, count, total) { var theTable = ""; - theTable = theTable +'
'; + theTable = theTable + '
'; theTable = theTable + '

Displaying ' + (start + 1) + " - " + (start + count) + ' of ' + total.toLocaleString() + ' Results Found

'; theTable = theTable + '
'; + theTable = theTable + '
'; + theTable = theTable + '
* - "Under Review" means the request was recently submitted and the agency has 5 business days to review the request for personal identifying information before it becomes publicly viewable.
'; theTable = theTable + '
'; - theTable = theTable + buildResultsTableHead (resultcol, sortSequence); + theTable = theTable + buildResultsTableHead(resultcol, sortSequence); theTable = theTable + '' + theResults + '
'; return theTable; } @@ -297,7 +287,7 @@ $(function() { $.ajax({ url: "/search/requests", data: $("#search-form").serializeArray(), - success: function(data) { + success: function (data) { if (data.total !== 0) { noResultsFound = false; results.html(buildResultsTable(data.results, data.count, data.total)); @@ -313,16 +303,14 @@ $(function() { if (end === total) { next.attr("aria-disabled", true); next.addClass("disabled") - } - else { + } else { next.attr("aria-disabled", false); next.removeClass("disabled") } if (start === 0) { prev.attr("aria-disabled", true); prev.addClass("disabled") - } - else { + } else { prev.attr("aria-disabled", false); prev.removeClass("disabled") } @@ -332,15 +320,14 @@ $(function() { scrollToElement(resultsHeader); toFocus = false; } - } - else { + } else { noResultsFound = true; results.html("
" + - "

No results found.

"); + "

No results found.

"); generateDocBtn.attr("disabled", true); } }, - error: function(e) { + error: function (e) { results.html("
" + "

Hmmmm.... Looks like something's gone wrong.

"); generateDocBtn.attr("disabled", true); @@ -349,12 +336,12 @@ $(function() { } // search on load - $(document).ready(function(){ + $(document).ready(function () { search(); }); // disable other filters if searching by FOIL-ID - $("input[name='foil_id']").click(function() { + $("input[name='foil_id']").click(function () { var query = $("#query"); var names = ["title", "description", "agency_request_summary", "requester_name"]; var i; @@ -364,8 +351,7 @@ $(function() { $("input[name='" + names[i] + "']").prop("disabled", true); $("input[name='" + names[i] + "']").addClass("disabled"); } - } - else { + } else { query.attr("placeholder", "Enter keywords"); for (i = 0; i < names.length; i++) { $("input[name='" + names[i] + "']").prop("disabled", false); @@ -380,11 +366,11 @@ $(function() { } // Sorting - function updateSorting(theHeadingId,sequence){ + function updateSorting(theHeadingId, sequence) { - for (var i=0; i< resultcol.length; i++){ + for (var i = 0; i < resultcol.length; i++) { - if (resultcol[i][1] === theHeadingId){ + if (resultcol[i][1] === theHeadingId) { resultcol[i][2] = sequence; } } @@ -399,8 +385,8 @@ $(function() { elem.attr( "data-sort-order", sortSequence[ - (sortSequence.indexOf(elem.attr("data-sort-order")) + 1 + sortSequence.length) - % sortSequence.length]); + (sortSequence.indexOf(elem.attr("data-sort-order")) + 1 + sortSequence.length) + % sortSequence.length]); } @@ -444,7 +430,7 @@ $(function() { }, success: function (data) { // Populate users - $.each(data.active_users ,function() { + $.each(data.active_users, function () { agencyUserSelect.append($("