Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Admin extensions #185

Merged
merged 31 commits into from
Aug 14, 2015
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
2e202fe
Whitespace
mjumbewu Apr 9, 2015
d3127d1
Add an admin page for adding/editing users.
mjumbewu Apr 10, 2015
a3b3521
Simplify the `UsersView` by using the `current_department` field dire…
mjumbewu Apr 10, 2015
ea41796
Correct a typo: change `UsersView` to `UserView`
mjumbewu Apr 10, 2015
296d37b
Whitespace
mjumbewu Apr 10, 2015
ffc6b67
Get rid of unused import, `contextfunction`
mjumbewu Apr 10, 2015
1043701
UNDO THIS COMMIT!
mjumbewu Apr 10, 2015
178fe0c
Add better column labels to the user admin page
mjumbewu Apr 10, 2015
2838210
Add ability for user to administer departments
mjumbewu Apr 15, 2015
86e1480
Explain that contacts are just users
mjumbewu Apr 15, 2015
fddf0c0
Whitespace
mjumbewu Apr 17, 2015
ed8ca98
Update `User.department_id` field references
mjumbewu Apr 20, 2015
c927f35
Whitespace
mjumbewu Apr 20, 2015
2bb599b
Undoing commit 1043701b
mjumbewu Apr 20, 2015
61f731e
Add a link in the admin to export request data
mjumbewu Apr 21, 2015
02f5f9d
Allow disabling `@login_required` via environment var
mjumbewu Apr 21, 2015
9ee1feb
Give the records export data a filename
mjumbewu Apr 21, 2015
82bbefb
Whitespace
mjumbewu Apr 21, 2015
52c370b
Add a pretty string representation for owners and subscribers
mjumbewu Apr 27, 2015
8811121
Use eager loading of owners and subscribers
mjumbewu Apr 27, 2015
fed1b92
Add post-date protection to admin
mjumbewu Apr 27, 2015
45384c0
Whitespace
mjumbewu Apr 27, 2015
af60797
Add post date protection to the main UI
mjumbewu Apr 27, 2015
27745b8
Correct the `is_supported_browser` method
mjumbewu Apr 28, 2015
5851abe
Remove LOGIN_DISABLED environment setting
mjumbewu May 7, 2015
8256dcc
Add an alembic migration for the User and Deparment models
mjumbewu May 19, 2015
5599ac1
Require an extension reason before submitting
mjumbewu May 20, 2015
7a9b0ac
Convert the Request.date_created column to UTC
mjumbewu May 20, 2015
9c420e7
Show the requested time with on the case template
mjumbewu May 20, 2015
89cd96f
Whitespace
mjumbewu May 20, 2015
ef33cd7
Display requested times in request list
mjumbewu May 20, 2015
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
16 changes: 12 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,10 +46,12 @@ 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")

set_bool_env(key = 'LOGIN_DISABLED', default = False)

# Set rest of the variables that don't have defaults:
envvars = [
'DEFAULT_MAIL_SENDER', # The e-mail address used as the FROM field for all notifications
Expand Down Expand Up @@ -71,7 +79,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
49 changes: 34 additions & 15 deletions public_records_portal/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,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"))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it necessary to do this renaming? If so, a migration would be helpful.

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 +66,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 +91,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 +116,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 Down Expand Up @@ -142,7 +157,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 +169,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 +222,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 +251,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 +274,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 +294,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 +306,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
91 changes: 61 additions & 30 deletions public_records_portal/prflask.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@



from datetime import date
from public_records_portal import app, models, db, views
from views import * # Import all the functions that render templates
from flask.ext.restless import APIManager
from flask.ext.admin import Admin, expose, BaseView, AdminIndexView
from flask.ext.admin.contrib.sqlamodel import ModelView
from flask.ext.admin.contrib.sqla import ModelView
from jinja2.filters import do_mark_safe
from wtforms.validators import ValidationError


# Create API
Expand All @@ -33,52 +36,80 @@ class HomeView(AdminIndexView):
def home(self):
return self.render('admin.html')
def is_accessible(self):
if current_user.is_authenticated():
if 'LIST_OF_ADMINS' in app.config:
admins = app.config['LIST_OF_ADMINS'].split(",")
if current_user.email.lower() in admins:
return True
return False
if current_user.is_authenticated():
if 'LIST_OF_ADMINS' in app.config:
admins = app.config['LIST_OF_ADMINS'].split(",")
if current_user.email.lower() in admins:
return True
return False

# Create Admin
admin = Admin(app, name='RecordTrac Admin', url='/admin', index_view = HomeView(name='Home'))

class AdminView(ModelView):
def is_accessible(self):
if current_user.is_authenticated():
if 'LIST_OF_ADMINS' in app.config:
admins = app.config['LIST_OF_ADMINS'].split(",")
if current_user.email.lower() in admins:
return True
if current_user.is_authenticated():
if 'LIST_OF_ADMINS' in app.config:
admins = app.config['LIST_OF_ADMINS'].split(",")
if current_user.email.lower() in admins:
return True
return False

def postdate_check(form, field):
if field.data.date() > date.today():
raise ValidationError('This field cannot be post-dated')

class RequestView(AdminView):
can_create = False
can_edit = True
column_list = ('id', 'text', 'date_created', 'status') # The fields the admin can view
column_searchable_list = ('status', 'text') # The fields the admin can search a request by
form_excluded_columns = ('date_created', 'extended', 'status', 'status_updated', 'current_owner') # The fields the admin cannot edit.
can_create = False
can_edit = True
column_list = ('id', 'text', 'date_created', 'status') # The fields the admin can view
column_searchable_list = ('status', 'text') # The fields the admin can search a request by
form_excluded_columns = ('date_created', 'extended', 'status', 'status_updated', 'current_owner') # The fields the admin cannot edit.
form_args = dict(date_received={
'validators': [postdate_check]
})

class RecordView(AdminView):
can_create = False
column_searchable_list = ('description', 'filename', 'url', 'download_url', 'access')
column_list = ('request_id', 'description', 'filename', 'url', 'download_url', 'access')
can_edit = False
can_create = False
column_searchable_list = ('description', 'filename', 'url', 'download_url', 'access')
column_list = ('request_id', 'description', 'filename', 'url', 'download_url', 'access')
can_edit = False

class QAView(AdminView):
can_create = False
can_edit = True
column_list = ('request_id', 'question', 'answer', 'date_created')
form_excluded_columns = ('date_created')
can_create = False
can_edit = True
column_list = ('request_id', 'question', 'answer', 'date_created')
form_excluded_columns = ('date_created')

class NoteView(AdminView):
can_create = False
can_edit = True
column_list = ('request_id', 'text', 'date_created')
form_excluded_columns = ('date_created')

can_create = False
can_edit = True
column_list = ('request_id', 'text', 'date_created')
form_excluded_columns = ('date_created')

class UserView(AdminView):
can_create = True
can_edit = True
column_list = ('alias', 'email', 'current_department', 'phone', 'is_staff')
column_labels = dict(alias='Name', current_department='Department', phone='Phone #')
column_descriptions = dict(is_staff='Determines whether the user can log in and edit data through this interface.')
form_excluded_columns = ('date_created', 'password', 'contact_for', 'backup_for')

class DepartmentView(AdminView):
can_create = True
can_edit = True
column_list = ('name', 'primary_contact', 'backup_contact')
column_descriptions = dict(backup_contact='Note that if you want to assign a user that does not yet exist as the primary or backup contact for this department, you must <a href="/admin/userview/new/?url=%2Fadmin%2Fdepartmentview%2Fnew%2F">create the user</a> first.')

form_columns = column_list
form_excluded_columns = ('date_created', 'date_updated')
form_args = dict(backup_contact={
'description': do_mark_safe(column_descriptions['backup_contact'])
})

admin.add_view(RequestView(models.Request, db.session))
admin.add_view(RecordView(models.Record, db.session))
admin.add_view(NoteView(models.Note, db.session))
admin.add_view(QAView(models.QA, db.session))
admin.add_view(UserView(models.User, db.session))
admin.add_view(DepartmentView(models.Department, db.session))
Loading