Skip to content

Commit

Permalink
Merge pull request #185 from codeforamerica/admin-extensions
Browse files Browse the repository at this point in the history
Admin extensions
  • Loading branch information
richaagarwal committed Aug 14, 2015
2 parents 75e9995 + ef33cd7 commit 03958e0
Show file tree
Hide file tree
Showing 19 changed files with 347 additions and 159 deletions.
26 changes: 26 additions & 0 deletions alembic/versions/10855dae57b7_add_foreign_keys_bet.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""add foreign keys between User and Department
Revision ID: 10855dae57b7
Revises: 134e5fea7601
Create Date: 2015-05-19 10:59:16.315012
"""

# revision identifiers, used by Alembic.
revision = '10855dae57b7'
down_revision = '134e5fea7601'

from alembic import op
import sqlalchemy as sa


def upgrade():
op.alter_column('user', 'department', new_column_name='department_id')
op.add_column('department', sa.Column('primary_contact_id', sa.INTEGER, sa.ForeignKey("user.id")))
op.add_column('department', sa.Column('backup_contact_id', sa.INTEGER, sa.ForeignKey("user.id")))


def downgrade():
op.drop_column('department', 'primary_contact_id')
op.drop_column('department', 'backup_contact_id')
op.alter_column('user', 'department_id', new_column_name='department')
51 changes: 51 additions & 0 deletions alembic/versions/2de765e466ca_convert_created_date.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""convert created datetimes to utc
Revision ID: 2de765e466ca
Revises: 10855dae57b7
Create Date: 2015-05-19 23:01:13.266389
"""

# revision identifiers, used by Alembic.
revision = '2de765e466ca'
down_revision = '10855dae57b7'

from alembic import op
import sqlalchemy as sa
from datetime import datetime
from pytz import timezone, utc

pacific = timezone('US/Pacific')
Request = sa.Table(
'request',
sa.MetaData(),
sa.Column('id', sa.Integer, primary_key=True),
sa.Column('date_created', sa.DateTime),
)

def upgrade():
connection = op.get_bind()

# Assume all existing requests were entered in Pacific time, and
# convert them to UTC.
for request in connection.execute(Request.select()):
date_created = request.date_created.replace(tzinfo=pacific)
date_created = date_created.astimezone(utc)
connection.execute(
Request.update().where(Request.c.id == request.id)
.values(date_created=date_created)
)


def downgrade():
connection = op.get_bind()

# Assume all existing requests were entered in UTC, and convert
# them to Pacific time.
for request in connection.execute(Request.select()):
date_created = request.date_created.replace(tzinfo=utc)
date_created = date_created.astimezone(pacific)
connection.execute(
Request.update().where(Request.c.id == request.id)
.values(date_created=date_created)
)
10 changes: 5 additions & 5 deletions public_records_portal/RequestPresenter.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ def __init__(self, request, qa = None, note = None, index = None, public = False
if self.staff:
if self.staff.email:
self.staff_email = self.staff.email
if self.staff.department:
self.staff_department = self.staff.department
if self.staff.department_id:
self.staff_department = self.staff.department_id
if self.staff.phone:
self.staff_phone = self.staff.phone
if self.staff.alias:
Expand All @@ -41,7 +41,7 @@ def __init__(self, request, qa = None, note = None, index = None, public = False
self.response = note
self.type = "note"
self.icon = "icon-edit icon-large"

def get_id(self):
return self.response.id

Expand All @@ -60,14 +60,14 @@ def display_text(self):
<label class='control-label'>Answer</label><input type='hidden' name='qa_id' value='%s'/><input type='hidden' name='request_id' value='%s'/>
<textarea id='answerTextarea' name='answer_text' class='input-xlarge' rows="2" type='text' rows='1' placeholder='Can you respond to the above question?' required/></textarea>
<button id='askQuestion' class='btn btn-primary' type='submit'>Respond</button>
</form>
</form>
""" % (self.response.id, self.request.id)
else:
text = text + "<p>Requester hasn't answered yet.</p>"
return text
elif self.type == "note":
return "%s - Requester" %(self.response.text)

def get_icon(self):
return self.icon

Expand Down
14 changes: 10 additions & 4 deletions public_records_portal/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from flask import Flask
from flask.ext.sqlalchemy import SQLAlchemy

# Initialize Flask app
# Initialize Flask app
app = Flask(__name__)
app.debug = True

Expand All @@ -24,9 +24,15 @@ def set_env(key, default = None):
elif default:
app.config[key] = default

def set_bool_env(key, default = None):
if key in environ:
app.config[key] = environ[key].lower() in ('true', 'yes', 'on')
elif default is not None:
app.config[key] = default

# UPDATES TO THESE DEFAULTS SHOULD OCCUR IN YOUR .env FILE.

set_env(key = 'APPLICATION_URL', default = "http://127.0.0.1:5000/")
set_env(key = 'APPLICATION_URL', default = "http://127.0.0.1:5000/")
set_env(key = 'ENVIRONMENT', default="LOCAL")
# The default records liaison, to whom requests get routed to if no department is selected:
set_env(key = 'DEFAULT_OWNER_EMAIL', default = '[email protected]')
Expand All @@ -40,7 +46,7 @@ def set_env(key, default = None):
# Currently due dates and overdue status is only showed to logged in agency staff
set_env(key = 'DAYS_TO_FULFILL', default = '10')
set_env(key = 'DAYS_AFTER_EXTENSION', default = '14')
set_env(key = 'DAYS_UNTIL_OVERDUE', default = '2')
set_env(key = 'DAYS_UNTIL_OVERDUE', default = '2')

set_env(key = 'TIMEZONE', default = "US/Pacific")

Expand Down Expand Up @@ -71,7 +77,7 @@ def set_env(key, default = None):
set_env(key = envvar)

# Database gets set slightly differently, to support difference between Flask and Heroku naming:
app.config['SQLALCHEMY_DATABASE_URI'] = environ['DATABASE_URL']
app.config['SQLALCHEMY_DATABASE_URI'] = environ['DATABASE_URL']

# Initialize database
db = SQLAlchemy(app)
Expand Down
10 changes: 5 additions & 5 deletions public_records_portal/db_helpers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""
"""
.. module:: db_helpers
:synopsis: Functions that interact with the Postgres database via Flask-SQLAlchemy
.. modlueauthor:: Richa Agarwal <[email protected]>
Expand All @@ -14,7 +14,7 @@
import uuid
import json
import os
import logging
import logging


### @export "get_subscriber"
Expand Down Expand Up @@ -264,9 +264,9 @@ def update_user(user, alias = None, phone = None, department = None, contact_for
if type(department) != int and not department.isdigit():
d = Department.query.filter_by(name = department).first()
if d:
user.department = d.id
user.department_id = d.id
else:
user.department = department
user.department_id = department
if contact_for:
if user.contact_for and contact_for not in user.contact_for:
contact_for = user.contact_for + "," + contact_for
Expand Down Expand Up @@ -328,7 +328,7 @@ def add_staff_participant(request_id, is_point_person = False, email = None, use
participant.is_point_person = True
participant.date_updated = datetime.now().isoformat()
if reason: # Update the reason
participant.reason = reason
participant.reason = reason
app.logger.info("\n\nStaff participant with owner ID: %s is now the point of contact for request %s" %(participant.id, request_id))
else:
is_new = False
Expand Down
1 change: 1 addition & 0 deletions public_records_portal/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
app.jinja_env.filters['get_request_data_chronologically'] = prr.get_request_data_chronologically
app.jinja_env.filters['get_gravatar_url'] = gravatar.get_gravatar_url
app.jinja_env.filters['date'] = helpers.date
app.jinja_env.filters['format_datetime'] = helpers.format_datetime
app.jinja_env.filters['explain_action'] = helpers.explain_action
app.jinja_env.filters['tutorial'] = helpers.tutorial
app.jinja_env.filters['new_lines'] = helpers.new_lines
Expand Down
10 changes: 10 additions & 0 deletions public_records_portal/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,16 @@ def date(obj):
except: # Not a datetime object
return notifications.format_date(datetime.strptime(obj, "%Y-%m-%dT%H:%M:%S.%f"))

def format_datetime(obj, format):
""" Take a datetime or datetime-like object and return a formatted datetime string. """
if not obj:
return None
try:
return localize(obj).strftime(format)
except: # Not a datetime object
date_obj = datetime.strptime(obj, "%Y-%m-%dT%H:%M:%S.%f")
return localize(date_obj).strftime(format)

def timestamp(obj):
return localize(obj).strftime('%H:%M:%S')

Expand Down
54 changes: 37 additions & 17 deletions public_records_portal/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from sqlalchemy import and_, or_

from datetime import datetime, timedelta
from pytz import utc
from public_records_portal import db, app
from werkzeug.security import generate_password_hash, check_password_hash
import json
Expand All @@ -31,14 +32,17 @@ class User(db.Model):
phone = db.Column(db.String())
date_created = db.Column(db.DateTime)
password = db.Column(db.String(255))
department = db.Column(Integer, ForeignKey("department.id"))
current_department = relationship("Department", foreign_keys = [department], uselist = False)
department_id = db.Column(Integer, ForeignKey("department.id", use_alter=True, name="fk_department"))
contact_for = db.Column(db.String()) # comma separated list
backup_for = db.Column(db.String()) # comma separated list
owners = relationship("Owner")
subscribers = relationship("Subscriber")
is_staff = db.Column(db.Boolean, default = False) # Is this user an active agency member?

current_department = relationship("Department",
foreign_keys=[department_id],
lazy='joined', uselist=False)

def is_authenticated(self):
return True
def is_active(self):
Expand All @@ -63,7 +67,7 @@ def __init__(self, email=None, alias = None, phone=None, department = None, cont
self.phone = phone
self.date_created = datetime.now().isoformat()
if department and department != "":
self.department = department
self.department_id = department
if contact_for and contact_for != "":
self.contact_for = contact_for
if backup_for and backup_for != "":
Expand All @@ -88,9 +92,21 @@ class Department(db.Model):
date_created = db.Column(db.DateTime)
date_updated = db.Column(db.DateTime)
name = db.Column(db.String(), unique=True)
users = relationship("User") # The list of users in this department
users = relationship("User", foreign_keys=[User.department_id], post_update=True) # The list of users in this department
requests = relationship("Request", order_by = "Request.date_created.asc()") # The list of requests currently associated with this department
def __init__(self, name):

primary_contact_id = db.Column(Integer, ForeignKey("user.id"))
backup_contact_id = db.Column(Integer, ForeignKey("user.id"))
primary_contact = relationship(User,
foreign_keys=[primary_contact_id],
primaryjoin=(primary_contact_id == User.id),
uselist=False, post_update=True)
backup_contact = relationship(User,
foreign_keys=[backup_contact_id],
primaryjoin=(backup_contact_id == User.id),
uselist=False, post_update=True)

def __init__(self, name=''):
self.name = name
self.date_created = datetime.now().isoformat()
def __repr__(self):
Expand All @@ -101,7 +117,7 @@ def get_name(self):
return self.name or "N/A"

### @export "Request"
class Request(db.Model):
class Request(db.Model):
# The public records request
__tablename__ = 'request'
id = db.Column(db.Integer, primary_key =True)
Expand All @@ -124,7 +140,7 @@ class Request(db.Model):

def __init__(self, text, creator_id = None, offline_submission_type = None, date_received = None):
self.text = text
self.date_created = datetime.now().isoformat()
self.date_created = datetime.now(utc).isoformat()
self.creator_id = creator_id
self.offline_submission_type = offline_submission_type
if date_received and type(date_received) is datetime:
Expand All @@ -142,7 +158,7 @@ def set_due_date(self):
self.due_date = self.date_received + timedelta(days = int(app.config['DAYS_TO_FULFILL']))

def extension(self):
self.extended = True
self.extended = True
self.due_date = self.due_date + timedelta(days = int(app.config['DAYS_AFTER_EXTENSION']))
def point_person(self):
for o in self.owners:
Expand All @@ -154,7 +170,7 @@ def all_owners(self):
for o in self.owners:
all_owners.append(o.user.get_alias())
return all_owners

def requester(self):
if self.subscribers:
return self.subscribers[0] or None # The first subscriber is always the requester
Expand Down Expand Up @@ -207,11 +223,11 @@ def open(self):
def due_soon(self):
two_days = datetime.now() + timedelta(days = 2)
return and_(self.due_date < two_days, self.due_date > datetime.now(), ~self.closed)

@hybrid_property
def overdue(self):
return and_(self.due_date < datetime.now(), ~self.closed)

@hybrid_property
def closed(self):
return Request.status.ilike("%closed%")
Expand All @@ -236,12 +252,12 @@ def __repr__(self):
return "<QA Q: %r A: %r>" %(self.question, self.answer)

### @export "Owner"
class Owner(db.Model):
class Owner(db.Model):
# A member of city staff assigned to a particular request, that may or may not upload records towards that request.
__tablename__ = 'owner'
id = db.Column(db.Integer, primary_key =True)
user_id = Column(Integer, ForeignKey('user.id'))
user = relationship("User", uselist = False)
user = relationship("User", uselist = False, lazy='joined')
request_id = db.Column(db.Integer, db.ForeignKey('request.id'))
request = relationship("Request", foreign_keys = [request_id])
active = db.Column(db.Boolean, default = True) # Indicate whether they're still involved in the request or not.
Expand All @@ -259,15 +275,17 @@ def __init__(self, request_id, user_id, reason= None, is_point_person = False):
self.is_point_person = is_point_person
def __repr__(self):
return '<Owner %r>' %self.id
def __str__(self):
return str(self.user)

### @export "Subscriber"
class Subscriber(db.Model):
class Subscriber(db.Model):
# A person subscribed to a request, who may or may not have created the request, and may or may not own a part of the request.
__tablename__ = 'subscriber'
id = db.Column(db.Integer, primary_key = True)
should_notify = db.Column(db.Boolean, default = True) # Allows a subscriber to unsubscribe
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
user = relationship("User", uselist = False)
user = relationship("User", uselist = False, lazy='joined')
request_id = db.Column(db.Integer, db.ForeignKey('request.id'))
date_created = db.Column(db.DateTime)
owner_id = db.Column(db.Integer, db.ForeignKey('owner.id')) # Not null if responsible for fulfilling a part of the request. UPDATE 6-11-2014: This isn't used. we should get rid of it.
Expand All @@ -277,6 +295,8 @@ def __init__(self, request_id, user_id, creator = False):
self.date_created = datetime.now().isoformat()
def __repr__(self):
return '<Subscriber %r>' %self.user_id
def __str__(self):
return str(self.user)

### @export "Record"
class Record(db.Model):
Expand All @@ -287,7 +307,7 @@ class Record(db.Model):
user_id = db.Column(db.Integer, db.ForeignKey('user.id')) # The user who uploaded the record, right now only city staff can
doc_id = db.Column(db.Integer) # The document ID. Currently using Scribd API to upload documents.
request_id = db.Column(db.Integer, db.ForeignKey('request.id')) # The request this record was uploaded for
description = db.Column(db.String(400)) # A short description of what the record is.
description = db.Column(db.String(400)) # A short description of what the record is.
filename = db.Column(db.String(400)) # The original name of the file being uploaded.
url = db.Column(db.String()) # Where it exists on the internet.
download_url = db.Column(db.String()) # Where it can be downloaded on the internet.
Expand Down Expand Up @@ -332,6 +352,6 @@ class Visualization(db.Model):
def __init__(self, type_viz, content):
self.type_viz = type_viz
self.content = content
self.date_created = datetime.now().isoformat()
self.date_created = datetime.now(utc).isoformat()
def __repr__(self):
return '<Visualization %r>' % self.type_viz
Loading

0 comments on commit 03958e0

Please sign in to comment.