Skip to content

Commit

Permalink
Merge pull request #510 from CityOfNewYork/develop
Browse files Browse the repository at this point in the history
OpenRecords v3.3.0
  • Loading branch information
johnyu95 authored Jun 13, 2019
2 parents 74cf72e + d695921 commit 4313ec7
Show file tree
Hide file tree
Showing 35 changed files with 1,095 additions and 151 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
language: python
python:
- 3.5.3
install: pip install -r requirements/dev.txt
install: pip install -r requirements/dev.txt -r requirements/common.txt
before_script:
- psql -c 'create database openrecords_test;' -U postgres
- psql -c 'create user testuser;' -U postgres
Expand Down
31 changes: 28 additions & 3 deletions app/admin/forms.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
from app.models import Agencies
from flask_wtf import Form
from wtforms import SelectField
from flask_login import current_user
from flask_wtf import Form
from wtforms import (
SelectField,
StringField,
SubmitField,
)
from wtforms.validators import (
Length,
Email,
DataRequired
)

from app.lib.db_utils import get_agency_choices
from app.models import Agencies


class ActivateAgencyUserForm(Form):
Expand Down Expand Up @@ -57,4 +68,18 @@ def __init__(self, agency_ein=None):
self.agencies.choices.insert(0, self.agencies.choices.pop(self.agencies.choices.index(agency)))
self.process()


# TODO: Add forms to modify agency_features (see models.py:183)


class AddAgencyUserForm(Form):
agency = SelectField('Agency', choices=None, validators=[DataRequired()])
first_name = StringField('First Name', validators=[Length(max=32), DataRequired()])
last_name = StringField('Last Name', validators=[Length(max=64), DataRequired()])
email = StringField('Email', validators=[Email(), Length(max=254), DataRequired()])
submit = SubmitField('Add User')

def __init__(self):
super(AddAgencyUserForm, self).__init__()
self.agency.choices = get_agency_choices()
self.agency.choices.insert(0, ('', ''))
118 changes: 111 additions & 7 deletions app/admin/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,37 @@
:synopsis: Endpoints for Agency Adminstrator Interface
"""
from flask import (
abort,
current_app,
flash,
redirect,
render_template,
url_for,
)
from flask_login import current_user
from sqlalchemy import func

from app.admin import admin
from app.models import Users, Agencies, AgencyUsers
from app.admin.forms import (
AddAgencyUserForm,
ActivateAgencyUserForm,
SelectAgencyForm,
ActivateAgencyUserForm
)
from flask import render_template, abort
from flask_login import current_user
from app.admin.utils import get_agency_active_users
from app.constants import OPENRECORDS_DL_EMAIL
from app.lib.db_utils import create_object
from app.lib.email_utils import (
get_agency_admin_emails,
send_email,
)
from app.lib.permission_utils import has_super
from app.models import (
Agencies,
AgencyUsers,
Users,
)
from app.request.utils import generate_guid


# TODO: View function to handle updates to agency wide settings (see models.py:183)
Expand All @@ -29,7 +51,7 @@ def main(agency_ein=None):
user_form = ActivateAgencyUserForm(agency_ein)
active_users = get_agency_active_users(agency_ein)
agency_is_active = Agencies.query.filter_by(ein=agency_ein).one().is_active
return render_template("admin/main.html",
return render_template('admin/main.html',
agency_ein=agency_ein,
users=active_users,
user_form=user_form,
Expand All @@ -41,15 +63,97 @@ def main(agency_ein=None):
del active_users[active_users.index(current_user)]
if len(current_user.agencies.all()) > 1:
agency_form = SelectAgencyForm(agency_ein)
return render_template("admin/main.html",
return render_template('admin/main.html',
agency_ein=agency_ein,
users=active_users,
agency_form=agency_form,
user_form=form,
multi_agency_admin=True)
return render_template("admin/main.html",
return render_template('admin/main.html',
users=active_users,
agency_ein=agency_ein,
user_form=form)

return abort(404)


@admin.route('/add-user', methods=['GET', 'POST'])
@has_super()
def add_user():
"""Adds a user to the users and agency_users tables.
Returns:
Template with context.
"""
form = AddAgencyUserForm()

if form.validate_on_submit():
agency_ein = form.agency.data
first_name = form.first_name.data
last_name = form.last_name.data
email = form.email.data

user = Users.query.filter(
func.lower(Users.email) == email.lower(),
Users.is_nyc_employee == True
).first()

if user is not None:
flash('{} {} has already been added.'.format(first_name, last_name), category='warning')
else:
new_user = Users(
guid=generate_guid(),
first_name=first_name,
last_name=last_name,
email=email,
email_validated=False,
is_nyc_employee=True,
is_anonymous_requester=False,
)
create_object(new_user)

agency_user = AgencyUsers(
user_guid=new_user.guid,
agency_ein=agency_ein,
is_agency_active=False,
is_agency_admin=False,
is_primary_agency=True
)
create_object(agency_user)

agency = Agencies.query.filter_by(ein=agency_ein).one()
admin_emails = get_agency_admin_emails(agency)
send_email(
subject='User {} Added'.format(new_user.fullname),
to=admin_emails,
template='email_templates/email_agency_user_added',
agency_name=agency.name,
name=new_user.fullname,
)

content_id = 'login_screenshot'
image = {'path': current_app.config['LOGIN_IMAGE_PATH'],
'content_id': content_id}
send_email(
subject='OpenRecords Portal',
to=[new_user.email],
email_content=render_template('email_templates/email_user_added.html',
agency_name=agency.name,
content_id=content_id,
domain=new_user.email.split('@')[1],
name=new_user.fullname),
image=image
)

send_email(
subject='User {} Added'.format(new_user.fullname),
to=[OPENRECORDS_DL_EMAIL],
email_content='{} has been added to OpenRecords. Add {} to the service desk.'.format(
new_user.fullname, new_user.email)
)

flash('{} has been added.'.format(new_user.fullname), category='success')
return redirect(url_for('admin.add_user'))

return render_template('admin/add_user.html',
form=form)
1 change: 1 addition & 0 deletions app/constants/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
determination_type.ACKNOWLEDGMENT: "email_response_acknowledgment.html",
determination_type.DENIAL: "email_response_denial.html",
determination_type.CLOSING: "email_response_closing.html",
determination_type.QUICK_CLOSING: "email_response_quick_closing.html",
determination_type.EXTENSION: "email_response_extension.html",
determination_type.REOPENING: "email_response_reopening.html"
}
Expand Down
17 changes: 17 additions & 0 deletions app/constants/dates.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
MONTHS = [
('', ''),
('01', 'January'),
('02', 'February'),
('03', 'March'),
('04', 'April'),
('05', 'May'),
('06', 'June'),
('07', 'July'),
('08', 'August'),
('09', 'September'),
('10', 'October'),
('11', 'November'),
('12', 'December')
]

PORTAL_START_YEAR = 2016
2 changes: 2 additions & 0 deletions app/constants/determination_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@
ACKNOWLEDGMENT = 'acknowledgment'
EXTENSION = 'extension'
CLOSING = 'closing'
QUICK_CLOSING = 'quick_closing'
REOPENING = 're-opening'

ALL = frozenset((
DENIAL,
ACKNOWLEDGMENT,
EXTENSION,
CLOSING,
QUICK_CLOSING,
REOPENING
))
1 change: 1 addition & 0 deletions app/constants/request_date.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
RELEASE_PUBLIC_DAYS = 20
DEFAULT_QUICK_CLOSING_DAYS = 20
49 changes: 36 additions & 13 deletions app/lib/email_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,16 @@
DEFAULT_MAIL_SENDER: 'Records Admin <[email protected]>'
"""

import os
from flask import current_app, render_template
from flask_mail import Message

from app import mail, celery, sentry
from app.models import Requests
from app.lib.file_utils import os_get_mime_type
from app.models import (
Agencies,
Requests,
)


@celery.task(serializer='pickle')
Expand Down Expand Up @@ -69,22 +73,41 @@ def send_email(subject, to=list(), cc=list(), bcc=list(), reply_to='', template=
filename = kwargs.get('filename')
mimetype = kwargs.get('mimetype', 'application/pdf')
msg.attach(filename, mimetype, attachment)

image = kwargs.get('image', None)
if image:
image_path = image['path']
mimetype = os_get_mime_type(image_path)
filename = os.path.basename(image_path)
content_id = image['content_id']
msg.attach(filename, mimetype, open(image_path, 'rb').read(),
'inline', headers=[['Content-ID', '<{}>'.format(content_id)], ])
send_async_email.delay(msg)


def get_agency_emails(request_id, admins_only=False):
"""
Gets a list of the agency emails (assigned users and default email)
def get_assigned_users_emails(request_id: str):
"""Gets a list of all the assigned users' emails on a request and the agency's default email.
:param request_id: FOIL request ID to query UserRequests
:param admins_only: return list of agency admin emails only
:return: list of agency emails or ['[email protected]'] (for testing)
Args:
request_id: Request ID
Returns:
A unique list of all the assigned users' emails on a request and the agency's default email.
"""
request = Requests.query.filter_by(id=request_id).one()

if admins_only:
return list(set(user.notification_email if user.notification_email is not None else user.email for user in
request.agency.administrators))

return list(set([user.notification_email if user.notification_email is not None else user.email for user in
request.agency_users] + [request.agency.default_email]))


def get_agency_admin_emails(agency: Agencies):
"""Gets a list of all the agency administrators' emails in an agency.
Args:
agency: An Agencies instance
Returns:
A unique list of all the agency administrators' emails in an agency.
"""
return list(
set(user.notification_email if user.notification_email is not None else user.email for user in
agency.administrators))
2 changes: 1 addition & 1 deletion app/lib/permission_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def has_super():
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if not current_user.is_super:
if not getattr(current_user, 'is_super', False):
return abort(403)
return f(*args, **kwargs)

Expand Down
17 changes: 17 additions & 0 deletions app/report/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from wtforms.validators import DataRequired

from app.lib.db_utils import get_agency_choices
from app.constants.dates import MONTHS, PORTAL_START_YEAR


class ReportFilterForm(Form):
Expand Down Expand Up @@ -57,3 +58,19 @@ def validate(self):
field.errors.append('The dates cannot be the same.')
is_valid = False
return is_valid


class MonthlyMetricsReportForm(Form):
"""Form to generate a monthly metrics report."""
year = SelectField('Year (required)', choices=None, validators=[DataRequired()])
month = SelectField('Month (required)', choices=MONTHS, validators=[DataRequired()])
submit_field = SubmitField('Generate Report')

def __init__(self):
super(MonthlyMetricsReportForm, self).__init__()
# Calculate years portal has been active
years_active = []
for year in range(date.today().year, PORTAL_START_YEAR-1, -1):
years_active.append((str(year), str(year)))
self.year.choices = years_active
self.year.choices.insert(0, ('', ''))
Loading

0 comments on commit 4313ec7

Please sign in to comment.