├── foia_hub ├── __init__.py ├── scripts │ └── __init__.py ├── tests │ ├── __init__.py │ ├── helpers.py │ ├── README.md │ └── test_models.py ├── fixtures │ ├── __init__.py │ ├── stats_test.json │ ├── offices_test.json │ └── agencies_test.json ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── clear_duplicate_offices.py │ │ ├── load_agency_contacts.py │ │ └── scss.py ├── migrations │ ├── __init__.py │ ├── 0020_delete_readingroomurls.py │ ├── 0011_remove_agency_usa_contact_id.py │ ├── 0005_auto_20141001_1652.py │ ├── 0002_office_top_level.py │ ├── 0013_add_request_relationship_to_agency.py │ ├── 0019_auto_20150301_1941.py │ ├── 0009_agency_usa_contact_id.py │ ├── 0008_auto_20141010_1652.py │ ├── 0007_auto_20141010_1519.py │ ├── 0021_readingroomurls.py │ ├── 0018_delete_office_dups.py │ ├── 0022_auto_20150301_2049.py │ ├── 0017_stats_less_than_one.py │ ├── 0015_adding_stats_obj.py │ ├── 0004_office_slug_data.py │ ├── 0012_add_office_slug.py │ ├── 0010_addresses_and_emails.py │ ├── 0016_auto_20141119_2040.py │ ├── 0014_freeze_contact_info_on_request.py │ ├── 0001_initial.py │ └── 0006_auto_20141002_1930.py ├── settings │ ├── __init__.py │ ├── staging.py │ ├── local_settings.py │ ├── test.py │ ├── production.py │ ├── dev.py │ ├── notdev.py │ └── base.py ├── templatetags │ ├── __init__.py │ └── get_domain.py ├── context_processors │ ├── __init__.py │ └── google_analytics.py ├── templates │ ├── robots.txt │ ├── request │ │ ├── noop.html │ │ ├── success.html │ │ └── form.html │ ├── 500.html │ ├── includes │ │ ├── footer.html │ │ ├── small_search.html │ │ ├── breadcrumb.html │ │ ├── search.html │ │ ├── head.html │ │ └── nav.html │ ├── 404.html │ ├── contacts │ │ ├── parent_profile.html │ │ ├── index.html │ │ └── contact.html │ ├── base.html │ ├── index.html │ ├── developers.html │ └── about.html ├── static │ ├── sass │ │ ├── base │ │ │ ├── extends │ │ │ │ ├── _clearfix.scss │ │ │ │ ├── _hide-text.scss │ │ │ │ ├── _errors.scss │ │ │ │ ├── _button.scss │ │ │ │ └── _flashes.scss │ │ │ ├── _buttons.scss │ │ │ ├── _tables.scss │ │ │ ├── _lists.scss │ │ │ ├── _base.scss │ │ │ ├── _grid-settings.scss │ │ │ ├── _forms.scss │ │ │ ├── _typography.scss │ │ │ └── _variables.scss │ │ ├── _fonts.scss │ │ ├── _grid-settings.scss │ │ ├── _accessibility.scss │ │ ├── contactupdater.scss │ │ └── _mixins.scss │ ├── images │ │ ├── logo-18f.png │ │ ├── square.png │ │ ├── us_flag_small.png │ │ └── favicon │ │ │ ├── favicon.ico │ │ │ ├── mstile-70x70.png │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-194x194.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon-96x96.png │ │ │ ├── mstile-144x144.png │ │ │ ├── mstile-150x150.png │ │ │ ├── mstile-310x150.png │ │ │ ├── mstile-310x310.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── android-chrome-36x36.png │ │ │ ├── android-chrome-48x48.png │ │ │ ├── android-chrome-72x72.png │ │ │ ├── android-chrome-96x96.png │ │ │ ├── android-chrome-144x144.png │ │ │ ├── android-chrome-192x192.png │ │ │ ├── apple-touch-icon-57x57.png │ │ │ ├── apple-touch-icon-60x60.png │ │ │ ├── apple-touch-icon-72x72.png │ │ │ ├── apple-touch-icon-76x76.png │ │ │ ├── apple-touch-icon-114x114.png │ │ │ ├── apple-touch-icon-120x120.png │ │ │ ├── apple-touch-icon-144x144.png │ │ │ ├── apple-touch-icon-152x152.png │ │ │ ├── apple-touch-icon-180x180.png │ │ │ └── apple-touch-icon-precomposed.png │ ├── font-awesome-4.2.0 │ │ ├── fonts │ │ │ ├── FontAwesome.otf │ │ │ ├── fontawesome-webfont.eot │ │ │ ├── fontawesome-webfont.ttf │ │ │ └── fontawesome-webfont.woff │ │ ├── less │ │ │ ├── fixed-width.less │ │ │ ├── bordered-pulled.less │ │ │ ├── larger.less │ │ │ ├── core.less │ │ │ ├── list.less │ │ │ ├── font-awesome.less │ │ │ ├── stacked.less │ │ │ ├── rotated-flipped.less │ │ │ ├── spinning.less │ │ │ ├── path.less │ │ │ └── mixins.less │ │ └── scss │ │ │ ├── _fixed-width.scss │ │ │ ├── _bordered-pulled.scss │ │ │ ├── _larger.scss │ │ │ ├── _core.scss │ │ │ ├── _list.scss │ │ │ ├── font-awesome.scss │ │ │ ├── _stacked.scss │ │ │ ├── _spinning.scss │ │ │ ├── _path.scss │ │ │ ├── _rotated-flipped.scss │ │ │ └── _mixins.scss │ └── js │ │ ├── contacts_index_gatracker.js │ │ ├── contacts_profile_gatracker.js │ │ ├── contacts │ │ └── main.js │ │ ├── contact-updater │ │ ├── contact-search.js │ │ └── form-controls.js │ │ ├── requests │ │ ├── form.js │ │ └── index.js │ │ ├── main.js │ │ └── search.js ├── admin.py ├── wsgi.py ├── urls.py └── views.py ├── .python-version ├── contact_updater ├── __init__.py ├── urls.py ├── README.md ├── templates │ ├── form_index.html │ ├── validated.html │ ├── decentralized.html │ ├── contact-updater-base.html │ └── agency_form.html └── views.py ├── runtime.txt ├── Procfile ├── docs ├── screens │ ├── doj.png │ ├── fbi.png │ ├── gsa.png │ ├── ostp.png │ └── doj-office.png ├── source │ ├── index.rst │ └── request.rst └── README.md ├── deploy ├── package.json ├── bin │ └── deploy-site.sh ├── config.py.example ├── hookshot.js ├── fabfile.py ├── README.md └── hub.conf ├── .coveragerc ├── requirements-dev.txt ├── manifest.yml ├── staging-manifest.yml ├── check-new-data.sh ├── manage.py ├── cf.sh ├── .cfignore ├── .gitignore ├── .jshintrc ├── .travis.yml ├── requirements.txt ├── .about.yml ├── deploy.sh ├── env.example ├── CONTRIBUTING.md ├── LICENSE.md └── fabfile.py /foia_hub/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.4.7 2 | -------------------------------------------------------------------------------- /contact_updater/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /foia_hub/scripts/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /foia_hub/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.4.7 2 | -------------------------------------------------------------------------------- /foia_hub/fixtures/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /foia_hub/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /foia_hub/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /foia_hub/settings/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /foia_hub/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /foia_hub/context_processors/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /foia_hub/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /foia_hub/templates/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn foia_hub.wsgi:application --log-file - 2 | -------------------------------------------------------------------------------- /docs/screens/doj.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/2015-foia-hub/HEAD/docs/screens/doj.png -------------------------------------------------------------------------------- /docs/screens/fbi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/2015-foia-hub/HEAD/docs/screens/fbi.png -------------------------------------------------------------------------------- /docs/screens/gsa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/2015-foia-hub/HEAD/docs/screens/gsa.png -------------------------------------------------------------------------------- /docs/screens/ostp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/2015-foia-hub/HEAD/docs/screens/ostp.png -------------------------------------------------------------------------------- /foia_hub/static/sass/base/extends/_clearfix.scss: -------------------------------------------------------------------------------- 1 | %clearfix { 2 | @include clearfix; 3 | } 4 | -------------------------------------------------------------------------------- /foia_hub/static/sass/base/extends/_hide-text.scss: -------------------------------------------------------------------------------- 1 | %hide-text { 2 | @include hide-text; 3 | } 4 | -------------------------------------------------------------------------------- /docs/screens/doj-office.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/2015-foia-hub/HEAD/docs/screens/doj-office.png -------------------------------------------------------------------------------- /deploy/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "hookshot": "*", 4 | "minimist": "*" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | omit = 3 | bin/* 4 | develop-eggs/* 5 | eggs/* 6 | */site-packages/* 7 | -------------------------------------------------------------------------------- /foia_hub/static/images/logo-18f.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/2015-foia-hub/HEAD/foia_hub/static/images/logo-18f.png -------------------------------------------------------------------------------- /foia_hub/static/images/square.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/2015-foia-hub/HEAD/foia_hub/static/images/square.png -------------------------------------------------------------------------------- /foia_hub/static/images/us_flag_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/2015-foia-hub/HEAD/foia_hub/static/images/us_flag_small.png -------------------------------------------------------------------------------- /foia_hub/static/images/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/2015-foia-hub/HEAD/foia_hub/static/images/favicon/favicon.ico -------------------------------------------------------------------------------- /foia_hub/static/images/favicon/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/2015-foia-hub/HEAD/foia_hub/static/images/favicon/mstile-70x70.png -------------------------------------------------------------------------------- /deploy/bin/deploy-site.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source /home/foia/.bashrc 4 | cd /home/foia/hub/current 5 | workon fab 6 | fab -H localhost deploy 7 | -------------------------------------------------------------------------------- /deploy/config.py.example: -------------------------------------------------------------------------------- 1 | 2 | workers = 2 3 | daemon = True 4 | 5 | pidfile = "/path/to/gunicorn.pid" 6 | accesslog = "/path/to/log/gunicorn.log" 7 | -------------------------------------------------------------------------------- /foia_hub/static/images/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/2015-foia-hub/HEAD/foia_hub/static/images/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /foia_hub/static/images/favicon/favicon-194x194.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/2015-foia-hub/HEAD/foia_hub/static/images/favicon/favicon-194x194.png -------------------------------------------------------------------------------- /foia_hub/static/images/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/2015-foia-hub/HEAD/foia_hub/static/images/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /foia_hub/static/images/favicon/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/2015-foia-hub/HEAD/foia_hub/static/images/favicon/favicon-96x96.png -------------------------------------------------------------------------------- /foia_hub/static/images/favicon/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/2015-foia-hub/HEAD/foia_hub/static/images/favicon/mstile-144x144.png -------------------------------------------------------------------------------- /foia_hub/static/images/favicon/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/2015-foia-hub/HEAD/foia_hub/static/images/favicon/mstile-150x150.png -------------------------------------------------------------------------------- /foia_hub/static/images/favicon/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/2015-foia-hub/HEAD/foia_hub/static/images/favicon/mstile-310x150.png -------------------------------------------------------------------------------- /foia_hub/static/images/favicon/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/2015-foia-hub/HEAD/foia_hub/static/images/favicon/mstile-310x310.png -------------------------------------------------------------------------------- /foia_hub/tests/helpers.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | def json_from(response): 5 | return json.loads(response.content.decode(encoding='UTF-8')) 6 | -------------------------------------------------------------------------------- /foia_hub/static/images/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/2015-foia-hub/HEAD/foia_hub/static/images/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /foia_hub/static/images/favicon/android-chrome-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/2015-foia-hub/HEAD/foia_hub/static/images/favicon/android-chrome-36x36.png -------------------------------------------------------------------------------- /foia_hub/static/images/favicon/android-chrome-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/2015-foia-hub/HEAD/foia_hub/static/images/favicon/android-chrome-48x48.png -------------------------------------------------------------------------------- /foia_hub/static/images/favicon/android-chrome-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/2015-foia-hub/HEAD/foia_hub/static/images/favicon/android-chrome-72x72.png -------------------------------------------------------------------------------- /foia_hub/static/images/favicon/android-chrome-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/2015-foia-hub/HEAD/foia_hub/static/images/favicon/android-chrome-96x96.png -------------------------------------------------------------------------------- /foia_hub/static/font-awesome-4.2.0/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/2015-foia-hub/HEAD/foia_hub/static/font-awesome-4.2.0/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /foia_hub/static/images/favicon/android-chrome-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/2015-foia-hub/HEAD/foia_hub/static/images/favicon/android-chrome-144x144.png -------------------------------------------------------------------------------- /foia_hub/static/images/favicon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/2015-foia-hub/HEAD/foia_hub/static/images/favicon/android-chrome-192x192.png -------------------------------------------------------------------------------- /foia_hub/static/images/favicon/apple-touch-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/2015-foia-hub/HEAD/foia_hub/static/images/favicon/apple-touch-icon-57x57.png -------------------------------------------------------------------------------- /foia_hub/static/images/favicon/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/2015-foia-hub/HEAD/foia_hub/static/images/favicon/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /foia_hub/static/images/favicon/apple-touch-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/2015-foia-hub/HEAD/foia_hub/static/images/favicon/apple-touch-icon-72x72.png -------------------------------------------------------------------------------- /foia_hub/static/images/favicon/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/2015-foia-hub/HEAD/foia_hub/static/images/favicon/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | #Requirements for development. 2 | 3 | flake8==2.5.1 4 | ipython==4.0.3 5 | mock 6 | sphinx==1.3.5 7 | coveralls 8 | django-debug-toolbar==1.4 9 | -------------------------------------------------------------------------------- /foia_hub/static/images/favicon/apple-touch-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/2015-foia-hub/HEAD/foia_hub/static/images/favicon/apple-touch-icon-114x114.png -------------------------------------------------------------------------------- /foia_hub/static/images/favicon/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/2015-foia-hub/HEAD/foia_hub/static/images/favicon/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /foia_hub/static/images/favicon/apple-touch-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/2015-foia-hub/HEAD/foia_hub/static/images/favicon/apple-touch-icon-144x144.png -------------------------------------------------------------------------------- /foia_hub/static/images/favicon/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/2015-foia-hub/HEAD/foia_hub/static/images/favicon/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /foia_hub/static/images/favicon/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/2015-foia-hub/HEAD/foia_hub/static/images/favicon/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /foia_hub/static/font-awesome-4.2.0/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/2015-foia-hub/HEAD/foia_hub/static/font-awesome-4.2.0/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /foia_hub/static/font-awesome-4.2.0/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/2015-foia-hub/HEAD/foia_hub/static/font-awesome-4.2.0/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /foia_hub/static/font-awesome-4.2.0/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/2015-foia-hub/HEAD/foia_hub/static/font-awesome-4.2.0/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /foia_hub/static/images/favicon/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/2015-foia-hub/HEAD/foia_hub/static/images/favicon/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /foia_hub/static/font-awesome-4.2.0/less/fixed-width.less: -------------------------------------------------------------------------------- 1 | // Fixed Width Icons 2 | // ------------------------- 3 | .@{fa-css-prefix}-fw { 4 | width: (18em / 14); 5 | text-align: center; 6 | } 7 | -------------------------------------------------------------------------------- /foia_hub/static/font-awesome-4.2.0/scss/_fixed-width.scss: -------------------------------------------------------------------------------- 1 | // Fixed Width Icons 2 | // ------------------------- 3 | .#{$fa-css-prefix}-fw { 4 | width: (18em / 14); 5 | text-align: center; 6 | } 7 | -------------------------------------------------------------------------------- /manifest.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # all applications use these settings and services 3 | memory: 512mb 4 | instances: 1 5 | applications: 6 | - name: foia 7 | path: . 8 | stack: cflinuxfs2 9 | timeout: 180 10 | -------------------------------------------------------------------------------- /foia_hub/templatetags/get_domain.py: -------------------------------------------------------------------------------- 1 | from django_jinja import library 2 | from urllib.parse import urlparse 3 | 4 | @library.global_function 5 | def get_domain(url): 6 | return "%s/..." % urlparse(url).netloc 7 | -------------------------------------------------------------------------------- /staging-manifest.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # all applications use these settings and services 3 | memory: 512m 4 | instances: 1 5 | applications: 6 | - name: openfoia-staging 7 | path: . 8 | stack: cflinuxfs2 9 | timeout: 180 10 | -------------------------------------------------------------------------------- /foia_hub/static/sass/base/extends/_errors.scss: -------------------------------------------------------------------------------- 1 | %error { 2 | color: darken($error-color, 60); 3 | } 4 | 5 | %warning { 6 | color: darken($alert-color, 60); 7 | } 8 | 9 | %notice { 10 | color: darken($notice-color, 60); 11 | } 12 | -------------------------------------------------------------------------------- /foia_hub/settings/staging.py: -------------------------------------------------------------------------------- 1 | from .notdev import * 2 | 3 | # Some possible hosts 4 | ALLOWED_HOSTS = ['open-foia-staging.app.cloud.gov', '127.0.0.1'] 5 | 6 | try: 7 | from .local_settings import * 8 | except ImportError: 9 | pass 10 | -------------------------------------------------------------------------------- /foia_hub/context_processors/google_analytics.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | def google_analytics(request): 4 | """ This should make the Google ANALYTICS_ID available to all templates. 5 | """ 6 | return {'ANALYTICS_ID': settings.ANALYTICS_ID} 7 | -------------------------------------------------------------------------------- /foia_hub/static/sass/base/_buttons.scss: -------------------------------------------------------------------------------- 1 | button, 2 | input[type="submit"] { 3 | @extend %button; 4 | @include appearance(none); 5 | border: none; 6 | cursor: pointer; 7 | user-select: none; 8 | vertical-align: middle; 9 | white-space: nowrap; 10 | } 11 | -------------------------------------------------------------------------------- /foia_hub/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from foia_hub.models import Office, Agency 3 | 4 | 5 | class GenericAdmin(admin.ModelAdmin): 6 | pass 7 | 8 | 9 | admin.site.register(Office, GenericAdmin) 10 | admin.site.register(Agency, GenericAdmin) 11 | -------------------------------------------------------------------------------- /check-new-data.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | FOIA_DIR=${1:-"../foia"} 3 | 4 | pushd $FOIA_DIR 5 | git fetch 6 | if [[ `git log HEAD..origin/master --oneline` ]]; then 7 | git pull origin master 8 | popd 9 | python manage.py load_agency_contacts ${FOIA_DIR}/contacts/data 10 | fi 11 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "foia_hub.settings.dev") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /cf.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "------ Starting APP ------" 3 | if [ $CF_INSTANCE_INDEX = "0" ]; then 4 | echo "----- Migrating Database -----" 5 | python manage.py migrate --noinput 6 | echo "----- Loading Agency Contacts -----" 7 | python manage.py load_agency_contacts 8 | fi 9 | gunicorn foia_hub.wsgi:application --log-file - 10 | -------------------------------------------------------------------------------- /contact_updater/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, url 2 | from contact_updater.views import prepopulate_agency, form_index 3 | 4 | urlpatterns = patterns( 5 | '', 6 | url(r'^$', form_index, name='contact_updater_index'), 7 | url(r'^(?P[-\w]+)/?$', 8 | prepopulate_agency, 9 | name='contact_updater_form'), 10 | ) 11 | -------------------------------------------------------------------------------- /.cfignore: -------------------------------------------------------------------------------- 1 | /temp-data 2 | .env 3 | /staticfiles 4 | 5 | *.log 6 | *.db 7 | 8 | node_modules 9 | *__pycache__* 10 | *.pyc 11 | *.pyo 12 | *.DS_Store 13 | *.swp 14 | *.codekit 15 | 16 | # SCSS/SASS 17 | .sass-cache 18 | */static/sass/bourbon 19 | */static/sass/neat 20 | .webassets-cache 21 | 22 | /todo.txt 23 | 24 | /deploy/config.py 25 | /log 26 | 27 | docs/build 28 | .coverage 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /temp-data 2 | .env 3 | /staticfiles 4 | 5 | *.log 6 | *.db 7 | 8 | node_modules 9 | *__pycache__* 10 | *.pyc 11 | *.pyo 12 | *.DS_Store 13 | *.swp 14 | *.codekit 15 | 16 | # SCSS/SASS 17 | .sass-cache 18 | */static/sass/bourbon 19 | */static/sass/neat 20 | .webassets-cache 21 | 22 | /todo.txt 23 | 24 | /deploy/config.py 25 | /log 26 | 27 | docs/build 28 | .coverage 29 | -------------------------------------------------------------------------------- /foia_hub/templates/request/noop.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block body %} 4 | 5 |
6 |
7 |

The request submission form currently doesn't do anything. You 8 | should never reach this screen in production (the form should be turned 9 | off).

10 |
11 |
12 | 13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /foia_hub/migrations/0020_delete_readingroomurls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('foia_hub', '0019_auto_20150301_1941'), 11 | ] 12 | 13 | operations = [ 14 | migrations.DeleteModel( 15 | name='ReadingRoomUrls', 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /foia_hub/static/font-awesome-4.2.0/less/bordered-pulled.less: -------------------------------------------------------------------------------- 1 | // Bordered & Pulled 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-border { 5 | padding: .2em .25em .15em; 6 | border: solid .08em @fa-border-color; 7 | border-radius: .1em; 8 | } 9 | 10 | .pull-right { float: right; } 11 | .pull-left { float: left; } 12 | 13 | .@{fa-css-prefix} { 14 | &.pull-left { margin-right: .3em; } 15 | &.pull-right { margin-left: .3em; } 16 | } 17 | -------------------------------------------------------------------------------- /foia_hub/static/font-awesome-4.2.0/scss/_bordered-pulled.scss: -------------------------------------------------------------------------------- 1 | // Bordered & Pulled 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-border { 5 | padding: .2em .25em .15em; 6 | border: solid .08em $fa-border-color; 7 | border-radius: .1em; 8 | } 9 | 10 | .pull-right { float: right; } 11 | .pull-left { float: left; } 12 | 13 | .#{$fa-css-prefix} { 14 | &.pull-left { margin-right: .3em; } 15 | &.pull-right { margin-left: .3em; } 16 | } 17 | -------------------------------------------------------------------------------- /foia_hub/static/font-awesome-4.2.0/less/larger.less: -------------------------------------------------------------------------------- 1 | // Icon Sizes 2 | // ------------------------- 3 | 4 | /* makes the font 33% larger relative to the icon container */ 5 | .@{fa-css-prefix}-lg { 6 | font-size: (4em / 3); 7 | line-height: (3em / 4); 8 | vertical-align: -15%; 9 | } 10 | .@{fa-css-prefix}-2x { font-size: 2em; } 11 | .@{fa-css-prefix}-3x { font-size: 3em; } 12 | .@{fa-css-prefix}-4x { font-size: 4em; } 13 | .@{fa-css-prefix}-5x { font-size: 5em; } 14 | -------------------------------------------------------------------------------- /foia_hub/static/font-awesome-4.2.0/scss/_larger.scss: -------------------------------------------------------------------------------- 1 | // Icon Sizes 2 | // ------------------------- 3 | 4 | /* makes the font 33% larger relative to the icon container */ 5 | .#{$fa-css-prefix}-lg { 6 | font-size: (4em / 3); 7 | line-height: (3em / 4); 8 | vertical-align: -15%; 9 | } 10 | .#{$fa-css-prefix}-2x { font-size: 2em; } 11 | .#{$fa-css-prefix}-3x { font-size: 3em; } 12 | .#{$fa-css-prefix}-4x { font-size: 4em; } 13 | .#{$fa-css-prefix}-5x { font-size: 5em; } 14 | -------------------------------------------------------------------------------- /foia_hub/migrations/0011_remove_agency_usa_contact_id.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('foia_hub', '0010_addresses_and_emails'), 11 | ] 12 | 13 | operations = [ 14 | migrations.RemoveField( 15 | model_name='agency', 16 | name='usa_contact_id', 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /foia_hub/templates/500.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | 4 | {% block body %} 5 |
6 | 18 |
19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /foia_hub/static/js/contacts_index_gatracker.js: -------------------------------------------------------------------------------- 1 | // Script to track index item and position 2 | window.onload = function () { 3 | $('.agency--link').each(function(number) { 4 | this.onclick = function(){ 5 | ga('send', { 6 | 'hitType': 'event', 7 | 'eventCategory': 'agency--link', 8 | 'eventAction': 'click--agency--' + document.location.search, 9 | 'eventLabel': this.text + '--' + number 10 | }); 11 | } 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /foia_hub/templates/includes/footer.html: -------------------------------------------------------------------------------- 1 | 2 | 12 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "camelcase": true, 3 | "curly": true, 4 | "eqeqeq": true, 5 | "forin": true, 6 | "immed": true, 7 | "latedef": true, 8 | "newcap": true, 9 | "noarg": true, 10 | "quotmark": true, 11 | "undef": true, 12 | "strict": true, 13 | "unused": true, 14 | "boss": true, 15 | "browser": true, 16 | "globalstrict": true, 17 | "sub": true, 18 | "globals": { 19 | "ga": true, 20 | "$": true, 21 | "Bloodhound": true, 22 | "Handlebars": true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /foia_hub/static/font-awesome-4.2.0/less/core.less: -------------------------------------------------------------------------------- 1 | // Base Class Definition 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix} { 5 | display: inline-block; 6 | font: normal normal normal 14px/1 FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | } 12 | -------------------------------------------------------------------------------- /foia_hub/static/font-awesome-4.2.0/less/list.less: -------------------------------------------------------------------------------- 1 | // List Icons 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-ul { 5 | padding-left: 0; 6 | margin-left: @fa-li-width; 7 | list-style-type: none; 8 | > li { position: relative; } 9 | } 10 | .@{fa-css-prefix}-li { 11 | position: absolute; 12 | left: -@fa-li-width; 13 | width: @fa-li-width; 14 | top: (2em / 14); 15 | text-align: center; 16 | &.@{fa-css-prefix}-lg { 17 | left: (-@fa-li-width + (4em / 14)); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /foia_hub/static/font-awesome-4.2.0/scss/_core.scss: -------------------------------------------------------------------------------- 1 | // Base Class Definition 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix} { 5 | display: inline-block; 6 | font: normal normal normal 14px/1 FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | } 12 | -------------------------------------------------------------------------------- /foia_hub/static/font-awesome-4.2.0/scss/_list.scss: -------------------------------------------------------------------------------- 1 | // List Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-ul { 5 | padding-left: 0; 6 | margin-left: $fa-li-width; 7 | list-style-type: none; 8 | > li { position: relative; } 9 | } 10 | .#{$fa-css-prefix}-li { 11 | position: absolute; 12 | left: -$fa-li-width; 13 | width: $fa-li-width; 14 | top: (2em / 14); 15 | text-align: center; 16 | &.#{$fa-css-prefix}-lg { 17 | left: -$fa-li-width + (4em / 14); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /foia_hub/static/sass/base/_tables.scss: -------------------------------------------------------------------------------- 1 | table { 2 | border-collapse: collapse; 3 | margin: ($base-spacing / 2) 0; 4 | table-layout: fixed; 5 | width: 100%; 6 | } 7 | 8 | th { 9 | border-bottom: 1px solid darken($base-border-color, 15); 10 | font-weight: bold; 11 | padding: ($base-spacing / 2) 0; 12 | text-align: left; 13 | } 14 | 15 | td { 16 | border-bottom: $base-border; 17 | padding: ($base-spacing / 2) 0; 18 | } 19 | 20 | tr, 21 | td, 22 | th { 23 | vertical-align: middle; 24 | } 25 | -------------------------------------------------------------------------------- /foia_hub/static/sass/_fonts.scss: -------------------------------------------------------------------------------- 1 | @include font-face("proximanova", "https://18f-foia-static.s3.amazonaws.com/fonts/proximanova/proximanova@400", 400); 2 | @include font-face("proximanova", "https://18f-foia-static.s3.amazonaws.com/fonts/proximanova/proximanova@600", 600); 3 | @include font-face("proximanova", "https://18f-foia-static.s3.amazonaws.com/fonts/proximanova/proximanova-i@400", 400, italic); 4 | @include font-face("proximanova", "https://18f-foia-static.s3.amazonaws.com/fonts/proximanova/proximanova-i@600", 600, italic); 5 | -------------------------------------------------------------------------------- /foia_hub/static/font-awesome-4.2.0/scss/font-awesome.scss: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.2.0 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */ 5 | 6 | @import "variables"; 7 | @import "mixins"; 8 | @import "path"; 9 | @import "core"; 10 | @import "larger"; 11 | @import "fixed-width"; 12 | @import "list"; 13 | @import "bordered-pulled"; 14 | @import "spinning"; 15 | @import "rotated-flipped"; 16 | @import "stacked"; 17 | @import "icons"; 18 | -------------------------------------------------------------------------------- /foia_hub/migrations/0005_auto_20141001_1652.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('foia_hub', '0004_office_slug_data'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='office', 16 | name='slug', 17 | field=models.SlugField(max_length=100, unique=True), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /foia_hub/static/sass/_grid-settings.scss: -------------------------------------------------------------------------------- 1 | @import "neat/neat-helpers"; 2 | 3 | $max-width: em(1024); 4 | // $visual-grid: true; 5 | 6 | // Neat Breakpoints 7 | $grid-columns: 24; 8 | 9 | $extra-small-screen: em(320); 10 | $small-screen: em(480); 11 | $medium-screen: em(768); 12 | $large-screen: em(980); 13 | 14 | $large: new-breakpoint(max-width $large-screen 24); 15 | $medium: new-breakpoint(max-width $medium-screen 12); 16 | $small: new-breakpoint(max-width $small-screen 4); 17 | $extra-small: new-breakpoint(max-width $extra-small-screen 4); -------------------------------------------------------------------------------- /foia_hub/migrations/0002_office_top_level.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('foia_hub', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='office', 16 | name='top_level', 17 | field=models.BooleanField(default=False), 18 | preserve_default=True, 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /foia_hub/static/js/contacts_profile_gatracker.js: -------------------------------------------------------------------------------- 1 | //script to track item clicks 2 | $(function() { 3 | $('.foialibrary, .agency--email, .agency--site, .inaccurateinfo--email, .request--link, .request--email, .foiagov--search--link, .servicecenter--phone, .liaison--phone').on('click', function(item) { 4 | ga('send', { 5 | 'hitType': 'event', 6 | 'eventCategory': 'contact-info', 7 | 'eventAction': 'info-click' + document.location.pathname, 8 | 'eventLabel': this.className 9 | }); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /foia_hub/static/sass/base/extends/_button.scss: -------------------------------------------------------------------------------- 1 | %button { 2 | -webkit-font-smoothing: antialiased; 3 | background-color: $base-button-color; 4 | border-radius: $base-border-radius; 5 | color: white; 6 | display: inline-block; 7 | font-size: $base-font-size; 8 | font-weight: bold; 9 | line-height: 1; 10 | padding: 0.75em 1em; 11 | text-decoration: none; 12 | 13 | &:hover { 14 | background-color: $hover-button-color; 15 | color: white; 16 | } 17 | 18 | &:disabled { 19 | cursor: not-allowed; 20 | opacity: 0.5; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /foia_hub/static/font-awesome-4.2.0/less/font-awesome.less: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.2.0 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */ 5 | 6 | @import "variables.less"; 7 | @import "mixins.less"; 8 | @import "path.less"; 9 | @import "core.less"; 10 | @import "larger.less"; 11 | @import "fixed-width.less"; 12 | @import "list.less"; 13 | @import "bordered-pulled.less"; 14 | @import "spinning.less"; 15 | @import "rotated-flipped.less"; 16 | @import "stacked.less"; 17 | @import "icons.less"; 18 | -------------------------------------------------------------------------------- /foia_hub/migrations/0013_add_request_relationship_to_agency.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('foia_hub', '0012_add_office_slug'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='foiarequest', 16 | name='agency', 17 | field=models.ForeignKey(to='foia_hub.Agency', null=True), 18 | preserve_default=True, 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /contact_updater/README.md: -------------------------------------------------------------------------------- 1 | # Contacts Update Form 2 | 3 | ### Description 4 | A form modeled after [foia.gov's xmlgen](http://www.foia.gov/xmlgen/) to help Agencies provide us with contact information. The form pre-populates with data from openFOIA so the user only needs to make corrections to the existing data. Once the data is validated, the user is allowed to download a JSON file with the updated changes and presented with instructions on how to upload the data. 5 | 6 | ### Usage 7 | Runs as any Django app. Eventually, we should be able to combine move this app into openFOIA since it uses the same static files. 8 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. FOIA Modernization API documentation master file, created by 2 | sphinx-quickstart on Fri Sep 26 19:50:15 2014. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | 7 | This API has been deprecated and is no longer active. For a list of active GSA APIs, please visit https://open.gsa.gov/api/. 8 | ================================================== 9 | 10 | 11 | Welcome to FOIA Modernization API's documentation! 12 | 13 | 14 | .. toctree:: 15 | :maxdepth: 2 16 | 17 | api 18 | 19 | 20 | -------------------------------------------------------------------------------- /foia_hub/migrations/0019_auto_20150301_1941.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('foia_hub', '0018_delete_office_dups'), 11 | ] 12 | 13 | operations = [ 14 | migrations.RemoveField( 15 | model_name='agency', 16 | name='reading_room_urls', 17 | ), 18 | migrations.RemoveField( 19 | model_name='office', 20 | name='reading_room_urls', 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /foia_hub/migrations/0009_agency_usa_contact_id.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('foia_hub', '0008_auto_20141010_1652'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='agency', 16 | name='usa_contact_id', 17 | field=models.IntegerField(null=True, help_text='usa.gov contacts api id.'), 18 | preserve_default=True, 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /foia_hub/static/font-awesome-4.2.0/less/stacked.less: -------------------------------------------------------------------------------- 1 | // Stacked Icons 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-stack { 5 | position: relative; 6 | display: inline-block; 7 | width: 2em; 8 | height: 2em; 9 | line-height: 2em; 10 | vertical-align: middle; 11 | } 12 | .@{fa-css-prefix}-stack-1x, .@{fa-css-prefix}-stack-2x { 13 | position: absolute; 14 | left: 0; 15 | width: 100%; 16 | text-align: center; 17 | } 18 | .@{fa-css-prefix}-stack-1x { line-height: inherit; } 19 | .@{fa-css-prefix}-stack-2x { font-size: 2em; } 20 | .@{fa-css-prefix}-inverse { color: @fa-inverse; } 21 | -------------------------------------------------------------------------------- /foia_hub/static/sass/_accessibility.scss: -------------------------------------------------------------------------------- 1 | // see: 2 | #skip-nav { 3 | text-align: center; 4 | 5 | a { 6 | color: $light-blue; 7 | @include sr-only(true); 8 | } 9 | } 10 | 11 | .sr-only { 12 | @include sr-only(); 13 | } 14 | 15 | .sr-only--focus { 16 | @include sr-only(true); 17 | } 18 | 19 | #logo-18f:focus { 20 | text-decoration: none !important; 21 | img { 22 | @include prefixer(box-shadow, 0 0 5px #18f, $vendor-prefixes); 23 | } 24 | } 25 | 26 | // remove focus ring for anchor links 27 | [tabindex="-1"] { 28 | outline: none; 29 | } 30 | -------------------------------------------------------------------------------- /foia_hub/static/font-awesome-4.2.0/scss/_stacked.scss: -------------------------------------------------------------------------------- 1 | // Stacked Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-stack { 5 | position: relative; 6 | display: inline-block; 7 | width: 2em; 8 | height: 2em; 9 | line-height: 2em; 10 | vertical-align: middle; 11 | } 12 | .#{$fa-css-prefix}-stack-1x, .#{$fa-css-prefix}-stack-2x { 13 | position: absolute; 14 | left: 0; 15 | width: 100%; 16 | text-align: center; 17 | } 18 | .#{$fa-css-prefix}-stack-1x { line-height: inherit; } 19 | .#{$fa-css-prefix}-stack-2x { font-size: 2em; } 20 | .#{$fa-css-prefix}-inverse { color: $fa-inverse; } 21 | -------------------------------------------------------------------------------- /foia_hub/static/sass/base/_lists.scss: -------------------------------------------------------------------------------- 1 | ul, 2 | ol { 3 | margin: 0; 4 | padding: 0; 5 | list-style-type: none; 6 | 7 | &%default-ul { 8 | list-style-type: disc; 9 | margin-bottom: $base-spacing / 2; 10 | padding-left: $base-spacing; 11 | } 12 | 13 | &%default-ol { 14 | list-style-type: decimal; 15 | margin-bottom: $base-spacing / 2; 16 | padding-left: $base-spacing; 17 | } 18 | } 19 | 20 | dl { 21 | margin-bottom: $base-spacing / 2; 22 | 23 | dt { 24 | font-weight: bold; 25 | margin-top: $base-spacing / 2; 26 | } 27 | 28 | dd { 29 | margin: 0; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /deploy/hookshot.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var hookshot = require("hookshot"); 4 | var spawn = require("child_process").spawn; 5 | var options = require('minimist')(process.argv.slice(2)); 6 | 7 | var branch = options.b || options.branch; 8 | var command = options.c || options.command; 9 | var port = options.p || options.port; 10 | 11 | if (!branch || !command || !port) { 12 | console.error("--branch, --command, and --port are all required.") 13 | process.exit(1); 14 | } 15 | 16 | hookshot('refs/heads/' + branch, command).listen(port); 17 | 18 | console.log("Huzzah! Listening on port " + port + " for push events on " + branch + ".") 19 | -------------------------------------------------------------------------------- /foia_hub/static/sass/base/_base.scss: -------------------------------------------------------------------------------- 1 | /* Bitters 0.10.0 2 | * http://bitters.bourbon.io 3 | * Copyright 2013–2014 thoughtbot, inc. 4 | * MIT License */ 5 | 6 | // Variables 7 | @import "variables"; 8 | 9 | // Neat Settings -- uncomment if using Neat -- must be imported before Neat 10 | @import "grid-settings"; 11 | 12 | // Extends 13 | @import "extends/button"; 14 | @import "extends/clearfix"; 15 | @import "extends/errors"; 16 | @import "extends/flashes"; 17 | @import "extends/hide-text"; 18 | 19 | // Typography and Elements 20 | @import "typography"; 21 | @import "forms"; 22 | @import "tables"; 23 | @import "lists"; 24 | @import "buttons"; 25 | -------------------------------------------------------------------------------- /foia_hub/settings/local_settings.py: -------------------------------------------------------------------------------- 1 | from .base import * 2 | import os 3 | import re 4 | import dj_database_url 5 | from cfenv import AppEnv 6 | 7 | database_url = os.getenv("DATABASE_URL") 8 | 9 | env = AppEnv() 10 | cf_foia_db = env.get_service(name=re.compile('foia-db')) 11 | if cf_foia_db: 12 | database_url = cf_foia_db.credentials['uri'] 13 | 14 | # See env.example for an explanation of these settings. 15 | 16 | SECRET_KEY = os.getenv("FOIA_SECRET_SESSION_KEY") 17 | DATABASES = {'default': dj_database_url.parse(database_url)} 18 | SHOW_WEBFORM = (os.getenv("FOIA_SHOW_WEBFORM") == "true") 19 | ANALYTICS_ID = os.getenv("FOIA_ANALYTICS_ID") 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.4" 4 | install: 5 | - travis_retry pip install -r requirements.txt 6 | - travis_retry pip install -r requirements-dev.txt 7 | script: 8 | - coverage run --source=foia_hub manage.py test --settings=foia_hub.settings.test 9 | 10 | # ignore: 11 | # * E501 - line length limit 12 | # * E302,E303 - too many blank lines (come on) 13 | # * E123 - indentation on data structures (very restrictive) 14 | - flake8 . --ignore=E501,E302,E303,E123 --exclude=foia_hub/settings,docs/source 15 | 16 | # * F403 - "import *" 17 | - flake8 foia_hub/settings --ignore=F403,E501,E302,E303,E123 18 | after_success: 19 | - coveralls 20 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Django, using PostgreSQL 2 | Django==1.7 3 | psycopg2==2.5.3 4 | django-jinja==1.0.4 5 | django-jsonfield==0.9.13 6 | 7 | django-cors-headers 8 | restless==2.0.1 9 | 10 | # PyYAML is for contact loading 11 | PyYAML==3.11 12 | 13 | dj-static==0.0.6 14 | 15 | # use database strings instead of dicts 16 | dj-database-url==0.3.0 17 | 18 | django-localflavor 19 | 20 | boto3 21 | 22 | django-storages==1.6.5 23 | 24 | # for using New Relic 25 | newrelic 26 | 27 | # deployed/hosted with waitress 28 | waitress==0.8.9 29 | 30 | # enforce HTTPS in production 31 | django-secure 32 | 33 | # temporary, while we maintain an EC2 instance 34 | gunicorn 35 | 36 | cfenv==0.5.2 37 | -------------------------------------------------------------------------------- /foia_hub/migrations/0008_auto_20141010_1652.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('foia_hub', '0007_auto_20141010_1519'), 11 | ] 12 | 13 | operations = [ 14 | migrations.RenameField( 15 | model_name='agency', 16 | old_name='examples', 17 | new_name='common_requests', 18 | ), 19 | migrations.RenameField( 20 | model_name='agency', 21 | old_name='counter_examples', 22 | new_name='no_records_about', 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /foia_hub/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for foia_hub project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.6/howto/deployment/wsgi/ 8 | """ 9 | import os 10 | 11 | from django.core.wsgi import get_wsgi_application 12 | from dj_static import Cling 13 | from foia_hub.settings.base import BASE_DIR 14 | 15 | if os.getenv("NEW_RELIC_LICENSE_KEY"): 16 | import newrelic.agent 17 | newrelic.agent.initialize(os.path.join( 18 | BASE_DIR, 'newrelic.ini')) 19 | 20 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "foia_hub.settings") 21 | application = Cling(get_wsgi_application()) 22 | -------------------------------------------------------------------------------- /foia_hub/management/commands/clear_duplicate_offices.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | from foia_hub.models import Agency, Office 3 | 4 | 5 | class Command(BaseCommand): 6 | 7 | def handle(self, *args, **options): 8 | """ 9 | Deletes offices that have a sub-agency equivalent. 10 | """ 11 | 12 | agencies = Agency.objects.exclude( 13 | parent__isnull=True).values_list("name") 14 | offices = Office.objects.values_list("name").all() 15 | duplicate_offices = list(set(agencies) & set(offices)) 16 | for office_name in duplicate_offices: 17 | office = Office.objects.get(name=office_name[0]) 18 | office.delete() 19 | -------------------------------------------------------------------------------- /foia_hub/static/font-awesome-4.2.0/less/rotated-flipped.less: -------------------------------------------------------------------------------- 1 | // Rotated & Flipped Icons 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-rotate-90 { .fa-icon-rotate(90deg, 1); } 5 | .@{fa-css-prefix}-rotate-180 { .fa-icon-rotate(180deg, 2); } 6 | .@{fa-css-prefix}-rotate-270 { .fa-icon-rotate(270deg, 3); } 7 | 8 | .@{fa-css-prefix}-flip-horizontal { .fa-icon-flip(-1, 1, 0); } 9 | .@{fa-css-prefix}-flip-vertical { .fa-icon-flip(1, -1, 2); } 10 | 11 | // Hook for IE8-9 12 | // ------------------------- 13 | 14 | :root .@{fa-css-prefix}-rotate-90, 15 | :root .@{fa-css-prefix}-rotate-180, 16 | :root .@{fa-css-prefix}-rotate-270, 17 | :root .@{fa-css-prefix}-flip-horizontal, 18 | :root .@{fa-css-prefix}-flip-vertical { 19 | filter: none; 20 | } 21 | -------------------------------------------------------------------------------- /foia_hub/static/font-awesome-4.2.0/less/spinning.less: -------------------------------------------------------------------------------- 1 | // Spinning Icons 2 | // -------------------------- 3 | 4 | .@{fa-css-prefix}-spin { 5 | -webkit-animation: fa-spin 2s infinite linear; 6 | animation: fa-spin 2s infinite linear; 7 | } 8 | 9 | @-webkit-keyframes fa-spin { 10 | 0% { 11 | -webkit-transform: rotate(0deg); 12 | transform: rotate(0deg); 13 | } 14 | 100% { 15 | -webkit-transform: rotate(359deg); 16 | transform: rotate(359deg); 17 | } 18 | } 19 | 20 | @keyframes fa-spin { 21 | 0% { 22 | -webkit-transform: rotate(0deg); 23 | transform: rotate(0deg); 24 | } 25 | 100% { 26 | -webkit-transform: rotate(359deg); 27 | transform: rotate(359deg); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /foia_hub/static/font-awesome-4.2.0/scss/_spinning.scss: -------------------------------------------------------------------------------- 1 | // Spinning Icons 2 | // -------------------------- 3 | 4 | .#{$fa-css-prefix}-spin { 5 | -webkit-animation: fa-spin 2s infinite linear; 6 | animation: fa-spin 2s infinite linear; 7 | } 8 | 9 | @-webkit-keyframes fa-spin { 10 | 0% { 11 | -webkit-transform: rotate(0deg); 12 | transform: rotate(0deg); 13 | } 14 | 100% { 15 | -webkit-transform: rotate(359deg); 16 | transform: rotate(359deg); 17 | } 18 | } 19 | 20 | @keyframes fa-spin { 21 | 0% { 22 | -webkit-transform: rotate(0deg); 23 | transform: rotate(0deg); 24 | } 25 | 100% { 26 | -webkit-transform: rotate(359deg); 27 | transform: rotate(359deg); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /foia_hub/static/font-awesome-4.2.0/less/path.less: -------------------------------------------------------------------------------- 1 | /* FONT PATH 2 | * -------------------------- */ 3 | 4 | @font-face { 5 | font-family: 'FontAwesome'; 6 | src: url('@{fa-font-path}/fontawesome-webfont.eot?v=@{fa-version}'); 7 | src: url('@{fa-font-path}/fontawesome-webfont.eot?#iefix&v=@{fa-version}') format('embedded-opentype'), 8 | url('@{fa-font-path}/fontawesome-webfont.woff?v=@{fa-version}') format('woff'), 9 | url('@{fa-font-path}/fontawesome-webfont.ttf?v=@{fa-version}') format('truetype'), 10 | url('@{fa-font-path}/fontawesome-webfont.svg?v=@{fa-version}#fontawesomeregular') format('svg'); 11 | // src: url('@{fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts 12 | font-weight: normal; 13 | font-style: normal; 14 | } 15 | -------------------------------------------------------------------------------- /foia_hub/static/sass/base/extends/_flashes.scss: -------------------------------------------------------------------------------- 1 | @mixin flash($color) { 2 | background: $color; 3 | color: darken($color, 60); 4 | 5 | a { 6 | color: darken($color, 70); 7 | 8 | &:hover { 9 | color: darken($color, 90); 10 | } 11 | } 12 | } 13 | 14 | %flash-base { 15 | font-weight: bold; 16 | margin-bottom: $base-spacing / 2; 17 | padding: $base-spacing / 2; 18 | } 19 | 20 | %flash-alert { 21 | @extend %flash-base; 22 | @include flash($alert-color); 23 | } 24 | 25 | %flash-error { 26 | @extend %flash-base; 27 | @include flash($error-color); 28 | } 29 | 30 | %flash-notice { 31 | @extend %flash-base; 32 | @include flash($notice-color); 33 | } 34 | 35 | %flash-success { 36 | @extend %flash-base; 37 | @include flash($success-color); 38 | } 39 | -------------------------------------------------------------------------------- /foia_hub/static/font-awesome-4.2.0/scss/_path.scss: -------------------------------------------------------------------------------- 1 | /* FONT PATH 2 | * -------------------------- */ 3 | 4 | @font-face { 5 | font-family: 'FontAwesome'; 6 | src: url('#{$fa-font-path}/fontawesome-webfont.eot?v=#{$fa-version}'); 7 | src: url('#{$fa-font-path}/fontawesome-webfont.eot?#iefix&v=#{$fa-version}') format('embedded-opentype'), 8 | url('#{$fa-font-path}/fontawesome-webfont.woff?v=#{$fa-version}') format('woff'), 9 | url('#{$fa-font-path}/fontawesome-webfont.ttf?v=#{$fa-version}') format('truetype'), 10 | url('#{$fa-font-path}/fontawesome-webfont.svg?v=#{$fa-version}#fontawesomeregular') format('svg'); 11 | //src: url('#{$fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts 12 | font-weight: normal; 13 | font-style: normal; 14 | } 15 | -------------------------------------------------------------------------------- /foia_hub/templates/includes/small_search.html: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /foia_hub/static/font-awesome-4.2.0/scss/_rotated-flipped.scss: -------------------------------------------------------------------------------- 1 | // Rotated & Flipped Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-rotate-90 { @include fa-icon-rotate(90deg, 1); } 5 | .#{$fa-css-prefix}-rotate-180 { @include fa-icon-rotate(180deg, 2); } 6 | .#{$fa-css-prefix}-rotate-270 { @include fa-icon-rotate(270deg, 3); } 7 | 8 | .#{$fa-css-prefix}-flip-horizontal { @include fa-icon-flip(-1, 1, 0); } 9 | .#{$fa-css-prefix}-flip-vertical { @include fa-icon-flip(1, -1, 2); } 10 | 11 | // Hook for IE8-9 12 | // ------------------------- 13 | 14 | :root .#{$fa-css-prefix}-rotate-90, 15 | :root .#{$fa-css-prefix}-rotate-180, 16 | :root .#{$fa-css-prefix}-rotate-270, 17 | :root .#{$fa-css-prefix}-flip-horizontal, 18 | :root .#{$fa-css-prefix}-flip-vertical { 19 | filter: none; 20 | } 21 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | ## FOIA Docs 2 | 3 | To read the docs, visit [FOIA Hub on Read the Docs](http://foia-hub.readthedocs.org/). 4 | 5 | To build the docs locally, follow the instructions below. 6 | 7 | This folder contains docs that are built using [Sphinx](http://sphinx-doc.org/index.html). 8 | 9 | Sphinx is installed if you install the [dev-requirements.txt](https://github.com/18F/foia-hub/blob/master/requirements-dev.txt) or you can install it individually using the following command. 10 | 11 | ``` 12 | pip install sphinx 13 | ``` 14 | 15 | To build the docsusing Sphinx, from inside the docs folder, run the following. 16 | ``` 17 | make html 18 | ``` 19 | 20 | The Makefile will build the docs in the build directory. Once it is completely, you can preview the docs by visiting `build/html/index.html` in your browser. 21 | -------------------------------------------------------------------------------- /foia_hub/settings/test.py: -------------------------------------------------------------------------------- 1 | import os 2 | from .base import * 3 | 4 | SECRET_KEY = '#-nl=1b8yr*zr&6dmnv8rj5(f8w7^lv6lyd)7eyjg_xqk$zhe$' 5 | 6 | # Flag to determine whether the FOIA request form gets shown. 7 | SHOW_WEBFORM = True 8 | ANALYTICS_ID = 'MyAwesomeAnalyticsCode' 9 | 10 | # To run with postgresql `export TEST_ENGINE=postgresql_psycopg2` 11 | # Default will be sqlite3 12 | custom_backend = os.getenv("TEST_ENGINE") 13 | if custom_backend: 14 | ENGINE = "django.db.backends.%s" % custom_backend 15 | else: 16 | ENGINE = "django.db.backends.sqlite3" 17 | 18 | # We don't want to use a cache for testing 19 | CACHES = { 20 | 'default': { 21 | 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', 22 | } 23 | } 24 | 25 | DATABASES = { 26 | 'default': { 27 | 'ENGINE': ENGINE 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /foia_hub/templates/request/success.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block body %} 4 | 5 |
6 | 7 |
8 |

9 | You've successfully submitted your request! 10 |

11 | 12 |

13 | We've emailed a copy of your request to {{ requester.email }}. 14 |

15 | 16 |

17 | Your request's tracking ID is 000000{{ foia_request.id }}. 18 |

19 | 20 |

21 | You should receive a status update from the {{ agency.name }} by email within 20 working days. 22 |

23 | 24 |

25 | To reach the FOIA office of the directly, contact {{ office.public_liaison }}. 26 |

27 |
28 | 29 | {% endblock %} -------------------------------------------------------------------------------- /foia_hub/fixtures/stats_test.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pk": 25, 4 | "fields": { 5 | "agency": 15, 6 | "year": 2013, 7 | "office": null, 8 | "median": null, 9 | "stat_type": "S" 10 | }, 11 | "model": "foia_hub.stats" 12 | }, 13 | { 14 | "pk": 26, 15 | "fields": { 16 | "agency": 15, 17 | "year": 2013, 18 | "office": null, 19 | "median": 37.5, 20 | "stat_type": "C" 21 | }, 22 | "model": "foia_hub.stats" 23 | }, 24 | { 25 | "pk": 27, 26 | "fields": { 27 | "agency": 17, 28 | "year": 2013, 29 | "office": 33, 30 | "median": 12.2, 31 | "stat_type": "C" 32 | }, 33 | "model": "foia_hub.stats" 34 | } 35 | ] 36 | -------------------------------------------------------------------------------- /.about.yml: -------------------------------------------------------------------------------- 1 | --- 2 | project: FOIA Modernization 3 | name: foia 4 | github: 5 | - 18F/foia 6 | - 18F/foia-hub 7 | description: A new tool to search for the correct agency or office to make a FOIA 8 | request. 9 | partners: 10 | - Department of Justice 11 | - FOIA agency task force 12 | impact: The U.S. Federal Government processed nearly 700,000 FOIA requests in fiscal 13 | year 2013. 14 | stage: alpha 15 | milestones: 16 | - 'June 2014: Project Discovery stage started' 17 | - 'April 2015: open.foia.gov is deployed. ' 18 | contact: 19 | - 18f/foia/issues 20 | stack: python, django 21 | team: jackie, khandelwal, majma, eric, gramirez, jtag, victor, erica 22 | licenses: 23 | foia: Public Domain (CC0) 24 | foia-hub: Public Domain (CC0) 25 | licenselink: 18F/foia/blob/master/LICENSE.md 26 | links: 27 | - https://trello.com/b/D0r2UOz0/foia-scrum-board 28 | blog: 29 | - foia 30 | status: 31 | -------------------------------------------------------------------------------- /foia_hub/migrations/0007_auto_20141010_1519.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations 5 | import jsonfield.fields 6 | import foia_hub.models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('foia_hub', '0006_auto_20141002_1930'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name='agency', 18 | name='counter_examples', 19 | field=jsonfield.fields.JSONField(default=foia_hub.models.empty_list), 20 | preserve_default=True, 21 | ), 22 | migrations.AddField( 23 | model_name='agency', 24 | name='examples', 25 | field=jsonfield.fields.JSONField(default=foia_hub.models.empty_list), 26 | preserve_default=True, 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /foia_hub/settings/production.py: -------------------------------------------------------------------------------- 1 | from .notdev import * 2 | 3 | # Some possible hosts 4 | ALLOWED_HOSTS = [ 5 | 'open-foia-test.app.cloud.gov', 6 | 'open.foia.gov', 7 | 'foia-a.cf.18f.us', 8 | 'foia-b.cf.18f.us', 9 | 'openfoia-staging.cf.18f.us', 10 | 'foia.app.cloud.gov', 11 | ] 12 | 13 | # Force an HTTPS connection. 14 | # When testing production mode locally, this may require using ngrok. 15 | SECURE_SSL_REDIRECT = True 16 | 17 | # Amazon ELBs pass on X-Forwarded-Proto. 18 | SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") 19 | 20 | # Turn on once we're good and set. 21 | SECURE_HSTS_SECONDS = 31536000 22 | SECURE_HSTS_INCLUDE_SUBDOMAINS = True 23 | 24 | # In production we will have HTTPS setup. 25 | CSRF_COOKIE_SECURE = True 26 | SESSION_COOKIE_SECURE = True 27 | 28 | try: 29 | from .local_settings import * 30 | except ImportError: 31 | pass 32 | -------------------------------------------------------------------------------- /foia_hub/static/font-awesome-4.2.0/less/mixins.less: -------------------------------------------------------------------------------- 1 | // Mixins 2 | // -------------------------- 3 | 4 | .fa-icon() { 5 | display: inline-block; 6 | font: normal normal normal 14px/1 FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | } 12 | 13 | .fa-icon-rotate(@degrees, @rotation) { 14 | filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=@rotation); 15 | -webkit-transform: rotate(@degrees); 16 | -ms-transform: rotate(@degrees); 17 | transform: rotate(@degrees); 18 | } 19 | 20 | .fa-icon-flip(@horiz, @vert, @rotation) { 21 | filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=@rotation, mirror=1); 22 | -webkit-transform: scale(@horiz, @vert); 23 | -ms-transform: scale(@horiz, @vert); 24 | transform: scale(@horiz, @vert); 25 | } 26 | -------------------------------------------------------------------------------- /foia_hub/static/sass/base/_grid-settings.scss: -------------------------------------------------------------------------------- 1 | @import "../neat/neat-helpers"; // or "../neat/neat-helpers" when not in Rails 2 | 3 | // Neat Overrides 4 | // $column: 90px; 5 | // $gutter: 30px; 6 | // $grid-columns: 12; 7 | // $max-width: em(1088); 8 | 9 | // Neat Breakpoints 10 | // $medium-screen: em(640); 11 | // $large-screen: em(860); 12 | 13 | // $medium-screen-up: new-breakpoint(min-width $medium-screen 4); 14 | // $large-screen-up: new-breakpoint(min-width $large-screen 8); 15 | 16 | 17 | // Krang Overrides 18 | 19 | // $visual-grid: true; 20 | // $visual-grid-index: front; 21 | 22 | $column: 90px; 23 | $gutter: 30px; 24 | $grid-columns: 24; 25 | $max-width: em(1088); 26 | 27 | $extra-small-screen: em(320); 28 | $small-screen: em(480); 29 | $medium-screen: em(768); 30 | $large-screen: em(980); 31 | 32 | $large: new-breakpoint(max-width $large-screen 24); 33 | $medium: new-breakpoint(max-width $medium-screen 12); 34 | $small: new-breakpoint(max-width $small-screen 4); 35 | $extra-small: new-breakpoint(max-width $extra-small-screen 4); -------------------------------------------------------------------------------- /foia_hub/static/font-awesome-4.2.0/scss/_mixins.scss: -------------------------------------------------------------------------------- 1 | // Mixins 2 | // -------------------------- 3 | 4 | @mixin fa-icon() { 5 | display: inline-block; 6 | font: normal normal normal 14px/1 FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | } 12 | 13 | @mixin fa-icon-rotate($degrees, $rotation) { 14 | filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation}); 15 | -webkit-transform: rotate($degrees); 16 | -ms-transform: rotate($degrees); 17 | transform: rotate($degrees); 18 | } 19 | 20 | @mixin fa-icon-flip($horiz, $vert, $rotation) { 21 | filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation}); 22 | -webkit-transform: scale($horiz, $vert); 23 | -ms-transform: scale($horiz, $vert); 24 | transform: scale($horiz, $vert); 25 | } 26 | -------------------------------------------------------------------------------- /foia_hub/migrations/0021_readingroomurls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('foia_hub', '0020_delete_readingroomurls'), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='ReadingRoomUrls', 16 | fields=[ 17 | ('id', models.AutoField(primary_key=True, auto_created=True, verbose_name='ID', serialize=False)), 18 | ('link_text', models.CharField(max_length=512, help_text='This is the text associated with the reading room URL. ')), 19 | ('url', models.URLField(null=True, help_text="The URL to an agency's reading room.")), 20 | ('agency', models.ForeignKey(to='foia_hub.Agency')), 21 | ('office', models.ForeignKey(to='foia_hub.Office', blank=True, null=True)), 22 | ], 23 | options={ 24 | }, 25 | bases=(models.Model,), 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /contact_updater/templates/form_index.html: -------------------------------------------------------------------------------- 1 | {% extends "contact-updater-base.html" %} 2 | 3 | {% block body %} 4 | {# Pass data to JS #} 5 | 8 | 9 |
10 |

openFOIA Contact Updater

11 |

Update your agency's contact information on openFOIA.

12 |
13 |
14 | {% set search_placeholder = 'Search for an agency' %} 15 | 19 |
20 |
21 | {% endblock %} 22 | 23 | {% block footer %} 24 | {{ super() }} 25 | 26 | 27 | 28 | {% endblock %} 29 | -------------------------------------------------------------------------------- /foia_hub/migrations/0018_delete_office_dups.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations 5 | from foia_hub.models import Agency, Office 6 | 7 | 8 | def clear_duplicated_offices(apps, schema_editor): 9 | """ 10 | Deletes offices that have a sub-agency equivalent. 11 | """ 12 | agencies = Agency.objects.exclude(parent__isnull=True).values_list("name") 13 | offices = Office.objects.values_list("name").all() 14 | duplicate_offices = list(set(agencies) & set(offices)) 15 | for office_name in duplicate_offices: 16 | office = Office.objects.get(name=office_name[0]) 17 | office.delete() 18 | 19 | 20 | # no need to do anything when winding down 21 | def nothing(apps, schema_editor): 22 | pass 23 | 24 | 25 | class Migration(migrations.Migration): 26 | 27 | dependencies = [ 28 | ('foia_hub', '0017_stats_less_than_one'), 29 | ] 30 | 31 | operations = [ 32 | migrations.RunPython( 33 | clear_duplicated_offices, 34 | reverse_code=nothing 35 | ) 36 | ] 37 | -------------------------------------------------------------------------------- /foia_hub/migrations/0022_auto_20150301_2049.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('contenttypes', '0001_initial'), 11 | ('foia_hub', '0021_readingroomurls'), 12 | ] 13 | 14 | operations = [ 15 | migrations.RemoveField( 16 | model_name='readingroomurls', 17 | name='agency', 18 | ), 19 | migrations.RemoveField( 20 | model_name='readingroomurls', 21 | name='office', 22 | ), 23 | migrations.AddField( 24 | model_name='readingroomurls', 25 | name='content_type', 26 | field=models.ForeignKey(default=0, to='contenttypes.ContentType'), 27 | preserve_default=False, 28 | ), 29 | migrations.AddField( 30 | model_name='readingroomurls', 31 | name='object_id', 32 | field=models.PositiveIntegerField(default=0), 33 | preserve_default=False, 34 | ), 35 | ] 36 | -------------------------------------------------------------------------------- /foia_hub/templates/includes/breadcrumb.html: -------------------------------------------------------------------------------- 1 | {% macro create(pages) -%} 2 | 18 | {%- endmacro %} 19 | 20 | 35 | -------------------------------------------------------------------------------- /contact_updater/templates/validated.html: -------------------------------------------------------------------------------- 1 |

Instructions

2 |
    3 |
  1. Make sure that all the updated information is correct. The contact information you have entered will appear on your agency’s page.
  2. 4 |
  3. Download the contact_data.json file.
  4. 5 |
  5. Agency web managers will need to place the contact_data.json file in the /foia/quarterly/ directory of the agency's website. For example, the Department of Justice will place its file at www.justice.gov/foia/quarterly/contact_data.json. In some cases, the /foia/quarterly/ directory may not yet exist, and web managers will first need to create it.
  6. 6 |
  7. Within 2 days your updates should appear on your agency’s page. Please double-check to make sure your changes have been implemented. If not, please email us at 18f-foia@gsa.gov
  8. 7 |
8 | {# Invisible form to store data while validated this allows the user to 9 | not have data deleted #} 10 | {% for form in forms %} 11 | {% for field in form %} 12 | {{field.as_hidden()}} 13 | {% endfor %} 14 | {% endfor %} 15 | -------------------------------------------------------------------------------- /foia_hub/management/commands/load_agency_contacts.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | from foia_hub.settings.base import DEFAULT_DATA_REPO 3 | from foia_hub.scripts.load_agency_contacts import process_yamls 4 | import subprocess 5 | import tempfile 6 | import os 7 | 8 | 9 | class Command(BaseCommand): 10 | 11 | help = """ Loads FOIA Contacts from YAML webscrape. 12 | To download the data automatically: 13 | django-admin.py load_agency_contacts 14 | Or to override the directory: 15 | django-admin.py load_agency_contacts path/to/data 16 | """ 17 | 18 | def handle(self, *args, **options): 19 | if len(args) > 0: 20 | yaml_folder = args[0] 21 | process_yamls(yaml_folder) 22 | else: 23 | with tempfile.TemporaryDirectory() as tmpdirname: 24 | download_data(tmpdirname) 25 | yaml_folder = os.path.join(tmpdirname, 'contacts/data') 26 | process_yamls(yaml_folder) 27 | 28 | 29 | def download_data(directory): 30 | subprocess.check_call( 31 | ["git", "clone", DEFAULT_DATA_REPO, directory], shell=False) 32 | -------------------------------------------------------------------------------- /foia_hub/migrations/0017_stats_less_than_one.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | from foia_hub.models import Stats 6 | 7 | 8 | def clear_stats(apps, schema_editor): 9 | """ 10 | Need to delete old stats in case some of the old values are now 11 | nulls 12 | """ 13 | Stats.objects.all().delete() 14 | 15 | 16 | # no need to do anything when winding down 17 | def nothing(apps, schema_editor): 18 | pass 19 | 20 | 21 | class Migration(migrations.Migration): 22 | 23 | dependencies = [ 24 | ('foia_hub', '0016_auto_20141119_2040'), 25 | ] 26 | 27 | operations = [ 28 | migrations.AddField( 29 | model_name='stats', 30 | name='less_than_one', 31 | field=models.BooleanField( 32 | default=False, 33 | blank=True, 34 | help_text="flags when the value is `less than 1`"), 35 | preserve_default=True, 36 | ), 37 | migrations.RunPython( 38 | clear_stats, 39 | reverse_code=nothing 40 | ) 41 | ] 42 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ### 4 | # Zero downtime deploy script for foia-hub. 5 | # 6 | # Deploys a new version of the foia app, then once that works, 7 | # flips the route to point at the new version. 8 | # 9 | # The old version of the app stays around and can be reverted to 10 | # by running map-route and unmap-route. 11 | # 12 | # Modeled after http://docs.cloudfoundry.org/devguide/deploy-apps/blue-green.html 13 | # and https://github.com/dlapiduz/fugacious/blob/master/cf-deploy.sh 14 | ### 15 | 16 | ROUTE="open.foia.gov" 17 | PUSH="cf.sh" 18 | 19 | # The first grep can't have -q or the second grep won't have anything to grep! 20 | if $(cf app foia-a | grep "requested state" | grep -q started) 21 | then 22 | OLD="foia-a" 23 | NEW="foia-b" 24 | else 25 | OLD="foia-b" 26 | NEW="foia-a" 27 | fi 28 | 29 | echo "Pushing new app to $NEW, using $PUSH, and disabling $OLD." 30 | 31 | cf push $NEW -c "bash $PUSH" -i 4 32 | 33 | if [[ $? -ne 0 ]]; then 34 | echo "Error pushing to $NEW." 35 | cf stop $NEW 36 | exit 1 37 | fi 38 | 39 | echo "Re-routing $ROUTE to $NEW." 40 | cf map-route $NEW $ROUTE 41 | echo "Un-routing $ROUTE from $OLD." 42 | cf unmap-route $OLD $ROUTE 43 | cf stop $OLD 44 | 45 | echo "Done." 46 | -------------------------------------------------------------------------------- /contact_updater/templates/decentralized.html: -------------------------------------------------------------------------------- 1 | 2 |

{{agency_names[0]}}

3 | 4 |
5 |
6 | 7 | {{forms[0].description}} 8 |
9 |
10 |
11 | {{forms[0].errors.description}} 12 |

Here's where you can tell the public about what your agency or office does. People reading this description will use it to have a clearer understanding of the mission of the office and what kinds of records it processes.

13 |
14 |
15 | 16 | {{forms[0].slug.as_hidden()}} 17 | 18 | 19 |

Office

20 |
21 | 27 |
28 | 29 | 30 | {% for form in forms[1:] %} 31 | 34 | {% endfor %} 35 | -------------------------------------------------------------------------------- /foia_hub/settings/dev.py: -------------------------------------------------------------------------------- 1 | from .base import * 2 | import imp 3 | 4 | DEBUG = True 5 | TEMPLATE_DEBUG = True 6 | 7 | # Flag to determine whether the FOIA request form gets shown. 8 | SHOW_WEBFORM = False 9 | 10 | try: 11 | imp.find_module('debug_toolbar') 12 | INSTALLED_APPS = ( 13 | 'debug_toolbar', 14 | ) + INSTALLED_APPS 15 | except ImportError: 16 | pass 17 | 18 | DEFAULT_JINJA2_TEMPLATE_INTERCEPT_RE = r"^(?!debug_toolbar/).*" 19 | 20 | INTERNAL_IPS = ('127.0.0.1',) 21 | 22 | # We don't want to use a cache for testing 23 | CACHES = { 24 | 'default': { 25 | 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', 26 | } 27 | } 28 | 29 | LOGGING = { 30 | 'version': 1, 31 | 'disable_existing_loggers': False, 32 | 'handlers': { 33 | 'file': { 34 | 'level': 'DEBUG', 35 | 'class': 'logging.FileHandler', 36 | 'filename': 'debug.log', 37 | }, 38 | }, 39 | 'loggers': { 40 | 'django.request': { 41 | 'handlers': ['file'], 42 | 'level': 'DEBUG', 43 | 'propagate': True, 44 | }, 45 | }, 46 | } 47 | 48 | try: 49 | from .local_settings import * 50 | except ImportError: 51 | pass 52 | -------------------------------------------------------------------------------- /env.example: -------------------------------------------------------------------------------- 1 | # Copy this file to .env so that you can use tools like foreman or autoenv to 2 | # manage these environment variables. 3 | 4 | # Which environment? (defaults to development) 5 | DJANGO_SETTINGS_MODULE=foia_hub.settings.dev 6 | 7 | # Use with foreman or CloudFoundry. 8 | VCAP_APP_PORT=8000 9 | 10 | # Change this in production. 11 | FOIA_SECRET_SESSION_KEY="CHANGE THIS" 12 | 13 | # Replace with a Google Analytics ID in production 14 | FOIA_ANALYTICS_ID="" 15 | 16 | # Enable the app's own webform. (Not recommended.) 17 | # FOIA_SHOW_WEBFORM=true 18 | 19 | # To use a sqlite3 DB: 20 | # DATABASE_URL="sqlite:///./foia.db" 21 | 22 | # To use PostgreSQL: 23 | DATABASE_URL="postgres://username:password@host:5432/databasename" 24 | 25 | 26 | # If you use an S3 bucket for serving static files in production, you'll need 27 | # some credentials. 28 | FOIA_AWS_ACCESS_KEY_ID="CHANGE THIS" 29 | FOIA_AWS_SECRET_ACCESS_KEY="CHANGE THIS" 30 | 31 | 32 | # New Relic Key and Toggle, if there is a key New Relic will run 33 | # You'll need to install the New Relic agent available here: 34 | # https://docs.newrelic.com/docs/agents/python-agent/getting-started/python-agent-quick-start 35 | # NEW_RELIC_APP_NAME=`app name` 36 | # NEW_RELIC_LICENSE_KEY=`licence key` 37 | -------------------------------------------------------------------------------- /foia_hub/static/js/contacts/main.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | // question mark link 3 | // more info toggle 4 | $('.slidedown').on('click', function(e) { 5 | e.preventDefault(); 6 | 7 | var $link = $(this), 8 | $messageBox = $link.parent('li').next('.expandable'); 9 | 10 | $messageBox.slideToggle('fast', function() { 11 | var $icon = $link.find('span'); 12 | if ($messageBox.is(':visible')) { 13 | $icon.removeClass('fa-question-circle').addClass('fa-times-circle'); 14 | ga('send', { 15 | 'hitType': 'event', 16 | 'eventCategory': 'extrainfo--open', 17 | 'eventAction': 'openon' + document.location.pathname, 18 | 'eventLabel': this.id, 19 | }); 20 | } 21 | else { 22 | $icon.removeClass('fa-times-circle').addClass('fa-question-circle'); 23 | ga('send', { 24 | 'hitType': 'event', 25 | 'eventCategory': 'extrainfo--close', 26 | 'eventAction': 'closeon' + document.location.pathname, 27 | 'eventLabel': this.id, 28 | }); 29 | } 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /foia_hub/templates/includes/search.html: -------------------------------------------------------------------------------- 1 | 35 | -------------------------------------------------------------------------------- /contact_updater/templates/contact-updater-base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% block head %} 5 | {# Styling #} 6 | 7 | 8 | 9 | {# JS scripts needed for general functionality #} 10 | 11 | 12 | {% endblock %} 13 | 14 | 15 | 16 | {% block body %} 17 | {% endblock %} 18 | 19 | 20 |
21 | {% block footer %} 22 | 31 | {% endblock %} 32 |
33 | -------------------------------------------------------------------------------- /foia_hub/templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | 4 | {% block body %} 5 |
6 | 33 |
34 | {% endblock %} 35 | -------------------------------------------------------------------------------- /foia_hub/migrations/0015_adding_stats_obj.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import jsonfield.fields 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('foia_hub', '0014_freeze_contact_info_on_request'), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Stats', 17 | fields=[ 18 | ('id', models.AutoField(serialize=False, primary_key=True, verbose_name='ID', auto_created=True)), 19 | ('year', models.PositiveSmallIntegerField()), 20 | ('stat_type', models.CharField(choices=[('S', 'simple'), ('C', 'complex')], max_length=1)), 21 | ('median', models.FloatField(blank=True, null=True)), 22 | ('agency', models.ForeignKey(to='foia_hub.Agency')), 23 | ('office', models.ForeignKey(blank=True, to='foia_hub.Office', null=True)), 24 | ], 25 | options={ 26 | }, 27 | bases=(models.Model,), 28 | ), 29 | migrations.AlterUniqueTogether( 30 | name='stats', 31 | unique_together=set([('agency', 'office', 'year', 'stat_type')]), 32 | ), 33 | migrations.AlterField( 34 | model_name='foiarequest', 35 | name='emails', 36 | field=jsonfield.fields.JSONField(null=True), 37 | ), 38 | ] 39 | -------------------------------------------------------------------------------- /foia_hub/migrations/0004_office_slug_data.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations 5 | from django.utils.text import slugify 6 | 7 | # These migrations uses `slugify(office.name)[:50]` instead of 8 | # calling `Office.slug_for(office.name)` so that their operation 9 | # is frozen at how slugifying worked at the time of this migration, 10 | # not at whatever Office.slug_for currently does. 11 | 12 | def change_office_slugs(apps, schema_editor): 13 | """ Make the Office slugs unique. """ 14 | Office = apps.get_model("foia_hub", "Office") 15 | db_alias = schema_editor.connection.alias 16 | 17 | for office in Office.objects.using(db_alias).all(): 18 | office_slug = slugify(office.name)[:50] 19 | office.slug = ('%s--%s' % (office.agency.slug, office_slug))[:100] 20 | office.save() 21 | 22 | def reverse_office_slugs(apps, schema_editor): 23 | Office = apps.get_model("foia_hub", "Office") 24 | db_alias = schema_editor.connection.alias 25 | 26 | for office in Office.objects.using(db_alias).all(): 27 | office_slug = slugify(office.name)[:50] 28 | office.slug = office_slug 29 | office.save() 30 | 31 | class Migration(migrations.Migration): 32 | dependencies = [ 33 | ('foia_hub', '0003_auto_20141001_1633') 34 | ] 35 | 36 | operations = [ 37 | migrations.RunPython( 38 | change_office_slugs, 39 | reverse_code=reverse_office_slugs 40 | ) 41 | ] 42 | -------------------------------------------------------------------------------- /foia_hub/templates/contacts/parent_profile.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block class %}landing{% endblock %} 3 | 4 | {% block title %}{{ profile.name }} > Freedom of Information Act{% endblock %} 5 | 6 | {% if profile.description %} 7 | {% block description %} 8 | {{ profile.description }} 9 | {% endblock %} 10 | {% endif %} 11 | 12 | {% block breadcrumb %} 13 | {% import 'includes/breadcrumb.html' as breadcrumb %} 14 | {{breadcrumb.create([(url('contact_landing', profile.slug), profile.name)]) }} 15 | {% endblock %} 16 | 17 | 18 | {% block body %} 19 | 20 | {# 'profile' here is known to be an agency with sub-offices. #} 21 | 22 |
23 |
24 |

{{ profile.name }}

25 |
26 | {% if profile.description %} 27 |

28 | {{ profile.description }} 29 |

30 | {% endif %} 31 |
32 | 33 |
34 | 35 |
36 |

37 | Make your FOIA request directly to the most relevant group or component: 38 |

39 | 40 | 49 | 50 |
51 |
52 |
53 | 54 | {% endblock %} 55 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Team Processes 2 | 3 | * Use PEP8 as the coding standard for Python. 4 | * Pull requests for all commits, even typos. 5 | * Don't merge your own pull request. Find a friend to review your code and merge your pull request. 6 | * Pull requests some contain some tests. Ideally they would contain decent test coverage. 7 | * If you make changes to the API, please help update the API documentation. 8 | 9 | When creating a new pull request: 10 | 11 | * If the pull request is still a work-in-progress and should not be merged, say so in the description and then **assign the PR to yourself**. When the PR is ready to be merged, **unassign yourself and add a comment**. 12 | * If a new pull request is ready for review, **leave it unassigned**. This is the assumed state of new PRs, but work-in-progress PRs are quite welcome. 13 | * If you decide to review a pull request with the intent of merging it (or deciding what still needs to be done before merge), then **assign the PR to yourself** so that it's clear that someone's grabbed it. 14 | * Anyone is welcome to informally review a PR and comment on it at any time, no matter who is assigned. 15 | 16 | ## Public domain 17 | 18 | This project is in the public domain within the United States, and 19 | copyright and related rights in the work worldwide are waived through 20 | the [CC0 1.0 Universal public domain dedication](https://creativecommons.org/publicdomain/zero/1.0/). 21 | 22 | All contributions to this project will be released under the CC0 23 | dedication. By submitting a pull request, you are agreeing to comply 24 | with this waiver of copyright interest. 25 | 26 | -------------------------------------------------------------------------------- /foia_hub/settings/notdev.py: -------------------------------------------------------------------------------- 1 | from .base import * 2 | from cfenv import AppEnv 3 | import os 4 | import re 5 | 6 | # There are common settings between staging and production. This puts them 7 | # all in one place. 8 | 9 | DEBUG = False 10 | TEMPLATE_DEBUG = False 11 | 12 | # Flag to determine whether the FOIA request form gets shown. 13 | SHOW_WEBFORM = False 14 | 15 | CACHES = { 16 | 'default': { 17 | 'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache', 18 | 'LOCATION': os.getenv('TMPDIR', '/tmp'), 19 | 'TIMEOUT': 1440, 20 | 'OPTIONS': { 21 | 'MAX_ENTRIES': 1000 22 | }, 23 | } 24 | } 25 | 26 | AWS_STORAGE_BUCKET_NAME = os.getenv('FOIA_S3_STATIC_BUCKET_NAME') 27 | AWS_REGION = '' 28 | env = AppEnv() 29 | 30 | cf_s3_bucket = env.get_service(name=re.compile('foia-public-bucket')) 31 | if cf_s3_bucket: 32 | AWS_STORAGE_BUCKET_NAME = cf_s3_bucket.credentials['bucket'] 33 | AWS_REGION = '-%s' % cf_s3_bucket.credentials['region'] 34 | AWS_S3_REGION_NAME = cf_s3_bucket.credentials['region'] 35 | AWS_ACCESS_KEY_ID = cf_s3_bucket.credentials['access_key_id'] 36 | AWS_SECRET_ACCESS_KEY = cf_s3_bucket.credentials['secret_access_key'] 37 | 38 | 39 | AWS_S3_CUSTOM_DOMAIN = 's3%s.amazonaws.com/%s' % (AWS_REGION, AWS_STORAGE_BUCKET_NAME) 40 | STATIC_URL = 'https://%s/' % AWS_S3_CUSTOM_DOMAIN 41 | STATICFILES_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage' 42 | DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage' 43 | 44 | # Don't add complex authentication related query parameters for requests 45 | AWS_QUERYSTRING_AUTH = False 46 | -------------------------------------------------------------------------------- /foia_hub/static/sass/base/_forms.scss: -------------------------------------------------------------------------------- 1 | fieldset { 2 | background: lighten($base-border-color, 10); 3 | border: $base-border; 4 | margin: 0 0 ($base-spacing / 2) 0; 5 | padding: $base-spacing; 6 | } 7 | 8 | input, 9 | label, 10 | select { 11 | display: block; 12 | font-family: $form-font-family; 13 | font-size: $form-font-size; 14 | } 15 | 16 | label { 17 | font-weight: bold; 18 | margin-bottom: $base-spacing / 4; 19 | 20 | &.required:after { 21 | content: "*"; 22 | } 23 | 24 | abbr { 25 | display: none; 26 | } 27 | } 28 | 29 | textarea, 30 | #{$all-text-inputs}, 31 | select[multiple=multiple] { 32 | @include box-sizing(border-box); 33 | @include transition(border-color); 34 | background-color: white; 35 | border-radius: $form-border-radius; 36 | border: 1px solid $form-border-color; 37 | box-shadow: $form-box-shadow; 38 | font-family: $form-font-family; 39 | font-size: $form-font-size; 40 | margin-bottom: $base-spacing / 2; 41 | padding: ($base-spacing / 3) ($base-spacing / 3); 42 | width: 100%; 43 | 44 | &:hover { 45 | border-color: $form-border-color-hover; 46 | } 47 | 48 | &:focus { 49 | border-color: $form-border-color-focus; 50 | box-shadow: $form-box-shadow-focus; 51 | outline: none; 52 | } 53 | } 54 | 55 | textarea { 56 | resize: vertical; 57 | } 58 | 59 | input[type="search"] { 60 | @include appearance(none); 61 | } 62 | 63 | input[type="checkbox"], 64 | input[type="radio"] { 65 | display: inline; 66 | margin-right: $base-spacing / 4; 67 | } 68 | 69 | input[type="file"] { 70 | padding-bottom: $base-spacing / 2; 71 | width: 100%; 72 | } 73 | 74 | select { 75 | margin-bottom: $base-spacing; 76 | max-width: 100%; 77 | width: auto; 78 | } 79 | -------------------------------------------------------------------------------- /foia_hub/management/commands/scss.py: -------------------------------------------------------------------------------- 1 | from glob import glob 2 | import os 3 | import subprocess 4 | import optparse 5 | 6 | from django.core.management.base import BaseCommand 7 | 8 | class Command(BaseCommand): 9 | help = """Runs the sass executable to compile the scss. Add parameter 10 | "watch" to kick off a demon""" 11 | 12 | 13 | option_list = BaseCommand.option_list + ( 14 | optparse.make_option( 15 | "--style", 16 | dest="style", 17 | default="nested", 18 | help="specify the CSS output style: 'nested', 'expanded' (default), 'compact', or 'compressed'" 19 | ), 20 | ) 21 | 22 | def handle(self, *args, **options): 23 | self.style = options["style"] 24 | if args and args[0] == 'watch': 25 | self.watch() 26 | else: 27 | self.compile() 28 | 29 | def compile(self): 30 | """Cycle through each scss file, compiling them one by one""" 31 | static = os.path.join("foia_hub", "static") 32 | for path in glob(os.path.join(static, "sass", "*.scss")): 33 | filename = path.split(os.path.sep)[-1][:-5] 34 | if filename[0] == "_": 35 | continue 36 | subprocess.call(["sass", "--style", self.style, "--scss", path, 37 | os.path.join(static, "css", filename + ".css")]) 38 | 39 | def watch(self): 40 | """Kick off the 'watch' demon""" 41 | watch_path = os.path.join("foia_hub", "static", "sass") 42 | watch_path += ":" 43 | watch_path += os.path.join("foia_hub", "static", "css") 44 | subprocess.call(["sass", "--style", self.style, "--scss", "--watch", 45 | watch_path]) 46 | -------------------------------------------------------------------------------- /deploy/fabfile.py: -------------------------------------------------------------------------------- 1 | 2 | from fabric.api import run, env 3 | 4 | """ 5 | Manage auto-deploy webhooks remotely. 6 | 7 | Staging hook: 8 | 9 | forever start -l $HOME/hub/shared/log/hookshot.log -a $HOME/hub/current/deploy/hookshot.js -p 3000 -b master -c "bash $HOME/hub/current/deploy/bin/deploy-site.sh >> $HOME/hub/shared/log/hookshot.log" 10 | forever restart $HOME/hub/current/deploy/hookshot.js -p 3000 -b master -c "bash $HOME/hub/current/deploy/bin/deploy-site.sh >> $HOME/hub/shared/log/hookshot.log" 11 | forever stop $HOME/hub/current/deploy/hookshot.js -p 3000 -b master -c "bash $HOME/hub/current/deploy/bin/deploy-site.sh >> $HOME/hub/shared/log/hookshot.log" 12 | """ 13 | 14 | environment = env.get('env', 'staging') 15 | 16 | port = { 17 | "staging": 3000 18 | }[environment] 19 | 20 | env.use_ssh_config = True 21 | 22 | home = "/home/foia" 23 | shared = "%s/hub/shared" % home 24 | current = "%s/hub/current" % home 25 | log = "%s/log/hookshot.log" % shared 26 | 27 | # principal command to run when the main branch is updated 28 | branch = "master" 29 | command = "bash %s/deploy/bin/deploy-site.sh >> %s" % (current, log) 30 | 31 | 32 | # needs to be run out of $HOME, because this will run as a daemon across deploys 33 | def start(): 34 | run( 35 | "cd %s && forever start -l %s -a %s/deploy/hookshot.js -p %i -b %s -c \"%s\"" 36 | % (home, log, current, port, branch, command) 37 | ) 38 | 39 | def stop(): 40 | run( 41 | "cd %s && forever stop %s/deploy/hookshot.js -p %i -b %s -c \"%s\"" 42 | % (home, current, port, branch, command) 43 | ) 44 | 45 | def restart(): 46 | run( 47 | "cd %s && forever restart %s/deploy/hookshot.js -p %i -b %s -c \"%s\"" 48 | % (home, current, port, branch, command) 49 | ) 50 | -------------------------------------------------------------------------------- /foia_hub/static/js/contact-updater/contact-search.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | var agencyDatasource, 3 | gaTimeout = 500, 4 | getKeys, 5 | onSelection, 6 | substrRegex, 7 | substringMatcher, 8 | typeahead; 9 | // Set up the agency data source 10 | agencyDatasource = new Bloodhound({ 11 | queryTokenizer: Bloodhound.tokenizers.whitespace, 12 | limit: 500, 13 | prefetch: { 14 | url: '/api/agency/', 15 | filter: function(response) { 16 | return response.objects; 17 | } 18 | }, 19 | datumTokenizer: function(d) { 20 | return [] 21 | .concat(Bloodhound.tokenizers.whitespace(d.name)) 22 | .concat(Bloodhound.tokenizers.whitespace(d.abbreviation)); 23 | } 24 | }); 25 | onSelection = function(ev, suggestion) { 26 | var agency = suggestion.slug 27 | var callback = function() { 28 | clearTimeout(timeout); 29 | window.location = '/update-contacts/' + agency + '/'; 30 | }, 31 | timeout = setTimeout(callback, gaTimeout); 32 | ga('send', { 33 | 'hitType': 'event', 34 | 'eventCategory': 'contact-updater-select', 35 | 'eventAction': 'select-agency--' + agency, 36 | 'eventLabel': document.location.pathname}, 37 | {'hitCallback': callback}); 38 | } 39 | typeahead = $('#contact-typeahead').typeahead({ 40 | hint: false, 41 | highlight: true, 42 | minLength: 1 43 | }, 44 | { 45 | name: 'agencies', 46 | displayKey: 'name', 47 | source: agencyDatasource 48 | }).on('typeahead:selected', onSelection); 49 | }); 50 | -------------------------------------------------------------------------------- /foia_hub/migrations/0012_add_office_slug.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | from django.utils.text import slugify 6 | 7 | # This migration uses `slugify(office.name)[:50]` instead of 8 | # calling `Office.slug_for(office.name)` so that its operation 9 | # is frozen at how slugifying worked at the time of this migration, 10 | # not at whatever Office.slug_for currently does. 11 | 12 | def initialize_office_slugs(apps, schema_editor): 13 | """ Make the Office slugs unique. """ 14 | Office = apps.get_model("foia_hub", "Office") 15 | db_alias = schema_editor.connection.alias 16 | 17 | for office in Office.objects.using(db_alias).all(): 18 | office.office_slug = slugify(office.name)[:50] 19 | office.save() 20 | 21 | # they'll get removed when the field gets removed 22 | def remove_office_slugs(apps, schema_editor): 23 | pass 24 | 25 | class Migration(migrations.Migration): 26 | 27 | dependencies = [ 28 | ('foia_hub', '0011_remove_agency_usa_contact_id'), 29 | ] 30 | 31 | operations = [ 32 | # first, add the new office_slug field, temporarily allow NULL 33 | migrations.AddField( 34 | model_name='office', 35 | name='office_slug', 36 | field=models.SlugField(max_length=100, null=True), 37 | ), 38 | 39 | # then, give them an initial value 40 | migrations.RunPython( 41 | initialize_office_slugs, 42 | reverse_code=remove_office_slugs 43 | ), 44 | 45 | # finally, remove the NULL constraint 46 | migrations.AlterField( 47 | model_name='office', 48 | name='office_slug', 49 | field=models.SlugField(max_length=100, null=False), 50 | ), 51 | ] 52 | -------------------------------------------------------------------------------- /foia_hub/migrations/0010_addresses_and_emails.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations 5 | import jsonfield.fields 6 | import foia_hub.models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('foia_hub', '0009_agency_usa_contact_id'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name='agency', 18 | name='emails', 19 | field=jsonfield.fields.JSONField(default=foia_hub.models.empty_list), 20 | preserve_default=True, 21 | ), 22 | migrations.AddField( 23 | model_name='agency', 24 | name='address_lines', 25 | field=jsonfield.fields.JSONField(default=foia_hub.models.empty_list), 26 | preserve_default=True, 27 | ), 28 | migrations.AddField( 29 | model_name='office', 30 | name='emails', 31 | field=jsonfield.fields.JSONField(default=foia_hub.models.empty_list), 32 | preserve_default=True, 33 | ), 34 | migrations.AddField( 35 | model_name='office', 36 | name='address_lines', 37 | field=jsonfield.fields.JSONField(default=foia_hub.models.empty_list), 38 | preserve_default=True, 39 | ), 40 | migrations.RemoveField( 41 | model_name='agency', 42 | name='email' 43 | ), 44 | migrations.RemoveField( 45 | model_name='agency', 46 | name='address_line_1' 47 | ), 48 | migrations.RemoveField( 49 | model_name='office', 50 | name='email' 51 | ), 52 | migrations.RemoveField( 53 | model_name='office', 54 | name='address_line_1' 55 | ) 56 | ] 57 | -------------------------------------------------------------------------------- /foia_hub/static/js/requests/form.js: -------------------------------------------------------------------------------- 1 | var onRequestSuccess = function(data, status, xhr) { 2 | console.log("Success! Tracking: " + data.tracking_id); 3 | window.location = "/request/success/" + data.tracking_id; 4 | }; 5 | 6 | var onRequestFailure = function(xhr, status, err) { 7 | window.arguments = arguments; 8 | console.log("Failure."); 9 | console.log(xhr.responseJSON); 10 | 11 | $(".request.error").show(); 12 | return false; 13 | }; 14 | 15 | var collectRequestData = function() { 16 | var fee_limit = parseInt($(".upto").val()); 17 | fee_limit = isNaN(fee_limit) ? 0 : fee_limit; 18 | 19 | return { 20 | first_name: $(".first_name").val(), 21 | last_name: $(".last_name").val(), 22 | email: $(".email").val(), 23 | body: $(".body").val(), 24 | 25 | documents_start: $("#document_start").val(), 26 | documents_end: $("#document_end").val(), 27 | fee_limit: fee_limit, 28 | 29 | agency: $(".agency_agency").val(), 30 | office: $(".agency_office").val() 31 | }; 32 | }; 33 | 34 | $("form.request").submit(function() { 35 | var data = collectRequestData(); 36 | 37 | $.ajax({ 38 | type: "POST", 39 | url: "/api/request/", 40 | data: JSON.stringify(data), 41 | processData: false, 42 | contentType: 'application/json' 43 | }).done(onRequestSuccess).fail(onRequestFailure); 44 | 45 | return false; 46 | }); 47 | 48 | 49 | // date chooser 50 | $("#document_end, #document_start").pickadate({ 51 | format: 'mmmm d, yyyy' 52 | }); 53 | 54 | // fee sync 55 | var $upto = $(".upto"); 56 | var $fee = $(".fee-amount"); 57 | var onchange = function() { 58 | var val = parseInt($upto.val()); 59 | if (isNaN(val)) val = 0; 60 | $fee.html("$" + val); 61 | } 62 | $upto.keyup(onchange); 63 | 64 | // fee waiver toggle 65 | $("#fee-waiver-request").change(function() { 66 | $("#fee-waiver-justification").toggle(); 67 | }) 68 | -------------------------------------------------------------------------------- /foia_hub/migrations/0016_auto_20141119_2040.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('foia_hub', '0015_adding_stats_obj'), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='ReadingRoomUrls', 16 | fields=[ 17 | ('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')), 18 | ('link_text', models.CharField(max_length=512, help_text='This is the text associated with the reading room URL. ')), 19 | ('url', models.URLField(null=True, help_text="The URL to an agency's reading room.")), 20 | ], 21 | options={ 22 | }, 23 | bases=(models.Model,), 24 | ), 25 | migrations.AlterUniqueTogether( 26 | name='readingroomurls', 27 | unique_together=set([('link_text', 'url')]), 28 | ), 29 | migrations.RemoveField( 30 | model_name='agency', 31 | name='reading_room_url', 32 | ), 33 | migrations.RemoveField( 34 | model_name='office', 35 | name='reading_room_url', 36 | ), 37 | migrations.AddField( 38 | model_name='agency', 39 | name='reading_room_urls', 40 | field=models.ManyToManyField(to='foia_hub.ReadingRoomUrls', related_name='foia_hub_agency_related'), 41 | preserve_default=True, 42 | ), 43 | migrations.AddField( 44 | model_name='office', 45 | name='reading_room_urls', 46 | field=models.ManyToManyField(to='foia_hub.ReadingRoomUrls', related_name='foia_hub_office_related'), 47 | preserve_default=True, 48 | ), 49 | ] 50 | -------------------------------------------------------------------------------- /contact_updater/templates/agency_form.html: -------------------------------------------------------------------------------- 1 | {% extends "contact-updater-base.html" %} 2 | 3 | {% block head %} 4 | {{ super() }} 5 | 6 | 7 | 8 | 9 | {% endblock %} 10 | 11 | {% block body %} 12 |
13 |
14 | {% csrf_token %} 15 | {{management_form|safe}} 16 | 17 | {% if validated %} 18 | {% include 'validated.html' %} 19 | 20 | {% else %} 21 | 22 | {% if forms|length == 1 %} 23 |

{{agency_names.0}}

24 | {% for form in forms %} 25 | {% include 'centralized.html' %} 26 | {% endfor %} 27 | 28 | {% else %} 29 | {% include 'decentralized.html' %} 30 | {% endif %} 31 | {% endif %} 32 |
33 | 34 | {% if validated %} 35 | 36 | 37 | {# A tiny script that downloads the data when it gets to the validated 38 | page. #} 39 | 42 | {% else %} 43 | 44 |
45 |
    46 |
    47 | 48 | {% endif %} 49 |
    50 |
    51 | {% endblock %} 52 | -------------------------------------------------------------------------------- /foia_hub/migrations/0014_freeze_contact_info_on_request.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import jsonfield.fields 6 | import foia_hub.models 7 | from foia_hub.models import FOIARequest 8 | 9 | 10 | # no need to do anything on the way up 11 | def nothing(apps, schema_editor): 12 | pass 13 | 14 | # when winding down, requests made to an agency will be deleted. 15 | def assign_offices(apps, schema_editor): 16 | for request in FOIARequest.objects.filter(office=None): 17 | request.delete() 18 | 19 | class Migration(migrations.Migration): 20 | 21 | dependencies = [ 22 | ('foia_hub', '0013_add_request_relationship_to_agency'), 23 | ] 24 | 25 | operations = [ 26 | # link from foia request to office is now optional. 27 | migrations.AlterField( 28 | model_name='foiarequest', 29 | name='office', 30 | field=models.ForeignKey(to='foia_hub.Office', null=True), 31 | ), 32 | 33 | # end dates was meant to be optional. 34 | migrations.AlterField( 35 | model_name='foiarequest', 36 | name='date_end', 37 | field=models.DateField(null=True) 38 | ), 39 | 40 | # Add possible emails from agency or office. 41 | migrations.AddField( 42 | model_name='foiarequest', 43 | name='emails', 44 | field=jsonfield.fields.JSONField(default=foia_hub.models.empty_list, null=True), 45 | preserve_default=True, 46 | ), 47 | 48 | migrations.RemoveField( 49 | model_name='foiarequest', 50 | name='custom_fields' 51 | ), 52 | 53 | # needs to be done first on the way down, before column removal above 54 | # invalidates the lookup call. 55 | migrations.RunPython( 56 | nothing, 57 | reverse_code=assign_offices 58 | ), 59 | 60 | ] 61 | -------------------------------------------------------------------------------- /foia_hub/tests/README.md: -------------------------------------------------------------------------------- 1 | For the time being... 2 | 3 | To test out the request API in your python interpretor try the following: 4 | 5 | ``` 6 | import json 7 | import requests 8 | 9 | url = 'http://localhost:8000/api/request/' 10 | data = { 11 | "first_name": "Eric", 12 | "last_name": "Mill", 13 | "email": "eric.mill@gsa.gov", 14 | "body": "My request", 15 | "documents_start": "August 6, 2014", 16 | "documents_end": "August 8, 2014", 17 | "fee_limit": 0, 18 | "agency": "general-services-administration", 19 | "office": "headquarters", 20 | "agency_fields": { 21 | "region": "1A", 22 | "contract": "GS-1637848", 23 | } 24 | } 25 | 26 | r = requests.post(url, data=json.dumps(data)) 27 | r.json() 28 | ``` 29 | 30 | If it was successful this should output the following: 31 | ``` 32 | {'requester': 16, 33 | 'fee_limit': 0, 34 | 'status': 'O', 35 | 'date_end': '2014-08-08T00:00:00', 36 | 'date_start': '2014-08-06T00:00:00', 37 | 'request_body': 'My request', 38 | 'custom_fields': {'contract': 'GS-1637848', 'region': '1A'}} 39 | ``` 40 | 41 | 42 | If you use this sample data, which is a bad set, it will return a 500: 43 | 44 | ``` 45 | data = { 46 | "first_name": "EricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEricEric", 47 | "last_name": "Mill", 48 | "email": "eric.mill@gsa.gov", 49 | "body": "My request", 50 | "documents_start": "August 6, 2014", 51 | "documents_end": "August 8, 2014", 52 | "fee_limit": 0, 53 | "agency": "general-services-administration", 54 | "office": "headquarters", 55 | "agency_fields": { 56 | "region": "1A", 57 | "contract": "GS-1637848", 58 | } 59 | } 60 | ``` 61 | 62 | ``` 63 | r.status_code 64 | 500 65 | 66 | r.json()['error'] 67 | 'value too long for type character varying(250)\n' 68 | ``` 69 | -------------------------------------------------------------------------------- /foia_hub/static/sass/base/_typography.scss: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: $base-background-color; 3 | color: $base-font-color; 4 | font-family: $base-font-family; 5 | font-size: $base-font-size; 6 | line-height: $base-line-height; 7 | font-weight: $base-font-weight; 8 | } 9 | 10 | h1, 11 | h2, 12 | h3, 13 | h4, 14 | h5, 15 | h6 { 16 | font-weight: $header-font-weight; 17 | line-height: $header-line-height; 18 | margin: 0; 19 | text-rendering: optimizeLegibility; // Fix the character spacing for headings 20 | em { 21 | border-bottom: 0.15em solid $aqua-blue; 22 | line-height: $header-line-height * 1.25; 23 | font-style: normal; 24 | } 25 | } 26 | 27 | h1 { 28 | font-size: $h1-font-size; 29 | } 30 | 31 | h2 { 32 | font-size: $h2-font-size; 33 | } 34 | 35 | h3 { 36 | font-size: $h3-font-size; 37 | } 38 | 39 | h4 { 40 | font-size: $h4-font-size; 41 | } 42 | 43 | h5 { 44 | font-size: $h5-font-size; 45 | } 46 | 47 | h6 { 48 | font-size: $h6-font-size; 49 | } 50 | 51 | p { 52 | margin: 0 0 ($base-spacing / 2); 53 | } 54 | 55 | a { 56 | // @include transition(color 0.1s linear); 57 | color: $base-link-color; 58 | text-decoration: none; 59 | 60 | &:hover { 61 | color: $hover-link-color; 62 | text-decoration: underline; 63 | } 64 | 65 | &:active, &:focus { 66 | color: $hover-link-color; 67 | outline: none; 68 | } 69 | } 70 | 71 | hr { 72 | border-bottom: $base-border; 73 | border-left: none; 74 | border-right: none; 75 | border-top: none; 76 | margin: $base-spacing 0; 77 | } 78 | 79 | img, 80 | picture { 81 | margin: 0; 82 | max-width: 100%; 83 | } 84 | 85 | blockquote { 86 | border-left: 2px solid $base-border-color; 87 | color: lighten($base-font-color, 15); 88 | margin: $base-spacing 0; 89 | padding-left: $base-spacing / 2; 90 | } 91 | 92 | cite { 93 | color: lighten($base-font-color, 25); 94 | font-style: italic; 95 | 96 | &:before { 97 | content: "\2014 \00A0"; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /foia_hub/fixtures/offices_test.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "fields": { 4 | "TTY_phone": null, 5 | "address_lines": [], 6 | "agency": 17, 7 | "city": "", 8 | "emails": [], 9 | "fax": "301-763-6239", 10 | "name": "Census Bureau", 11 | "office_url": null, 12 | "person_name": null, 13 | "phone": null, 14 | "public_liaison_email": null, 15 | "public_liaison_name": null, 16 | "public_liaison_phone": null, 17 | "request_form_url": null, 18 | "slug": "department-of-commerce--census-bureau", 19 | "state": "", 20 | "street": "", 21 | "toll_free_phone": null, 22 | "zip_code": 0 23 | }, 24 | "model": "foia_hub.office", 25 | "pk": 33 26 | }, 27 | { 28 | "fields": { 29 | "TTY_phone": null, 30 | "address_lines": [], 31 | "agency": 15, 32 | "city": "", 33 | "emails": [], 34 | "fax": "202-646-3347", 35 | "name": "Federal Emergency Management Agency", 36 | "office_url": null, 37 | "person_name": null, 38 | "phone": null, 39 | "public_liaison_email": null, 40 | "public_liaison_name": null, 41 | "public_liaison_phone": null, 42 | "request_form_url": null, 43 | "slug": "department-of-homeland-security--federal-emergency-management-agency", 44 | "state": "", 45 | "street": "", 46 | "toll_free_phone": null, 47 | "zip_code": 0 48 | }, 49 | "model": "foia_hub.office", 50 | "pk": 20 51 | }, 52 | { 53 | "fields": { 54 | "link_text": "The Electronic Reading Room", 55 | "url": "http://www.usmint.gov/FOIA/?action=room", 56 | "object_id": 20, 57 | "content_type": ["foia_hub", "office"] 58 | }, 59 | "model": "foia_hub.readingroomurls", 60 | "pk": 157 61 | } 62 | ] 63 | -------------------------------------------------------------------------------- /foia_hub/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, include, url 2 | from django.conf import settings # For debugging. 3 | from django.views.generic import TemplateView 4 | 5 | from foia_hub.views import ( 6 | contact_landing, agencies, 7 | request_form, request_noop) 8 | 9 | from foia_hub.api import AgencyResource, OfficeResource, FOIARequestResource 10 | 11 | import contact_updater.urls as contact_updater_urls 12 | 13 | # Front-end 14 | urlpatterns = patterns( 15 | '', 16 | url(r'^$', TemplateView.as_view(template_name="index.html"), name='home'), 17 | url(r'^about/?$', TemplateView.as_view( 18 | template_name="about.html"), name='about'), 19 | url(r'^agencies/?$', agencies, name='agencies'), 20 | url(r'^developers/?$', TemplateView.as_view( 21 | template_name="developers.html"), name='developers'), 22 | # Add in the singular version as well. 23 | url(r'^developer/?$', TemplateView.as_view( 24 | template_name="developers.html"), name='developer'), 25 | url(r'^contacts/(?P[-\w]+)/?$', contact_landing, 26 | name='contact_landing'), 27 | url(r'^request/noop/$', request_noop, name='noop'), 28 | url(r'^request/(?P[-\w]+)/$', request_form, name='form'), 29 | url(r'^robots\.txt$', 30 | TemplateView.as_view( 31 | template_name='robots.txt', content_type='text/plain')), 32 | 33 | ) 34 | 35 | # APIs 36 | urlpatterns += patterns( 37 | '', 38 | url(r'^api/agency/', include(AgencyResource.urls())), 39 | url(r'^api/office/', include(OfficeResource.urls())), 40 | ) 41 | 42 | if settings.SHOW_WEBFORM: 43 | urlpatterns += patterns( 44 | '', 45 | url(r'^api/request/', include(FOIARequestResource.urls()))) 46 | 47 | # Contact updater 48 | urlpatterns += patterns( 49 | '', 50 | url(r'^update-contacts/', include(contact_updater_urls)), 51 | ) 52 | 53 | 54 | # For development 55 | if settings.DEBUG: 56 | try: 57 | import debug_toolbar 58 | urlpatterns += patterns( 59 | '', 60 | url(r'^__debug__/', include(debug_toolbar.urls)), 61 | ) 62 | except ImportError: 63 | pass 64 | -------------------------------------------------------------------------------- /foia_hub/views.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.shortcuts import render 3 | 4 | from foia_hub.api import AgencyResource, OfficeResource 5 | 6 | 7 | ### 8 | # Finding agencies and their contact information. 9 | ### 10 | 11 | 12 | def agencies(request): 13 | """Full agency listing.""" 14 | query = request.GET.get("query") 15 | 16 | agencies = AgencyResource().list(query) 17 | return render( 18 | request, 19 | 'contacts/index.html', 20 | { 21 | 'agencies': agencies, 22 | 'query': query 23 | }) 24 | 25 | 26 | def contact_landing(request, slug): 27 | """Principal landing page for agencies and offices.""" 28 | if '--' in slug: 29 | resource = OfficeResource() 30 | else: 31 | resource = AgencyResource() 32 | 33 | data = resource.detail(slug).value 34 | 35 | if (data['is_a'] == 'agency') and (len(data.get("offices", [])) > 0): 36 | return render( 37 | request, 38 | 'contacts/parent_profile.html', 39 | { 40 | 'profile': data, 41 | 'slug': slug, 42 | 'show_webform': settings.SHOW_WEBFORM 43 | }) 44 | else: 45 | return render( 46 | request, 47 | 'contacts/profile.html', 48 | { 49 | 'profile': data, 50 | 'slug': slug, 51 | 'show_webform': settings.SHOW_WEBFORM 52 | }) 53 | 54 | 55 | ### 56 | # API endpoints 57 | ### 58 | 59 | def get_agency_list(): 60 | resource = AgencyResource() 61 | agencies = resource.list() 62 | agency_list = [ 63 | {'name': agency.name, 'slug': agency.slug} for agency in agencies] 64 | return agency_list 65 | 66 | 67 | ### 68 | # Contacting agencies/offices that lack a webform of their own. 69 | ### 70 | 71 | 72 | def request_form(request, slug=None): 73 | """Request form for an agency or office.""" 74 | if '--' in slug: 75 | resource = OfficeResource() 76 | else: 77 | resource = AgencyResource() 78 | 79 | data = resource.detail(slug).value 80 | return render( 81 | request, 82 | 'request/form.html', 83 | {'profile': data, 'slug': slug}) 84 | 85 | 86 | def request_noop(request): 87 | """ We have a request form that does nothing. Let's ensure the user knows 88 | that in the slim chance the form gets turned on in an environment it 89 | shouldn't be on in. """ 90 | return render(request, 'request/noop.html', {}) 91 | -------------------------------------------------------------------------------- /foia_hub/static/sass/contactupdater.scss: -------------------------------------------------------------------------------- 1 | @import "normalize"; 2 | @import "bourbon/bourbon"; 3 | @import "base/grid-settings"; 4 | @import "base/base"; 5 | @import "neat/neat"; 6 | @import "mixins"; 7 | @import "accessibility"; 8 | @import "fonts"; 9 | 10 | body { 11 | 12 | } 13 | 14 | form.contactupdater { 15 | min-width: 640px; 16 | & section { 17 | @include clearfix(); 18 | border-bottom: 1px solid #ddd; 19 | padding: 1em 0; 20 | } 21 | textarea { 22 | height: 10em; 23 | } 24 | .field { 25 | @include span-columns(12); 26 | } 27 | .error { 28 | @include span-columns(8); 29 | @include clearfix(); 30 | color: #777; 31 | font-size: 0.85em; 32 | padding-left: 1.5em; 33 | padding-top: 0.85em; 34 | position: relative; 35 | p { 36 | // position: absolute; 37 | } 38 | h4 { 39 | margin-bottom: 0.5em; 40 | } 41 | } 42 | .nudge:before { 43 | /* content: "\f06a"; 44 | font-family: FontAwesome; 45 | margin-right: 0.5em;*/ 46 | } 47 | .required:after { 48 | content: "Required"; 49 | float:right; 50 | font-weight: 200; 51 | font-style: italic; 52 | } 53 | .nudge:after { 54 | content: "Optional but encouraged"; 55 | /* border-bottom: 1px solid $aqua-blue; */ 56 | float:right; 57 | font-weight: 200; 58 | font-style: italic; 59 | } 60 | 61 | h3 { 62 | font-size: 1.4em; 63 | margin-top: 1.5em; 64 | @include span-columns(24 of 24); 65 | } 66 | label { 67 | margin: 0.8em 0; 68 | font-weight: 300; 69 | font-size: 0.875em; 70 | } 71 | input[type="submit"] { 72 | @include span-columns(4 of 24); 73 | // @include clearfix(); 74 | clear: both; 75 | & .submit-invalid { 76 | background-color: #DDD; 77 | } 78 | } 79 | input.parsley-error { 80 | border: solid 1px red; 81 | } 82 | .parsley-errors-list li, ul.errorlist { 83 | &:before { 84 | content: "\f06a"; 85 | font-family: FontAwesome; 86 | margin-right: 0.5em; 87 | } 88 | color:red; 89 | margin-bottom: 0.5em; 90 | } 91 | } 92 | 93 | .contactUpdater { 94 | .search-icon { 95 | color: $gray; 96 | position: absolute; 97 | z-index: 100; 98 | padding-top: 0.75em; 99 | padding-left: 0.75em; 100 | .fa { 101 | color: inherit; 102 | } 103 | } 104 | input.typeahead { 105 | padding-left: 2.2em; 106 | } 107 | .search--box { 108 | @include span-columns(12 of 24); 109 | } 110 | } 111 | 112 | footer { 113 | background-color: inherit; 114 | padding-top: 0; 115 | margin-top: 0; 116 | } -------------------------------------------------------------------------------- /foia_hub/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% block head %} 4 | 5 | 6 | {# Render block here, so that future renderings can be passed through a filter. #} 7 | 8 | 9 | 10 | {{ self.title() | e }} 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | {% include "includes/head.html" %} 27 | 28 | 29 | {% endblock %} 30 | 31 | 32 | 33 | {% include "includes/nav.html" %} 34 | 35 | {% block breadcrumb %}{% endblock %} 36 | 37 |
    38 | {% block body %}This is not seen.{% endblock %} 39 |
    40 |
    41 | 42 | {% block footer %} 43 | {% include "includes/footer.html" %} 44 | {% endblock %} 45 | 46 | 47 | 48 | 49 | 50 | 51 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /foia_hub/static/sass/base/_variables.scss: -------------------------------------------------------------------------------- 1 | // Typography 2 | $sans-serif: proximanova, "proxima-nova", "proxima nova", "Open Sans","Gill Sans MT","Gill Sans", Corbel, Arial,sans-serif; 3 | $serif: $georgia; 4 | $base-font-family: $sans-serif; 5 | $header-font-family: $base-font-family; 6 | 7 | // Font Sizes 8 | $base-font-size: 1em; 9 | $h1-font-size: $base-font-size * 2.25; 10 | $h2-font-size: $base-font-size * 1.5; 11 | $h3-font-size: $base-font-size * 1.125; 12 | $h4-font-size: $base-font-size; 13 | $h5-font-size: $base-font-size; 14 | $h6-font-size: $base-font-size; 15 | 16 | //Font Weights 17 | $header-font-weight: 600; 18 | $base-font-weight: 400; 19 | 20 | // Line height 21 | $base-line-height: 1.5; 22 | $header-line-height: 1.25; 23 | 24 | // Other Sizes 25 | $base-border-radius: 3px; 26 | $base-spacing: $base-line-height * 1em; 27 | $base-z-index: 0; 28 | 29 | // Colors 30 | $light-red: #FBE3E4; 31 | $light-yellow: #FFF6BF; 32 | $light-green: #E6EFC2; 33 | 34 | // openFOIA Colors 35 | $dark-gray: #323A45; // Text/Rules 36 | $gray: #777777; // Text 2 37 | $blue: #0067BA; // Links 38 | $dark-blue: #005184; 39 | $aqua-blue: #00BFE7; // Graphics 40 | $medium-blue: #0096B5; // Rollover 41 | $light-blue: #CDEAF0; // Highlight 42 | $light-gray: #EAEDF1; // Background 43 | $medium-gray: #CCCCCC; // Rules 2 44 | // for submit button hover/focus background 45 | $the-other-blue: #92D6E5; 46 | $light-light-gray: #f7f8f9; 47 | 48 | // Background Color 49 | $base-background-color: $light-gray; 50 | 51 | // Font Colors 52 | $base-font-color: $dark-gray; 53 | $base-accent-color: $blue; 54 | 55 | // Link Colors 56 | $base-link-color: $base-accent-color; 57 | $hover-link-color: $dark-blue; 58 | $base-button-color: $aqua-blue; 59 | $hover-button-color: $aqua-blue; 60 | 61 | // Flash Colors 62 | $alert-color: $light-yellow; 63 | $error-color: $light-red; 64 | $notice-color: lighten($base-accent-color, 40); 65 | $success-color: $light-green; 66 | 67 | // Border color 68 | $base-border-color: $medium-gray; 69 | $base-border: 1px solid $base-border-color; 70 | 71 | // Forms 72 | $form-border-color: $base-border-color; 73 | $form-border-color-hover: darken($base-border-color, 10); 74 | $form-border-color-focus: $base-accent-color; 75 | $form-border-radius: $base-border-radius; 76 | $form-box-shadow: inset 0 1px 3px rgba(black,0.06); 77 | $form-box-shadow-focus: $form-box-shadow, 0 0 5px rgba(darken($form-border-color-focus, 5), 0.7); 78 | $form-font-size: $base-font-size; 79 | $form-font-family: $base-font-family; 80 | 81 | // for @include prefixer() 82 | $vendor-prefixes: webkit moz ms o spec; 83 | -------------------------------------------------------------------------------- /foia_hub/templates/includes/head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /foia_hub/templates/contacts/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block head %} 4 | {{ super() }} 5 | 6 | {% endblock %} 7 | 8 | {% block class %}agencies{% endblock %} 9 | 10 | {% block breadcrumb %} 11 | {% import 'includes/breadcrumb.html' as breadcrumb %} 12 | {{breadcrumb.create([(url('agencies'), 'Agencies')]) }} 13 | {% endblock %} 14 | 15 | {% block body %} 16 | 17 | 18 | {% macro format_agency(agency) -%} 19 |
  • 20 |
    21 |

    {{ agency.name }}

    22 |
    23 |
    24 |

    25 | {% if agency.description %} 26 | {{ agency.description }} 27 | {% else %} 28 | Click to find out more about this agency. 29 | {% endif %} 30 |

    31 |
    32 |
  • 33 | {%- endmacro %} 34 | 35 |
    36 |
    37 | 38 | {% if agencies %} 39 | 40 | {% if query %} 41 |

    Displaying {{agencies|length}} results for

    42 |

    {{ query }}

    43 | 44 |
    45 |
      46 | 47 | {% for agency in agencies %} 48 | {{ format_agency(agency) }} 49 | {% endfor %} 50 |
    51 |
    52 | {% else %} 53 |

    Federal Agencies

    54 |
      55 | 56 | {% for letter in agencies|groupby('name.0') %} 57 |
    • {{letter.grouper}}
    • 58 | {% endfor %} 59 |
    60 | {% for letter, agency_list in agencies|groupby('name.0') %} 61 | 62 |
    63 |

    {{letter}}

    64 |

    Back to Top

    65 |
    66 |
      67 | 68 | {% for agency in agency_list %} 69 | {{ format_agency(agency) }} 70 | {% endfor %} 71 |
    72 | {% endfor %} 73 | {% endif %} 74 | {% else %} 75 |

    76 | There are no agencies matching your search. 77 |

    78 | {% endif %} 79 | 80 |
    81 |
    82 | {% endblock %} 83 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | This project uses several third-party tools, each of which has its own license. 2 | 3 | In `foia_hub/static/css/`: 4 | 5 | * [`pure`](https://github.com/yahoo/pure/blob/cb8704cc167c7e8e7c7fbf7c5ec60cdc6e9987df/LICENSE.md) 6 | 7 | In `foia_hub/static/font-awesome-4.2.0/`: 8 | 9 | * [`font-awesome`](https://github.com/FortAwesome/Font-Awesome/blob/41b9ed01103e6820c3cb043ba7ddab30ecd3f4c0/README.md#license) 10 | 11 | In `foia_hub/static/pickadate/`: 12 | 13 | * [`pickadate.js`](https://github.com/amsul/pickadate.js/blob/7d08128177ff1512f8eae7aab5655d799193c833/LICENSE.md) 14 | 15 | In `foia_hub/static/js/`: 16 | 17 | * [`handlebars`](https://github.com/wycats/handlebars.js/blob/752a28f24c202cce85068f9c07f6997babc5f6b1/LICENSE) 18 | * [`jquery`](https://github.com/jquery/jquery/blob/e0673dfedb9ad07d8e68f28a54453b975c412c33/LICENSE.txt) 19 | * [`typeahead`](https://github.com/twitter/typeahead.js/blob/0fd2467baa3e0593b91794b485be0bd725b7d2cb/LICENSE) 20 | 21 | In `foia_hub/static/sass/`: 22 | 23 | * [Bourbon](https://github.com/thoughtbot/bourbon/blob/9d89e3f5af10ee39bfccb7be15b147448c670825/LICENSE.md) 24 | * [Neat](https://github.com/thoughtbot/neat/blob/ba9c78933a7d18e0e6aa3bcf035c486a04b8c8a6/LICENSE.md) 25 | 26 | Everything else is covered by the following text. 27 | 28 | 29 | # Public domain 30 | 31 | As a work of the United States Government, this project is in the 32 | public domain within the United States. 33 | 34 | Additionally, we waive copyright and related rights in the work 35 | worldwide through the CC0 1.0 Universal public domain dedication. 36 | 37 | ## CC0 1.0 Universal Summary 38 | 39 | This is a human-readable summary of the 40 | [Legal Code (read the full text)](https://creativecommons.org/publicdomain/zero/1.0/legalcode). 41 | 42 | ### No Copyright 43 | 44 | The person who associated a work with this deed has dedicated the work to 45 | the public domain by waiving all of his or her rights to the work worldwide 46 | under copyright law, including all related and neighboring rights, to the 47 | extent allowed by law. 48 | 49 | You can copy, modify, distribute and perform the work, even for commercial 50 | purposes, all without asking permission. 51 | 52 | ### Other Information 53 | 54 | In no way are the patent or trademark rights of any person affected by CC0, 55 | nor are the rights that other persons may have in the work or in how the 56 | work is used, such as publicity or privacy rights. 57 | 58 | Unless expressly stated otherwise, the person who associated a work with 59 | this deed makes no warranties about the work, and disclaims liability for 60 | all uses of the work, to the fullest extent permitted by applicable law. 61 | When using or citing the work, you should not imply endorsement by the 62 | author or the affirmer. 63 | -------------------------------------------------------------------------------- /foia_hub/static/sass/_mixins.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Mixin: inner 3 | * $columns should always be an even number 4 | */ 5 | @mixin inner($columns) { 6 | @include span-columns( $columns ); 7 | @include shift( ( 24 - $columns ) / 2 ); 8 | // @include media($large) { 9 | // @include shift(2); 10 | // @include span-columns(20) 11 | // } 12 | @include media($medium) { 13 | @include shift(1 of 12); 14 | @include span-columns(10 of 12); 15 | } 16 | @include media($small) { 17 | @include shift(0); 18 | @include span-columns(4); 19 | } 20 | } 21 | 22 | /* 23 | * Mixin: fullbleed 24 | * $columns should always be an even number 25 | */ 26 | @mixin fullbleed($columns) { 27 | // vz: TO BE IMPLEMENTED 28 | } 29 | 30 | /* 31 | * Mixin: small 32 | * $columns should always be an even number 33 | */ 34 | @mixin small($columns) { 35 | @include media($small) { 36 | @include span-columns($columns); 37 | } 38 | } 39 | 40 | // a mixin for screen-reader-specific content 41 | @mixin sr-only($focus:false) { 42 | position: absolute; 43 | width: 1px; 44 | height: 1px; 45 | padding: 0; 46 | margin: -1px; 47 | overflow: hidden; 48 | clip: rect(0, 0, 0, 0); 49 | border: 0; 50 | @if ($focus == true) { 51 | &:focus, 52 | &:active { 53 | position: static; 54 | width: auto; 55 | height: auto; 56 | margin: 0; 57 | overflow: visible; 58 | clip: auto; 59 | } 60 | } 61 | } 62 | 63 | @mixin arrow-top($size:10px, $background:#fff, $border-width:1px, $border-color:#ccc) { 64 | border: 1px solid red; 65 | position: relative; 66 | background: $background; 67 | border: $border-width solid $border-color; 68 | 69 | &:after, 70 | &:before { 71 | bottom: 100%; 72 | left: 50%; 73 | border: solid transparent; 74 | content: " "; 75 | height: 0; 76 | width: 0; 77 | position: absolute; 78 | pointer-events: none; 79 | } 80 | 81 | &:after { 82 | border-color: transparent; 83 | border-bottom-color: $background; 84 | border-width: $size; 85 | margin-left: -$size; 86 | } 87 | 88 | &:before { 89 | border-color: transparent; 90 | border-bottom-color: $border-color; 91 | border-width: $size + $border-width; 92 | margin-left: -($size + $border-width); 93 | } 94 | } 95 | 96 | // set left and right padding (but keep vertical padding the same) 97 | @mixin padding-horizontal($padding) { 98 | padding-left: $padding; 99 | padding-right: $padding; 100 | } 101 | 102 | // set top and bottom padding (but keep horizontal padding the same) 103 | @mixin padding-vertical($padding) { 104 | padding-top: $padding; 105 | padding-bottom: $padding; 106 | } 107 | -------------------------------------------------------------------------------- /foia_hub/templates/includes/nav.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 | Skip navigation 5 |
    6 | 7 |
    8 |
    9 | US flag signifying that this is a United States Federal Government website  An official website of the U.S. Government 10 | This website is in development. Learn more 11 | Go to FOIA.gov 12 |
    13 |
    14 |
    15 | 36 |
    37 |
    38 | 43 | 44 | 56 | 57 | {% if not view or view.template_name != 'index.html' %} 58 | {% include 'includes/small_search.html' %} 59 | {% endif %} 60 |
    61 |
    62 |
    63 | -------------------------------------------------------------------------------- /foia_hub/tests/test_models.py: -------------------------------------------------------------------------------- 1 | from django.test import SimpleTestCase, TestCase 2 | 3 | from foia_hub.models import Agency, Office, Stats, ReadingRoomUrls 4 | 5 | 6 | class AgencyTests(SimpleTestCase): 7 | def test_common_requests_field(self): 8 | """Verify that we can treat a JSONField as a list""" 9 | agency = Agency(name='Dr. Suess', 10 | common_requests=['Thing one', 'Thing two', 'Red Fish']) 11 | agency.save() 12 | # Serialized to the DB and back again 13 | retrieved = Agency.objects.get(pk=agency.pk) 14 | self.assertEqual(retrieved.common_requests, 15 | ['Thing one', 'Thing two', 'Red Fish']) 16 | agency.delete() 17 | 18 | def test_agency_save(self): 19 | agency = Agency(name='Department of Transportation') 20 | agency.save() 21 | 22 | retrieved = Agency.objects.get(pk=agency.pk) 23 | self.assertEqual( 24 | 'Agency: Department of Transportation', str(retrieved)) 25 | self.assertEqual(retrieved.slug, 'department-of-transportation') 26 | 27 | def test_office_save(self): 28 | agency = Agency(name='Department of Commerce') 29 | agency.save() 30 | 31 | office = Office(name='Commerce Is FUNdamental', agency=agency) 32 | office.save() 33 | retrieved = Office.objects.get(pk=office.pk) 34 | 35 | self.assertEqual(retrieved.office_slug, 'commerce-is-fundamental') 36 | self.assertEqual( 37 | retrieved.slug, 'department-of-commerce--commerce-is-fundamental') 38 | 39 | 40 | class MoreAgencyTests(TestCase): 41 | fixtures = ['agencies_test.json', 'offices_test.json'] 42 | 43 | def test_get_all_components(self): 44 | agency = Agency.objects.get(slug='department-of-commerce') 45 | all_offices = agency.get_all_components() 46 | names = [a.name for a in all_offices] 47 | self.assertTrue('U.S. Patent and Trademark Office' in names) 48 | self.assertTrue('Census Bureau' in names) 49 | 50 | 51 | class StatsTest(SimpleTestCase): 52 | def test_statstest_save(self): 53 | """ Confirm obj saves and validate number handling. """ 54 | agency = Agency(name='Department of Homeland Security') 55 | agency.save() 56 | 57 | stats = Stats( 58 | agency=agency, 59 | year=2014, 60 | stat_type='S', 61 | median=1.26, 62 | ) 63 | stats.save() 64 | 65 | retrieved = Stats.objects.get(pk=stats.pk) 66 | self.assertEqual(retrieved.median, 1.26) 67 | self.assertEqual(retrieved.less_than_one, False) 68 | 69 | 70 | class ReadingRoomUrlsTest(TestCase): 71 | def test_str_output(self): 72 | r = ReadingRoomUrls(link_text='Link One', url='http://one.gov') 73 | self.assertEqual(str(r), 'Link One http://one.gov') 74 | -------------------------------------------------------------------------------- /foia_hub/templates/request/form.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block body %} 4 | 5 |
    6 |

    {{ profile.name }}

    7 |
    8 | 9 |
    10 | {% if profile.description %} 11 |

    12 | {{ profile.description }} 13 |

    14 | {% endif %} 15 |
    16 | 17 | 18 |
    19 | 20 |
    21 | {% csrf_token %} 22 | 23 | 24 | {% if profile.is_a == "office" %} 25 | 26 | {% endif %} 27 | 28 | 29 | 30 | 31 |
    32 | 33 | 34 | 35 |
    36 | 37 |
    38 | Describe the records you are requesting 39 | 40 |
    41 | 42 |
    43 | 44 |
    45 | 46 | 47 | 48 |
    49 | 50 |
    51 | Fees 52 | I'll pay up to 53 | $ 54 | in request processing fees. 55 |

    56 | If it's higher than $0, stop and inform me before continuing. 57 |
    58 | 59 |
    60 |
    61 | 62 | 65 |
    66 | 67 | 68 |
    69 | 70 | 71 |
    72 | There was an error submitting your request. Please fill out all the required fields above, and try again. 73 |
    74 | 75 | 76 |
    77 | 78 |
    79 | 80 | {% endblock %} 81 | -------------------------------------------------------------------------------- /docs/source/request.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Request APIs 3 | ============ 4 | 5 | --------------------- 6 | Making a FOIA request 7 | --------------------- 8 | 9 | """""""""""""""""""""""""""""" 10 | Caveat 11 | """""""""""""""""""""""""""""" 12 | 13 | The request API is still a work in progress and does not actually submit FOIA requests. 14 | 15 | This starts the process of documenting the API. 16 | 17 | 18 | """""""""""""""""""""""""""""" 19 | POST /api/request/ 20 | """""""""""""""""""""""""""""" 21 | 22 | Inputs 23 | 24 | A JSON object with at least the following fields: 25 | 26 | +-----------+--------------------------------+ 27 | | Field | Description | 28 | +===========+================================+ 29 | | agency | The slug for the Agency. | 30 | +-----------+--------------------------------+ 31 | | office | The slug for the Office. | 32 | +-----------+--------------------------------+ 33 | | first_name| The requester's first name | 34 | +-----------+--------------------------------+ 35 | | last_name | The requester's last name | 36 | +-----------+--------------------------------+ 37 | | email | The requester's emailxxx | 38 | +-----------+--------------------------------+ 39 | | body | The body of the FOIA request | 40 | +-----------+--------------------------------+ 41 | 42 | Either ``agency``, or ``agency`` and ``office``, must be provided. 43 | 44 | Additional Fields 45 | 46 | +-----------------+--------------------------------------------------------+ 47 | | Field | Description | 48 | +=================+========================================================+ 49 | | documents_start | The beginning date for the documents requested. | 50 | +-----------------+--------------------------------------------------------+ 51 | | documents_end | The end date for the documents requested. | 52 | +-----------------+--------------------------------------------------------+ 53 | 54 | If you would like to attach a start and end date to the documents you are 55 | requesting, you can do that through documents_start and documents_end. Dates 56 | are represented as strings in the following format: January 01, 1979. 57 | 58 | """""""""""""""""""""""""""""" 59 | POST /api/request/ 60 | """""""""""""""""""""""""""""" 61 | 62 | This simply returns all the requests that have been created in the system. 63 | 64 | This currently returns:: 65 | 66 | { 67 | "objects":[ 68 | { 69 | "tracking_id":1, 70 | "status":"O" 71 | }, 72 | { 73 | "tracking_id":2, 74 | "status":"O" 75 | }, 76 | { 77 | "tracking_id":3, 78 | "status":"O" 79 | }, 80 | { 81 | "tracking_id":4, 82 | "status":"O" 83 | } 84 | ] 85 | } 86 | -------------------------------------------------------------------------------- /foia_hub/static/js/requests/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | $(document).ready(function() { 3 | var currentText = '', 4 | longestText = '', 5 | onUserStroke, 6 | onSelection, 7 | agencyDatasource, 8 | agencyAdaptor, 9 | footerAdaptor; 10 | 11 | // Set up the agency data source 12 | agencyDatasource = new Bloodhound({ 13 | queryTokenizer: Bloodhound.tokenizers.whitespace, 14 | limit: 500, // infinity 15 | prefetch: {url: '/api/agency/', filter: function(response) { 16 | return response.objects; }}, 17 | datumTokenizer: function(d) { 18 | return [] 19 | .concat(Bloodhound.tokenizers.whitespace(d.name)) 20 | .concat(Bloodhound.tokenizers.whitespace(d.abbreviation)); 21 | } 22 | }); 23 | // always clear local storage for new requests, at least in dev 24 | agencyDatasource.clearPrefetchCache(); 25 | agencyDatasource.initialize(); 26 | 27 | // Set up the agency adaptor 28 | agencyAdaptor = { 29 | name: 'agencies', 30 | displayKey: 'value', 31 | source: agencyDatasource.ttAdapter(), 32 | templates: { 33 | suggestion: Handlebars.compile('
    {{name}}
    ') 34 | } 35 | }; 36 | // Set up the footer link adaptor 37 | footerAdaptor = { 38 | name: 'footer', 39 | source: function(query, callback) { 40 | if (query.length > 0) { 41 | callback([{'query': query, 'isFooter': true}]); 42 | } 43 | }, 44 | templates: { 45 | suggestion: Handlebars.compile('
    Search for "{{query}}" in keyterms and descriptions
    ') 46 | } 47 | }; 48 | 49 | // Track the text as the user types 50 | onUserStroke = function(ev) { 51 | currentText = $(ev.target).val(); 52 | if (currentText.length > longestText.length) { 53 | longestText = currentText; 54 | } 55 | // blanked out the text after initially typing something 56 | else if (currentText.length === 0 && longestText.length > 0) { 57 | ga('send', 'event', 'contacts', 'did-not-want', longestText); 58 | longestText = ''; 59 | } 60 | }; 61 | 62 | // If an agency was selected, notify analytics and redirect 63 | // If the footer was selected, submit the form to redirect 64 | onSelection = function(ev, suggestion) { 65 | if (suggestion.isFooter) { 66 | $(ev.target).val(suggestion.query).closest('form').submit(); 67 | } 68 | else { 69 | var callback = function() { 70 | window.location = '/contacts/' + suggestion.slug + '/'; 71 | }; 72 | ga('send', 'event', 'contacts', 'select-' + suggestion.slug, 73 | currentText, {'hitCallback': callback}); 74 | } 75 | }; 76 | 77 | // Initialize typeahead 78 | $('.scrollable-dropdown-menu .typeahead').typeahead({ 79 | hint: false, 80 | highlight: true, 81 | minLength: 1 82 | }, agencyAdaptor, footerAdaptor 83 | ).on('keyup', onUserStroke).on('typeahead:selected', onSelection); 84 | }); 85 | -------------------------------------------------------------------------------- /deploy/README.md: -------------------------------------------------------------------------------- 1 | ## foia.18f.us deployment details 2 | 3 | Quick file overview: 4 | 5 | * [`hub.conf`](hub.conf) - Our nginx config for the staging site. Not synced to version control automatically, but we'll try to keep them in sync. 6 | * [`fabfile.py`](fabfile.py) - Fabric deployment script to start/stop/restart our webhook processes. 7 | * [`hookshot.js`](hookshot.js) - Tiny webhook app, runs a command when a branch is updated. Uses [`hookshot`](https://github.com/coreh/hookshot) to do the heavy lifting. Daemonized on our server using [`forever`](https://github.com/nodejitsu/forever). 8 | * [`bin/deploy-site.sh`](bin/deploy-site.sh) - The script run by the webhook every time a commit is pushed to `master`. 9 | 10 | ### Automatic deployment 11 | 12 | On the staging server, this project uses [Node](http://nodejs.org) and [`hookshot`](https://github.com/coreh/hookshot) to receive GitHub post-receive webhooks and update the project. 13 | 14 | Ideally, these webhooks just run forever and never need to be maintained! 15 | 16 | But just in case, this project includes [fabric tasks](http://www.fabfile.org/) for easy remote stop/start/restart of the hook processes on the FOIA web server. 17 | 18 | The fabric tasks can start, stop, and restart the staging hook like so: 19 | 20 | ``` 21 | fab stop 22 | fab start 23 | fab restart 24 | ``` 25 | 26 | They expect a hostname called `foia` in your `$HOME/.ssh/config`. 27 | 28 | #### Setting it up yourself 29 | 30 | These instructions can be applied locally (for development) or on the server (for deployment). 31 | 32 | Install the Node dependencies with: 33 | 34 | ```bash 35 | npm install 36 | npm install -g forever 37 | ``` 38 | 39 | The FOIA staging server uses the `hookshot` command to listen for hooks. 40 | 41 | From `/deploy`, run the hook with the appropriate port and command. It can be helpful to have `forever` and your command both log to the same file. 42 | 43 | In development, you might use: 44 | 45 | ```bash 46 | forever start -l $HOME/hookshot.log -a deploy/hookshot.js -p 3000 -b your-branch -c "cd $HOME/foia/hub && git pull && jekyll build >> $HOME/hookshot.log" 47 | ``` 48 | 49 | You can stop and restart your hooks by supplying the same arguments you gave. 50 | 51 | You may wish to use [ngrok](https://ngrok.com/) or [localtunnel](https://localtunnel.me/) in development to test out the webhook. 52 | 53 | ### Staging server 54 | 55 | On the staging server, this hookshot daemon is run: 56 | 57 | ``` 58 | forever start -l $HOME/hub/shared/log/hookshot.log -a deploy/hookshot.js -p 3000 -b master -c "bash $HOME/hub/current/deploy/bin/deploy-site.sh >> $HOME/hub/shared/log/hookshot.log" 59 | ``` 60 | 61 | It should be run from the project root (the `current` dir/symlink). Both the hook and the output log to the same file, and when it's hit it will execute a small bash script: 62 | 63 | ```bash 64 | #!/bin/bash 65 | 66 | source /home/foia/.bashrc 67 | cd /home/foia/hub/current 68 | workon fab 69 | fab -H localhost deploy 70 | ``` 71 | 72 | This loads in the `foia` user's environment, navigates to the project root, activates the Fabric (Python 2.x) virtualenv, `fab`, and then runs the deploy script against itself. 73 | 74 | Fabric is able to deploy to itself because the `foia` user's public key is also on the `foia` user's `.ssh/authorized_keys` list. It has been previously authorized as a known host and should run without warnings. 75 | 76 | -------------------------------------------------------------------------------- /fabfile.py: -------------------------------------------------------------------------------- 1 | import time 2 | from fabric.api import run, execute, env 3 | 4 | # which settings file to use in production 5 | environment = "production" 6 | 7 | # depends on virtualenvwrapper with a named virtualenv to `workon` 8 | virtualenv = "foia" 9 | 10 | # name of WSGI wrapper for gunicorn to work with 11 | wsgi = "foia_hub.wsgi:application" 12 | 13 | # use `-H foia` or something to control where fabric runs 14 | env.use_ssh_config = True 15 | 16 | # using master branch for now, we could change this 17 | branch = "master" 18 | repo = "git@github.com:18f/foia-hub.git" 19 | 20 | # base directory structure - 21 | # current/ 22 | # versions/ 23 | # 20140914123412/ 24 | # ... 25 | # shared/ 26 | # log/ 27 | # settings.py 28 | 29 | # always UTC datestamp, so it doesn't matter what computer does the deploy 30 | datestamp = time.strftime("%Y%m%d%H%M%S", time.gmtime(time.mktime(time.localtime()))) 31 | 32 | home = "/home/foia/hub" 33 | shared_path = "%s/shared" % home 34 | versions_path = "%s/versions" % home 35 | version_path = "%s/%s" % (versions_path, datestamp) 36 | current_path = "%s/current" % home 37 | logs = "%s/log" % shared_path 38 | env = "%s/foia-env" % shared_path 39 | 40 | # keep the last 5 deployed versions on disk 41 | keep = 5 42 | 43 | 44 | def checkout(): 45 | run('git clone -q -b %s %s %s' % (branch, repo, version_path)) 46 | 47 | def dependencies(): 48 | run('workon %s && cd %s && pip install -r requirements.txt' % (virtualenv, version_path)) 49 | run('cd %s/deploy && npm install' % version_path) 50 | 51 | def migrate(): 52 | run('workon %s && source %s && cd %s && python manage.py migrate' % (virtualenv, env, version_path)) 53 | 54 | def make_current(): 55 | run('rm -f %s && ln -s %s %s' % (current_path, version_path, current_path)) 56 | 57 | def cleanup(): 58 | versions = run("ls -x %s" % versions_path).split() 59 | destroy = versions[:-keep] 60 | 61 | for version in destroy: 62 | command = "rm -rf %s/%s" % (versions_path, version) 63 | run(command) 64 | 65 | 66 | # can be run on their own 67 | 68 | def start(): 69 | """ 70 | Gunicorn start. 71 | """ 72 | 73 | run( 74 | ( 75 | "workon %s && source %s && " + 76 | "cd %s && " + 77 | "gunicorn -c %s/config.py %s" 78 | ) % (virtualenv, env, current_path, shared_path, wsgi), pty=False 79 | ) 80 | 81 | # config.py is expected to point the .pid to the shared/ dir 82 | def stop(): 83 | run("kill `cat %s/gunicorn.pid`" % shared_path) 84 | 85 | def restart(): 86 | run("kill -HUP `cat %s/gunicorn.pid`" % shared_path) 87 | 88 | def deploy(): 89 | """ 90 | Usual deploy script. Expects app to be running. 91 | Checks out code, makes symlinks, installs dependencies, 92 | restarts app, cleans up old deploys. 93 | """ 94 | execute(checkout) 95 | execute(dependencies) 96 | execute(migrate) 97 | execute(make_current) 98 | execute(restart) 99 | execute(cleanup) 100 | 101 | def deploy_cold(): 102 | """ 103 | Similar to normal deploy, but expects the app to be stopped. 104 | Doesn't bother doing cleanup. 105 | """ 106 | execute(checkout) 107 | execute(dependencies) 108 | execute(migrate) 109 | execute(make_current) 110 | execute(start) 111 | 112 | def test(): 113 | run("uname -a") 114 | -------------------------------------------------------------------------------- /foia_hub/static/js/main.js: -------------------------------------------------------------------------------- 1 | // global variables for the app 2 | 3 | Env = { 4 | }; 5 | 6 | Utils = { 7 | substringMatcher: function(strs) { 8 | return function findMatches(q, cb) { 9 | var matches, substrRegex; 10 | 11 | // an array that will be populated with substring matches 12 | matches = []; 13 | 14 | // regex used to determine if a string contains the substring `q` 15 | substrRegex = new RegExp(q, 'i'); 16 | 17 | // iterate through the pool of strings and for any string that 18 | // contains the substring `q`, add it to the `matches` array 19 | $.each(strs, function(i, str) { 20 | if (substrRegex.test(str)) { 21 | // the typeahead jQuery plugin expects suggestions to a 22 | // JavaScript object, refer to typeahead docs for more info 23 | matches.push({ value: str }); 24 | } 25 | }); 26 | 27 | cb(matches); 28 | }; 29 | } 30 | }; 31 | 32 | $(document).ready(function(){ 33 | var times_toggled = 0; 34 | var $bannerCloseButton = $("#notice--close"); 35 | 36 | // if this browser supports localstorage, check for the value 37 | // that gets set to keep the banner closed once a user has clicked 38 | // the close button. if not present, show the banner. 39 | if (typeof window.localStorage !== 'undefined') { 40 | if (window.localStorage.getItem('keep-banner-closed--sunset') !== '1') { 41 | $('#notice').removeClass('hidden'); 42 | } 43 | } 44 | $bannerCloseButton.click(function(){ 45 | $("#notice").slideUp(function () { 46 | times_toggled++ 47 | ga('send', { 48 | 'hitType': 'event', 49 | 'eventCategory': 'banner--close', 50 | 'eventAction': 'toggledon' + document.location.pathname, 51 | 'eventLabel': 'toggled-' + times_toggled, 52 | }); 53 | }); 54 | 55 | // when a user clicks or tabs to and hits enter on the banner 56 | // close button, set a local storage value that gets checked 57 | // on page load and determines whether banner is shown 58 | if (typeof window.localStorage !== 'undefined') { 59 | window.localStorage.setItem('keep-banner-closed--sunset', '1'); 60 | } 61 | }); 62 | // if someone tabs to the close button and hits enter, trigger 63 | // a click event 64 | $bannerCloseButton.keypress(function(e){ 65 | // no need to check for e.keyCode vs e.which, jQuery fills which 66 | if (e.which === 13) { 67 | $(e.target).click(); 68 | } 69 | }); 70 | $("#notice--toggle").click(function(){ 71 | $("#notice").slideToggle("slow", function() { 72 | times_toggled++ 73 | if ($(this).is(":hidden")) 74 | { 75 | ga('send', { 76 | 'hitType': 'event', 77 | 'eventCategory': 'banner--hide', 78 | 'eventAction': 'toggledon' + document.location.pathname, 79 | 'eventLabel': 'toggled-' + times_toggled, 80 | }); 81 | } 82 | else 83 | { 84 | ga('send', { 85 | 'hitType': 'event', 86 | 'eventCategory': 'banner--show', 87 | 'eventAction': 'toggledon' + document.location.pathname, 88 | 'eventLabel': 'toggled-' + times_toggled, 89 | }); 90 | } 91 | }); 92 | }); 93 | }); 94 | 95 | -------------------------------------------------------------------------------- /foia_hub/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block class %}home{% endblock %} 4 | 5 | {% block body %} 6 | {% include "includes/search.html" %} 7 | 8 |
    9 |
    10 |
    11 | 12 |
    13 |

    What is FOIA?

    14 | 15 |

    The U.S. Freedom of Information Act (FOIA, for short) allows the 16 | public the right to request access to government records.

    17 | 18 |

    Under FOIA, government agencies are required to disclose requested 19 | information unless it falls under one of nine exemptions that protect 20 | interests such as personal privacy, national security, and law 21 | enforcement.

    22 | 23 | Learn more at FOIA.gov 24 |
    25 | 26 |
    27 |

    How does the process work?

    28 | 29 |

    1. Identify an agency

    30 | 31 |

    32 | You (the requester) want information, but aren’t sure where to 33 | look. There are 100 agencies subject to the FOIA and, within these 34 | agencies, multiple FOIA offices, all of which process requests for 35 | their own records. openFOIA helps you find the right FOIA office 36 | to contact with your request. There are two ways you can find this 37 | information: First, using the search bar, you can search for FOIA 38 | contact information by agency name or by keyword. Second, you can 39 | browse a list of all federal 40 | agencies. 41 |

    42 | 43 |

    2. Do your research

    44 | 45 |

    46 | When you know which agency or office you want to contact, visit its 47 | website. Pay extra attention to its FOIA Library or Reading Room 48 | (linked on the openFOIA listing) to see what information is already 49 | available online. It’s possible that the information you want is 50 | already publicly available. If that’s the case, you won’t need to 51 | make a FOIA request. 52 |

    53 | 54 |

    3. Submit a request

    55 | 56 |

    57 | If the information you want isn’t publicly available, you’ll need 58 | to submit a FOIA request to the agency. Each agency has specific 59 | requirements about how to make a FOIA request. Before you submit 60 | your request, review the agency’s requirements, which are provided 61 | on the agency’s FOIA website. You can access each agency’s FOIA 62 | website from the openFOIA listing. 63 |

    64 | 65 |

    4. Receive a response

    66 | 67 |

    68 | Once the agency receives your request, the FOIA office will begin 69 | processing it. Processing times vary based on each request’s 70 | complexity and the number of records you’ve requested. Though each 71 | request is different, you can view the median processing times for 72 | simple and complex requests for each agency on its openFOIA 73 | listing. You will also find contact information for the agency’s 74 | FOIA Requester Service Center, where you can direct questions about 75 | your request. To learn more about the FOIA, visit FOIA.gov. 77 |

    78 | 79 |
    80 |
    81 |
    82 |
    83 | {% endblock %} 84 | -------------------------------------------------------------------------------- /foia_hub/settings/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for foia_hub project. 3 | 4 | For more information on this file, see 5 | https://docs.djangoproject.com/en/1.6/topics/settings/ 6 | 7 | For the full list of settings and their values, see 8 | https://docs.djangoproject.com/en/1.6/ref/settings/ 9 | """ 10 | 11 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 12 | import os 13 | BASE_DIR = os.path.dirname(os.path.dirname(__file__)) 14 | STATICFILES_DIRS = ( 15 | os.path.join(BASE_DIR, "static"), 16 | ) 17 | 18 | DEFAULT_DATA_REPO = "https://github.com/18F/2015-foia.git" 19 | 20 | DATABASES = {} 21 | 22 | # ALLOWED_HOSTS = [] 23 | 24 | # Application definition 25 | INSTALLED_APPS = ( 26 | 'django.contrib.admin', 27 | 'django.contrib.auth', 28 | 'django.contrib.contenttypes', 29 | 'django.contrib.sessions', 30 | 'django.contrib.messages', 31 | 'django.contrib.staticfiles', 32 | 33 | 'djangosecure', 34 | 'corsheaders', 35 | 'storages', 36 | 'localflavor', 37 | 'foia_hub', 38 | 'contact_updater' 39 | ) 40 | 41 | MIDDLEWARE_CLASSES = ( 42 | 'djangosecure.middleware.SecurityMiddleware', 43 | 'django.middleware.cache.UpdateCacheMiddleware', 44 | 'django.contrib.sessions.middleware.SessionMiddleware', 45 | 'corsheaders.middleware.CorsMiddleware', 46 | 'django.middleware.common.CommonMiddleware', 47 | 'django.middleware.csrf.CsrfViewMiddleware', 48 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 49 | 'django.contrib.messages.middleware.MessageMiddleware', 50 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 51 | 'django.middleware.cache.FetchFromCacheMiddleware', 52 | ) 53 | 54 | CACHE_MIDDLEWARE_ALIAS = 'default' 55 | CACHE_MIDDLEWARE_SECONDS = 86400 56 | CACHE_MIDDLEWARE_KEY_PREFIX = 'openfoia_cache' 57 | 58 | ROOT_URLCONF = 'foia_hub.urls' 59 | WSGI_APPLICATION = 'foia_hub.wsgi.application' 60 | 61 | # # JINJA SETTINGS 62 | TEMPLATE_LOADERS = ( 63 | 'django_jinja.loaders.AppLoader', 64 | 'django_jinja.loaders.FileSystemLoader', 65 | ) 66 | 67 | 68 | # We have to add the default processors 69 | TEMPLATE_CONTEXT_PROCESSORS = ( 70 | "django.contrib.auth.context_processors.auth", 71 | "django.core.context_processors.debug", 72 | "django.core.context_processors.i18n", 73 | "django.core.context_processors.media", 74 | "django.core.context_processors.static", 75 | "django.core.context_processors.tz", 76 | "django.core.context_processors.request", 77 | "django.contrib.messages.context_processors.messages", 78 | "foia_hub.context_processors.google_analytics.google_analytics") 79 | 80 | INSTALLED_APPS += ('django_jinja',) 81 | DEFAULT_JINJA2_TEMPLATE_EXTENSION = '.html' 82 | 83 | 84 | # Enable bytecode cache (default: False) 85 | # JINJA2_BYTECODE_CACHE_ENABLE = False 86 | 87 | # Cache backend name for bytecode cache (default: "default") 88 | # JINJA2_BYTECODE_CACHE_NAME = "default" 89 | 90 | # Specify custom bytecode cache subclass (default: None) 91 | # JINJA2_BYTECODE_CACHE_BACKEND = "path.to.you.cache.class" 92 | # #/ END JINJA SETTINGS 93 | 94 | # Internationalization 95 | # https://docs.djangoproject.com/en/1.6/topics/i18n/ 96 | 97 | LANGUAGE_CODE = 'en-us' 98 | TIME_ZONE = 'UTC' 99 | USE_I18N = True 100 | USE_L10N = True 101 | USE_TZ = True 102 | 103 | 104 | # Static files (CSS, JavaScript, Images) 105 | # https://docs.djangoproject.com/en/1.6/howto/static-files/ 106 | STATIC_URL = '/static/' 107 | STATIC_ROOT = 'staticfiles' 108 | 109 | # turn on CORS for everything (will be locked down later) 110 | CORS_ORIGIN_ALLOW_ALL = True 111 | 112 | ANALYTICS_ID = "" 113 | 114 | # Don't allow client-side JS to access CSRF cookie 115 | CSRF_COOKIE_HTTPONLY = True 116 | 117 | # Cache only as long as necessary, otherwise the CDN will cache for the 24 hour 118 | # default 119 | USE_ETAGS = True 120 | -------------------------------------------------------------------------------- /foia_hub/templates/contacts/contact.html: -------------------------------------------------------------------------------- 1 | {% if profile.office_url %} 2 |
    3 |
    4 | 5 |
    6 |
    7 |

    Website

    8 | 15 |
    16 |
    17 | {% endif %} 18 | 19 | {% if profile.simple_processing_time or profile.complex_processing_time %} 20 |
    21 |
    22 | 23 |
    24 |
    25 |

    Median processing time

    26 |
      27 | {% if profile.simple_processing_time %} 28 |
    • 29 | {{ profile.simple_processing_time|int }} working days for simple requests 30 |
    • 31 | 32 | 37 | {% endif %} 38 | 39 | {% if profile.complex_processing_time %} 40 |
    • 41 | {{ profile.complex_processing_time|int }} working days for complex requests 42 |
    • 43 | 44 | 48 | 49 | {% endif %} 50 |
    51 |
    52 |
    53 | {% endif %} 54 | 55 | {% if profile.phone or profile.public_liaison_name %} 56 |
    57 |
    58 | 59 |
    60 |
    61 |

    Have questions?

    62 |
      63 |
    • FOIA Requester Service Center 64 | 65 | {{ profile.phone }} 66 | 67 |
    • 68 | 72 | 73 | {% if profile.public_liaison_name %} 74 |
    • {{profile.public_liaison_name}}, Public Liaison 75 | 76 | 77 | {% if profile.public_liaison_phone %} 78 | 79 | {{ profile.public_liaison_phone}} 80 | 81 | {% endif %} 82 | 83 | {% if profile.public_liaison_email %} 84 | {{ profile.public_liaison_email }} 85 | {% endif %} 86 | 87 |
    • 88 | 89 | 94 | 95 | {% endif %} 96 |
    97 |
    98 |
    99 | {% endif %} 100 | -------------------------------------------------------------------------------- /deploy/hub.conf: -------------------------------------------------------------------------------- 1 | ### 2 | # 3 | # nginx configuration for foia.18f.us and foia-proto.18f.us. 4 | # 5 | # For our SSL configuration (ssl.rules), please see 6 | # https://github.com/18f/tls-standards 7 | # 8 | # NOTE: This nginx configuration is not currently synced automatically to version 9 | # control, so the deployed version may drift out of sync, though we'll try 10 | # not to let that happen. 11 | # 12 | ### 13 | 14 | 15 | 16 | # Active development on FOIA hub, https://foia.18f.us 17 | server { 18 | listen 443 ssl spdy; 19 | server_name foia.18f.us; 20 | 21 | ssl_certificate /etc/nginx/ssl/keys/star.18f.us-chain.crt; 22 | ssl_certificate_key /etc/nginx/ssl/keys/star.18f.us.key; 23 | include ssl/ssl.rules; 24 | 25 | location / { 26 | 27 | proxy_pass http://main_hub; 28 | proxy_http_version 1.1; 29 | proxy_redirect off; 30 | 31 | proxy_set_header Host $host; 32 | proxy_set_header X-Real-IP $remote_addr; 33 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 34 | proxy_set_header X-Forwarded-Proto https; 35 | proxy_max_temp_file_size 0; 36 | 37 | proxy_connect_timeout 10; 38 | proxy_send_timeout 30; 39 | proxy_read_timeout 30; 40 | 41 | access_log /home/foia/hub/shared/log/nginx_access.log; 42 | error_log /home/foia/hub/shared/log/nginx_error.log; 43 | 44 | } 45 | 46 | location /deploy { 47 | proxy_pass http://localhost:3000/; 48 | proxy_http_version 1.1; 49 | proxy_redirect off; 50 | 51 | proxy_set_header Host $host; 52 | proxy_set_header X-Real-IP $remote_addr; 53 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 54 | proxy_set_header X-Forwarded-Proto https; 55 | proxy_max_temp_file_size 0; 56 | 57 | proxy_connect_timeout 10; 58 | proxy_send_timeout 30; 59 | proxy_read_timeout 30; 60 | } 61 | 62 | location /static/ { 63 | autoindex off; 64 | port_in_redirect off; 65 | root /home/foia/hub/current/foia_hub/; 66 | expires 1w; 67 | } 68 | 69 | } 70 | 71 | # active development - lifecycle managed through fabric, primary server 72 | upstream main_hub { 73 | server 127.0.0.1:8000; 74 | keepalive 64; 75 | } 76 | 77 | # old prototype - not managed through fabric, not as important 78 | upstream proto_hub { 79 | server 127.0.0.1:7000; 80 | keepalive 64; 81 | } 82 | 83 | server { 84 | listen 80; 85 | server_name foia.18f.us; 86 | return 301 https://$host$request_uri; 87 | } 88 | 89 | server { 90 | listen 80; 91 | server_name foia-proto.18f.us; 92 | return 301 https://$host$request_uri; 93 | } 94 | 95 | # Old artifact of FOIA hub, https://foia-proto.18f.us 96 | server { 97 | listen 443 ssl spdy; 98 | server_name foia-proto.18f.us; 99 | 100 | ssl_certificate /etc/nginx/ssl/keys/star.18f.us-chain.crt; 101 | ssl_certificate_key /etc/nginx/ssl/keys/star.18f.us.key; 102 | include ssl/ssl.rules; 103 | 104 | location / { 105 | 106 | proxy_pass http://proto_hub; 107 | proxy_http_version 1.1; 108 | proxy_redirect off; 109 | 110 | proxy_set_header Host $host; 111 | proxy_set_header X-Real-IP $remote_addr; 112 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 113 | proxy_set_header X-Forwarded-Proto https; 114 | proxy_max_temp_file_size 0; 115 | 116 | proxy_connect_timeout 10; 117 | proxy_send_timeout 30; 118 | proxy_read_timeout 30; 119 | 120 | access_log /home/foia/hub/shared/log/proto-nginx_access.log; 121 | error_log /home/foia/hub/shared/log/proto-nginx_error.log; 122 | 123 | } 124 | 125 | location /static/ { 126 | autoindex off; 127 | port_in_redirect off; 128 | root /home/foia/proto/foia_hub/; 129 | expires 1w; 130 | } 131 | 132 | } 133 | -------------------------------------------------------------------------------- /foia_hub/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import jsonfield.fields 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Agency', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, verbose_name='ID', serialize=False, primary_key=True)), 18 | ('name', models.CharField(unique=True, max_length=250)), 19 | ('abbreviation', models.CharField(max_length=30, unique=True, null=True)), 20 | ('description', models.TextField(null=True)), 21 | ('keywords', jsonfield.fields.JSONField(null=True)), 22 | ('slug', models.SlugField(unique=True)), 23 | ], 24 | options={ 25 | }, 26 | bases=(models.Model,), 27 | ), 28 | migrations.CreateModel( 29 | name='FOIARequest', 30 | fields=[ 31 | ('id', models.AutoField(auto_created=True, verbose_name='ID', serialize=False, primary_key=True)), 32 | ('status', models.CharField(choices=[('O', 'open'), ('S', 'submitted'), ('P', 'processing'), ('C', 'closed')], default='O', max_length=1)), 33 | ('date_start', models.DateField(null=True)), 34 | ('date_end', models.DateField()), 35 | ('fee_limit', models.PositiveIntegerField(default=0)), 36 | ('request_body', models.TextField()), 37 | ('custom_fields', jsonfield.fields.JSONField(null=True)), 38 | ], 39 | options={ 40 | }, 41 | bases=(models.Model,), 42 | ), 43 | migrations.CreateModel( 44 | name='Office', 45 | fields=[ 46 | ('id', models.AutoField(auto_created=True, verbose_name='ID', serialize=False, primary_key=True)), 47 | ('name', models.CharField(max_length=250)), 48 | ('slug', models.SlugField()), 49 | ('service_center', models.CharField(max_length=250, null=True)), 50 | ('fax', models.CharField(max_length=50, null=True)), 51 | ('request_form', models.URLField(null=True)), 52 | ('website', models.URLField(null=True)), 53 | ('emails', models.CharField(max_length=250, null=True)), 54 | ('contact', models.TextField(null=True)), 55 | ('contact_phone', models.CharField(max_length=50, null=True)), 56 | ('public_liaison', models.TextField(null=True)), 57 | ('notes', models.TextField(null=True)), 58 | ('agency', models.ForeignKey(to='foia_hub.Agency')), 59 | ], 60 | options={ 61 | }, 62 | bases=(models.Model,), 63 | ), 64 | migrations.CreateModel( 65 | name='Requester', 66 | fields=[ 67 | ('id', models.AutoField(auto_created=True, verbose_name='ID', serialize=False, primary_key=True)), 68 | ('first_name', models.CharField(max_length=250)), 69 | ('last_name', models.CharField(max_length=250)), 70 | ('email', models.EmailField(max_length=75)), 71 | ], 72 | options={ 73 | }, 74 | bases=(models.Model,), 75 | ), 76 | migrations.AlterUniqueTogether( 77 | name='office', 78 | unique_together=set([('slug', 'agency')]), 79 | ), 80 | migrations.AddField( 81 | model_name='foiarequest', 82 | name='office', 83 | field=models.ForeignKey(to='foia_hub.Office'), 84 | preserve_default=True, 85 | ), 86 | migrations.AddField( 87 | model_name='foiarequest', 88 | name='requester', 89 | field=models.ForeignKey(to='foia_hub.Requester'), 90 | preserve_default=True, 91 | ), 92 | ] 93 | -------------------------------------------------------------------------------- /contact_updater/views.py: -------------------------------------------------------------------------------- 1 | import json 2 | import time 3 | 4 | from contact_updater.forms import AgencyForm 5 | 6 | from django.forms.formsets import formset_factory 7 | from django.shortcuts import render 8 | from django.http import HttpResponse 9 | 10 | from foia_hub.api import AgencyResource, OfficeResource 11 | 12 | 13 | def form_index(request): 14 | """ 15 | This function renders the landing page of the contact updater, which 16 | contains a links to each agencies update form. 17 | """ 18 | return render(request, "form_index.html") 19 | 20 | 21 | def download_data(request): 22 | """ Converts POST request into JSON file ready for download """ 23 | 24 | data = dict(request.POST) 25 | data['timestamp'] = int(time.time()) 26 | del data['csrfmiddlewaretoken'] 27 | res = HttpResponse(json.dumps(data), content_type="application/javascript") 28 | res['Content-Disposition'] = 'attachment; filename=contact_data.json' 29 | return res 30 | 31 | 32 | def unpack_libraries(libraries): 33 | """ Given a list of libraries returns url """ 34 | if libraries: 35 | return libraries[0].get('url') 36 | else: 37 | return '' 38 | 39 | 40 | def join_array(array): 41 | """ Joins array feilds using `\n` """ 42 | if array: 43 | return "\n".join(array) 44 | 45 | 46 | def prepare_address_lines(lines): 47 | """ Splits address lines into two lines for form """ 48 | data = {} 49 | if len(lines) > 0: 50 | data['address_line_1'] = lines[0] 51 | if len(lines) > 1: 52 | data['address_line_2'] = " ".join(lines[1:]) 53 | return data 54 | 55 | 56 | def prepare_emails(emails): 57 | """ Returns only the first email, else returns a blank line """ 58 | data = {'emails': ''} 59 | if emails: 60 | data['emails'] = emails[0] 61 | return data 62 | 63 | 64 | def transform_data(data): 65 | """ Returns only first email """ 66 | data['foia_libraries'] = unpack_libraries(data.get('foia_libraries')) 67 | data['common_requests'] = join_array(data.get('common_requests')) 68 | data['no_records_about'] = join_array(data.get('no_records_about')) 69 | data.update(prepare_address_lines(data.get('address_lines'))) 70 | data.update(prepare_emails(data.get('emails'))) 71 | return data 72 | 73 | 74 | def get_agency_data(slug): 75 | """ 76 | Given an agency slug parse through the agency API and collect agency 77 | info to populate agency form. """ 78 | agency_resource = AgencyResource() 79 | agency_data = [transform_data(agency_resource.detail(slug).value)] 80 | if agency_data[0].get('offices'): 81 | office_resource = OfficeResource() 82 | for office in agency_data[0].get('offices'): 83 | if '--' in office.get('slug'): 84 | office_data = office_resource.detail(office['slug']).value 85 | else: 86 | office_data = agency_resource.detail(office['slug']).value 87 | agency_data.append(transform_data(office_data)) 88 | return agency_data 89 | 90 | 91 | def prepopulate_agency(request, slug): 92 | """ 93 | If GET request Collects agency and office data from foia_hub to 94 | populate the form. If POST request responds an attachment 95 | """ 96 | return_data = {} 97 | 98 | agency_data = get_agency_data(slug=slug) 99 | agency_form_set = formset_factory(AgencyForm) 100 | 101 | if request.method == 'POST': 102 | formset = agency_form_set(request.POST) 103 | if formset.is_valid(): 104 | return_data['validated'] = True 105 | if request.POST.get('download'): 106 | return download_data(request=request) 107 | elif request.POST.get('return'): 108 | return_data['validated'] = False 109 | else: 110 | formset = agency_form_set(initial=agency_data) 111 | 112 | management_form = formset.management_form 113 | return_data.update( 114 | { 115 | 'agency_names': [item.get('name') for item in agency_data], 116 | 'forms': formset[:-1], 117 | 'management_form': management_form, 118 | }) 119 | return render(request, "agency_form.html", return_data) 120 | -------------------------------------------------------------------------------- /foia_hub/fixtures/agencies_test.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "fields": { 4 | "TTY_phone": null, 5 | "abbreviation": "DHS", 6 | "address_lines": ["Stop 1"], 7 | "chief_name": null, 8 | "city": "Washington", 9 | "description": "The Department of Homeland Security has a vital mission: to secure the nation from the many threats we face. This requires the dedication of more than 230,000 employees in jobs that range from aviation and border security to emergency response, from cybersecurity analyst to chemical facility inspector", 10 | "emails": ["foia@hq.dhs.gov"], 11 | "fax": "202-343-1743", 12 | "keywords": ["trademark"], 13 | "name": "Department of Homeland Security", 14 | "office_url": "http://www.dhs.gov/freedom-information-act-foia", 15 | "parent": null, 16 | "person_name": "Joe Bureaucrat", 17 | "phone": null, 18 | "public_liaison_email": "liaison@email.gov", 19 | "public_liaison_name": "Joe Liaison", 20 | "public_liaison_phone": "202-555-5555", 21 | "request_form_url": "http://dhs.gov/xfoia/editorial_0579.html", 22 | "slug": "department-of-homeland-security", 23 | "state": "DC", 24 | "street": "245 Murray Lane, SW", 25 | "toll_free_phone": null, 26 | "zip_code": 20528 27 | }, 28 | "model": "foia_hub.agency", 29 | "pk": 15 30 | }, 31 | { 32 | "fields": { 33 | "TTY_phone": null, 34 | "abbreviation": "DOC", 35 | "address_lines": [], 36 | "chief_name": null, 37 | "city": "", 38 | "description": "The historic mission of the Department of Commerce is \"to foster, promote, and develop the foreign and domestic commerce\" of the United States. This has evolved, as a result of legislative and administrative additions, to encompass broadly the responsibility to foster, serve, and promote the Nation's economic development, tax, technological advancement and trademark", 39 | "emails": ["foia@doc.gov"], 40 | "fax": null, 41 | "keywords": ["business", "industry", "forests", "petroleum"], 42 | "name": "Department of Commerce", 43 | "office_url": null, 44 | "parent": null, 45 | "person_name": null, 46 | "phone": null, 47 | "public_liaison_email": null, 48 | "public_liaison_name": null, 49 | "public_liaison_phone": null, 50 | "request_form_url": null, 51 | "slug": "department-of-commerce", 52 | "state": "", 53 | "street": "", 54 | "toll_free_phone": null, 55 | "zip_code": 0 56 | }, 57 | "model": "foia_hub.agency", 58 | "pk": 17 59 | }, 60 | { 61 | "fields": { 62 | "link_text": "The Electronic Reading Room", 63 | "url": "http://www.doc.gov/err/", 64 | "object_id": 17, 65 | "content_type": ["foia_hub", "agency"] 66 | }, 67 | "model": "foia_hub.readingroomurls", 68 | "pk": 175 69 | }, 70 | { 71 | "fields": { 72 | "TTY_phone": null, 73 | "abbreviation": "USPTO", 74 | "address_lines": [], 75 | "chief_name": null, 76 | "city": "", 77 | "description": "The U.S. Patent and Trademark Office is the agency responsible for granting U.S. patents and registering trademarks for the taxpayer", 78 | "emails": [], 79 | "fax": null, 80 | "keywords": [], 81 | "name": "U.S. Patent and Trademark Office", 82 | "office_url": null, 83 | "parent": 17, 84 | "person_name": "Dahlia Rose", 85 | "phone": "571-272-3000", 86 | "public_liaison_email": null, 87 | "public_liaison_name": null, 88 | "public_liaison_phone": null, 89 | "request_form_url": null, 90 | "slug": "us-patent-and-trademark-office", 91 | "state": "", 92 | "street": "P.O. Box 1450", 93 | "toll_free_phone": null, 94 | "zip_code": 22313 95 | }, 96 | "model": "foia_hub.agency", 97 | "pk": 18 98 | } 99 | ] 100 | -------------------------------------------------------------------------------- /foia_hub/templates/developers.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block body %} 4 |
    5 |
    6 |
    7 |

    Developers

    8 | 9 |

    10 | This effort is a work in progress by 18F. 11 | However, we welcome contributions, suggestions and comments by everyone. 12 |

    13 | 14 |
    15 | 16 |

    Technology

    17 | 18 |

    19 | openFOIA is a Python/Django application. Many of the scripts written to collect 20 | data are also written in Python. 21 |

    22 | 23 |
    24 | 25 |

    API

    26 | 27 |

    28 | All of the information presented is available through an API. 29 | The API 30 | documentation provides more details. 31 |

    32 | 33 |
    34 | 35 | 36 |

    Bulk Data

    37 | 38 |

    39 | The data source for the API and the tool is a set of YAML files that contains 40 | all the data. These files are the canonical source for our data and are updated 41 | through a combination of automated and manual processes. 42 |

    43 | 44 | 49 | 50 |
    51 | 52 |

    Repositories

    53 | 54 |
      55 |
    • 56 | foia 57 | Our main repository (also contains the bulk data) 58 |
    • 59 | 60 |
    • 61 | foia-hub 62 | The repository for openFOIA. 63 |
    • 64 |
    65 | 66 |
    67 | 68 |

    Data Sources

    69 | 70 |

    We use multiple sources for the information we display about each 71 | government component.

    72 | 73 |

    Our contact and processing times data come from both foia.gov and manually culling government FOIA web pages. 75 | Descriptions and acronyms come from the USAGOV Contacts API. 76 | We populate keyword search terms using the Federal Register API. Finally, the 78 | FOIA library links are scraped from each agency's FOIA web page where relevant. 79 |

    80 | 81 |

    Our source 83 | code provides more information and details on how each data source is 84 | processed.

    85 | 86 |
    87 | 88 | 89 |

    Open Source and Contributing

    90 | 91 |

    92 | This project is open source and we welcome contributions from the public. We 93 | work in an agile fashion in two-week sprints. Our project backlog is here. 95 |

    96 | 97 |

    License

    98 | 99 |

    100 | This project is in the public domain within the United States, and copyright 101 | and related rights in the work worldwide are waived through CC0 1.0 Universal 102 | public domain dedication. 103 |

    104 | 105 |

    106 | All contributions to any repositories of this project will be released under 107 | the CC0 dedication. By submitting a pull request, you are agreeing to comply 108 | with this waiver of copyright interest. 109 |

    110 | 111 |
    112 | 113 |
    114 |
    115 |
    116 | {% endblock %} 117 | -------------------------------------------------------------------------------- /foia_hub/migrations/0006_auto_20141002_1930.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import localflavor.us.models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('foia_hub', '0005_auto_20141001_1652'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='agency', 17 | name='city', 18 | field=models.CharField(null=True, max_length=64), 19 | ), 20 | migrations.AlterField( 21 | model_name='agency', 22 | name='state', 23 | field=localflavor.us.models.USPostalCodeField(null=True, choices=[('AL', 'Alabama'), ('AK', 'Alaska'), ('AS', 'American Samoa'), ('AZ', 'Arizona'), ('AR', 'Arkansas'), ('AA', 'Armed Forces Americas'), ('AE', 'Armed Forces Europe'), ('AP', 'Armed Forces Pacific'), ('CA', 'California'), ('CO', 'Colorado'), ('CT', 'Connecticut'), ('DE', 'Delaware'), ('DC', 'District of Columbia'), ('FM', 'Federated States of Micronesia'), ('FL', 'Florida'), ('GA', 'Georgia'), ('GU', 'Guam'), ('HI', 'Hawaii'), ('ID', 'Idaho'), ('IL', 'Illinois'), ('IN', 'Indiana'), ('IA', 'Iowa'), ('KS', 'Kansas'), ('KY', 'Kentucky'), ('LA', 'Louisiana'), ('ME', 'Maine'), ('MH', 'Marshall Islands'), ('MD', 'Maryland'), ('MA', 'Massachusetts'), ('MI', 'Michigan'), ('MN', 'Minnesota'), ('MS', 'Mississippi'), ('MO', 'Missouri'), ('MT', 'Montana'), ('NE', 'Nebraska'), ('NV', 'Nevada'), ('NH', 'New Hampshire'), ('NJ', 'New Jersey'), ('NM', 'New Mexico'), ('NY', 'New York'), ('NC', 'North Carolina'), ('ND', 'North Dakota'), ('MP', 'Northern Mariana Islands'), ('OH', 'Ohio'), ('OK', 'Oklahoma'), ('OR', 'Oregon'), ('PW', 'Palau'), ('PA', 'Pennsylvania'), ('PR', 'Puerto Rico'), ('RI', 'Rhode Island'), ('SC', 'South Carolina'), ('SD', 'South Dakota'), ('TN', 'Tennessee'), ('TX', 'Texas'), ('UT', 'Utah'), ('VT', 'Vermont'), ('VI', 'Virgin Islands'), ('VA', 'Virginia'), ('WA', 'Washington'), ('WV', 'West Virginia'), ('WI', 'Wisconsin'), ('WY', 'Wyoming')], max_length=2), 24 | ), 25 | migrations.AlterField( 26 | model_name='agency', 27 | name='street', 28 | field=models.CharField(null=True, max_length=128), 29 | ), 30 | migrations.AlterField( 31 | model_name='agency', 32 | name='zip_code', 33 | field=models.CharField(null=True, max_length=10), 34 | ), 35 | migrations.AlterField( 36 | model_name='office', 37 | name='city', 38 | field=models.CharField(null=True, max_length=64), 39 | ), 40 | migrations.AlterField( 41 | model_name='office', 42 | name='state', 43 | field=localflavor.us.models.USPostalCodeField(null=True, choices=[('AL', 'Alabama'), ('AK', 'Alaska'), ('AS', 'American Samoa'), ('AZ', 'Arizona'), ('AR', 'Arkansas'), ('AA', 'Armed Forces Americas'), ('AE', 'Armed Forces Europe'), ('AP', 'Armed Forces Pacific'), ('CA', 'California'), ('CO', 'Colorado'), ('CT', 'Connecticut'), ('DE', 'Delaware'), ('DC', 'District of Columbia'), ('FM', 'Federated States of Micronesia'), ('FL', 'Florida'), ('GA', 'Georgia'), ('GU', 'Guam'), ('HI', 'Hawaii'), ('ID', 'Idaho'), ('IL', 'Illinois'), ('IN', 'Indiana'), ('IA', 'Iowa'), ('KS', 'Kansas'), ('KY', 'Kentucky'), ('LA', 'Louisiana'), ('ME', 'Maine'), ('MH', 'Marshall Islands'), ('MD', 'Maryland'), ('MA', 'Massachusetts'), ('MI', 'Michigan'), ('MN', 'Minnesota'), ('MS', 'Mississippi'), ('MO', 'Missouri'), ('MT', 'Montana'), ('NE', 'Nebraska'), ('NV', 'Nevada'), ('NH', 'New Hampshire'), ('NJ', 'New Jersey'), ('NM', 'New Mexico'), ('NY', 'New York'), ('NC', 'North Carolina'), ('ND', 'North Dakota'), ('MP', 'Northern Mariana Islands'), ('OH', 'Ohio'), ('OK', 'Oklahoma'), ('OR', 'Oregon'), ('PW', 'Palau'), ('PA', 'Pennsylvania'), ('PR', 'Puerto Rico'), ('RI', 'Rhode Island'), ('SC', 'South Carolina'), ('SD', 'South Dakota'), ('TN', 'Tennessee'), ('TX', 'Texas'), ('UT', 'Utah'), ('VT', 'Vermont'), ('VI', 'Virgin Islands'), ('VA', 'Virginia'), ('WA', 'Washington'), ('WV', 'West Virginia'), ('WI', 'Wisconsin'), ('WY', 'Wyoming')], max_length=2), 44 | ), 45 | migrations.AlterField( 46 | model_name='office', 47 | name='street', 48 | field=models.CharField(null=True, max_length=128), 49 | ), 50 | migrations.AlterField( 51 | model_name='office', 52 | name='zip_code', 53 | field=models.CharField(null=True, max_length=10), 54 | ), 55 | ] 56 | -------------------------------------------------------------------------------- /foia_hub/static/js/contact-updater/form-controls.js: -------------------------------------------------------------------------------- 1 | $(function(){ 2 | var error_list = $('#error-list'), 3 | addErrorItem, 4 | removeErrorItem; 5 | 6 | // Function to allow error list 7 | addErrorItem = function (fieldInstance) { 8 | var errorMsg = ParsleyUI.getErrorsMessages(fieldInstance).join(';'); 9 | var errorElement = fieldInstance.$element[0]; 10 | var errorElementLabel = $(errorElement).parent().find('label')[0].innerHTML; 11 | var errorOffice = $(errorElement).parent()[0].getAttribute('agency'); 12 | var error = "" + errorOffice + "'s " + errorElementLabel 13 | if (errorMsg == "This field is required.") 14 | error += " is missing."; 15 | else 16 | error += " is invalid."; 17 | var lis = error_list.find('li').filter(function (){ return this.id == errorElement.id}) 18 | if (lis.length == 1){ 19 | lis[0].innerHTML = error 20 | } 21 | else{ 22 | var li = document.createElement("li"); 23 | li.id = errorElement.id 24 | li.innerHTML = error 25 | error_list[0].appendChild(li); 26 | error_list.find("li").sort(function(first, second) { 27 | return $(first).text().toLowerCase().localeCompare($(second).text().toLowerCase()); 28 | }).each(function() { 29 | error_list.append(this); 30 | }); 31 | } 32 | }; 33 | removeErrorItem = function (fieldInstance){ 34 | var errorElement = fieldInstance.$element[0]; 35 | var error_list_items = error_list.find('li'); 36 | if (error_list_items.length > 0){ 37 | error_list_items.each(function () { 38 | if(errorElement.id == this.id){ 39 | error_list[0].removeChild(this); 40 | } 41 | }) 42 | } 43 | else { 44 | document.getElementById('submit-agency').className = 'submit-valid'; 45 | } 46 | }; 47 | 48 | // Make some fields required 49 | $(".public_liaison_name,.public_liaison_email,#id_form-0-description,.address_line_1,.city,.state,.zip_code").prop('required', true) 50 | 51 | // Setting the default required message for parsely 52 | window.ParsleyConfig.i18n.en.required="This field is required." 53 | 54 | // Setup Parsely keyup triggers 55 | $('form').find(':input').each(function(){ 56 | this.setAttribute("data-parsley-trigger", "keyup"); 57 | }); 58 | 59 | // Setup error listners 60 | $.listen('parsley:field:error', function (fieldInstance) { 61 | document.getElementById('submit-agency').className = 'submit-invalid'; 62 | // Only create error list if the agency is decentralized 63 | if (document.getElementById("form_selector")) { 64 | addErrorItem(fieldInstance); 65 | } 66 | }); 67 | 68 | // Set up success listener to remove items from error list 69 | $.listen('parsley:field:success', function (fieldInstance) { 70 | removeErrorItem(fieldInstance) 71 | }); 72 | 73 | // Init Parsely 74 | $('form').parsley().validate(); 75 | // Validate form, but scroll to top 76 | $('body').scrollTop(0); 77 | 78 | 79 | // Make forms visible 80 | $( "#form_selector" ).change( function() { 81 | // Keep the rest of the forms hidden 82 | $("div[id^=office_]").hide(); 83 | var form_id = document.getElementById("form_selector").value; 84 | // Show selected form 85 | document.getElementById("office_" + form_id).style.display = "block"; 86 | $("#office_" + form_id).show(); 87 | }); 88 | 89 | // Set Char counters 90 | $('textarea[id$=-description]').each(function() { 91 | // Get the current char count 92 | var max = 500; 93 | var current_len = $(this).val().length 94 | // Create and insert text counters 95 | var charDiv = document.createElement("div"); 96 | charDiv.setAttribute("id", "charNum" + this.name); 97 | charDiv.innerHTML = current_len + "/500 characters"; 98 | this.parentNode.insertBefore(charDiv, this.nextSibling); 99 | // Set a listener to change on keyup 100 | $(this).keyup(function () { 101 | var len = $(this).val().length; 102 | if (len >= max) { 103 | $("#charNum" + this.name).text('You have reached the limit'); 104 | } else { 105 | var char = len; 106 | $("#charNum" + this.name).text(char + '/500 characters'); 107 | } 108 | }); 109 | }); 110 | }); 111 | -------------------------------------------------------------------------------- /foia_hub/static/js/search.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | $(document).ready(function() { 3 | var currentText = '', 4 | longestText = '', 5 | onChange, 6 | onCursorChange, 7 | onSelection, 8 | agencyDatasource, 9 | agencyAdaptor, 10 | footerAdaptor, 11 | updateEmptyState, 12 | typeahead, 13 | form, 14 | // how long we'll give Google Analytics to record a an action 15 | // before just going ahead with it, in milliseconds 16 | gaTimeout = 500; 17 | 18 | // Set up the agency data source 19 | agencyDatasource = new Bloodhound({ 20 | queryTokenizer: Bloodhound.tokenizers.whitespace, 21 | limit: 500, // infinity 22 | prefetch: { 23 | url: '/api/agency/', 24 | filter: function(response) { 25 | return response.objects; 26 | } 27 | }, 28 | datumTokenizer: function(d) { 29 | return [] 30 | .concat(Bloodhound.tokenizers.whitespace(d.name)) 31 | .concat(Bloodhound.tokenizers.whitespace(d.abbreviation)); 32 | } 33 | }); 34 | // always clear local storage for new requests, at least in dev 35 | agencyDatasource.clearPrefetchCache(); 36 | agencyDatasource.initialize(); 37 | 38 | // Set up the agency adaptor 39 | agencyAdaptor = { 40 | name: 'agencies', 41 | displayKey: 'value', 42 | source: agencyDatasource.ttAdapter(), 43 | templates: { 44 | suggestion: Handlebars.compile('

    {{name}}

    ') 45 | } 46 | }; 47 | // Set up the footer link adaptor 48 | footerAdaptor = { 49 | name: 'footer', 50 | source: function(query, callback) { 51 | if (query.length > 0) { 52 | callback([{'query': query, 'isFooter': true}]); 53 | } 54 | }, 55 | templates: { 56 | suggestion: Handlebars.compile('

    Search for "{{query}}" in keywords and descriptions

    ') 57 | } 58 | }; 59 | 60 | // Track the text as the user types 61 | onChange = function(ev) { 62 | currentText = typeahead.val(); 63 | if (currentText.length > longestText.length) { 64 | longestText = currentText; 65 | } else if (currentText.length === 0 && longestText.length > 0) { 66 | // blanked out the text after initially typing something 67 | ga('send', { 68 | 'hitType': 'event', 69 | 'eventCategory': $('#query')[0].getAttribute('search-type'), 70 | 'eventAction': 'did-not-want--' + longestText, 71 | 'eventLabel': document.location.pathname}); 72 | longestText = ''; 73 | } 74 | form.toggleClass('tt-filled', currentText.length > 0); 75 | }; 76 | 77 | // If an agency was selected, notify analytics and redirect 78 | // If the footer was selected, submit the form to redirect 79 | onSelection = function(ev, suggestion) { 80 | if (suggestion.isFooter) { 81 | typeahead.val(suggestion.query); 82 | form.submit(); 83 | } else { 84 | var callback = function() { 85 | clearTimeout(timeout); 86 | window.location = '/contacts/' + suggestion.slug + '/'; 87 | }, 88 | timeout = setTimeout(callback, gaTimeout); 89 | ga('send', { 90 | 'hitType': 'event', 91 | 'eventCategory': $('#query')[0].getAttribute('search-type'), 92 | 'eventAction': 'select-agency--' + suggestion.slug, 93 | 'eventLabel': document.location.pathname}, 94 | {'hitCallback': callback}); 95 | } 96 | }; 97 | 98 | onCursorChange = function(ev, suggestion) { 99 | if (suggestion) { 100 | typeahead.val(suggestion.name || suggestion.query); 101 | } 102 | var first = form 103 | .find('.tt-dataset-agencies .tt-suggestion, .tt-dataset-footer .tt-suggestion') 104 | .first(); 105 | form.find('.tt-dropdown-menu') 106 | .toggleClass('tt-cursor-first', first.hasClass('tt-cursor')); 107 | }; 108 | 109 | // Initialize typeahead 110 | typeahead = $('#query') 111 | .typeahead({ 112 | hint: false, 113 | highlight: true, 114 | minLength: 1 115 | }, agencyAdaptor, footerAdaptor) 116 | .on('keyup', onChange) 117 | // .on('keydown', onCursorChange) 118 | .on('typeahead:cursorchanged', onCursorChange) 119 | .on('typeahead:selected', onSelection); 120 | 121 | // uuuuugggghhhhh 122 | var menu = $('.tt-dropdown-menu') 123 | .append('
    '); 124 | menu.find('[class^="tt-dataset-"]') 125 | .appendTo(menu.find('.tt-dropdown-wrap')); 126 | 127 | form = typeahead.closest('form'); 128 | 129 | // when the clear button is clicked, clear the input and 130 | // trigger the change handler to toggle the tt-filled class 131 | form.find('.clear') 132 | .on('click', function() { 133 | typeahead 134 | .typeahead('val', '') 135 | .focus(); 136 | onChange(); 137 | }); 138 | }); 139 | -------------------------------------------------------------------------------- /foia_hub/templates/about.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block breadcrumb %} 4 | {% import 'includes/breadcrumb.html' as breadcrumb %} 5 | {{ breadcrumb.create([(url('about'), 'About this Project')]) }} 6 | {% endblock %} 7 | 8 | {% block body %} 9 |
    10 |
    11 |
    12 |
    13 |

    About this Project

    14 | 15 |

    openFOIA aims to improve the FOIA request experience.

    16 | 17 |

    openFOIA is an online tool that helps users find current 18 | FOIA contact and request information across the federal 19 | government. It is a work in progress by the Department of Justice and 18F.

    22 | 23 |

    Our work is one of several initiatives currently in progress 24 | to modernize the Freedom of Information Act, as part of the 25 | United States’ Second 27 | Open Government Partnership National Action Plan. openFOIA 28 | supplements the resources available on FOIA.gov, the 29 | government’s central website on FOIA. To learn more about the 30 | FOIA, please visit FOIA.gov.

    32 |
    33 |
    34 |

    Getting better all the time

    35 | 36 |

    We're currently in the alpha stage of 38 | development, which means our site might not have every feature 39 | you're looking for.

    40 | 41 |

    Please let us know what's working and what you'd like to see 42 | in the future - we're actively improving openFOIA, and we base 43 | our improvements on feedback from users like you! Email your 44 | suggestions to 18f-foia@gsa.gov, or visit 46 | our issue 47 | tracker here.

    48 | 49 |

    Developers should check out the developer resources page.

    51 |
    52 | 53 |
    54 |

    Current Features

    55 |
      56 |
    • 57 |

      Searchable contact information

      58 | 59 |

      openFOIA consolidates the contact information for 60 | all federal agencies’ and components’ FOIA offices. 61 | It’s easy to search, and it provides links to each 62 | agency’s online FOIA request form (when available), 63 | allowing users to start their requests more 64 | quickly.

      65 |
    • 66 | 67 |
    • 68 |

      Agency description search

      69 | 70 |

      openFOIA lets you search by some keywords, which is 71 | helpful if you’re not sure which agency to contact. For 72 | example, a search for 'credit cards' brings up the 73 | Consumer Financial Protection Bureau, and a search for 74 | 'criminal laws' brings up the Bureau of Alcohol, 75 | Tobacco, Firearms and Explosives.

      76 |
    • 77 | 78 |
    • 79 |

      Acronym search

      80 | 81 |

      Some agencies are better known by their acronyms. 82 | openFOIA allows users to search by acronym instead of 83 | full agency (or component) title. For example, a search 84 | for 'FBI' brings up the Federal Bureau of 85 | Investigation.

      86 |
    • 87 |
    • 88 |

      FOIA libraries

      89 | 90 |

      Many agencies provide commonly requested responsive 91 | documents in online libraries or reading rooms. 92 | openFOIA links to these libraries, letting users see if 93 | the information they want is already available.

      94 |
    • 95 |
    96 |
    97 |
    98 |

    Data

    99 |

    100 | The median processing times we use are from the Annual FOIA Reports from 101 | data collected for 2014. 102 |

    103 |
    104 | 105 |
    106 |
    107 |
    108 | {% endblock %} 109 | --------------------------------------------------------------------------------