├── .python-version ├── runtime.txt ├── Procfile ├── .gitignore ├── AgileEstimator.xlsx ├── app ├── assets │ ├── img │ │ ├── minus.png │ │ ├── plus.png │ │ ├── search.png │ │ ├── correct8.png │ │ ├── correct9.png │ │ ├── logo-img.png │ │ ├── alerts │ │ │ ├── info.png │ │ │ ├── error.png │ │ │ ├── success.png │ │ │ ├── warning.png │ │ │ ├── success.svg │ │ │ ├── warning.svg │ │ │ ├── error.svg │ │ │ └── info.svg │ │ ├── arrow-down.png │ │ ├── arrow-right.png │ │ ├── us_flag_small.png │ │ ├── favicons │ │ │ ├── favicon.ico │ │ │ ├── favicon.png │ │ │ ├── favicon-114.png │ │ │ ├── favicon-144.png │ │ │ ├── favicon-16.png │ │ │ ├── favicon-192.png │ │ │ ├── favicon-57.png │ │ │ └── favicon-72.png │ │ ├── social-icons │ │ │ ├── png │ │ │ │ ├── rss25.png │ │ │ │ ├── twitter16.png │ │ │ │ ├── youtube15.png │ │ │ │ └── facebook25.png │ │ │ └── svg │ │ │ │ ├── facebook25.svg │ │ │ │ ├── twitter16.svg │ │ │ │ ├── rss25.svg │ │ │ │ └── youtube15.svg │ │ ├── arrow-down.svg │ │ ├── minus.svg │ │ ├── correct8.svg │ │ ├── correct9.svg │ │ ├── arrow-right.svg │ │ ├── plus.svg │ │ └── search.svg │ ├── fonts │ │ ├── merriweather-bold-webfont.eot │ │ ├── merriweather-bold-webfont.ttf │ │ ├── merriweather-bold-webfont.woff │ │ ├── merriweather-light-webfont.eot │ │ ├── merriweather-light-webfont.ttf │ │ ├── sourcesanspro-bold-webfont.eot │ │ ├── sourcesanspro-bold-webfont.ttf │ │ ├── merriweather-bold-webfont.woff2 │ │ ├── merriweather-italic-webfont.eot │ │ ├── merriweather-italic-webfont.ttf │ │ ├── merriweather-italic-webfont.woff │ │ ├── merriweather-italic-webfont.woff2 │ │ ├── merriweather-light-webfont.woff │ │ ├── merriweather-light-webfont.woff2 │ │ ├── merriweather-regular-webfont.eot │ │ ├── merriweather-regular-webfont.ttf │ │ ├── merriweather-regular-webfont.woff │ │ ├── sourcesanspro-bold-webfont.woff │ │ ├── sourcesanspro-bold-webfont.woff2 │ │ ├── sourcesanspro-italic-webfont.eot │ │ ├── sourcesanspro-italic-webfont.ttf │ │ ├── sourcesanspro-italic-webfont.woff │ │ ├── sourcesanspro-light-webfont.eot │ │ ├── sourcesanspro-light-webfont.ttf │ │ ├── sourcesanspro-light-webfont.woff │ │ ├── sourcesanspro-light-webfont.woff2 │ │ ├── sourcesanspro-regular-webfont.eot │ │ ├── sourcesanspro-regular-webfont.ttf │ │ ├── merriweather-regular-webfont.woff2 │ │ ├── sourcesanspro-italic-webfont.woff2 │ │ ├── sourcesanspro-regular-webfont.woff │ │ └── sourcesanspro-regular-webfont.woff2 │ ├── css │ │ ├── main.css │ │ ├── normalize.min.css │ │ ├── google-fonts.css │ │ ├── style.css │ │ └── style.scss │ └── js │ │ └── affix.js ├── src │ ├── state_mixin.js │ ├── question_list.js │ ├── auth │ │ ├── login-button.js │ │ ├── mixin.js │ │ └── register-button.js │ ├── results.js │ ├── questions │ │ ├── 09_contract_clauses.js │ │ ├── 11_evaluation_criteria.js │ │ ├── 01_definitions.js │ │ ├── 05_post_award.js │ │ ├── 10_instructions_to_offerors.js │ │ ├── XX_sample.js │ │ ├── 08_special_requirements.js │ │ ├── 07_government_roles.js │ │ ├── 06_inspection.js │ │ ├── 04_personnel.js │ │ └── 03_objectives.js │ ├── edit_box.js │ ├── question.js │ ├── app.js │ ├── welcome.js │ ├── request.js │ └── request_overview.js ├── index.html ├── package.json ├── gulpfile.js └── helpers.js ├── .cfignore ├── manifest.yml ├── manifest-staging.yml ├── requirements.txt ├── tests.py ├── config.py ├── deploy.sh ├── CONTRIBUTING.md ├── LICENSE.md ├── .travis.yml ├── server.py ├── README.md ├── models.py ├── resources.py └── create_document.py /.python-version: -------------------------------------------------------------------------------- 1 | 3.5.1 2 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.5.2 2 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: python server.py 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | app/node_modules/ 2 | app/bower_components/ 3 | .DS_Store 4 | venv/ 5 | *.pyc 6 | cf-ssh.yml 7 | -------------------------------------------------------------------------------- /AgileEstimator.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/agile-solicitation-builder/master/AgileEstimator.xlsx -------------------------------------------------------------------------------- /app/assets/img/minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/agile-solicitation-builder/master/app/assets/img/minus.png -------------------------------------------------------------------------------- /app/assets/img/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/agile-solicitation-builder/master/app/assets/img/plus.png -------------------------------------------------------------------------------- /app/assets/img/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/agile-solicitation-builder/master/app/assets/img/search.png -------------------------------------------------------------------------------- /app/assets/img/correct8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/agile-solicitation-builder/master/app/assets/img/correct8.png -------------------------------------------------------------------------------- /app/assets/img/correct9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/agile-solicitation-builder/master/app/assets/img/correct9.png -------------------------------------------------------------------------------- /app/assets/img/logo-img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/agile-solicitation-builder/master/app/assets/img/logo-img.png -------------------------------------------------------------------------------- /app/assets/img/alerts/info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/agile-solicitation-builder/master/app/assets/img/alerts/info.png -------------------------------------------------------------------------------- /app/assets/img/arrow-down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/agile-solicitation-builder/master/app/assets/img/arrow-down.png -------------------------------------------------------------------------------- /app/assets/img/arrow-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/agile-solicitation-builder/master/app/assets/img/arrow-right.png -------------------------------------------------------------------------------- /app/assets/img/alerts/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/agile-solicitation-builder/master/app/assets/img/alerts/error.png -------------------------------------------------------------------------------- /app/assets/img/us_flag_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/agile-solicitation-builder/master/app/assets/img/us_flag_small.png -------------------------------------------------------------------------------- /app/assets/img/alerts/success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/agile-solicitation-builder/master/app/assets/img/alerts/success.png -------------------------------------------------------------------------------- /app/assets/img/alerts/warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/agile-solicitation-builder/master/app/assets/img/alerts/warning.png -------------------------------------------------------------------------------- /app/assets/img/favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/agile-solicitation-builder/master/app/assets/img/favicons/favicon.ico -------------------------------------------------------------------------------- /app/assets/img/favicons/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/agile-solicitation-builder/master/app/assets/img/favicons/favicon.png -------------------------------------------------------------------------------- /.cfignore: -------------------------------------------------------------------------------- 1 | app/node_modules/ 2 | app/bower_components/ 3 | .DS_Store 4 | venv/ 5 | *.pyc 6 | cf-ssh.yml 7 | app/package.json 8 | app/src 9 | -------------------------------------------------------------------------------- /app/assets/img/favicons/favicon-114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/agile-solicitation-builder/master/app/assets/img/favicons/favicon-114.png -------------------------------------------------------------------------------- /app/assets/img/favicons/favicon-144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/agile-solicitation-builder/master/app/assets/img/favicons/favicon-144.png -------------------------------------------------------------------------------- /app/assets/img/favicons/favicon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/agile-solicitation-builder/master/app/assets/img/favicons/favicon-16.png -------------------------------------------------------------------------------- /app/assets/img/favicons/favicon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/agile-solicitation-builder/master/app/assets/img/favicons/favicon-192.png -------------------------------------------------------------------------------- /app/assets/img/favicons/favicon-57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/agile-solicitation-builder/master/app/assets/img/favicons/favicon-57.png -------------------------------------------------------------------------------- /app/assets/img/favicons/favicon-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/agile-solicitation-builder/master/app/assets/img/favicons/favicon-72.png -------------------------------------------------------------------------------- /app/assets/img/social-icons/png/rss25.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/agile-solicitation-builder/master/app/assets/img/social-icons/png/rss25.png -------------------------------------------------------------------------------- /app/assets/img/social-icons/png/twitter16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/agile-solicitation-builder/master/app/assets/img/social-icons/png/twitter16.png -------------------------------------------------------------------------------- /app/assets/img/social-icons/png/youtube15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/agile-solicitation-builder/master/app/assets/img/social-icons/png/youtube15.png -------------------------------------------------------------------------------- /app/assets/fonts/merriweather-bold-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/agile-solicitation-builder/master/app/assets/fonts/merriweather-bold-webfont.eot -------------------------------------------------------------------------------- /app/assets/fonts/merriweather-bold-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/agile-solicitation-builder/master/app/assets/fonts/merriweather-bold-webfont.ttf -------------------------------------------------------------------------------- /app/assets/fonts/merriweather-bold-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/agile-solicitation-builder/master/app/assets/fonts/merriweather-bold-webfont.woff -------------------------------------------------------------------------------- /app/assets/fonts/merriweather-light-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/agile-solicitation-builder/master/app/assets/fonts/merriweather-light-webfont.eot -------------------------------------------------------------------------------- /app/assets/fonts/merriweather-light-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/agile-solicitation-builder/master/app/assets/fonts/merriweather-light-webfont.ttf -------------------------------------------------------------------------------- /app/assets/fonts/sourcesanspro-bold-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/agile-solicitation-builder/master/app/assets/fonts/sourcesanspro-bold-webfont.eot -------------------------------------------------------------------------------- /app/assets/fonts/sourcesanspro-bold-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/agile-solicitation-builder/master/app/assets/fonts/sourcesanspro-bold-webfont.ttf -------------------------------------------------------------------------------- /app/assets/img/social-icons/png/facebook25.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/agile-solicitation-builder/master/app/assets/img/social-icons/png/facebook25.png -------------------------------------------------------------------------------- /app/assets/fonts/merriweather-bold-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/agile-solicitation-builder/master/app/assets/fonts/merriweather-bold-webfont.woff2 -------------------------------------------------------------------------------- /app/assets/fonts/merriweather-italic-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/agile-solicitation-builder/master/app/assets/fonts/merriweather-italic-webfont.eot -------------------------------------------------------------------------------- /app/assets/fonts/merriweather-italic-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/agile-solicitation-builder/master/app/assets/fonts/merriweather-italic-webfont.ttf -------------------------------------------------------------------------------- /app/assets/fonts/merriweather-italic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/agile-solicitation-builder/master/app/assets/fonts/merriweather-italic-webfont.woff -------------------------------------------------------------------------------- /app/assets/fonts/merriweather-italic-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/agile-solicitation-builder/master/app/assets/fonts/merriweather-italic-webfont.woff2 -------------------------------------------------------------------------------- /app/assets/fonts/merriweather-light-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/agile-solicitation-builder/master/app/assets/fonts/merriweather-light-webfont.woff -------------------------------------------------------------------------------- /app/assets/fonts/merriweather-light-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/agile-solicitation-builder/master/app/assets/fonts/merriweather-light-webfont.woff2 -------------------------------------------------------------------------------- /app/assets/fonts/merriweather-regular-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/agile-solicitation-builder/master/app/assets/fonts/merriweather-regular-webfont.eot -------------------------------------------------------------------------------- /app/assets/fonts/merriweather-regular-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/agile-solicitation-builder/master/app/assets/fonts/merriweather-regular-webfont.ttf -------------------------------------------------------------------------------- /app/assets/fonts/merriweather-regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/agile-solicitation-builder/master/app/assets/fonts/merriweather-regular-webfont.woff -------------------------------------------------------------------------------- /app/assets/fonts/sourcesanspro-bold-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/agile-solicitation-builder/master/app/assets/fonts/sourcesanspro-bold-webfont.woff -------------------------------------------------------------------------------- /app/assets/fonts/sourcesanspro-bold-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/agile-solicitation-builder/master/app/assets/fonts/sourcesanspro-bold-webfont.woff2 -------------------------------------------------------------------------------- /app/assets/fonts/sourcesanspro-italic-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/agile-solicitation-builder/master/app/assets/fonts/sourcesanspro-italic-webfont.eot -------------------------------------------------------------------------------- /app/assets/fonts/sourcesanspro-italic-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/agile-solicitation-builder/master/app/assets/fonts/sourcesanspro-italic-webfont.ttf -------------------------------------------------------------------------------- /app/assets/fonts/sourcesanspro-italic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/agile-solicitation-builder/master/app/assets/fonts/sourcesanspro-italic-webfont.woff -------------------------------------------------------------------------------- /app/assets/fonts/sourcesanspro-light-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/agile-solicitation-builder/master/app/assets/fonts/sourcesanspro-light-webfont.eot -------------------------------------------------------------------------------- /app/assets/fonts/sourcesanspro-light-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/agile-solicitation-builder/master/app/assets/fonts/sourcesanspro-light-webfont.ttf -------------------------------------------------------------------------------- /app/assets/fonts/sourcesanspro-light-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/agile-solicitation-builder/master/app/assets/fonts/sourcesanspro-light-webfont.woff -------------------------------------------------------------------------------- /app/assets/fonts/sourcesanspro-light-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/agile-solicitation-builder/master/app/assets/fonts/sourcesanspro-light-webfont.woff2 -------------------------------------------------------------------------------- /app/assets/fonts/sourcesanspro-regular-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/agile-solicitation-builder/master/app/assets/fonts/sourcesanspro-regular-webfont.eot -------------------------------------------------------------------------------- /app/assets/fonts/sourcesanspro-regular-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/agile-solicitation-builder/master/app/assets/fonts/sourcesanspro-regular-webfont.ttf -------------------------------------------------------------------------------- /app/assets/fonts/merriweather-regular-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/agile-solicitation-builder/master/app/assets/fonts/merriweather-regular-webfont.woff2 -------------------------------------------------------------------------------- /app/assets/fonts/sourcesanspro-italic-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/agile-solicitation-builder/master/app/assets/fonts/sourcesanspro-italic-webfont.woff2 -------------------------------------------------------------------------------- /app/assets/fonts/sourcesanspro-regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/agile-solicitation-builder/master/app/assets/fonts/sourcesanspro-regular-webfont.woff -------------------------------------------------------------------------------- /app/assets/fonts/sourcesanspro-regular-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/agile-solicitation-builder/master/app/assets/fonts/sourcesanspro-regular-webfont.woff2 -------------------------------------------------------------------------------- /manifest.yml: -------------------------------------------------------------------------------- 1 | --- 2 | applications: 3 | - name: agile-solicitation-builder 4 | memory: 512M 5 | instances: 1 6 | host: agile-solicitation-builder 7 | domain: app.cloud.gov 8 | services: 9 | - asb-prod-psql 10 | -------------------------------------------------------------------------------- /manifest-staging.yml: -------------------------------------------------------------------------------- 1 | --- 2 | applications: 3 | - name: agile-solicitation-builder 4 | memory: 512M 5 | instances: 1 6 | host: agile-solicitation-builder-staging 7 | domain: app.cloud.gov 8 | services: 9 | - asb-staging-psql 10 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | flask==0.10.1 2 | flask-restful==0.3.5 3 | python-docx==0.8.5 4 | flask-sqlalchemy==2.1 5 | flask-cli==0.3.0 6 | psycopg2==2.6.1 7 | waitress==0.9.0 8 | Flask-Testing==0.4.2 9 | Flask-HTTPAuth==3.1.2 10 | passlib==1.6.1 11 | -------------------------------------------------------------------------------- /app/assets/img/arrow-down.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/src/state_mixin.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | handleChange: function(key, event) { 3 | var newState = {}; 4 | newState[key] = event.target.value; 5 | this.setState(newState); 6 | }, 7 | toggleEdit: function(key) { 8 | if(this.state.edit === key) { 9 | this.setState({ 10 | edit: null, 11 | }); 12 | } else { 13 | this.setState({ 14 | edit: key, 15 | }); 16 | } 17 | }, 18 | }; -------------------------------------------------------------------------------- /app/assets/img/social-icons/svg/facebook25.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/img/minus.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/img/correct8.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/img/correct9.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/img/arrow-right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import flask.ext.testing 3 | from flask.ext.testing import TestCase 4 | 5 | from server import app, db 6 | 7 | class MyTest(TestCase): 8 | 9 | SQLALCHEMY_DATABASE_URI = "sqlite://" 10 | TESTING = True 11 | 12 | def create_app(self): 13 | 14 | # pass in test configuration 15 | return create_app(self) 16 | 17 | def setUp(self): 18 | 19 | db.create_all() 20 | 21 | def tearDown(self): 22 | 23 | db.session.remove() 24 | db.drop_all() 25 | 26 | if __name__ == '__main__': 27 | unittest.main() 28 | -------------------------------------------------------------------------------- /app/assets/img/plus.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | class Config(object): 4 | DEBUG = False 5 | TESTING = False 6 | CSRF_ENABLED = True 7 | SECRET_KEY = os.environ.get('SECRET_KEY', "None") 8 | SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') 9 | SQLALCHEMY_TRACK_MODIFICATIONS = False 10 | 11 | 12 | class ProductionConfig(Config): 13 | DEBUG = False 14 | SQLALCHEMY_TRACK_MODIFICATIONS = False 15 | 16 | 17 | class StagingConfig(Config): 18 | DEVELOPMENT = True 19 | DEBUG = True 20 | SQLALCHEMY_TRACK_MODIFICATIONS = False 21 | 22 | 23 | class DevelopmentConfig(Config): 24 | DEVELOPMENT = True 25 | DEBUG = True 26 | SQLALCHEMY_TRACK_MODIFICATIONS = False 27 | 28 | 29 | class TestingConfig(Config): 30 | TESTING = True 31 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | 3 | API="https://api.fr.cloud.gov" 4 | ORG="gsa-acq-agile-solicitation-builder" 5 | SPACE=$1 6 | 7 | if [ $# -ne 1 ]; then 8 | echo "Usage: deploy " 9 | exit 10 | fi 11 | 12 | if [ $SPACE = 'prod' ]; then 13 | NAME="agile-solicitation-builder" 14 | MANIFEST="manifest.yml" 15 | CF_USERNAME=$CF_USERNAME_PRODUCTION 16 | CF_PASSWORD=$CF_PASSWORD_PRODUCTION 17 | elif [ $SPACE = 'staging' ]; then 18 | NAME="agile-solicitation-builder" 19 | MANIFEST="manifest-staging.yml" 20 | CF_USERNAME=$CF_USERNAME_STAGING 21 | CF_PASSWORD=$CF_PASSWORD_STAGING 22 | else 23 | echo "Unknown space: $SPACE" 24 | exit 25 | fi 26 | 27 | cf login -a $API -u $CF_USERNAME -p $CF_PASSWORD -o $ORG -s $SPACE 28 | cf zero-downtime-push $NAME -f $MANIFEST 29 | -------------------------------------------------------------------------------- /app/assets/img/search.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/img/social-icons/svg/twitter16.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Agile Solicitation Builder 5 | 6 | 7 | 8 | 18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/assets/img/alerts/success.svg: -------------------------------------------------------------------------------- 1 | 3 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/question_list.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | {code: "1", key: "1", title: "Definitions", component: require('./questions/01_definitions')}, 3 | {code: "2", key: "2", title: "Overview", component: require('./questions/02_services')}, 4 | {code: "3", key: "3", title: "Objectives", component: require('./questions/03_objectives')}, 5 | {code: "4", key: "4", title: "Key Personnel", component: require('./questions/04_personnel')}, 6 | {code: "5", key: "5", title: "Invoicing & Funding", component: require('./questions/05_post_award')}, 7 | {code: "6", key: "6", title: "Inspection & Acceptance", component: require('./questions/06_inspection')}, 8 | {code: "7", key: "7", title: "Government Roles", component: require('./questions/07_government_roles')}, 9 | {code: "8", key: "8", title: "Special Requirements", component: require('./questions/08_special_requirements')}, 10 | {code: "9", key: "9", title: "Contract Clauses", component: require('./questions/09_contract_clauses')}, 11 | {code: "10", key: "10", title: "Instructions to Offerors", component: require('./questions/10_instructions_to_offerors')}, 12 | {code: "11", key: "11", title: "Evaluation Criteria", component: require('./questions/11_evaluation_criteria')} 13 | ]; 14 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Welcome! 2 | 3 | We're so glad you're thinking about contributing to a USDS and 18F open source project! If you're unsure about anything, just ask -- or submit the issue or pull request anyway. The worst that can happen is you'll be politely asked to change something. We love all friendly contributions. 4 | 5 | We want to ensure a welcoming environment for all of our projects. Our staff follow the [18F Code of Conduct](https://github.com/18F/code-of-conduct/blob/master/code-of-conduct.md) and all contributors should do the same. 6 | 7 | We encourage you to read this project's CONTRIBUTING policy (you are here), its [LICENSE](LICENSE.md), and its [README](README.md). 8 | 9 | If you have any questions or want to read more, check out the [18F Open Source Policy GitHub repository]( https://github.com/18f/open-source-policy), or just [shoot us an email](mailto:18f@gsa.gov). 10 | 11 | ## Public domain 12 | 13 | This project is in the public domain within the United States, and 14 | copyright and related rights in the work worldwide are waived through 15 | the [CC0 1.0 Universal public domain dedication](https://creativecommons.org/publicdomain/zero/1.0/). 16 | 17 | All contributions to this project will be released under the CC0 18 | dedication. By submitting a pull request, you are agreeing to comply 19 | with this waiver of copyright interest. 20 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "agile-solicitation-builder", 3 | "version": "0.0.1", 4 | "description": "Agile Solicitation Builder", 5 | "repository": { 6 | "type": "git", 7 | "url": "git://github.com/18F/agile-solicitation-builder.git" 8 | }, 9 | "main": "index.js", 10 | "scripts": { 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "author": "", 14 | "license": "ISC", 15 | "dependencies": { 16 | "jquery": "^2.2.1", 17 | "babel-preset-react": "^6.1.2", 18 | "babelify": "^7.2.0", 19 | "gulp-notify": "^2.2.0", 20 | "history": "^1.13.0", 21 | "marked": "^0.3.5", 22 | "react": "^0.14.2", 23 | "react-bootstrap": "^0.27.3", 24 | "react-dom": "^0.14.2", 25 | "react-flexbox": "^3.0.0", 26 | "react-popover": "^0.4.4", 27 | "react-router": "^1.0.0-rc4", 28 | "uswds": "^0.9.4" 29 | }, 30 | "devDependencies": { 31 | "@18f/stylelint-rules": "^1.0.3", 32 | "browserify": "^12.0.1", 33 | "gulp": "^3.9.0", 34 | "gulp-eslint": "^2.0.0", 35 | "gulp-rename": "^1.2.2", 36 | "gulp-sass": "^2.3.2", 37 | "gulp-stylelint": "^2.0.2", 38 | "gulp-uglify": "^1.5.3", 39 | "gulp-util": "^3.0.7", 40 | "reactify": "^1.1.1", 41 | "vinyl-buffer": "^1.0.0", 42 | "vinyl-source-stream": "^1.1.0", 43 | "watchify": "^3.6.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/auth/login-button.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var AuthMixin = require('./mixin'); 3 | 4 | var LoginButton = React.createClass({ 5 | mixins: [AuthMixin], 6 | 7 | getInitialState: function() { 8 | return { 9 | text: 'Log In' 10 | } 11 | }, 12 | 13 | componentDidMount: function() { 14 | this.loginStateChanged(); 15 | }, 16 | 17 | loginStateChanged: function() { 18 | this.setState({ text: this.state.loggedIn ? 'Log Out' : 'Log In' }); 19 | }, 20 | 21 | click: function() { 22 | if(this.state.loggedIn) { 23 | this.doAuthLogout(function() { 24 | this.setAuthenticationState(false); 25 | }.bind(this)); 26 | } else { 27 | this.doAuthLogin(function(token) { 28 | if(token) { 29 | this.setAuthenticationState(true); 30 | } else { 31 | this.setAuthenticationState(false); 32 | } 33 | }.bind(this)); 34 | } 35 | }, 36 | 37 | hideButton: function() { 38 | return ( 39 | (this.props.hideIfLoggedIn && this.state.loggedIn) || 40 | (this.props.hideIfLoggedOut && !this.state.loggedIn) 41 | ); 42 | }, 43 | 44 | render: function() { 45 | return( 46 | 47 | {this.hideButton() ? null : } 48 | 49 | ); 50 | } 51 | }); 52 | 53 | module.exports = LoginButton; 54 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | As a work of the United States Government, this project is in the 2 | public domain within the United States. 3 | 4 | Additionally, we waive copyright and related rights in the work 5 | worldwide through the CC0 1.0 Universal public domain dedication. 6 | 7 | ## CC0 1.0 Universal Summary 8 | 9 | This is a human-readable summary of the [Legal Code (read the full text)](https://creativecommons.org/publicdomain/zero/1.0/legalcode). 10 | 11 | ### No Copyright 12 | 13 | The person who associated a work with this deed has dedicated the work to 14 | the public domain by waiving all of his or her rights to the work worldwide 15 | under copyright law, including all related and neighboring rights, to the 16 | extent allowed by law. 17 | 18 | You can copy, modify, distribute and perform the work, even for commercial 19 | purposes, all without asking permission. 20 | 21 | ### Other Information 22 | 23 | In no way are the patent or trademark rights of any person affected by CC0, 24 | nor are the rights that other persons may have in the work or in how the 25 | work is used, such as publicity or privacy rights. 26 | 27 | Unless expressly stated otherwise, the person who associated a work with 28 | this deed makes no warranties about the work, and disclaims liability for 29 | all uses of the work, to the fullest extent permitted by applicable law. 30 | When using or citing the work, you should not imply endorsement by the 31 | author or the affirmer. 32 | -------------------------------------------------------------------------------- /app/assets/img/social-icons/svg/rss25.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/img/alerts/warning.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/results.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | var Results = React.createClass({ 4 | getInitialState: function() { 5 | return {}; 6 | }, 7 | componentDidMount: function() { 8 | this.setState({rfqDelete: false}); 9 | $(".confirm-delete").hide(); 10 | }, 11 | handleDelete: function(value){ 12 | if (value === "yes"){ 13 | $(".confirm-delete").show(); 14 | } 15 | else { 16 | $(".confirm-delete").hide(); 17 | } 18 | }, 19 | deleteRFQ: function(){ 20 | var rfqId = getId(window.location.hash); 21 | deleteRFQ(rfqId, function(message){ 22 | console.log(message); 23 | alert(message["message"]); 24 | window.location.replace("/"); 25 | }); 26 | }, 27 | render: function() { 28 | var rfqId = window.location.hash.split("#/rfp/")[1].split("/results")[0]; 29 | var url = "/download/" + rfqId; 30 | return ( 31 |
32 |
Resulting RFQ
33 | 34 | 35 |
36 |
37 |
38 |

Are you sure you want to delete this RFQ?

39 |
40 |
41 | 42 | 43 |
44 |
45 | ); 46 | }, 47 | }); 48 | 49 | module.exports = Results; -------------------------------------------------------------------------------- /app/src/questions/09_contract_clauses.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var StateMixin = require("../state_mixin"); 3 | 4 | STATES = [ 5 | "contractClauses", 6 | ]; 7 | 8 | var ContractClauses = React.createClass({ 9 | mixins: [StateMixin], 10 | getInitialState: function() { 11 | var initialStates = getStates(STATES); 12 | return initialStates; 13 | }, 14 | save: function(cb) { 15 | var data = {}; 16 | 17 | for (i=0; i < STATES.length; i++){ 18 | var stateName = STATES[i]; 19 | data[stateName] = this.state[stateName]; 20 | } 21 | 22 | var rfqId = getId(window.location.hash); 23 | put_data(9, "get_content", rfqId, data, cb); 24 | 25 | }, 26 | componentDidMount: function() { 27 | var rfqId = getId(window.location.hash); 28 | get_data(9, rfqId, function(content){ 29 | var data = content["data"]; 30 | console.log(data); 31 | this.setState({ 32 | contractClauses: data[STATES[0]], 33 | }); 34 | }.bind(this)); 35 | }, 36 | render: function() { 37 | return ( 38 |
39 |
Contract Clauses
40 |
The PM and the CO may both contribute content to this section.
41 | 42 |
Additional Clauses
43 | 44 |
Please feel free to add anything else specific to your contract. You will also be able to edit the Microsoft Word document that is produced.
45 | 46 | 47 |
48 | ); 49 | }, 50 | }); 51 | 52 | 53 | module.exports = ContractClauses; 54 | -------------------------------------------------------------------------------- /app/assets/img/alerts/error.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/questions/11_evaluation_criteria.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var StateMixin = require("../state_mixin"); 3 | var EditBox = require("../edit_box"); 4 | 5 | var STATES = [ 6 | "evaluationCriteria" 7 | ]; 8 | 9 | var page_number = 11; 10 | 11 | var EvaluationCriteria= React.createClass({ 12 | // include mixins 13 | mixins: [StateMixin], 14 | 15 | getInitialState: function() { 16 | var initialStates = getStates(STATES); 17 | return initialStates; 18 | }, 19 | componentDidMount: function() { 20 | var rfqId = getId(window.location.hash); 21 | 22 | get_data(page_number, rfqId, function(content){ 23 | var componentStates = getComponents(content["data"]); 24 | this.setState( componentStates ); 25 | }.bind(this)); 26 | }, 27 | save: function(cb) { 28 | var data = {}; 29 | 30 | 31 | var rfqId = getId(window.location.hash); 32 | 33 | // get the most recent state data for each STATE that will be saved 34 | for (i=0; i < STATES.length; i++){ 35 | var stateName = STATES[i]; 36 | data[stateName] = this.state[stateName]; 37 | } 38 | // you can save content_components using the get_content API (the custom_component API is also an option) 39 | put_data(page_number, 'get_content', rfqId, data, cb); 40 | 41 | }, 42 | render: function() { 43 | return ( 44 |
45 |
Evaluation Criteria
46 |
The content in this section can be decided upon by either the PM or the CO.
47 | 48 | 49 |
50 | ); 51 | }, 52 | }); 53 | 54 | 55 | module.exports = EvaluationCriteria; 56 | -------------------------------------------------------------------------------- /app/src/questions/01_definitions.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var StateMixin = require("../state_mixin"); 3 | var EditBox = require("../edit_box"); 4 | 5 | 6 | var Definition = React.createClass({ 7 | mixins: [StateMixin], 8 | save: function(cb) { 9 | var rfqId = getId(window.location.hash); 10 | data = {"definitions": this.state.definitions}; 11 | put_data(1, "get_content", rfqId, data, cb); 12 | }, 13 | // React functions 14 | getInitialState: function(){ 15 | return { 16 | definitions: "", 17 | }; 18 | }, 19 | componentDidMount: function() { 20 | var rfqId = getId(window.location.hash); 21 | get_data(1, rfqId, function(data){ 22 | this.setState({ 23 | definitions: data["data"]["definitions"], 24 | }); 25 | }.bind(this)); 26 | }, 27 | 28 | render: function() { 29 | return ( 30 |
31 |
Definitions
32 |
The content in this section can be decided upon by either the PM or the CO.
33 |

These are the standard definitions for agile development terms in alignment with the USDS Playbook. You can also modify the definitions and add additional terms. When you are done click the "Next" button at the bottom of the page.

34 | 39 | 40 |
41 | ); 42 | }, 43 | }); 44 | 45 | module.exports = Definition; -------------------------------------------------------------------------------- /app/assets/css/main.css: -------------------------------------------------------------------------------- 1 | .Popover-body { 2 | display: inline-flex; 3 | flex-direction: column; 4 | padding: 2rem 4rem; 5 | background: #454545; 6 | color: white; 7 | border-radius: 0.3rem; } 8 | 9 | .Popover-tipShape { 10 | fill: #454545; } 11 | 12 | .menu-btn { 13 | padding: 1.5rem null; 14 | float: left; 15 | margin-top: -4px; 16 | color: #ffffff; 17 | background-color: #0071bc; 18 | font-size: 1.5rem; 19 | width: 15%; 20 | text-align: center; } 21 | @media all and (850px) { 22 | .menu-btn { 23 | display: none; } } 24 | .menu-btn:hover { 25 | text-decoration: none; 26 | color: #ffffff; 27 | background-color: #205493; } 28 | .menu-btn:visited { 29 | color: #ffffff; } 30 | 31 | .overlay { 32 | background: #000000; 33 | opacity: 0; 34 | visibility: hidden; 35 | z-index: 9999; } 36 | .overlay.is-visible { 37 | visibility: visible; } 38 | 39 | .sidenav { 40 | width: 250px; 41 | border-right: 1px solid #aeb0b5; 42 | padding: 5rem 3rem 3rem 3rem; 43 | overflow: auto; 44 | display: none; 45 | z-index: -1; } 46 | @media (max-width: 850px) { 47 | .sidenav.menu-content { 48 | background: #ffffff; 49 | -webkit-overflow-scrolling: touch; 50 | overflow-y: auto; 51 | z-index: 999999; 52 | display: block; } } 53 | .lt-ie9 .sidenav { 54 | width: 25%; } 55 | .sidenav .usa-sidenav-sub_list { 56 | display: none; } 57 | @media screen and (min-width: 850px) { 58 | .sidenav { 59 | display: block; } } 60 | 61 | .visual-style .sidenav .visual-style-sublist { 62 | display: block; } 63 | .visual-style .sidenav .visual-style-sublist ul { 64 | display: block; } 65 | 66 | .form-controls .sidenav .form-controls-sublist { 67 | display: block; } 68 | 69 | .form-templates .sidenav .form-templates-sublist { 70 | display: block; } 71 | 72 | .footers .sidenav .footers-sublist { 73 | display: block; } 74 | -------------------------------------------------------------------------------- /app/assets/img/alerts/info.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 18 | 19 | -------------------------------------------------------------------------------- /app/assets/css/normalize.min.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v3.0.2 | MIT License | git.io/normalize */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-collapse:collapse;border-spacing:0}td,th{padding:0} -------------------------------------------------------------------------------- /app/src/edit_box.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | var marked = require('marked'); 4 | marked.setOptions({ 5 | renderer: new marked.Renderer(), 6 | 7 | // Enabled 8 | sanitize: true, // Sanitize output 9 | smartLists: true, // Smarter list behavior 10 | 11 | // Disabled 12 | gfm: false, // Github-flavored markdown 13 | tables: false, // Github-flavored markdown tables 14 | breaks: false, // Github-flavored markdown linebreaks (?) 15 | pedantic: false, // Don't fix original markdown bugs 16 | smartypants: false // Smart typographic punctuation 17 | }); 18 | 19 | var EditBox = React.createClass({ 20 | propTypes: { 21 | text: React.PropTypes.string.isRequired, 22 | editing: React.PropTypes.bool.isRequired, 23 | onStatusChange: React.PropTypes.func.isRequired, 24 | onTextChange: React.PropTypes.func.isRequired, 25 | }, 26 | getInitialState: function() { 27 | return {}; 28 | }, 29 | toggleEdit: function(editing) { 30 | this.props.onStatusChange(editing); 31 | }, 32 | handleChange: function(event){ 33 | this.props.onTextChange(event); 34 | }, 35 | calculateRows: function(text){ 36 | var chars = text.length; 37 | var rows = Math.ceil(chars / 113); 38 | var newLines = (text.match(/\n\n/g) || []).length; 39 | rows += newLines; 40 | return Math.max(rows, 4); 41 | }, 42 | render: function() { 43 | var renderedMarkdown = {__html: marked(this.props.text)}; 44 | var displayStyle = { 45 | border: 'thin dashed black', 46 | padding: '1rem' 47 | }; 48 | 49 | if(this.props.editing) { 50 | return ( 51 |
52 |
Done
53 | 54 |
55 | ); 56 | } else { 57 | return ( 58 |
59 |
Edit
60 |
61 |
62 | ); 63 | } 64 | }, 65 | }); 66 | 67 | module.exports = EditBox; -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | sudo: false 3 | python: 4 | - 3.5.1 5 | node: 6 | env: 7 | global: 8 | - CF_USERNAME_PRODUCTION=cb4feb94-4695-45bb-9d54-73adfa4bed76 9 | - secure: "g2eDqkEhlaG9jCfH3qS0+LyTDL6FZ4O6ju0uFM1OJ8fmz3FjfzdZ4F7BNCMrC8ZeH/tkR8zisMJnUBOTlA0n78/jcKJNrQYTYrDYEWCzlS80q07rZrqBmfxruFwxwVQwqCIx9wVpoqSKTewO+mLgAXW2YkWSMKQbPYm2oDaAeqlIuRi6YCWQ7PYhyc7P4PND4Drhz+fkPRsOOe67MLrHAgMpXk/aiU2uWw1UctSCTtPYYj8V6E7nl4DRHVT8RReMF/bx6z7aKRBVGtQoQWuXnpLGc2+2WAM4rAthGdEZHAM92NNhQDP1BhshlaMUcN/3TYyj3DzxQ21CxuCiHYhHzuoufKCpbPc/XBOZ1uw07NRa/LM63Boue0TmEO5qUTsSCTSEMhasf7XxxK30ojVXyYTSNcLuuQ9vpBn10mLnlcceuHE1NWxqSbr1T2fdHox94T3S7oGekNNBIT7wXvCC2WHpHaxbXXzoc+SL/x+RqntwNaoRN6Hmx6udNyavLSqGbIj9FP/GOHzFTvNiNO+Rdr+DPkwozWUT1Pw0t7604I1jSuPKDz7enixdECSa1RIRCLqGnoyH7E3uapr/I8Fs8tKoYS76j/F0NM0Z9auf9SvqOXCATMWxg9F1EsTNiUEEd78bIr+23zajXJRek2JNX1byPGzeeJIcIVFqcl763MU=" 10 | - CF_USERNAME_STAGING=711e2876-d872-4c7c-8046-a29d9dfa3e93 11 | - secure: "Kvx7PtBvytpixhGoj8iTIzjpNgtNh7JvMEeQ48+RR7wVHzc5Nz3i5t6qNLLQcCLdrNN3z3tu6Y+PpxOtHIso9nwnpO1iqr+S3CUDxOZmiH3Xgm/8NwF6l+VTItNIY5ANigX8Kj/pb3X1H0XSaZ0sNffl86CltPkBl9ViiSGWo0d2xERHBpsggp+3Zx9Ui5aQWoUP/VWDxbVWH1IftRJ22QgyP28SiZxA5tpFij8T0vQd2DzrcVOnwskbU4rMKaOz1KqsIMQ/KwW+Y7lJeDX6N2TevsynAGGYPcz7Z+fOPnvFFjmxtxI9c7g2cUuH6ujnXuSiucxeCLnYY3EKtGpaTxWjaowmLrciuB2mVLFBL738+oYcURTKY55F0ojWtkfHVoQUDGgjzIwhwjG19CB6a/1tTHkVBnz2yC4qdUSBqQBra+qII/8KsYVep0zvA1zS6keNsRIXpqrz0uz4uWMxvAFRW+iuDzRuFEjbk7CfezohvzSea2vgnc/k5rHYmxQopwW+DmUK4TBuorew+iIGULU+Hsj2Qop/xxFV8JVd5sr0EblLEHCWNraUYmXsXlxCKsR6rFma2zwZTOrtQm0jl3ZCxO1ebOdH6QbKH+3bbuDneb4RzdL8vx8Yg89+a12Rva4TghpWtWzrVLFfEIqVDy25ntsxnIqjebVd0XW3aMA=" 12 | before_install: 13 | - rm -rf ~/.nvm && git clone https://github.com/creationix/nvm.git ~/.nvm && (cd ~/.nvm && git checkout `git describe --abbrev=0 --tags`) && source ~/.nvm/nvm.sh && nvm install 6.1.0 14 | install: 15 | - pip install -r requirements.txt 16 | - cd app 17 | - npm install 18 | - cd .. 19 | - psql -c 'create database travis_ci_test;' -U postgres 20 | script: 21 | - python tests.py 22 | before_deploy: 23 | - export PATH=$HOME:$PATH 24 | - travis_retry curl -L -o $HOME/cf.tgz "https://cli.run.pivotal.io/stable?release=linux64-binary&version=6.22.2" 25 | - tar xzvf $HOME/cf.tgz -C $HOME 26 | - cf install-plugin autopilot -f -r CF-Community 27 | cache: pip 28 | deploy: 29 | - provider: script 30 | script: "./deploy.sh staging" 31 | skip_cleanup: true 32 | on: 33 | branch: develop 34 | - provider: script 35 | script: "./deploy.sh prod" 36 | skip_cleanup: true 37 | on: 38 | branch: master 39 | -------------------------------------------------------------------------------- /app/src/questions/05_post_award.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var StateMixin = require("../state_mixin"); 3 | var EditBox = require("../edit_box"); 4 | 5 | var STATES = [ 6 | "invoicing", 7 | "billingAddress", 8 | "duplicateInvoice", 9 | ]; 10 | 11 | var PostAward = React.createClass({ 12 | mixins: [StateMixin], 13 | getInitialState: function() { 14 | var initialStates = getStates(STATES); 15 | return initialStates; 16 | }, 17 | componentDidMount: function() { 18 | var rfqId = getId(window.location.hash); 19 | get_data(5, rfqId, function(content){ 20 | var componentStates = getComponents(content["data"]); 21 | this.setState( componentStates ); 22 | }.bind(this)); 23 | }, 24 | save: function(cb) { 25 | var data = {}; 26 | 27 | for (i=0; i < STATES.length; i++){ 28 | var stateName = STATES[i]; 29 | data[stateName] = this.state[stateName]; 30 | } 31 | 32 | var rfqId = getId(window.location.hash); 33 | put_data(5, "get_content", rfqId, data, cb); 34 | }, 35 | render: function() { 36 | var placeholderText = "Mailing Address Phone Number Fax Number"; 37 | return ( 38 |
39 |
Invoicing & Funding
40 |
The content in this section is typically decided on by the CO.
41 |

If you wish to add additional text you may do so in the resulting word document.

42 | 43 |
Contractor Instructions
44 | 45 | 50 | 51 | 52 |
The Contractor shall submit an original invoice for payment to the following office:
53 | 54 | 55 | 56 | 61 | 62 |
63 | ); 64 | }, 65 | }); 66 | 67 | 68 | module.exports = PostAward; 69 | 70 | -------------------------------------------------------------------------------- /app/assets/img/social-icons/svg/youtube15.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/src/question.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | // Bootstrap 4 | var ButtonToolbar = require('react-bootstrap').ButtonToolbar; 5 | var Button = require('react-bootstrap').Button; 6 | 7 | // Router stuff 8 | var Link = require('react-router').Link; 9 | var History = require('react-router').History; 10 | 11 | // Custom components 12 | var questionList = require('./question_list'); 13 | 14 | var Question = React.createClass({ 15 | mixins: [History], 16 | 17 | linkForOffset: function(offset) { 18 | var currentIndex; 19 | questionList.forEach(function(question, i) { 20 | if(question.code == this.props.params.qid) { 21 | currentIndex = i; 22 | } 23 | }.bind(this)); 24 | 25 | var baseURL = "/rfp/"+this.props.params.id; 26 | var nextIndex = currentIndex + offset; 27 | if(nextIndex < 0) { 28 | return baseURL; 29 | } else if(nextIndex >= questionList.length) { 30 | return baseURL + "/results"; 31 | } else { 32 | return baseURL + "/question/" + questionList[nextIndex].code; 33 | } 34 | }, 35 | handlePrev: function() { 36 | this.save(function() { 37 | var prevLink = this.linkForOffset(-1); 38 | this.history.pushState(null, prevLink, null); 39 | }.bind(this)); 40 | }, 41 | handleNext: function() { 42 | this.save(function() { 43 | var nextLink = this.linkForOffset(1); 44 | this.history.pushState(null, nextLink, null); 45 | }.bind(this)); 46 | }, 47 | save: function(callback) { 48 | this.refs.question.save(callback); 49 | }, 50 | getComponentForQuestionID: function(qid) { 51 | for(var i = 0; i < questionList.length; i++) { 52 | var question = questionList[i]; 53 | if(question.code == qid) { 54 | return question.component; 55 | } 56 | } 57 | return null; 58 | }, 59 | 60 | getInitialState: function() { 61 | return {saving: false}; 62 | }, 63 | render: function() { 64 | var Component = this.getComponentForQuestionID(this.props.params.qid); 65 | 66 | if(Component) { 67 | return ( 68 |
69 | 74 | 75 | 76 | 77 | 78 |
79 | ); 80 | } else { 81 | return ( 82 |
83 |
{"Unknown question: '"+this.props.params.qid+"'"}
84 |
85 | ); 86 | } 87 | }, 88 | }); 89 | 90 | module.exports = Question; -------------------------------------------------------------------------------- /app/src/app.js: -------------------------------------------------------------------------------- 1 | /* eslint react/prop-types: 0 */ 2 | var React = require('react'); 3 | var ReactDOM = require('react-dom'); 4 | 5 | // Dependencies 6 | var View = require('react-flexbox'); 7 | 8 | // Router stuff 9 | var Router = require('react-router').Router; 10 | var Route = require('react-router').Route; 11 | var IndexRoute = require('react-router').IndexRoute; 12 | var Link = require('react-router').Link; 13 | var IndexLink = require('react-router').IndexLink; 14 | var Redirect = require('react-router').Redirect; 15 | 16 | // Custom elements 17 | var Welcome = require('./welcome'); 18 | var Request = require('./request'); 19 | var RequestOverview = require('./request_overview'); 20 | var Question = require('./question'); 21 | var Results = require('./results'); 22 | var LogoutButton = require('./auth/login-button'); 23 | var RepoLink = React.createElement('a', { 24 | href: 'https://github.com/18F/agile-solicitation-builder/issues', 25 | 'target': '_blank', 26 | }, 'Help us improve'); 27 | 28 | var Header = React.createClass({ 29 | render: function() { 30 | var inheritStyle = { 31 | color: "inherit", 32 | textDecoration: "inherit", 33 | }; 34 | 35 | return ( 36 |
37 |
38 | 39 | US flag signifying that this is a United States Federal Government website 40 | An official website of the United States Government 41 | 42 | This site is currently in alpha. {RepoLink}. 43 |
44 | 45 |
46 |
47 |

48 | Agile Solicitation Builder 49 |

50 |
51 |
52 |
53 | ); 54 | }, 55 | }); 56 | 57 | var App = React.createClass({ 58 | render: function() { 59 | var appStyle = { 60 | padding: 8, 61 | }; 62 | 63 | return ( 64 |
65 |
66 | {this.props.children} 67 |
68 | ); 69 | }, 70 | }); 71 | 72 | ReactDOM.render( 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | , 83 | document.getElementById('mount') 84 | ); 85 | -------------------------------------------------------------------------------- /app/src/auth/mixin.js: -------------------------------------------------------------------------------- 1 | var _components = [ ]; 2 | var _currentState = false; 3 | var _history = false; 4 | var History = require('react-router').History; 5 | 6 | function updateComponents() { 7 | _components.forEach(function(c) { 8 | c.setState({ loggedIn: _currentState }); 9 | if(typeof c.loginStateChanged === 'function') { 10 | c.loginStateChanged(); 11 | } 12 | }); 13 | } 14 | 15 | function login(username, password, callback) { 16 | if(typeof username === 'function') { 17 | callback = username; 18 | username = undefined; 19 | password = undefined; 20 | } 21 | if(typeof callback !== 'function') { 22 | callback = function() { }; 23 | } 24 | 25 | var failed = true; 26 | var opts = { 27 | type: 'GET', 28 | url: '/api/token', 29 | dataType: 'json', 30 | success: function(data) { 31 | var token = data.token; 32 | failed = false; 33 | $.ajax({ 34 | type: "GET", 35 | url: "/api/token", 36 | username: token, 37 | password: 'none', 38 | success: function() { 39 | callback(token); 40 | } 41 | }); 42 | }, 43 | complete: function() { 44 | if(failed) { 45 | callback(false); 46 | } 47 | } 48 | }; 49 | 50 | if(username && password) { 51 | opts.username = username; 52 | opts.password = password; 53 | } 54 | 55 | $.ajax(opts); 56 | } 57 | 58 | function logout(callback) { 59 | $.ajax({ 60 | type: 'GET', 61 | url: '/api/token', 62 | username: '--invalid--', 63 | password: '--invalid--', 64 | complete: function() { 65 | if(_history) { 66 | _history.pushState(null, '/'); 67 | } 68 | if(typeof callback === 'function') { 69 | callback(); 70 | } 71 | } 72 | }); 73 | } 74 | 75 | $.ajax({ 76 | type: 'GET', 77 | url: '/api/isLoggedIn', 78 | dataType: 'json', 79 | success: function(data) { 80 | if(typeof data === 'object') { 81 | _currentState = !!data.loggedIn; 82 | updateComponents(); 83 | } 84 | } 85 | }); 86 | 87 | module.exports = { 88 | mixins: [History], 89 | 90 | componentWillMount: function() { 91 | _components.push(this); 92 | if(this.history && !_history) { 93 | _history = this.history; 94 | } 95 | this.setState({ loggedIn: _currentState }); 96 | }, 97 | 98 | componentWillUnmount: function() { 99 | var index = _components.indexOf(this); 100 | if(index >= 0) { 101 | _components.splice(index, 1); 102 | } 103 | }, 104 | 105 | setAuthenticationState: function(loggedIn) { 106 | _currentState = loggedIn; 107 | updateComponents(); 108 | }, 109 | 110 | doAuthLogin: login, 111 | doAuthLogout: logout 112 | } 113 | -------------------------------------------------------------------------------- /app/src/questions/10_instructions_to_offerors.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var StateMixin = require("../state_mixin"); 3 | var EditBox = require("../edit_box"); 4 | 5 | // states data is defined in seeds.py, and must be listed here to be accessed and saved in the database 6 | var STATES = [ 7 | "instructionsToOfferors" 8 | ]; 9 | 10 | // page_number would be replaced by the number of this page, the data associated with this section would be established in seed.py 11 | var page_number = 10; 12 | 13 | var InstructionsToOfferors= React.createClass({ 14 | // include mixins 15 | mixins: [StateMixin], 16 | 17 | // for a state to be accessible with this.state.stateName you must include it here 18 | getInitialState: function() { 19 | // you may also want to establish temporary states for this page that will not be saved 20 | var initialStates = getStates(STATES); 21 | return initialStates; 22 | }, 23 | // componentDidMount is where you pull the latest data from the database to populate the states 24 | componentDidMount: function() { 25 | // this identifies the section number identifying the data 26 | var rfqId = getId(window.location.hash); 27 | 28 | // this calls a helpers.js function which calls server.py to access this RFQs data for this section number (XX) 29 | // the names the states are established with in seed.py should correspond to the state names used on this page 30 | get_data(page_number, rfqId, function(content){ 31 | var componentStates = getComponents(content["data"]); 32 | console.log(componentStates); 33 | this.setState( componentStates ); 34 | }.bind(this)); 35 | }, 36 | customFuction: function() { 37 | // you can also create functions that will only be used by this page 38 | alert('custom function called!'); 39 | }, 40 | save: function(cb) { 41 | var data = {}; 42 | 43 | // this identifies the section number identifying the data 44 | var rfqId = getId(window.location.hash); 45 | 46 | // get the most recent state data for each STATE that will be saved 47 | for (i=0; i < STATES.length; i++){ 48 | var stateName = STATES[i]; 49 | data[stateName] = this.state[stateName]; 50 | } 51 | console.log(data); 52 | // you can save content_components using the get_content API (the custom_component API is also an option) 53 | put_data(page_number, 'get_content', rfqId, data, cb); 54 | 55 | }, 56 | render: function() { 57 | return ( 58 |
59 |
Instructions to Offerors
60 |
The content in this section can be decided upon by either the PM or the CO.
61 | 62 | 63 |
64 | ); 65 | }, 66 | }); 67 | 68 | 69 | module.exports = InstructionsToOfferors; 70 | -------------------------------------------------------------------------------- /app/src/questions/XX_sample.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var StateMixin = require("../state_mixin"); 3 | var EditBox = require("../edit_box"); 4 | 5 | // states data is defined in seeds.py, and must be listed here to be accessed and saved in the database 6 | var STATES = [ 7 | "sampleState1", 8 | "sampleState2", 9 | ]; 10 | 11 | // XX would be replaced by the number of this page, the data associated with this section would be established in seed.py 12 | var page_number = 0; 13 | 14 | var Sample= React.createClass({ 15 | // include mixins 16 | mixins: [StateMixin], 17 | 18 | // for a state to be accessible with this.state.stateName you must include it here 19 | getInitialState: function() { 20 | // you may also want to establish temporary states for this page that will not be saved 21 | var initialStates = getStates(STATES); 22 | return initialStates; 23 | }, 24 | // componentDidMount is where you pull the latest data from the database to populate the states 25 | componentDidMount: function() { 26 | // this identifies the section number identifying the data 27 | var rfqId = getId(window.location.hash); 28 | 29 | // this calls a helpers.js function which calls server.py to access this RFQs data for this section number (XX) 30 | // the names the states are established with in seed.py should correspond to the state names used on this page 31 | get_data(page_number, rfqId, function(content){ 32 | var componentStates = getComponents(content["data"]); 33 | this.setState( componentStates ); 34 | }.bind(this)); 35 | }, 36 | customFuction: function() { 37 | // you can also create functions that will only be used by this page 38 | alert('custom function called!'); 39 | }, 40 | save: function(cb) { 41 | var data = {}; 42 | 43 | // this identifies the section number identifying the data 44 | var rfqId = getId(window.location.hash); 45 | 46 | // get the most recent state data for each STATE that will be saved 47 | for (i=0; i < STATES.length; i++){ 48 | var stateName = STATES[i]; 49 | data[stateName] = this.state[stateName]; 50 | } 51 | // you can save content_components using the get_content API (the custom_component API is also an option) 52 | put_data(page_number, 'get_content', rfqId, data, cb); 53 | 54 | }, 55 | render: function() { 56 | return ( 57 |
58 |
Sample
59 |
The content in this section can be decided upon by either the PM or the CO.
60 | 65 | 66 |
sampleState2 is updated in this textbox
67 | 68 |
69 | ); 70 | }, 71 | }); 72 | 73 | 74 | module.exports = Sample; 75 | -------------------------------------------------------------------------------- /app/src/auth/register-button.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var Popover = require('react-popover'); 3 | var AuthMixin = require('./mixin'); 4 | 5 | var RegisterButton = React.createClass({ 6 | mixins: [AuthMixin], 7 | 8 | getInitialState: function() { 9 | return { 10 | registrationOpen: false, 11 | username: '', 12 | password1: '', 13 | password2: '', 14 | passwordsMatch: true 15 | }; 16 | }, 17 | 18 | openRegistrationPopover: function() { 19 | this.setState({ registrationOpen: true }); 20 | }, 21 | 22 | closeRegistrationPopover: function() { 23 | this.setState({ registrationOpen: false }); 24 | }, 25 | 26 | setUsername: function(e) { 27 | this.setState({ username: e.target.value }); 28 | }, 29 | 30 | setPassword1: function(e) { 31 | this.setState({ password1: e.target.value, passwordsMatch: (e.target.value === this.state.password2) }); 32 | }, 33 | 34 | setPassword2: function(e) { 35 | this.setState({ password2: e.target.value, passwordsMatch: (e.target.value === this.state.password1) }); 36 | }, 37 | 38 | register: function(e) { 39 | if(this.state.username && this.state.password1 && this.state.password1 === this.state.password2) { 40 | $.ajax({ 41 | type: 'POST', 42 | url: '/api/users', 43 | contentType: 'application/json', 44 | data: JSON.stringify({ 45 | username: this.state.username, 46 | password: this.state.password1 47 | }), 48 | success: function(data) { 49 | this.doAuthLogin(this.state.username, this.state.password1, function(success) { 50 | this.setAuthenticationState(true); 51 | this.setState({ 52 | registrationOpen: false, 53 | username: '', 54 | password1: '', 55 | password2: '', 56 | passwordsMatch: true 57 | }); 58 | }.bind(this)); 59 | }.bind(this) 60 | }); 61 | } 62 | e.preventDefault(); 63 | }, 64 | 65 | render: function() { 66 | return( 67 | 68 | 70 |
71 | Create Account 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 |
80 | 81 | {this.state.passwordsMatch ? '' : 'Passwords must match'} 82 | 83 |
84 | 85 | 86 |
87 | 88 | }> 89 | 90 |
91 |
92 | ); 93 | } 94 | }); 95 | 96 | module.exports = RegisterButton; 97 | -------------------------------------------------------------------------------- /app/src/welcome.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | // Bootstrap 4 | var Button = require('react-bootstrap').Button; 5 | 6 | // Router stuff 7 | var IndexLink = require('react-router').IndexLink; 8 | 9 | // Auth stuff 10 | var AuthMixin = require('./auth/mixin'); 11 | var LoginButton = require('./auth/login-button'); 12 | var RegisterButton = require('./auth/register-button'); 13 | 14 | var Welcome = React.createClass({ 15 | mixins: [AuthMixin], 16 | 17 | getInitialState: function() { 18 | // This component could be loaded sometime 19 | // after the initial page load, in which 20 | // case it won't get an event for the initial 21 | // login status check. So, to mitigate that, 22 | // pretend we got the event after a short 23 | // delay. The delay is to allow React time 24 | // to setup the component state. 25 | setTimeout(this.loginStateChanged, 50); 26 | return { 27 | rfqs: "", 28 | }; 29 | }, 30 | 31 | loginStateChanged: function() { 32 | if(this.state.loggedIn) { 33 | getRFQs(function(content){ 34 | this.setState({ 35 | rfqs: content['data'], 36 | }); 37 | }.bind(this)); 38 | } else { 39 | this.setState({ rfqs: "" }); 40 | } 41 | }, 42 | 43 | render: function() { 44 | var rfqs = []; 45 | for (var i=0; i < this.state.rfqs.length; i++) { 46 | var this_rfq = this.state.rfqs[i]; 47 | var agency = this_rfq['agency']; 48 | var doctype = this_rfq['doc_type']; 49 | var url = '#/rfp/' + this_rfq['id'] + '/question/1'; 50 | rfqs.push( 51 |
  • 52 | 53 | #{this_rfq['id']}, {this_rfq['doc_type']} for {this_rfq['program_name']} with {this_rfq['agency']} 54 | 55 |
  • 56 | ); 57 | } 58 | return ( 59 |
    60 |
    61 |

    Welcome to the Agile Solicitation Builder, formerly the Playbook in Action! Before you begin, please consider the following:

    62 |
    63 |
      64 |
    • The intent of this tool is to assist in the creation of requirements documents 65 | for agile software development using best practices from the USDS 66 | Playbook and TechFAR.
    • 67 | 68 |
    • The PM and the CO should use this tool jointly in partnership. Certain pages will only be applicable to only the CO or the PM.
    • 69 | 70 |
    • V1 is for firm fixed price contracts only. The firm fixed price will be per iteration.
    • 71 |
    • This tool is not built to support waterfall development requirements documents.
    • 72 | 73 |
    • All documents should be approved by a warranted contracting officer and in consultation with your legal counsel as required.
    • 74 |
    75 |

    Also please note that this product is only in alpha, therefore any of the following may occur:

    76 |
      77 |
    • Content may unexpectedly change
    • 78 |
    • Documents you have created may be deleted without warning
    • 79 |
    • Certain pages may not always be functioning. We recommend you refresh the page if this happens
    • 80 |
    81 |
    82 | {(rfqs.length > 0)?
    Resume RFQ
    83 |
      84 | {rfqs} 85 |
    : null} 86 | 87 |
    88 | 89 | 90 | {(this.state.loggedIn) ? 91 | 92 | 95 | : null} 96 |
    97 |
    98 | ); 99 | }, 100 | }); 101 | 102 | module.exports = Welcome; 103 | -------------------------------------------------------------------------------- /app/assets/css/google-fonts.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Source Sans Pro'; 3 | font-style: normal; 4 | font-weight: 300; 5 | src: url('../fonts/sourcesanspro-light-webfont.eot'); 6 | src: url('../fonts/sourcesanspro-light-webfont.eot?#iefix') format('embedded-opentype'), 7 | url('../fonts/sourcesanspro-light-webfont.woff2') format('woff2'), 8 | url('../fonts/sourcesanspro-light-webfont.woff') format('woff'), 9 | url('../fonts/sourcesanspro-light-webfont.ttf') format('truetype'); 10 | } 11 | 12 | @font-face { 13 | font-family: 'Source Sans Pro'; 14 | font-style: normal; 15 | font-weight: 400; 16 | src: url('../fonts/sourcesanspro-regular-webfont.eot'); 17 | src: url('../fonts/sourcesanspro-regular-webfont.eot?#iefix') format('embedded-opentype'), 18 | url('../fonts/sourcesanspro-regular-webfont.woff2') format('woff2'), 19 | url('../fonts/sourcesanspro-regular-webfont.woff') format('woff'), 20 | url('../fonts/sourcesanspro-regular-webfont.ttf') format('truetype'); 21 | 22 | } 23 | 24 | @font-face { 25 | font-family: 'Source Sans Pro'; 26 | font-style: italic; 27 | font-weight: 400; 28 | src: url('../fonts/sourcesanspro-italic-webfont.eot'); 29 | src: url('../fonts/sourcesanspro-italic-webfont.eot?#iefix') format('embedded-opentype'), 30 | url('../fonts/sourcesanspro-italic-webfont.woff2') format('woff2'), 31 | url('../fonts/sourcesanspro-italic-webfont.woff') format('woff'), 32 | url('../fonts/sourcesanspro-italic-webfont.ttf') format('truetype'); 33 | 34 | } 35 | 36 | @font-face { 37 | font-family: 'Source Sans Pro'; 38 | font-style: normal; 39 | font-weight: 700; 40 | src: url('../fonts/sourcesanspro-bold-webfont.eot'); 41 | src: url('../fonts/sourcesanspro-bold-webfont.eot?#iefix') format('embedded-opentype'), 42 | url('../fonts/sourcesanspro-bold-webfont.woff2') format('woff2'), 43 | url('../fonts/sourcesanspro-bold-webfont.woff') format('woff'), 44 | url('../fonts/sourcesanspro-bold-webfont.ttf') format('truetype'); 45 | 46 | } 47 | 48 | @font-face { 49 | font-family: 'Merriweather'; 50 | font-style: normal; 51 | font-weight: 300; 52 | src: url('../fonts/merriweather-light-webfont.eot'); 53 | src: url('../fonts/merriweather-light-webfont.eot?#iefix') format('embedded-opentype'), 54 | url('../fonts/merriweather-light-webfont.woff2') format('woff2'), 55 | url('../fonts/merriweather-light-webfont.woff') format('woff'), 56 | url('../fonts/merriweather-light-webfont.ttf') format('truetype'); 57 | 58 | } 59 | 60 | @font-face { 61 | font-family: 'Merriweather'; 62 | font-style: normal; 63 | font-weight: 400; 64 | src: url('../fonts/merriweather-regular-webfont.eot'); 65 | src: url('../fonts/merriweather-regular-webfont.eot?#iefix') format('embedded-opentype'), 66 | url('../fonts/merriweather-regular-webfont.woff2') format('woff2'), 67 | url('../fonts/merriweather-regular-webfont.woff') format('woff'), 68 | url('../fonts/merriweather-regular-webfont.ttf') format('truetype'); 69 | 70 | } 71 | 72 | @font-face { 73 | font-family: 'Merriweather'; 74 | font-style: italic; 75 | font-weight: 400; 76 | src: url('../fonts/merriweather-italic-webfont.eot'); 77 | src: url('../fonts/merriweather-italic-webfont.eot?#iefix') format('embedded-opentype'), 78 | url('../fonts/merriweather-italic-webfont.woff2') format('woff2'), 79 | url('../fonts/merriweather-italic-webfont.woff') format('woff'), 80 | url('../fonts/merriweather-italic-webfont.ttf') format('truetype'); 81 | 82 | } 83 | 84 | @font-face { 85 | font-family: 'Merriweather'; 86 | font-style: normal; 87 | font-weight: 700; 88 | src: url('../fonts/merriweather-bold-webfont.eot'); 89 | src: url('../fonts/merriweather-bold-webfont.eot?#iefix') format('embedded-opentype'), 90 | url('../fonts/merriweather-bold-webfont.woff2') format('woff2'), 91 | url('../fonts/merriweather-bold-webfont.woff') format('woff'), 92 | url('../fonts/merriweather-bold-webfont.ttf') format('truetype'); 93 | 94 | } -------------------------------------------------------------------------------- /app/gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var browserify = require('browserify'); 3 | var reactify = require('reactify'); 4 | var watchify = require('watchify'); 5 | var uglify = require('gulp-uglify'); 6 | var source = require('vinyl-source-stream'); 7 | var buffer = require('vinyl-buffer'); 8 | var rename = require('gulp-rename'); 9 | var notify = require("gulp-notify"); 10 | // var jshint = require('gulp-jshint'); 11 | var gutil = require('gulp-util'); 12 | var eslint = require('gulp-eslint'); 13 | var stylelint = require('@18f/stylelint-rules'); 14 | var sass = require('gulp-sass'); 15 | 16 | function handleErrors() { 17 | var args = Array.prototype.slice.call(arguments); 18 | notify.onError({ 19 | title: "Compile Error", 20 | message: "<%= error.message %>" 21 | }).apply(this, args); 22 | this.emit('end'); // Keep gulp from hanging on this task 23 | } 24 | 25 | gulp.task('copyjsanduswds', function(){ 26 | gulp.src('./node_modules/jquery/dist/jquery.min.js').pipe(gulp.dest('./build')); 27 | gulp.src(['./node_modules/uswds/dist/**/*', ]).pipe(gulp.dest('./assets/')) 28 | 29 | }); 30 | 31 | var bundler = browserify({ 32 | entries: ['./src/app.js'], // Only need initial file, browserify finds the deps 33 | transform: [reactify], // We want to convert JSX to normal javascript 34 | debug: true, // Gives us sourcemapping 35 | cache: {}, packageCache: {}, fullPaths: false // Requirement of watchify 36 | }); 37 | 38 | gulp.task('bundling', function(){ 39 | bundler 40 | .bundle() 41 | .pipe(source('./src/app.js')) 42 | .pipe(eslint({ 43 | baseConfig: { 44 | "ecmaFeatures": { 45 | "jsx": true 46 | } 47 | } 48 | })) 49 | .pipe(eslint.format()) 50 | // .pipe(eslint.failAfterError()) 51 | .pipe(buffer()) // <----- convert from streaming to buffered vinyl file object 52 | .pipe(uglify().on('error', gutil.log)) 53 | .pipe(rename('bundle.js')) 54 | .pipe(gulp.dest('./build')); 55 | }); 56 | 57 | gulp.task('bundlingWatch', function () { 58 | var watcher = watchify(bundler); 59 | 60 | return watcher 61 | .on('update', function () { // When any files update 62 | var updateStart = Date.now(); 63 | console.log('Updating!'); 64 | 65 | watcher.bundle() // Create new bundle that uses the cache for high performance 66 | .on('error', handleErrors) 67 | .pipe(source('./src/app.js')) 68 | .pipe(eslint({ 69 | baseConfig: { 70 | "ecmaFeatures": { 71 | "jsx": true 72 | } 73 | } 74 | })) 75 | .pipe(eslint.format()) 76 | // .pipe(eslint.failAfterError()) 77 | .pipe(buffer()) // <----- convert from streaming to buffered vinyl file object 78 | .pipe(uglify().on('error', gutil.log)) 79 | .pipe(rename('bundle.js')) 80 | .pipe(gulp.dest('./build')); 81 | console.log('Updated!', (Date.now() - updateStart) + 'ms'); 82 | }) 83 | .bundle() // Create the initial bundle when starting the task 84 | .on('error', handleErrors) 85 | .pipe(source('./src/app.js')) 86 | .pipe(eslint({ 87 | baseConfig: { 88 | "ecmaFeatures": { 89 | "jsx": true 90 | } 91 | } 92 | })) 93 | .pipe(eslint.format()) 94 | // .pipe(eslint.failAfterError()) 95 | .pipe(buffer()) // <----- convert from streaming to buffered vinyl file object 96 | .pipe(uglify().on('error', gutil.log)) 97 | .pipe(rename('bundle.js')) 98 | .pipe(gulp.dest('./build')); 99 | }); 100 | 101 | var lintFunction = stylelint('./assets/css/**/*.scss', {}); 102 | 103 | gulp.task('scssLint', lintFunction); 104 | 105 | gulp.task('sass', function(){ 106 | return gulp.src('./assets/css/**/*.scss') 107 | .pipe(sass()) // Using gulp-sass 108 | .pipe(gulp.dest('./assets/css')) 109 | }); 110 | 111 | gulp.task('watch', ['bundlingWatch', 'copyjsanduswds', 'scssLint', 'sass'], function() { 112 | gulp.watch('./src/**/*.js', function(){ 113 | gulp.run('bundling'); 114 | }); 115 | }); 116 | 117 | gulp.task('default', ['bundling', 'copyjsanduswds', 'scssLint', 'sass'], function(){}); 118 | -------------------------------------------------------------------------------- /app/src/questions/08_special_requirements.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var StateMixin = require("../state_mixin"); 3 | var EditBox = require("../edit_box"); 4 | 5 | var STATES = [ 6 | "accessibility", 7 | "nonDisclosure", 8 | "orderOfPrecedence", 9 | "security", 10 | "smallBusinessStatus", 11 | "titleToMaterials", 12 | "useOfData", 13 | "federalHolidays", 14 | "conflictOfInterest", 15 | "commercialSoftware", 16 | ]; 17 | 18 | var SpecialRequirements = React.createClass({ 19 | mixins: [StateMixin], 20 | getInitialState: function() { 21 | var initialStates = getStates(STATES); 22 | initialStates["addRequirement"] = false; 23 | initialStates["requirementsData"] = []; 24 | initialStates["title"] = ""; 25 | initialStates["text"] = ""; 26 | return initialStates; 27 | }, 28 | componentDidMount: function() { 29 | var rfqId = getId(window.location.hash); 30 | getCustomComponents(rfqId, 8, function(data){ 31 | var newStates = {}; 32 | for (i=0; i < data['data'].length; i++){ 33 | var requirement = data['data'][i]; 34 | newStates[requirement['name']] = requirement['text']; 35 | } 36 | this.setState( newStates ); 37 | this.setState({requirementsData: data["data"]}); 38 | }.bind(this)); 39 | }, 40 | addRequirement: function() { 41 | if (this.state.addRequirement){ 42 | // check to see if info has been filled in 43 | if (this.state.title.length > 0 && this.state.text.length > 0){ 44 | var rfqId = getId(window.location.hash); 45 | var requirementData = {}; 46 | requirementData["title"] = this.state.title; 47 | requirementData["text"] = this.state.text; 48 | 49 | // save the data and update state to include new component 50 | createComponent(requirementData, rfqId, 8, function(data){ 51 | console.log(data); 52 | this.setState({ 53 | addRequirement: false, 54 | title: "", 55 | text: "", 56 | }); 57 | }.bind(this)); 58 | location.reload(); 59 | } 60 | else { 61 | alert("Please fill out the title and text components of the form before saving the new role."); 62 | } 63 | } 64 | else { 65 | this.setState({ addRequirement: true }); 66 | } 67 | }, 68 | cancelAddRequirement: function() { 69 | this.setState( {addRequirement: false }); 70 | }, 71 | save: function(cb) { 72 | var data = {}; 73 | 74 | for (i=0; i < STATES.length; i++){ 75 | var stateName = STATES[i]; 76 | data[stateName] = this.state[stateName]; 77 | } 78 | 79 | var rfqId = getId(window.location.hash); 80 | put_data(8, "custom_component", rfqId, data, cb); 81 | }, 82 | render: function() { 83 | var requirements = []; 84 | for (i=0; i < this.state.requirementsData.length; i++){ 85 | var role = this.state.requirementsData[i]; 86 | 87 | requirements.push( 88 |
    89 |
    {role['title']}
    90 | 91 | 96 | 97 |
    98 | ); 99 | } 100 | return ( 101 |
    102 |
    Special Contract Requirements
    103 |
    The content in this section is typically decided upon by the CO.
    104 | 105 | {requirements} 106 | 107 | {this.state.addRequirement? 108 |
    109 |
    110 | 111 |
    112 | 113 | 114 | 115 |
    116 | : 117 | } 118 | 119 |
    120 | ); 121 | }, 122 | }); 123 | 124 | 125 | module.exports = SpecialRequirements; -------------------------------------------------------------------------------- /app/src/request.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | // Dependencies 4 | 5 | // Bootstrap 6 | var Nav = require('react-bootstrap').Nav; 7 | var NavItem = require('react-bootstrap').NavItem; 8 | 9 | // Router stuff 10 | var Link = require('react-router').Link; 11 | var IndexLink = require('react-router').IndexLink; 12 | var History = require('react-router').History; 13 | 14 | // Custom elements 15 | var questionList = require('./question_list'); 16 | 17 | var Sidebar = React.createClass({ 18 | mixins: [History], 19 | 20 | propTypes: { 21 | rfpId: React.PropTypes.string.isRequired, 22 | currentPage: React.PropTypes.string.isRequired, 23 | width: React.PropTypes.number.isRequired, 24 | }, 25 | 26 | componentDidMount: function() { 27 | // Make menu affixed 28 | // $("#sidenav").affix({ 29 | // offset: { 30 | // top: 93, 31 | // } 32 | // }); 33 | $('.menu-btn, .overlay, .sliding-panel-close').on('click touchstart',function (e) { 34 | $('.sidenav, .overlay').toggleClass('is-visible'); 35 | e.preventDefault(); 36 | }); 37 | }, 38 | handleFollowLink: function(e) { 39 | e.preventDefault(); 40 | this.props.onChange(function(){ 41 | // @TODO error if page doesn't save 42 | var link = e.target.getAttribute("href"); 43 | this.history.pushState(null, link, null); 44 | }.bind(this)); 45 | }, 46 | render: function() { 47 | var baseURL = "/rfp/" + this.props.rfpId; 48 | 49 | // var prefixLinks = [{link: "/rfp", title: "Overview"}]; 50 | var postfixLinks = [{link: baseURL+"/results", title: "Results"}]; 51 | 52 | // Generate subpages 53 | var questionLinks = questionList.map(function(question) { 54 | return { 55 | link: baseURL+"/question/"+question.code, 56 | title: question.title, 57 | }; 58 | }); 59 | var subpages = questionLinks.concat(postfixLinks); 60 | 61 | var links = subpages.map(function(subpage, i) { 62 | var active = (subpage.link == this.props.currentPage); 63 | return ( 64 |
  • 65 | {subpage.title} 66 |
  • 67 | ); 68 | }.bind(this)); 69 | 70 | var style = { 71 | width: this.props.width, 72 | }; 73 | 74 | return ( 75 | 78 | ); 79 | }, 80 | }); 81 | 82 | 83 | var Request = React.createClass({ 84 | loadData: function(cb) { 85 | cb("Error: no data found"); 86 | }, 87 | updateQuestion: function(questionName, data) { 88 | var updates = {}; 89 | updates[questionName] = data; 90 | this.setState(updates); 91 | }, 92 | componentDidMount: function() { 93 | this.loadData(function(err, data) { 94 | if(err) { 95 | // console.log("Error fetching data for questions: "+err); 96 | return; 97 | } 98 | 99 | this.setState(data); 100 | }.bind(this)); 101 | }, 102 | 103 | getInitialState: function() { 104 | return { 105 | question1: {}, 106 | question2: {}, 107 | }; 108 | }, 109 | 110 | handleSidebarChange: function(callback) { 111 | // Get child 112 | var child = React.Children.only(this.props.children); 113 | 114 | // console.log(child); 115 | if(child.type.displayName == "Question") { 116 | // If child is question, call save() with the callback 117 | // console.log("saving the child! it's a question..."); 118 | this._child.save(callback); 119 | } else { 120 | // Otherwise, call callback 121 | // console.log("ignore the child, it's not a question (it's worthless)"); 122 | callback(); 123 | } 124 | }, 125 | 126 | renderChildren: function() { 127 | return React.Children.map(this.props.children, function(child) { 128 | return React.cloneElement(child, { 129 | questionData: this.state, 130 | updateQuestion: this.updateQuestion, 131 | ref: function(child) { 132 | this._child = child; 133 | }.bind(this), 134 | }); 135 | }.bind(this)); 136 | }, 137 | 138 | render: function() { 139 | var mainStyle = { 140 | paddingLeft: 16, 141 | }; 142 | return ( 143 |
    144 |
    145 | {this.renderChildren()} 146 |
    147 |
    148 |
    149 | 152 |
    153 | ); 154 | }, 155 | }); 156 | 157 | //
    158 | 159 | module.exports = Request; 160 | -------------------------------------------------------------------------------- /app/src/questions/07_government_roles.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var StateMixin = require("../state_mixin"); 3 | var EditBox = require("../edit_box"); 4 | 5 | var STATES = [ 6 | "stakeholderIntro", 7 | "contractingOfficer", 8 | "contractingOfficerRepresentative", 9 | "productOwner", 10 | "endUsers", 11 | ]; 12 | 13 | var ContractingOfficer = React.createClass({ 14 | mixins: [StateMixin], 15 | 16 | save: function(cb) { 17 | var data = {}; 18 | var rfqId = getId(window.location.hash); 19 | 20 | // this skips stakeholderIntro 21 | for (i=1; i < STATES.length; i++){ 22 | var stateName = STATES[i]; 23 | data[stateName] = this.state[stateName]; 24 | } 25 | // save stakeholderIntro 26 | put_data(7, "custom_component", rfqId, data, cb); 27 | put_data(7, 'get_content', rfqId, {'stakeholderIntro': this.state.stakeholderIntro}, cb); 28 | 29 | }, 30 | getInitialState: function() { 31 | var initialStates = getStates(STATES); 32 | initialStates["addRole"] = false; 33 | initialStates["title"] = ""; 34 | initialStates["text"] = ""; 35 | initialStates["rolesData"] = []; 36 | return initialStates; 37 | }, 38 | componentDidMount: function() { 39 | var rfqId = getId(window.location.hash); 40 | get_data(7, rfqId, function(content){ 41 | var componentStates = getComponents(content["data"]); 42 | this.setState( componentStates ); 43 | }.bind(this)); 44 | getCustomComponents(rfqId, 7, function(data){ 45 | var newStates = {}; 46 | for (i=0; i < data['data'].length; i++){ 47 | var role = data['data'][i]; 48 | newStates[role['name']] = role['text']; 49 | } 50 | this.setState( newStates ); 51 | this.setState({rolesData: data["data"]}); 52 | }.bind(this)); 53 | }, 54 | addRole: function() { 55 | if (this.state.addRole){ 56 | // check to see if info has been filled in 57 | if (this.state.title.length > 0 && this.state.text.length > 0){ 58 | var rfqId = getId(window.location.hash); 59 | var roleData = {}; 60 | roleData["title"] = this.state.title; 61 | roleData["text"] = this.state.text; 62 | 63 | // save the data and update 64 | createComponent(roleData, rfqId, 7, function(data){ 65 | this.setState({ 66 | addRole: false, 67 | title: "", 68 | text: "", 69 | }); 70 | }.bind(this)); 71 | location.reload(); 72 | } 73 | else { 74 | alert("Please fill out the title and text components of the form before saving the new role."); 75 | } 76 | } 77 | else { 78 | this.setState({ addRole: true }); 79 | } 80 | }, 81 | cancelAddRole: function() { 82 | this.setState({ addRole: false}); 83 | }, 84 | render: function() { 85 | var roles = []; 86 | for (i=0; i < this.state.rolesData.length; i++){ 87 | var role = this.state.rolesData[i]; 88 | roles.push( 89 |
    90 |
    {role['title']}
    91 | 92 | 97 | 98 |
    99 | ); 100 | } 101 | 102 | return ( 103 |
    104 |
    Roles and Responsibilities
    105 |
    The content in this section should be decided upon by both the PM and the CO.
    106 | 107 | 112 | 113 | 114 | {roles} 115 | 116 | {this.state.addRole? 117 |
    118 |
    119 | 120 |
    121 | 122 | 123 | 124 |
    125 | : 126 | } 127 | 128 |
    You may also add elaborate on these roles, or add additional roles in the generated RFQ.
    129 |
    130 | ); 131 | }, 132 | }); 133 | 134 | 135 | module.exports = ContractingOfficer; -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | import os, sys 4 | import shutil 5 | import config 6 | import logging 7 | from io import BytesIO 8 | import base64 9 | 10 | from flask import Flask, send_from_directory, send_file, g, request, jsonify 11 | from urllib.parse import urlparse, urlunparse 12 | from waitress import serve 13 | port = os.getenv("PORT") or 5000 14 | from flask_restful import Api 15 | from flask_sqlalchemy import SQLAlchemy 16 | 17 | from flask_cli import FlaskCLI 18 | from sqlalchemy import create_engine 19 | from sqlalchemy.engine import reflection 20 | 21 | import create_document 22 | from models import User, Base, Agency, session, engine 23 | from seed import agencies 24 | from resources import auth, Users, Agencies, Data, Deliverables, Clin, CustomComponents, Create, DeleteRFQ 25 | 26 | logger = logging.getLogger('waitress') 27 | logger.setLevel(logging.INFO) 28 | 29 | # set the project root directory as the static folder, you can set others. 30 | app = Flask(__name__, static_folder='app') 31 | FlaskCLI(app) 32 | app.config['APP_SETTINGS'] = config.DevelopmentConfig 33 | # app.config.from_object(os.environ['APP_SETTINGS']) 34 | db = SQLAlchemy(app) 35 | api = Api(app, prefix="/api") 36 | 37 | api.add_resource(Users, '/users') 38 | api.add_resource(Agencies, '/agencies') 39 | api.add_resource(Data, '/get_content//section/') 40 | api.add_resource(Deliverables, '/deliverables/') 41 | api.add_resource(Create, '/rfqs') 42 | api.add_resource(Clin, '/clins/') 43 | api.add_resource(CustomComponents, '/custom_component//section/') 44 | api.add_resource(DeleteRFQ, '/delete/rfqs/') 45 | 46 | 47 | 48 | def create_tables(): 49 | # delete old records 50 | Base.metadata.drop_all(engine) 51 | # 52 | Base.metadata.create_all(engine) 53 | # 54 | for agency in agencies: 55 | a = Agency(abbreviation=agency, full_name=agencies[agency]) 56 | session.add(a) 57 | session.commit() 58 | 59 | 60 | 61 | @app.route('/') 62 | def index(): 63 | return send_from_directory("app", "index.html") 64 | 65 | @auth.verify_password 66 | def verify_password(username, password): 67 | user = User.verify_auth_token(username); 68 | if not user: 69 | user = session.query(User).filter_by(username = username).first() 70 | if not user or not user.verify_password(password): 71 | return False 72 | g.user = user 73 | return True 74 | 75 | @app.route('/api/authtest') 76 | @auth.login_required 77 | def get_resource(): 78 | return jsonify({ 'data': 'Hello, %s!' % g.user.username }) 79 | 80 | @app.route('/api/isLoggedIn') 81 | def isLoggedIn(): 82 | auth = request.headers.get('Authorization') 83 | result = { 'loggedIn': False } 84 | # The Authorization header should look like "Basic username:password", 85 | # so it must be at least 6 characters long or it's invalid. 86 | if auth is not None and len(auth) > 6: 87 | # Decode the username:password part. 88 | pair = base64.b64decode(auth[6:]).decode('utf-8') 89 | # If the user is logged in, the username should be their token 90 | # and the password should be "none". Assume that the decoded 91 | # string is ":none" and ditch it. If that's not right, the 92 | # verification will fail and that's a-okay. 93 | result['loggedIn'] = verify_password(pair[:-5], ''); 94 | return jsonify(result) 95 | 96 | @app.route('/api/token') 97 | @auth.login_required 98 | def get_auth_token(): 99 | token = g.user.generate_auth_token() 100 | return jsonify({ 'token': token.decode('ascii') }) 101 | 102 | @app.route('/') 103 | def send_js(path): 104 | return send_from_directory("app", path) 105 | 106 | 107 | @app.route('/download/') 108 | def download(rfq_id): 109 | document = create_document.create_document(rfq_id) 110 | strIO = BytesIO() 111 | document.save(strIO) 112 | strIO.seek(0) 113 | return send_file(strIO, attachment_filename="RFQ.docx", as_attachment=True) 114 | 115 | 116 | @app.route('/agile_estimator') 117 | def agile_estimator(): 118 | return send_file("AgileEstimator.xlsx") 119 | 120 | @app.cli.command() 121 | def seed_db(): 122 | create_tables() 123 | 124 | @app.before_request 125 | def redirect_fromoldurl(): 126 | """Redirect old playbook-in-action to agile-solicitation-builder.""" 127 | urlparts = urlparse(request.url) 128 | if urlparts.netloc == 'playbook-in-action.apps.cloud.gov': 129 | urlparts_list = list(urlparts) 130 | urlparts_list[1] = 'agile-solicitation-builder.apps.cloud.gov' 131 | return redirect(urlunparse(urlparts_list), code=301) 132 | 133 | if __name__ == "__main__": 134 | serve(app, port=port) 135 | -------------------------------------------------------------------------------- /app/helpers.js: -------------------------------------------------------------------------------- 1 | function getId(url){ 2 | var number = url.split("#/rfp/")[1].split("/question")[0]; 3 | return parseInt(number); 4 | } 5 | 6 | function getStates(states){ 7 | var statesDict = {"edit": null}; 8 | for (i=0; i < states.length; i++){ 9 | var state = states[i]; 10 | statesDict[state] = ""; 11 | } 12 | return statesDict; 13 | } 14 | 15 | function getComponents(data){ 16 | states = Object.keys(data); 17 | // for (i=0; i < states.length; i++){ 18 | // console.log('"' + states[i] + '",'); 19 | // } 20 | componentStates = {}; 21 | for (i=0; i < states.length; i++){ 22 | var state = states[i]; 23 | componentStates[state] = data[state]; 24 | } 25 | return componentStates; 26 | } 27 | 28 | function getDeliverables(doc_id, callback){ 29 | $.ajax({ 30 | type: "GET", 31 | url: "/api/deliverables/" + doc_id, 32 | dataType: 'json', 33 | success: function(data){ 34 | if (callback){ 35 | callback(data); 36 | } 37 | } 38 | }); 39 | } 40 | 41 | function putDeliverables(doc_id, data, callback){ 42 | $.ajax({ 43 | type: "PUT", 44 | url: "/api/deliverables/" + doc_id, 45 | data: JSON.stringify({data: data}), 46 | contentType: 'application/json', 47 | dataType: 'json', 48 | success: function(data){ 49 | if (callback){ 50 | callback(data); 51 | } 52 | } 53 | }); 54 | } 55 | 56 | function deleteRFQ(doc_id, callback){ 57 | $.ajax({ 58 | type: "DELETE", 59 | url: "/api/delete/rfqs/" + doc_id, 60 | dataType: 'json', 61 | success: function(data){ 62 | if (callback){ 63 | callback(data); 64 | } 65 | } 66 | }); 67 | } 68 | 69 | function get_data(section, doc_id, callback){ 70 | $.ajax({ 71 | type: "GET", 72 | url: "/api/get_content/" + doc_id + "/section/" + section, 73 | dataType: 'json', 74 | success: function(data){ 75 | if (callback){ 76 | callback(data); 77 | } 78 | } 79 | }); 80 | } 81 | 82 | function put_data(section, url, doc_id, data, callback){ 83 | $.ajax({ 84 | type: "PUT", 85 | url: "/api/" + url + "/" + doc_id + "/section/" + section, 86 | data: JSON.stringify({data: data}), 87 | contentType: 'application/json', 88 | dataType: 'json', 89 | success: function(data){ 90 | if (callback){ 91 | callback(data); 92 | } 93 | } 94 | }); 95 | } 96 | 97 | function getRFQs(callback){ 98 | $.ajax({ 99 | type: "GET", 100 | url: "/api/rfqs", 101 | dataType: 'json', 102 | success: function(data){ 103 | if (callback){ 104 | callback(data); 105 | } 106 | } 107 | }); 108 | } 109 | 110 | function getAgencies(callback){ 111 | $.ajax({ 112 | type: "GET", 113 | url: "/api/agencies", 114 | dataType: 'json', 115 | success: function(data){ 116 | if (callback){ 117 | callback(data); 118 | } 119 | } 120 | }); 121 | } 122 | 123 | function createRFQ(dataDict, callback){ 124 | $.ajax({ 125 | type: "POST", 126 | url: "/api/rfqs", 127 | data: dataDict, 128 | dataType: 'json', 129 | success: function(data){ 130 | if (callback){ 131 | callback(data); 132 | } 133 | } 134 | }); 135 | } 136 | 137 | function createComponent(roleData, rfqId, sectionId, callback){ 138 | $.ajax({ 139 | type: "POST", 140 | url: "/api/custom_component/" + rfqId + "/section/" + sectionId, 141 | data: JSON.stringify({data: roleData}), 142 | contentType: 'application/json', 143 | dataType: 'json', 144 | success: function(data){ 145 | if (callback){ 146 | callback(data); 147 | } 148 | } 149 | }); 150 | } 151 | 152 | function getCustomComponents(rfqId, sectionId, callback){ 153 | $.ajax({ 154 | type: "GET", 155 | url: "/api/custom_component/" + rfqId + '/section/' + sectionId, 156 | dataType: 'json', 157 | success: function(data){ 158 | if (callback){ 159 | callback(data); 160 | } 161 | } 162 | }); 163 | } 164 | 165 | function createCLIN(clinData, rfqId, callback){ 166 | window.cd = clinData; 167 | $.ajax({ 168 | type: "POST", 169 | url: "/api/clins/" + rfqId, 170 | data: JSON.stringify({data: clinData}), 171 | contentType: 'application/json', 172 | dataType: 'json', 173 | success: function(data){ 174 | if (callback){ 175 | callback(data); 176 | } 177 | } 178 | }); 179 | } 180 | 181 | function getCLINs(rfqId, callback){ 182 | $.ajax({ 183 | type: "GET", 184 | url: "/api/clins/" + rfqId, 185 | dataType: 'json', 186 | success: function(data){ 187 | if (callback){ 188 | callback(data); 189 | } 190 | } 191 | }); 192 | } 193 | 194 | function createString(userTypes){ 195 | var usersString = ""; 196 | if (userTypes.length == 1){ 197 | usersString = userTypes[0]; 198 | } 199 | if (userTypes.length > 1){ 200 | for (i=0; i < userTypes.length - 1; i++){ 201 | usersString += userTypes[i] + ", "; 202 | } 203 | usersString += "and " + userTypes[userTypes.length-1]; 204 | } 205 | return usersString; 206 | } 207 | -------------------------------------------------------------------------------- /app/assets/js/affix.js: -------------------------------------------------------------------------------- 1 | /* ======================================================================== 2 | * Bootstrap: affix.js v3.3.5 3 | * http://getbootstrap.com/javascript/#affix 4 | * ======================================================================== 5 | * Copyright 2011-2015 Twitter, Inc. 6 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 7 | * ======================================================================== */ 8 | 9 | 10 | +function ($) { 11 | 'use strict'; 12 | 13 | // AFFIX CLASS DEFINITION 14 | // ====================== 15 | 16 | var Affix = function (element, options) { 17 | this.options = $.extend({}, Affix.DEFAULTS, options) 18 | 19 | this.$target = $(this.options.target) 20 | .on('scroll.bs.affix.data-api', $.proxy(this.checkPosition, this)) 21 | .on('click.bs.affix.data-api', $.proxy(this.checkPositionWithEventLoop, this)) 22 | 23 | this.$element = $(element) 24 | this.affixed = null 25 | this.unpin = null 26 | this.pinnedOffset = null 27 | 28 | this.checkPosition() 29 | } 30 | 31 | Affix.VERSION = '3.3.5' 32 | 33 | Affix.RESET = 'affix affix-top affix-bottom' 34 | 35 | Affix.DEFAULTS = { 36 | offset: 0, 37 | target: window 38 | } 39 | 40 | Affix.prototype.getState = function (scrollHeight, height, offsetTop, offsetBottom) { 41 | var scrollTop = this.$target.scrollTop() 42 | var position = this.$element.offset() 43 | var targetHeight = this.$target.height() 44 | 45 | if (offsetTop != null && this.affixed == 'top') return scrollTop < offsetTop ? 'top' : false 46 | 47 | if (this.affixed == 'bottom') { 48 | if (offsetTop != null) return (scrollTop + this.unpin <= position.top) ? false : 'bottom' 49 | return (scrollTop + targetHeight <= scrollHeight - offsetBottom) ? false : 'bottom' 50 | } 51 | 52 | var initializing = this.affixed == null 53 | var colliderTop = initializing ? scrollTop : position.top 54 | var colliderHeight = initializing ? targetHeight : height 55 | 56 | if (offsetTop != null && scrollTop <= offsetTop) return 'top' 57 | if (offsetBottom != null && (colliderTop + colliderHeight >= scrollHeight - offsetBottom)) return 'bottom' 58 | 59 | return false 60 | } 61 | 62 | Affix.prototype.getPinnedOffset = function () { 63 | if (this.pinnedOffset) return this.pinnedOffset 64 | this.$element.removeClass(Affix.RESET).addClass('affix') 65 | var scrollTop = this.$target.scrollTop() 66 | var position = this.$element.offset() 67 | return (this.pinnedOffset = position.top - scrollTop) 68 | } 69 | 70 | Affix.prototype.checkPositionWithEventLoop = function () { 71 | setTimeout($.proxy(this.checkPosition, this), 1) 72 | } 73 | 74 | Affix.prototype.checkPosition = function () { 75 | if (!this.$element.is(':visible')) return 76 | 77 | var height = this.$element.height() 78 | var offset = this.options.offset 79 | var offsetTop = offset.top 80 | var offsetBottom = offset.bottom 81 | var scrollHeight = Math.max($(document).height(), $(document.body).height()) 82 | 83 | if (typeof offset != 'object') offsetBottom = offsetTop = offset 84 | if (typeof offsetTop == 'function') offsetTop = offset.top(this.$element) 85 | if (typeof offsetBottom == 'function') offsetBottom = offset.bottom(this.$element) 86 | 87 | var affix = this.getState(scrollHeight, height, offsetTop, offsetBottom) 88 | 89 | if (this.affixed != affix) { 90 | if (this.unpin != null) this.$element.css('top', '') 91 | 92 | var affixType = 'affix' + (affix ? '-' + affix : '') 93 | var e = $.Event(affixType + '.bs.affix') 94 | 95 | this.$element.trigger(e) 96 | 97 | if (e.isDefaultPrevented()) return 98 | 99 | this.affixed = affix 100 | this.unpin = affix == 'bottom' ? this.getPinnedOffset() : null 101 | 102 | this.$element 103 | .removeClass(Affix.RESET) 104 | .addClass(affixType) 105 | .trigger(affixType.replace('affix', 'affixed') + '.bs.affix') 106 | } 107 | 108 | if (affix == 'bottom') { 109 | this.$element.offset({ 110 | top: scrollHeight - height - offsetBottom 111 | }) 112 | } 113 | } 114 | 115 | 116 | // AFFIX PLUGIN DEFINITION 117 | // ======================= 118 | 119 | function Plugin(option) { 120 | return this.each(function () { 121 | var $this = $(this) 122 | var data = $this.data('bs.affix') 123 | var options = typeof option == 'object' && option 124 | 125 | if (!data) $this.data('bs.affix', (data = new Affix(this, options))) 126 | if (typeof option == 'string') data[option]() 127 | }) 128 | } 129 | 130 | var old = $.fn.affix 131 | 132 | $.fn.affix = Plugin 133 | $.fn.affix.Constructor = Affix 134 | 135 | 136 | // AFFIX NO CONFLICT 137 | // ================= 138 | 139 | $.fn.affix.noConflict = function () { 140 | $.fn.affix = old 141 | return this 142 | } 143 | 144 | 145 | // AFFIX DATA-API 146 | // ============== 147 | 148 | $(window).on('load', function () { 149 | $('[data-spy="affix"]').each(function () { 150 | var $spy = $(this) 151 | var data = $spy.data() 152 | 153 | data.offset = data.offset || {} 154 | 155 | if (data.offsetBottom != null) data.offset.bottom = data.offsetBottom 156 | if (data.offsetTop != null) data.offset.top = data.offsetTop 157 | 158 | Plugin.call($spy, data) 159 | }) 160 | }) 161 | 162 | }(jQuery); 163 | -------------------------------------------------------------------------------- /app/src/questions/06_inspection.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var StateMixin = require("../state_mixin"); 3 | var EditBox = require("../edit_box"); 4 | 5 | var STATES = [ 6 | "guidingPrinciples", 7 | "inspectionOverview", 8 | "lateDelivery", 9 | "workspaceIntro", 10 | "workspaceExists", 11 | "workspaceName", 12 | "transitionActivities", 13 | "deliveringDeliverables", 14 | ]; 15 | 16 | var Inspection = React.createClass({ 17 | mixins: [StateMixin], 18 | getInitialState: function() { 19 | var initialStates = getStates(STATES); 20 | return initialStates; 21 | }, 22 | componentDidMount: function() { 23 | var rfqId = getId(window.location.hash); 24 | get_data(6, rfqId, function(content){ 25 | var componentStates = getComponents(content["data"]); 26 | this.setState( componentStates ); 27 | }.bind(this)); 28 | }, 29 | save: function(cb) { 30 | var data = {}; 31 | 32 | for (i=0; i < STATES.length; i++){ 33 | var stateName = STATES[i]; 34 | data[stateName] = this.state[stateName]; 35 | } 36 | 37 | var rfqId = getId(window.location.hash); 38 | put_data(6, "get_content", rfqId, data, cb); 39 | 40 | }, 41 | render: function() { 42 | return ( 43 |
    44 |
    Inspection, Acceptance, Delivery, Transition
    45 |
    The content in this section should be decided on by both the CO and the PM.
    46 | 47 |
    Overview
    48 | 49 | 54 | 55 | 56 |
    Delivery & Timing
    57 | 58 |
    59 |
    Government Acceptance
    60 | 61 | 66 | 67 |
    68 | 69 |
    70 |
    Notice Regarding Late Delivery
    71 | 72 | 77 | 78 |
    79 | 80 |
    Delivering Deliverables
    81 |
    The US Digital Service Playbook strongly recommends the use of a version control system such as Github, or similar for storing code and system documentation.
    82 | 83 | 88 | 89 | 90 |
    91 |
    Is your team currently using a collaborative workspace?
    92 |
    Ex: Sharepoint, JIRA, Rally, Google Drive, Box, etc.
    93 | 94 |
    95 | Is your team currently using a collaborative workspace? 96 |
      97 |
    • 98 | 99 | 100 |
    • 101 |
    • 102 | 103 | 104 |
    • 105 |
    106 |
    107 | 108 |
    The contractor will work with the PM and CO to establish a collaborative workspace that is acceptable for both parties.
    109 | {(this.state.workspaceExists == "yes")? 110 |
    111 |
    What workspace are you currently using?
    112 | 113 | {(this.state.workspaceName.length > 0)? 114 |
    Currently the government team is using {this.state.workspaceName}.
    : null 115 | } 116 |
    117 | : null 118 | } 119 | 120 | 125 | 126 |
    127 | 128 |
    Transition Activities
    129 | 130 | 135 | 136 |
    137 | ); 138 | }, 139 | }); 140 | 141 | 142 | module.exports = Inspection; -------------------------------------------------------------------------------- /app/assets/css/style.css: -------------------------------------------------------------------------------- 1 | .Popover-body { 2 | display: inline-flex; 3 | flex-direction: column; 4 | padding: 2rem 4rem; 5 | background: #454545; 6 | color: white; 7 | border-radius: 0.3rem; } 8 | 9 | .Popover-tipShape { 10 | fill: #454545; } 11 | 12 | .menu-btn { 13 | padding: 1.5rem null; 14 | float: left; 15 | margin-top: -4px; 16 | color: #ffffff; 17 | background-color: #0071bc; 18 | font-size: 1.5rem; 19 | width: 15%; 20 | text-align: center; } 21 | @media all and (850px) { 22 | .menu-btn { 23 | display: none; } } 24 | .menu-btn:hover { 25 | text-decoration: none; 26 | color: #ffffff; 27 | background-color: #205493; } 28 | .menu-btn:visited { 29 | color: #ffffff; } 30 | 31 | .overlay { 32 | background: #000000; 33 | opacity: 0; 34 | visibility: hidden; 35 | z-index: 9999; } 36 | .overlay.is-visible { 37 | visibility: visible; } 38 | 39 | .sidenav { 40 | width: 250px; 41 | border-right: 1px solid #aeb0b5; 42 | padding: 5rem 3rem 3rem 3rem; 43 | overflow: auto; 44 | display: none; 45 | z-index: -1; } 46 | @media (max-width: 850px) { 47 | .sidenav.menu-content { 48 | background: #ffffff; 49 | -webkit-overflow-scrolling: touch; 50 | overflow-y: auto; 51 | z-index: 999999; 52 | display: block; } } 53 | .lt-ie9 .sidenav { 54 | width: 25%; } 55 | .sidenav .usa-sidenav-sub_list { 56 | display: none; } 57 | @media screen and (min-width: 850px) { 58 | .sidenav { 59 | display: block; } } 60 | 61 | .visual-style .sidenav .visual-style-sublist { 62 | display: block; } 63 | .visual-style .sidenav .visual-style-sublist ul { 64 | display: block; } 65 | 66 | .form-controls .sidenav .form-controls-sublist { 67 | display: block; } 68 | 69 | .form-templates .sidenav .form-templates-sublist { 70 | display: block; } 71 | 72 | .footers .sidenav .footers-sublist { 73 | display: block; } 74 | 75 | .header { 76 | border-bottom: solid 1px #aeb0b5; 77 | margin-bottom: 15px; } 78 | 79 | .term { 80 | font-weight: 500; } 81 | 82 | .mount { 83 | font-family: 'Source Sans Pro', sans-serif; 84 | font-weight: 400; 85 | font-size: 16px; 86 | line-height: 1.5em/26px; } 87 | .mount input { 88 | font-size: 15px; } 89 | .mount select { 90 | font-size: 15px; } 91 | .mount textarea { 92 | font-size: 16px; } 93 | 94 | .nav > li > a { 95 | padding: 7px 15px; } 96 | 97 | a { 98 | color: #0071bc; } 99 | 100 | h1 { 101 | font-family: 'Merriweather', serif; 102 | font-weight: 700; 103 | font-size: 25px; 104 | line-height: 1.3em/26px; 105 | color: #5b616b; } 106 | 107 | .affix { 108 | position: fixed; } 109 | 110 | form .usa-grid { 111 | margin-left: -30px; } 112 | 113 | .edit { 114 | color: #337ab7; 115 | cursor: pointer; 116 | cursor: hand; } 117 | 118 | .edit-box .edit-content > *:first-child { 119 | margin-top: 0; } 120 | 121 | .edit-box .edit-content > *:last-child { 122 | margin-bottom: 0; } 123 | 124 | button .add { 125 | margin-bottom: 10px; } 126 | 127 | .clin { 128 | border: 1px solid; } 129 | 130 | .fake-table { 131 | margin-top: 5px; 132 | margin-bottom: 10px; } 133 | 134 | .row { 135 | border-top: 0; } 136 | 137 | .btn-toolbar { 138 | margin-top: 20px; } 139 | 140 | .table-content { 141 | margin-top: 5px; 142 | margin-bottom: 5px; } 143 | 144 | .yes-no { 145 | margin-right: 5px; 146 | margin-left: 5px; } 147 | 148 | .table-wrapper { 149 | border: solid 1px; } 150 | 151 | .page-heading { 152 | font-family: 'Merriweather', serif; 153 | font-weight: 700; 154 | font-size: 30px; 155 | margin-bottom: 5px; 156 | line-height: 1.3em/26px; 157 | color: #212121; } 158 | 159 | .section-heading { 160 | font-weight: 700; 161 | font-family: 'Merriweather', serif; 162 | font-size: 20px; 163 | line-height: 1.3em/22px; } 164 | 165 | .sub-heading { 166 | color: #205493; 167 | font-weight: 700; 168 | margin: 25px 0 10px 0; 169 | font-family: 'Merriweather', serif; 170 | font-size: 20px; 171 | line-height: 1.3em/22px; } 172 | 173 | .question { 174 | margin: 5vh auto; } 175 | 176 | .question-text { 177 | margin: 25px 0 5px 0; 178 | font-size: 16px; 179 | font-family: 'Merriweather', serif; 180 | color: #323a45; 181 | font-weight: 700; } 182 | 183 | .question-description { 184 | font-style: italic; 185 | margin: 0 0 10px 0; 186 | font-size: 95%; } 187 | 188 | .responder-instructions { 189 | font-style: italic; 190 | margin-bottom: 10px; 191 | font-size: 95%; } 192 | 193 | .resulting-text { 194 | font-family: 'Times', serif; 195 | border: solid 3px #aeb0b5; 196 | font-weight: 500; 197 | font-size: 105%; 198 | color: #212121; 199 | background-color: #ffffff; 200 | padding: 5px; 201 | margin-bottom: 10px; } 202 | 203 | .edit-box { 204 | margin: 5px 0 10px 0; } 205 | 206 | .short-response { 207 | width: 150px; } 208 | 209 | .usa-width-one-sixth.sidebar-nav { 210 | margin-left: 28px; } 211 | 212 | .medium-response { 213 | width: 300px; } 214 | 215 | .long-response { 216 | width: 100%; } 217 | 218 | .additional-clin { 219 | max-width: none; } 220 | 221 | textarea { 222 | max-width: none; 223 | height: auto; 224 | margin: 10px 0; } 225 | 226 | .usa-disclaimer { 227 | background-color: #f1f1f1; 228 | font-size: 15px; 229 | padding: 5px; 230 | margin: -8px -23px 0 -23px; } 231 | 232 | .guidance-text { 233 | margin: 0 0 10px 0; } 234 | 235 | .usa-disclaimer-official { 236 | padding-left: 15px; } 237 | 238 | .usa-flag_icon { 239 | margin: 0 5px 0 15px; } 240 | 241 | .usa-disclaimer-stage { 242 | float: right; 243 | padding-right: 15px; } 244 | 245 | .top-right-auth-button { 246 | float: right; } 247 | 248 | .Popover-body { 249 | display: inline-flex; 250 | flex-direction: column; 251 | padding: 2rem 4rem; 252 | background: white; 253 | color: black; 254 | border-radius: 0.3rem; 255 | border: 1px solid #888; 256 | box-shadow: 10px 10px 25px #555; 257 | width: 400px; } 258 | 259 | .Popover-tipShape { 260 | fill: black; } 261 | 262 | @media screen and (min-width: 600px) and (max-width: 1200px) { 263 | .side-bar { 264 | display: none !important; } } 265 | -------------------------------------------------------------------------------- /app/src/request_overview.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var StateMixin = require("./state_mixin"); 3 | 4 | // Bootstrap 5 | var Button = require('react-bootstrap').Button; 6 | 7 | // Router stuff 8 | var Link = require('react-router').Link; 9 | 10 | // "Contact": "a new purchase under FAR 15 (Contract)", 11 | var DOC_TYPES = { 12 | "Purchase Order": "a new purchase under FAR 13 (Purchase Order)", 13 | "Task Order": "being issued off an existing Indefinite Delivery Indefinite Quantity (ID/IQ) (Task Order)", 14 | "Call": "being ordered off an existing Blanket Purchase Agreement (BPA) (Call)", 15 | }; 16 | 17 | var SETASIDES = { 18 | "Small Business": "Small Business", 19 | "8(a) Business Development Participants": "8(a) Business Development Participants", 20 | "HUBZone Small Business Concerns": "HUBZone Small Business Concerns", 21 | "Service-disabled Veteran-owned Small Business Concerns": "Service-disabled Veteran-owned Small Business Concerns", 22 | "Economically Disadvantaged Women-owned Small Business Concerns": "Economically Disadvantaged Women-owned Small Business Concerns", 23 | "The Women-Owned Small Business Program": "The Women-Owned Small Business Program", 24 | "none": "None of the above", 25 | }; 26 | 27 | // IDIQ & BPA require 28 | // This is an RFQ for the alliant BPA #XXXXX 29 | // This is an RFQ for an award under ID/IQ #XXXXX 30 | 31 | var RequestOverview = React.createClass({ 32 | mixins: [StateMixin], 33 | getInitialState: function() { 34 | return { 35 | docType: "", 36 | agency: "", 37 | setaside: "none", 38 | baseNumber: "", 39 | baseNumberNeeded: false, 40 | programName: "", 41 | agencies: [], 42 | }; 43 | }, 44 | componentDidMount: function() { 45 | getAgencies(function(content){ 46 | this.setState({ agencies: content["data"] }); 47 | }.bind(this)); 48 | }, 49 | handleCreateRFQ: function() { 50 | createRFQ({ 51 | doc_type: this.state.docType, 52 | agency: this.state.agency, 53 | setaside: this.state.setaside, 54 | base_number: this.state.baseNumber, 55 | program_name: this.state.programName, 56 | }, function(data) { 57 | // TODO add error handler 58 | var rfqId = data.id; 59 | var url = '#/rfp/' + rfqId + '/question/1'; 60 | window.location.replace(url); 61 | }); 62 | }, 63 | updateDocType: function(event) { 64 | var base = false; 65 | var value = event.target.value; 66 | if (value === "Task Order" || value === "Call"){ 67 | base = true; 68 | } 69 | this.setState({ 70 | docType: event.target.value, 71 | baseNumberNeeded: base, 72 | }); 73 | }, 74 | render: function() { 75 | // Create the agency names list 76 | var agencyNameOptions = [( 77 | 78 | )]; 79 | for (i=0; i < this.state.agencies.length; i++) { 80 | var agency = this.state.agencies[i]; 81 | agencyNameOptions.push( 82 | 83 | ); 84 | } 85 | 86 | // Create the doc type radio list 87 | var docTypeOptions = []; 88 | for(var key in DOC_TYPES) { 89 | docTypeOptions.push( 90 |
  • 91 | 92 | 93 |
  • 94 | ); 95 | } 96 | 97 | // Create the setaside radio list 98 | var setasideOptions = []; 99 | for(var key in SETASIDES) { 100 | setasideOptions.push( 101 |
  • 102 | 103 | 104 |
  • 105 | ); 106 | } 107 | 108 | var validAgency = this.state.agency != "null" && this.state.agency != "none"; 109 | var validDocType = this.state.docType != ""; 110 | var continueDisabled = !(validAgency && validDocType); 111 | 112 | var mainStyle = { 113 | paddingLeft: 16 114 | }; 115 | 116 | return ( 117 |
    118 |
    119 |
    Preliminary Questions
    120 |
    These questions are typically answered by the CO.
    121 | 122 |

    We'll ask you some questions to understand what you want to build, 123 | and then let you download the generated documents.

    124 | 125 |
    126 |
    To begin, what agency is this for?
    127 | 130 |
    131 | 132 |
    133 |
    Program Name:
    134 | 135 |
    136 | 137 |
    138 |
    This will be ...
    139 |
    140 | This will be ... 141 |
      142 | {docTypeOptions} 143 |
    144 |
    145 |
    146 | 147 | {this.state.baseNumberNeeded? 148 |
    149 |
    Vehicle Name:
    150 | 151 |
    152 | : null} 153 | 154 |
    155 |
    Do you intend to set aside this acquisition for any of the following under FAR part 19?
    156 |
    157 | Do you intend to set aside this acquisition for any of the following under FAR part 19? 158 |
      159 | {setasideOptions} 160 |
    161 |
    162 |
    163 | 164 | 165 |
    166 |
    167 | ); 168 | }, 169 | }); 170 | 171 | module.exports = RequestOverview; -------------------------------------------------------------------------------- /app/src/questions/04_personnel.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var StateMixin = require("../state_mixin"); 3 | var EditBox = require("../edit_box"); 4 | 5 | var STATES = [ 6 | "keyPersonnelIntro", 7 | "performanceWorkStatement", 8 | "keyPersonnelRequirements", 9 | "evaluateKeyPersonnel", 10 | "notEvaluateKeyPersonnel", 11 | "clearanceRequired", 12 | "onSiteRequired", 13 | ]; 14 | 15 | var CLEARANCE_LEVELS = ["None", "Confidential", "Secret", "Top Secret"]; 16 | 17 | var Requirement = React.createClass({ 18 | mixins: [StateMixin], 19 | save: function(cb) { 20 | var data = {}; 21 | 22 | for (i=0; i < STATES.length; i++){ 23 | var stateName = STATES[i]; 24 | data[stateName] = this.state[stateName]; 25 | } 26 | var rfqId = getId(window.location.hash); 27 | put_data(4, "get_content", rfqId, data, cb); 28 | }, 29 | getInitialState: function() { 30 | var initialStates = getStates(STATES); 31 | return initialStates; 32 | }, 33 | componentDidMount: function() { 34 | var rfqId = getId(window.location.hash); 35 | get_data(4, rfqId, function(content){ 36 | var componentStates = getComponents(content["data"]); 37 | this.setState( componentStates ); 38 | }.bind(this)); 39 | }, 40 | render: function() { 41 | var clearance_options = []; 42 | for (i=0; i < CLEARANCE_LEVELS.length; i++){ 43 | var level = CLEARANCE_LEVELS[i]; 44 | clearance_options.push( 45 |
  • 46 | 47 | 48 |
  • 49 | ); 50 | } 51 | 52 | return ( 53 |
    54 |
    Contractor Personnel
    55 |
    These questions are typically answered by the PM.
    56 | 57 | 62 | 63 | 64 |
    Security Clearances
    65 | 66 |
    67 |
    What is the highest level of clearance that will be required?
    68 | 69 |
    70 | What is the highest level of clearance that will be required? 71 |
      72 | {clearance_options} 73 |
    74 |
    75 |
    76 | 77 |
    78 |
    Will any of the work be done onsite?
    79 | 80 |
    81 | Will any of the work be done onsite? 82 |
      83 |
    • 84 | 85 | 86 |
    • 87 |
    • 88 | 89 | 90 |
    • 91 |
    92 |
    93 | 94 | {(this.state.clearanceRequired == "None")? 95 |
    Contractor personnel will not be required to have a security clearance.
    : 96 |
    Some contractor personnel will be required to have a clearance at the level of {this.state.clearanceRequired}.
    97 | } 98 | {(this.state.onSiteRequired == "yes")? 99 |
    An onsite presence by the contractor will be required.
    : 100 |
    An onsite presence by the contractor will not be required.
    101 | } 102 |
    103 | 104 |
    Key Personnel Evaluation Process
    105 | 106 |
    107 |
    Do you want to require and evaluate key personnel?
    108 | 109 |
    110 | Do you want to require and evaluate key personnel? 111 |
      112 |
    • 113 | 114 | 115 |
    • 116 |
    • 117 | 118 | 119 |
    • 120 |
    121 |
    122 | 123 | {(this.state.evaluateKeyPersonnel === "yes")? 124 |
    125 |
    Key Personnel
    126 | 131 | 132 |
    : 133 |
    134 | 139 | 140 |
    141 | } 142 |
    143 | 144 |
    145 |
    Performance Work Statement
    146 | 147 | 152 | 153 |
    154 |
    155 | ); 156 | }, 157 | }); 158 | 159 | 160 | module.exports = Requirement; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #This repository is archived and deprecated. Please see the [USDS Agile Solicitation Builder](https://github.com/usds/agile-solicitation-builder) for current updates. 2 | 3 | 4 | [![Stories in Ready](https://badge.waffle.io/18F/agile-solicitation-builder.png?label=ready&title=Ready)](https://waffle.io/18F/agile-solicitation-builder) 5 | 6 | *Master:* 7 | [![Build Status](https://travis-ci.org/18F/agile-solicitation-builder.svg?branch=master)](https://travis-ci.org/18F/agile-solicitation-builder) 8 | 9 | ## About Agile Solicitation Builder (Formerly Playbook in Action) 10 | The intent of this tool is to assist in the creation of requirements documents for agile software development using best practices from the USDS Playbook and TechFAR. In the alpha release the tool can help Contracting Officer working with Program Managers develop an RFQ for a firm-fixed price procurement. 11 | 12 | 13 | ### Local Installation 14 | Clone the repository 15 | ``` 16 | git clone https://github.com/18F/agile-solicitation-builder.git 17 | ``` 18 | ### Flask app 19 | 20 | Create a [virtual environment](https://github.com/yyuu/pyenv-virtualenvwrapper) with `Python 3.5.1` 21 | To create a virtualenv setup on mac check out [this gist](https://gist.github.com/lauraGgit/06204a1bdf297ce5e08788364b0b47e0). 22 | 23 | ``` 24 | # pyenv install 3.5.1 25 | pyenv local 3.5.1 26 | mkvirtualenv asb 27 | pip install -r requirements.txt 28 | ``` 29 | 30 | ### Create the database. 31 | If you do not have postgresql installed run: 32 | ``` 33 | brew install postgres 34 | initdb /usr/local/var/postgres 35 | ``` 36 | 37 | To create an app database run: 38 | ``` 39 | createdb your_database_name 40 | export DATABASE_URL=postgresql://localhost/your_database_name 41 | ``` 42 | 43 | Replacing `your_database_name` with the db you'd like. 44 | 45 | You can then seed the database by running: 46 | ``` 47 | flask -a server.py seed_db 48 | ``` 49 | 50 | ### Install the Front end. 51 | If you plan on developing the front-end, make sure you have npm installed (`brew install npm`). Then run: 52 | ``` 53 | cd app 54 | npm install 55 | npm install -g gulp 56 | gulp 57 | cd .. 58 | ``` 59 | 60 | ### Start the app 61 | From the root directory of the project run 62 | ``` 63 | python server.py 64 | ``` 65 | 66 | 67 | ## Further Development 68 | 69 | ##### Watching changes 70 | When performing any front-end changes please run `gulp developing`. 71 | 72 | ##### Adding a new page 73 | 74 | To add a new "questions" page (all pages are listed in the right sidebar): 75 | * Create a new file in the [questions](https://github.com/18F/playbook-in-action/tree/master/app/src/questions) folder. 76 | 77 | * See [`XX_sample.js`](https://github.com/18F/playbook-in-action/blob/master/app/src/questions/XX_sample.js) to get an idea of what needs to be included in a page. 78 | 79 | 1. Update the states to reflect the data fields you would like to collect on the page. (On line 6) 80 | 81 | 2. Update the page number to order in the questions list on line 12. (This will need to match the custom components in the backend) 82 | 83 | 3. Update the name of the `React class` (line 14) to `ComponentName` (reflecting your component) and change that to the same on line 74. 84 | 85 | 4. Update the render function to reflect your states that need to be changed, and add additional components as needed. (Lines 55-70) 86 | 87 | * To make the page visible in and accessible from the side bar you must add it to [`question_list.js`](https://github.com/18F/playbook-in-action/blob/master/app/src/question_list.js). 88 | 89 | * Make sure you have run `gulp` or are running `gulp developing` to update the resulting javascript file. 90 | 91 | * Update the `create_document.py` file. 92 | 1. Add a function to add the custom text: 93 | ``` 94 | def component_name(document, rfq): 95 | document.add_heading("XX. Name of Section", level=BIG_HEADING) 96 | component_name = session.query(ContentComponent).filter_by(document_id=rfq.id).filter_by(section=XX).first() 97 | document.add_paragraph(component_name.text) 98 | 99 | return document 100 | ``` 101 | Add additional dictionary in the content_components list in `seed.py`, for each additional field (state on the front end) 102 | 103 | You will likely need to run seed.py or add sample text to the database to prevent API errors. In the future, these errors will be handled. 104 | 105 | 2. and add a line in the `create_document` function 106 | 107 | ``` 108 | document = component_name(document, rfq) 109 | ``` 110 | 3. Add the section to the `section` array in the `overview` function 111 | 112 | ##### Removing an existing page 113 | 114 | * Delete the corresponding page file from the [questions](https://github.com/18F/playbook-in-action/tree/master/app/src/questions) folder 115 | 116 | * Remove the reference to the question from [`question_list.js`](https://github.com/18F/playbook-in-action/blob/master/app/src/question_list.js). 117 | 118 | * Remove the reference from seed.py. 119 | 120 | * Remove the function from the `create_document.py` file and the `create_document` function of the same file, and from the `section` variable in the `overview` function. 121 | 122 | ##### Modifying the content 123 | 124 | Content that can be modified is created in seed.py. There are 3 types of content, ContentComponents, CustomComponents, and Deliverables. Content types are declared in [`models.py`](https://github.com/18F/agile-solicitation-builder/blob/master/models.py). 125 | 126 | To remove content you need to both remove the content object from seed.py and if it is referenced by name on a page you need to remove that reference. CustomComponents are not referenced individually so this second step is not necessary. 127 | 128 | Please note that any documents created prior to the removal or addition of new content will be incompatible and will break the site so they should be deleted as soon as the changes go live. 129 | 130 | ##### The API 131 | 132 | The code for the API can be found in [`server.py`](https://github.com/18F/agile-solicitation-builder/blob/master/server.py). Each "questions" page (found in the "questions" folder) calls a function in [`helpers.js`](https://github.com/18F/agile-solicitation-builder/blob/master/app/helpers.js). which in turn sends an ajax request to `server.py` which sends the request to the database. 133 | 134 | ##### Creating a Word Document 135 | 136 | This is managed in the file [`create_document.py`](https://github.com/18F/agile-solicitation-builder/blob/master/create_document.py). Currently everything is added to the document manually. 137 | 138 | ##### Node Dependencies 139 | This project uses the following node modules: 140 | 141 | - babel-preset-react 142 | - babelify 143 | - browserify 144 | - gulp 145 | - gulp-notify 146 | - gulp-rename 147 | - history 148 | - react 149 | - react-bootstrap 150 | - react-dom 151 | - react-flexbox 152 | - react-router 153 | - reactify 154 | - vinyl-source-stream 155 | - watchify 156 | 157 | 158 | ### Public domain 159 | 160 | This project is in the worldwide [public domain](LICENSE.md). As stated in [CONTRIBUTING](CONTRIBUTING.md): 161 | 162 | > This project is in the public domain within the United States, and copyright and related rights in the work worldwide are waived through the [CC0 1.0 Universal public domain dedication](https://creativecommons.org/publicdomain/zero/1.0/). 163 | > 164 | > All contributions to this project will be released under the CC0 dedication. By submitting a pull request, you are agreeing to comply with this waiver of copyright interest. 165 | -------------------------------------------------------------------------------- /models.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import sys, os 4 | 5 | from sqlalchemy.ext.declarative import declarative_base 6 | from sqlalchemy import Column, Integer, Text, Boolean, String, ForeignKey, create_engine 7 | from sqlalchemy.orm import sessionmaker, relationship, scoped_session 8 | from flask_sqlalchemy import SQLAlchemy 9 | from config import ProductionConfig 10 | from passlib.apps import custom_app_context as pwd_context 11 | from itsdangerous import (TimedJSONWebSignatureSerializer as Serializer, BadSignature, SignatureExpired) 12 | 13 | import seed 14 | 15 | engine = create_engine(ProductionConfig.SQLALCHEMY_DATABASE_URI) 16 | session_factory = sessionmaker(bind=engine) 17 | session = scoped_session(session_factory) 18 | 19 | 20 | Base = declarative_base() 21 | 22 | content_components = seed.content_components 23 | deliverables = seed.deliverables 24 | custom_components = seed.custom_components 25 | 26 | class User(Base): 27 | __tablename__ = 'users' 28 | 29 | id = Column(Integer, primary_key = True) 30 | username = Column(String(32), index = True) 31 | password_hash = Column(String(128)) 32 | 33 | def hash_password(self, password): 34 | self.password_hash = pwd_context.encrypt(password) 35 | 36 | def verify_password(self, password): 37 | return pwd_context.verify(password, self.password_hash) 38 | 39 | def generate_auth_token(self, expiration = 600): 40 | s = Serializer(os.environ.get('SECRET_KEY', "None"), expires_in = expiration) 41 | return s.dumps({ 'id': self.id }) 42 | 43 | @staticmethod 44 | def verify_auth_token(token): 45 | s = Serializer(os.environ.get('SECRET_KEY', "None")) 46 | try: 47 | data = s.loads(token) 48 | except SignatureExpired: 49 | return None # valid token, but expired 50 | except BadSignature: 51 | return None # invalid token 52 | user = session.query(User).get(data['id']) 53 | return user 54 | 55 | class Agency(Base): 56 | __tablename__ = 'agencies' 57 | 58 | id = Column(Integer, primary_key=True) 59 | full_name = Column(String, unique=True) 60 | abbreviation = Column(String, unique=True) 61 | 62 | def __repr__(self): 63 | return "" % (self.id, self.full_name, self.abbreviation) 64 | 65 | def to_dict(self): 66 | return {c.name: getattr(self, c.name) for c in self.__table__.columns} 67 | 68 | 69 | class RFQ(Base): 70 | __tablename__ = 'rfqs' 71 | 72 | id = Column(Integer, primary_key=True) 73 | user_id = Column(Integer, ForeignKey('users.id')) 74 | agency = Column(String) 75 | doc_type = Column(String) 76 | program_name = Column(String) 77 | setaside = Column(String) 78 | base_number = Column(String) 79 | content_components = relationship("ContentComponent") 80 | deliverables = relationship("Deliverable") 81 | custom_components = relationship("CustomComponent") 82 | 83 | def to_dict(self): 84 | return {c.name: getattr(self, c.name) for c in self.__table__.columns} 85 | 86 | def __repr__(self): 87 | return "" % (self.id, self.agency, self.doc_type, self.program_name) 88 | 89 | def __init__(self, user_id, agency, doc_type, program_name, setaside, base_number=None): 90 | # working-with-related-objects 91 | base_number_value = None 92 | if len(base_number) > 0: 93 | base_number_value = base_number 94 | 95 | # seed each section of the new document with the template content 96 | self.user_id = user_id 97 | self.agency = agency 98 | self.doc_type = doc_type 99 | self.program_name = program_name 100 | self.setaside = setaside 101 | self.base_number = base_number_value 102 | 103 | vehicle = "" 104 | 105 | agency_full_name = session.query(Agency).filter_by(abbreviation=agency).first().full_name 106 | 107 | if doc_type != "Purchase Order": 108 | vehicle = "(vehicle number " + base_number_value + ") " 109 | 110 | for section in content_components: 111 | text = section['text'] 112 | section['text'] = text.replace("{AGENCY}", agency).replace("{DOC_TYPE}", doc_type).replace("{AGENCY_FULL_NAME}", agency_full_name).replace("{PROGRAM_NAME}", program_name).replace("{VEHICLE}", vehicle) 113 | self.content_components.append(ContentComponent(**section)) 114 | 115 | for deliverable in deliverables: 116 | deliverable["text"] = str(deliverable['text']) 117 | deliverable["display"] = str(deliverable['display']) 118 | self.deliverables.append(Deliverable(**deliverable)) 119 | 120 | for component in custom_components: 121 | text = component['text'] 122 | title = component['title'] 123 | component['text'] = text.replace("{AGENCY}", agency).replace("{DOC_TYPE}", doc_type).replace("{AGENCY_FULL_NAME}", agency_full_name).replace("{PROGRAM_NAME}", program_name).replace("{VEHICLE}", vehicle) 124 | component['title'] = title.replace("{AGENCY}", agency).replace("{DOC_TYPE}", doc_type).replace("{AGENCY_FULL_NAME}", agency_full_name).replace("{PROGRAM_NAME}", program_name).replace("{VEHICLE}", vehicle) 125 | self.custom_components.append(CustomComponent(**component)) 126 | 127 | 128 | class ContentComponent(Base): 129 | __tablename__ = 'content_components' 130 | 131 | document_id = Column(Integer, ForeignKey('rfqs.id'), primary_key=True) 132 | section = Column(Integer, primary_key=True) 133 | name = Column(String, primary_key=True) 134 | text = Column(Text) 135 | 136 | def to_dict(self): 137 | return {c.name: getattr(self, c.name) for c in self.__table__.columns} 138 | 139 | def __repr__(self): 140 | return "" % (self.name, self.document_id, self.text) 141 | 142 | 143 | class Deliverable(Base): 144 | __tablename__ = 'deliverables' 145 | 146 | id = Column(Integer, primary_key=True) 147 | document_id = Column(Integer, ForeignKey('rfqs.id'), primary_key=True) 148 | name = Column(String, primary_key=True) 149 | display = Column(String) 150 | value = Column(String) 151 | text = Column(Text) 152 | 153 | def to_dict(self): 154 | return {c.name: getattr(self, c.name) for c in self.__table__.columns} 155 | 156 | def __repr__(self): 157 | return "" % (self.name, self.document_id, self.text, self.value, self.display) 158 | 159 | 160 | class AdditionalClin(Base): 161 | __tablename__ = 'additional_clins' 162 | 163 | id = Column(Integer, primary_key=True) 164 | document_id = Column(Integer, ForeignKey('rfqs.id')) 165 | row1 = Column(Text) 166 | row2 = Column(Text) 167 | row3a = Column(Text) 168 | row3b = Column(Text) 169 | row4a = Column(Text) 170 | row4b = Column(Text) 171 | row5a = Column(Text) 172 | row5b = Column(Text) 173 | row6a = Column(Text) 174 | row6b = Column(Text) 175 | 176 | def to_dict(self): 177 | return {c.name: getattr(self, c.name) for c in self.__table__.columns} 178 | 179 | def __repr__(self): 180 | return "" % (self.document_id, self.row1, self.row2, self.row3a) 181 | 182 | 183 | class CustomComponent(Base): 184 | __tablename__ = 'custom_components' 185 | 186 | id = Column(Integer, primary_key=True) 187 | document_id = Column(Integer, ForeignKey('rfqs.id')) 188 | title = Column(String) 189 | name = Column(String) 190 | text = Column(Text) 191 | section = Column(Integer) 192 | 193 | def to_dict(self): 194 | return {c.name: getattr(self, c.name) for c in self.__table__.columns} 195 | 196 | def __repr__(self): 197 | return "" % (self.document_id, self.title, self.text) 198 | -------------------------------------------------------------------------------- /resources.py: -------------------------------------------------------------------------------- 1 | from models import User, Agency, RFQ, ContentComponent, AdditionalClin, CustomComponent, session, Deliverable 2 | from flask import jsonify, request, g 3 | from flask_restful import Resource, reqparse, abort 4 | from flask.ext.httpauth import HTTPBasicAuth 5 | auth = HTTPBasicAuth() 6 | 7 | class Users(Resource): 8 | def get(self): 9 | users = session.query(User).order_by(User.username).all() 10 | return jsonify(data=[{'id': u.id, 'username': u.username} for u in users]) 11 | 12 | def post(self): 13 | data = request.get_json() 14 | username = data['username'] 15 | password = data['password'] 16 | if username is None or password is None: 17 | abort(400) # missing arguments 18 | if session.query(User).filter_by(username = username).first() is not None: 19 | abort(400) # existing user 20 | user = User(username = username) 21 | user.hash_password(password) 22 | session.add(user) 23 | session.commit() 24 | if session.query(User).filter_by(username = username).first() is not None: 25 | return jsonify({ 'username': user.username, 'id': user.id }) 26 | else: 27 | return jsonify({'error': "The user request was not completed."}) 28 | 29 | class Agencies(Resource): 30 | def get(self): 31 | agencies = session.query(Agency).order_by(Agency.full_name).all() 32 | return jsonify(data=[a.to_dict() for a in agencies]) 33 | 34 | 35 | class Data(Resource): 36 | decorators = [auth.login_required] 37 | def get(self, rfq_id, section_id): 38 | content = session.query(ContentComponent).filter_by(document_id=rfq_id).filter_by(section=int(section_id)) 39 | return jsonify(data=dicts_to_dict([c.to_dict() for c in content], "name")) 40 | 41 | def put(self, rfq_id, section_id): 42 | parser = reqparse.RequestParser() 43 | parser.add_argument('data') 44 | data = request.get_json()['data'] 45 | for key in data: 46 | component = session.query(ContentComponent).filter_by(document_id=rfq_id).filter_by(name=key).first() 47 | component.text = data[key] 48 | session.merge(component) 49 | session.commit() 50 | 51 | # this needs to be done client side to allow for jumping between sections 52 | if section_id < 10: 53 | url = '#/rfp/' + str(rfq_id) + '/question/' + str(int(section_id) + 1) 54 | else: 55 | url = "#/rfp/" + str(rfq_id) + "/results" 56 | return jsonify({"url": url}) 57 | 58 | 59 | class Deliverables(Resource): 60 | decorators = [auth.login_required] 61 | def get(self, rfq_id): 62 | deliverables = session.query(Deliverable).filter_by(document_id=rfq_id).order_by(Deliverable.id).all() 63 | return jsonify(data=[d.to_dict() for d in deliverables]) 64 | 65 | def put(self, rfq_id): 66 | data = request.get_json()['data'] 67 | for item in data: 68 | deliverable = session.query(Deliverable).filter_by(document_id=rfq_id).filter_by(name=item["name"]).first() 69 | deliverable.value = item["value"] 70 | deliverable.text = item["text"] 71 | session.merge(deliverable) 72 | session.commit() 73 | 74 | 75 | class Clin(Resource): 76 | decorators = [auth.login_required] 77 | def get(self, rfq_id): 78 | clins = session.query(AdditionalClin).filter_by(document_id=rfq_id).all() 79 | return jsonify(data=[c.to_dict() for c in clins]) 80 | 81 | def post(self, rfq_id): 82 | data = request.get_json()["data"] 83 | 84 | row1 = data['row1'] 85 | row2 = data['row2'] 86 | row3a = data['row3a'] 87 | row3b = data['row3b'] 88 | row4a = data['row4a'] 89 | row4b = data['row4b'] 90 | row5a = data['row5a'] 91 | row5b = data['row5b'] 92 | row6a = data['row6a'] 93 | row6b = data['row6b'] 94 | 95 | additional_clin = AdditionalClin(document_id=int(rfq_id), row1=row1, row2=row2, row3a=row3a, row3b=row3b, row4a=row4a, row4b=row4b, row5a=row5a, row5b=row5b, row6a=row6a, row6b=row6b) 96 | session.add(additional_clin) 97 | session.commit() 98 | 99 | clins = session.query(AdditionalClin).filter_by(document_id=rfq_id).all() 100 | return jsonify(data=[c.to_dict() for c in clins]) 101 | 102 | 103 | class CustomComponents(Resource): 104 | decorators = [auth.login_required] 105 | def get(self, rfq_id, section_id): 106 | components = session.query(CustomComponent).filter_by(document_id=rfq_id).filter_by(section=section_id).order_by(CustomComponent.id).all() 107 | return jsonify(data=[c.to_dict() for c in components]) 108 | 109 | def put(self, rfq_id, section_id): 110 | data = request.get_json()['data'] 111 | for key in data: 112 | component = session.query(CustomComponent).filter_by(document_id=rfq_id).filter_by(name=key).first() 113 | component.text = data[key] 114 | session.merge(component) 115 | session.commit() 116 | 117 | def post(self, rfq_id, section_id): 118 | data = request.get_json()["data"] 119 | title = data['title'] 120 | text = data['text'] 121 | 122 | # give component a name 123 | current_components = session.query(CustomComponent).filter_by(document_id=rfq_id).filter_by(section=section_id).all() 124 | name = "component" + str(len(current_components) + 1) 125 | 126 | custom_component = CustomComponent(document_id=int(rfq_id), section=int(section_id), name=name, title=title, text=text) 127 | 128 | session.add(custom_component) 129 | session.commit() 130 | 131 | components = session.query(CustomComponent).filter_by(document_id=rfq_id).filter_by(section=int(section_id)).all() 132 | return jsonify(data=[c.to_dict() for c in components]) 133 | 134 | 135 | class Create(Resource): 136 | decorators = [auth.login_required] 137 | def get(self): 138 | rfqs = session.query(RFQ).filter_by(user_id=g.user.id).all() 139 | return jsonify(data=[r.to_dict() for r in rfqs]) 140 | 141 | def post(self, **kwargs): 142 | parser = reqparse.RequestParser() 143 | parser.add_argument('agency') 144 | parser.add_argument('doc_type') 145 | parser.add_argument('setaside') 146 | parser.add_argument('base_number') 147 | parser.add_argument('program_name') 148 | 149 | args = parser.parse_args() 150 | 151 | agency = args['agency'] 152 | doc_type = args['doc_type'] 153 | program_name = args['program_name'] 154 | setaside = args['setaside'] 155 | base_number = args['base_number'] 156 | 157 | rfq = RFQ(user_id=g.user.id, agency=agency, doc_type=doc_type, program_name=program_name, setaside=setaside, base_number=base_number) 158 | session.add(rfq) 159 | session.commit() 160 | 161 | return jsonify({'id': rfq.id}) 162 | 163 | 164 | class DeleteRFQ(Resource): 165 | decorators = [auth.login_required] 166 | def delete(self, rfq_id): 167 | 168 | deliverables = session.query(Deliverable).filter_by(document_id=rfq_id).all() 169 | for d in deliverables: 170 | session.delete(d) 171 | 172 | content_components = session.query(ContentComponent).filter_by(document_id=rfq_id).all() 173 | for c in content_components: 174 | session.delete(c) 175 | 176 | custom_components = session.query(CustomComponent).filter_by(document_id=rfq_id).all() 177 | for c in custom_components: 178 | session.delete(c) 179 | 180 | additional_clins = session.query(AdditionalClin).filter_by(document_id=rfq_id).all() 181 | for a in additional_clins: 182 | session.delete(a) 183 | 184 | rfq = session.query(RFQ).filter_by(id=int(rfq_id)).first() 185 | 186 | session.delete(rfq) 187 | session.commit() 188 | message = "RFQ #" + str(rfq_id) + " deleted." 189 | 190 | return jsonify({'message': message}) 191 | 192 | def dicts_to_dict(dicts, key): 193 | new_dict = {} 194 | for i, d in enumerate(dicts): 195 | new_key = d[key] 196 | new_dict[new_key] = dicts[i]['text'] 197 | return new_dict 198 | -------------------------------------------------------------------------------- /app/assets/css/style.scss: -------------------------------------------------------------------------------- 1 | // Place list of your partial imports below, located in assets/_scss. 2 | 3 | // @import 'all'; 4 | $medium-screen: 600px; 5 | $large-screen: 1200px; 6 | $width-nav-sidebar: 250px; 7 | $color-primary: #0071bc; 8 | $color-primary-darker: #205493; 9 | 10 | $color-white: #ffffff; 11 | $color-base: #212121; 12 | $color-black: #000000; 13 | 14 | $color-gray-dark: #323a45; 15 | $color-gray: #5b616b; // lighten($color-gray-dark, 20%) 16 | $color-gray-medium: #757575; // lightest gray that passes color contrast 17 | $color-gray-light: #aeb0b5; // lighten($color-gray-dark, 60%) 18 | $color-gray-lighter: #d6d7d9; // lighten($color-gray-dark, 80%) 19 | $color-gray-lightest: #f1f1f1; // lighten($color-gray-dark, 91%) 20 | 21 | $med-width-plus-side-nav: $medium-screen + $width-nav-sidebar; 22 | 23 | @mixin media($bp) { 24 | @media screen and (min-width: #{$bp}) { 25 | @content; 26 | } 27 | } 28 | 29 | // @mixin transition{ 30 | // transition: all 0.2s ease-in-out; 31 | // } 32 | 33 | .Popover-body { 34 | display: inline-flex; 35 | flex-direction: column; 36 | padding: 2rem 4rem; 37 | background: hsl(0, 0%, 27%); 38 | color: white; 39 | border-radius: 0.3rem; 40 | } 41 | 42 | .Popover-tipShape { 43 | fill: hsl(0, 0%, 27%); 44 | } 45 | 46 | .menu-btn { 47 | @media all and ($med-width-plus-side-nav) { 48 | display: none; 49 | } 50 | 51 | padding: 1.5rem null; 52 | // display: inline; 53 | float: left; 54 | margin-top: -4px; 55 | color: $color-white; 56 | background-color: $color-primary; 57 | font-size: 1.5rem; 58 | width: 15%; 59 | text-align: center; 60 | 61 | &:hover { 62 | text-decoration: none; 63 | color: $color-white; 64 | background-color: $color-primary-darker; 65 | } 66 | 67 | &:visited { 68 | color: $color-white; 69 | } 70 | } 71 | 72 | .overlay { 73 | // @include position(fixed, 0); 74 | 75 | background: $color-black; 76 | opacity: 0; 77 | visibility: hidden; 78 | z-index: 9999; 79 | 80 | &.is-visible { 81 | visibility: visible; 82 | } 83 | } 84 | 85 | // Based on code by Diego Eis 86 | 87 | // Sidebar Nav --------- // 88 | 89 | .sidenav { 90 | // @include position(fixed, $site-top null 0 0); 91 | width: $width-nav-sidebar; 92 | border-right: 1px solid $color-gray-light; 93 | padding: 5rem 3rem 3rem 3rem; 94 | overflow: auto; 95 | display: none; 96 | z-index: -1; 97 | $med-width-plus-side-nav: $medium-screen + $width-nav-sidebar; 98 | &.menu-content { 99 | @media (max-width: $med-width-plus-side-nav) { 100 | $sliding-panel-width: 220px; 101 | 102 | // @include position(fixed, 0 auto 0 0); 103 | // @include size($sliding-panel-width 100%); 104 | // @include transform(translateX(- $sliding-panel-width)); 105 | background: $color-white; 106 | -webkit-overflow-scrolling: touch; 107 | overflow-y: auto; 108 | z-index: 999999; 109 | display: block; 110 | 111 | &.is-visible { 112 | // @include transition(all 0.25s linear); 113 | // @include transform(translateX(0)); 114 | } 115 | } 116 | } 117 | 118 | .lt-ie9 & { 119 | width: 25%; 120 | } 121 | 122 | .usa-sidenav-sub_list { 123 | display: none; 124 | } 125 | 126 | @include media($medium-screen + $width-nav-sidebar) { 127 | display: block; 128 | } 129 | } 130 | 131 | .visual-style .sidenav .visual-style-sublist { 132 | display: block; 133 | 134 | // scss-lint:disable SelectorDepth 135 | ul { 136 | display: block; 137 | } 138 | } 139 | 140 | .form-controls .sidenav .form-controls-sublist { 141 | display: block; 142 | } 143 | 144 | .form-templates .sidenav .form-templates-sublist { 145 | display: block; 146 | } 147 | 148 | .footers .sidenav .footers-sublist { 149 | display: block; 150 | } 151 | 152 | .header { 153 | border-bottom: solid 1px #aeb0b5; 154 | margin-bottom: 15px; 155 | } 156 | 157 | .term { 158 | font-weight: 500; 159 | } 160 | 161 | .mount { 162 | font-family: 'Source Sans Pro', sans-serif; 163 | font-weight: 400; 164 | font-size: 16px; 165 | line-height: 1.5em/26px; 166 | 167 | input { 168 | font-size: 15px; 169 | } 170 | 171 | select { 172 | font-size: 15px; 173 | } 174 | 175 | textarea { 176 | font-size: 16px; 177 | } 178 | } 179 | 180 | .nav > li > a { 181 | padding: 7px 15px; 182 | } 183 | 184 | a { 185 | color: #0071bc; 186 | } 187 | 188 | h1 { 189 | font-family: 'Merriweather', serif; 190 | font-weight: 700; 191 | font-size: 25px; 192 | line-height: 1.3em/26px; 193 | color: #5b616b; 194 | } 195 | 196 | .affix { 197 | position: fixed; 198 | } 199 | 200 | form .usa-grid { 201 | margin-left: -30px; 202 | } 203 | 204 | .edit { 205 | color: #337ab7; 206 | cursor: pointer; 207 | cursor: hand; 208 | } 209 | 210 | .edit-box .edit-content > *:first-child { 211 | margin-top: 0; 212 | } 213 | 214 | .edit-box .edit-content > *:last-child { 215 | margin-bottom: 0; 216 | } 217 | 218 | button { 219 | .add { 220 | margin-bottom: 10px; 221 | } 222 | } 223 | 224 | .clin { 225 | border: 1px solid; 226 | } 227 | 228 | .fake-table { 229 | margin-top: 5px; 230 | margin-bottom: 10px; 231 | } 232 | 233 | .row { 234 | border-top: 0; 235 | } 236 | 237 | .btn-toolbar { 238 | margin-top: 20px; 239 | } 240 | 241 | .table-content { 242 | margin-top: 5px; 243 | margin-bottom: 5px; 244 | } 245 | 246 | .yes-no { 247 | margin-right: 5px; 248 | margin-left: 5px; 249 | } 250 | 251 | .table-wrapper { 252 | border: solid 1px; 253 | } 254 | 255 | .page-heading { 256 | font-family: 'Merriweather', serif; 257 | font-weight: 700; 258 | font-size: 30px; 259 | margin-bottom: 5px; 260 | line-height: 1.3em/26px; 261 | color: #212121; 262 | } 263 | 264 | .section-heading { 265 | font-weight: 700; 266 | font-family: 'Merriweather', serif; 267 | font-size: 20px; 268 | line-height: 1.3em/22px; 269 | } 270 | 271 | .sub-heading { 272 | color: #205493; 273 | font-weight: 700; 274 | margin: 25px 0 10px 0; 275 | font-family: 'Merriweather', serif; 276 | font-size: 20px; 277 | line-height: 1.3em/22px; 278 | } 279 | 280 | .question { 281 | margin: 5vh auto; 282 | } 283 | 284 | .question-text { 285 | margin: 25px 0 5px 0; 286 | font-size: 16px; 287 | font-family: 'Merriweather', serif; 288 | color: #323a45; 289 | font-weight: 700; 290 | } 291 | 292 | .question-description { 293 | font-style: italic; 294 | margin: 0 0 10px 0; 295 | font-size: 95%; 296 | } 297 | 298 | .responder-instructions { 299 | font-style: italic; 300 | margin-bottom: 10px; 301 | font-size: 95%; 302 | } 303 | 304 | .resulting-text { 305 | font-family: 'Times', serif; 306 | border: solid 3px #aeb0b5; 307 | font-weight: 500; 308 | font-size: 105%; 309 | color: #212121; 310 | background-color: #ffffff; 311 | padding: 5px; 312 | margin-bottom: 10px; 313 | } 314 | 315 | .edit-box { 316 | margin: 5px 0 10px 0; 317 | } 318 | 319 | .short-response { 320 | width: 150px; 321 | } 322 | 323 | .usa-width-one-sixth.sidebar-nav { 324 | margin-left: 28px; 325 | } 326 | 327 | .medium-response { 328 | width: 300px; 329 | } 330 | 331 | .long-response { 332 | width: 100%; 333 | } 334 | 335 | .additional-clin { 336 | max-width: none; 337 | } 338 | 339 | textarea { 340 | max-width: none; 341 | height: auto; 342 | margin: 10px 0; 343 | } 344 | 345 | .usa-disclaimer { 346 | background-color: #f1f1f1; 347 | font-size: 15px; 348 | padding: 5px; 349 | margin: -8px -23px 0 -23px; 350 | } 351 | 352 | .guidance-text { 353 | margin: 0 0 10px 0; 354 | } 355 | 356 | .usa-disclaimer-official { 357 | padding-left: 15px; 358 | } 359 | 360 | .usa-flag_icon { 361 | margin: 0 5px 0 15px; 362 | } 363 | 364 | .usa-disclaimer-stage { 365 | float: right; 366 | padding-right: 15px; 367 | } 368 | 369 | .top-right-auth-button { 370 | float: right; 371 | } 372 | 373 | // #sidenav.affix { 374 | // top: 0; 375 | // } 376 | 377 | .Popover-body { 378 | display: inline-flex; 379 | flex-direction: column; 380 | padding: 2rem 4rem; 381 | background: white; 382 | color: black; 383 | border-radius: 0.3rem; 384 | border: 1px solid #888; 385 | box-shadow: 10px 10px 25px #555; 386 | width: 400px; 387 | } 388 | 389 | .Popover-tipShape { 390 | fill: black; 391 | } 392 | 393 | // aside { 394 | // } 395 | 396 | @media screen and (min-width: $medium-screen) and (max-width: $large-screen) { 397 | .side-bar { 398 | display: none !important; 399 | } 400 | } 401 | -------------------------------------------------------------------------------- /create_document.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | 4 | from docx import Document 5 | from models import Agency, RFQ, ContentComponent, Deliverable, CustomComponent, session 6 | 7 | 8 | BIG_HEADING = 1 9 | SUB_HEADING = 2 10 | SMALL_HEADING = 4 11 | 12 | user_dict = { 13 | "external_people": "External People/The Public", 14 | "external_it": "External IT/Developers", 15 | "internal_people": "Internal People/Government Employees", 16 | "internal_it": "Internal IT/Developers", 17 | } 18 | 19 | 20 | def make_dict(components): 21 | component_dict = {} 22 | for component in components: 23 | component_dict[component.name] = component.text 24 | return component_dict 25 | 26 | 27 | def make_custom_component_list(components): 28 | custom_component_list = [] 29 | for component in components: 30 | this_component = {} 31 | this_component['name'] = component.name 32 | this_component['text'] = component.text 33 | this_component['title'] = component.title 34 | custom_component_list.append(this_component) 35 | 36 | return custom_component_list 37 | 38 | 39 | def get_users(cc, user_types): 40 | users = [] 41 | for user in user_types: 42 | if cc[user] == "true": 43 | users.append(user) 44 | return users 45 | 46 | # Add global counter for headings 47 | # def section_heading(document, headingTitle): 48 | # title = str(sectionNumber)+'. '+headingTitle 49 | # document.add_heading(headingTitle, level=BIG_HEADING) 50 | # global sectionNumberCounter 51 | # sectionNumberCounter = sectionNumberCounter+1 52 | # return document 53 | 54 | def overview(document, rfq): 55 | # table of contents & basic info 56 | 57 | agency_full_name = session.query(Agency).filter_by(abbreviation=rfq.agency).first().full_name 58 | title = "RFQ for the " + agency_full_name 59 | document.add_heading(title, level=BIG_HEADING) 60 | doc_date = str(datetime.date.today()) 61 | document.add_heading(doc_date, level=3) 62 | 63 | # table of contents 64 | document.add_heading("Table of Contents", level=SUB_HEADING) 65 | sections = ["Definitions", "Services", "Statement of Objectives", "Personnel Requirements", "Inspection and Delivery", "Government Roles", "Special Requirements", "Additional Contract Clauses", "Appendix"] 66 | for section in sections: 67 | document.add_paragraph(section, style='ListNumber') 68 | 69 | text = "Note: All sections of this RFQ will be incorporated into the contract except the Statement of Objectives, Instructions, and Evaluation Factors." 70 | document.add_paragraph(text) 71 | document.add_page_break() 72 | 73 | return document 74 | 75 | 76 | def definitions(document, rfq): 77 | 78 | document.add_heading("1. Definitions", level=BIG_HEADING) 79 | all_definitions = session.query(ContentComponent).filter_by(document_id=rfq.id).filter_by(section=1).first() 80 | for definition in all_definitions.text.split("\n\n"): 81 | document.add_paragraph(definition) 82 | 83 | return document 84 | 85 | 86 | def services(document, rfq): 87 | document.add_heading("2. Services", level=BIG_HEADING) 88 | content_components = session.query(ContentComponent).filter_by(document_id=rfq.id).filter_by(section=2).all() 89 | # include vendor number 90 | cc = make_dict(content_components) 91 | optionPeriods = cc["optionPeriods"] 92 | document.add_heading("Brief Description of Services & Type of Contract", level=SUB_HEADING) 93 | document.add_paragraph(cc["descriptionOfServices"]) 94 | document.add_paragraph(cc["naicsText"]) 95 | 96 | document.add_heading("Budget", level=SUB_HEADING) 97 | max_text = "The government is willing to invest a maximum budget of $" + cc["maxBudget"] + " in this endeavor." 98 | document.add_paragraph(max_text) 99 | 100 | # travel 101 | if cc["travelRequirement"] == "yes": 102 | travel_text = "The Government anticipates travel will be required under this effort. Contractor travel expenses will not exceed $" + cc["travelBudget"] + "." 103 | document.add_paragraph(travel_text) 104 | document.add_paragraph(cc["travelLanguage"]) 105 | else: 106 | document.add_paragraph("The Government does not anticipate significant travel under this effort.") 107 | 108 | # @TODO make top column bold, add award fee/incentive information (if applicable) 109 | # base period 110 | document.add_heading("Contract Line Item Number (CLIN) Format", level=SUB_HEADING) 111 | document.add_paragraph("\n") 112 | 113 | table = document.add_table(rows=2, cols=1) 114 | table.style = 'TableGrid' 115 | table.rows[0].cells[0].text = "Base Period: " + str(cc["basePeriodDurationNumber"]) + ' ' + cc["basePeriodDurationUnit"] 116 | table.rows[1].cells[0].text = "CLIN 0001, FFP- Completion - The Contractor shall provide services for the Government in accordance with the Performance Work Statement (PWS)" 117 | 118 | table = document.add_table(rows=4, cols=2) 119 | table.style = 'TableGrid' 120 | table.rows[0].cells[0].text = "Iteration Period of Performance" 121 | table.rows[0].cells[1].text = cc["iterationPoPNumber"] + ' ' + cc["iterationPoPUnit"] 122 | table.rows[1].cells[0].text = "Price Per Iteration" 123 | table.rows[1].cells[1].text = "$XXXXX (Vendor Completes)" 124 | table.rows[2].cells[0].text = "Period of Performance" 125 | table.rows[2].cells[1].text = cc["basePeriodDurationNumber"] + ' ' + cc["basePeriodDurationUnit"] 126 | table.rows[3].cells[0].text = "Firm Fixed Price (Completion):" 127 | table.rows[3].cells[1].text = "$XXXXX (Vendor Completes)" 128 | # @TODO if base fee, add base fee clin row 129 | 130 | document.add_paragraph("\n") 131 | 132 | # option periods 133 | for i in range(1, int(optionPeriods)+1): 134 | table = document.add_table(rows=2, cols=1) 135 | table.style = 'TableGrid' 136 | table.rows[0].cells[0].text = "Option Period " + str(i) + ": " + str(cc["optionPeriodDurationNumber"]) + ' ' + cc["optionPeriodDurationUnit"] 137 | table.rows[1].cells[0].text = "CLIN " + str(i) + "0001, FFP- Completion - The Contractor shall provide services for the Government in accordance with the Performance Work Statement (PWS)" 138 | 139 | table = document.add_table(rows=4, cols=2) 140 | table.style = 'TableGrid' 141 | table.rows[0].cells[0].text = "Iteration Period of Performance" 142 | table.rows[0].cells[1].text = cc["iterationPoPNumber"] + ' ' + cc["iterationPoPUnit"] 143 | table.rows[1].cells[0].text = "Price Per Iteration" 144 | table.rows[1].cells[1].text = "$XXXXX (Vendor Completes)" 145 | table.rows[2].cells[0].text = "Period of Performance" 146 | table.rows[2].cells[1].text = cc["optionPeriodDurationNumber"] + ' ' + cc["optionPeriodDurationUnit"] 147 | table.rows[3].cells[0].text = "Firm Fixed Price (Completion):" 148 | table.rows[3].cells[1].text = "$XXXXX (Vendor Completes)" 149 | # @TODO if option fee, add option fee clin row 150 | 151 | document.add_paragraph("\n") 152 | 153 | # @TODO add custom CLIN(s) 154 | 155 | document.add_heading("Payment Schedule", level=SUB_HEADING) 156 | document.add_paragraph(cc["paymentSchedule"]) 157 | 158 | return document 159 | 160 | 161 | def objectives(document, rfq): 162 | document.add_heading("3. Objectives", level=BIG_HEADING) 163 | content_components = session.query(ContentComponent).filter_by(document_id=rfq.id).filter_by(section=3).all() 164 | cc = make_dict(content_components) 165 | document.add_heading("General Background", level=SUB_HEADING) 166 | if len(cc["generalBackground"]) > 0: 167 | document.add_paragraph(cc["generalBackground"]) 168 | else: 169 | document.add_paragraph("Please provide several paragraphs about your project's history, mission, and current state.") 170 | 171 | document.add_heading("Program History", level=SUB_HEADING) 172 | if len(cc["programHistory"]) > 0: 173 | document.add_paragraph(cc["programHistory"]) 174 | else: 175 | document.add_paragraph("If you have any information about the current vendors and specific technology being used please provide it here.") 176 | 177 | document.add_heading("Users", level=SUB_HEADING) 178 | user_types = ["external_people", "external_it", "internal_people", "internal_it"] 179 | users = get_users(cc, user_types) 180 | if len(users) == 0: 181 | document.add_paragraph("The primary users may include any of the following:") 182 | for i, user in enumerate(user_dict): 183 | document.add_paragraph(str(i+1) + ". " + user_dict[user]) 184 | 185 | else: 186 | document.add_paragraph("The primary users will include the following:") 187 | for i, user in enumerate(users): 188 | document.add_paragraph(str(i+1) + ". " + user_dict[user]) 189 | 190 | # for user in users, add text of each user's needs 191 | needs = ['external_people_needs', 'external_it_needs', 'internal_it_needs', 'internal_people_needs'] 192 | 193 | document.add_heading("User Research", level=SUB_HEADING) 194 | user_research_options = { 195 | "done": "Research has already been conducted, either internally or by another vendor.", 196 | "internal": "We intend to conduct user research internally prior to the start date of this engagement.", 197 | "vendor": "The vendor will be responsible for the user research.", 198 | } 199 | 200 | if cc["userResearchStrategy"] == "vendor": 201 | document.add_paragraph(user_research_options["vendor"]) 202 | document.add_heading("Understand What People Need", level=SUB_HEADING) 203 | document.add_paragraph(cc["whatPeopleNeed"]) 204 | 205 | document.add_heading("Address the whole experience, from start to finish", level=SUB_HEADING) 206 | document.add_paragraph(cc["startToFinish"]) 207 | 208 | if cc["userResearchStrategy"] == "done": 209 | document.add_paragraph(user_research_options["done"]) 210 | 211 | if cc["userResearchStrategy"] == "internal": 212 | document.add_paragraph(user_research_options["internal"]) 213 | 214 | if cc["userResearchStrategy"] == "none": 215 | pass 216 | 217 | document.add_paragraph(cc['userAccess']) 218 | 219 | document.add_heading("Universal Requirements", level=BIG_HEADING) 220 | 221 | document.add_heading("Build the service using agile and iterative practices", level=SUB_HEADING) 222 | document.add_paragraph(cc["agileIterativePractices"]) 223 | 224 | document.add_heading("Make it simple and intuitive", level=SUB_HEADING) 225 | document.add_paragraph(cc["simpleAndIntuitive"]) 226 | 227 | document.add_heading("Use data to drive decisions", level=SUB_HEADING) 228 | document.add_paragraph(cc["dataDrivenDecisions"]) 229 | 230 | document.add_heading("Specific Tasks and Deliverables", level=SUB_HEADING) 231 | text = "This " + rfq.doc_type + " will require the following services:" 232 | document.add_paragraph(text) 233 | 234 | deliverables = session.query(Deliverable).filter_by(document_id=rfq.id).filter_by(value="true").all() 235 | for deliverable in deliverables: 236 | document.add_paragraph(" " + deliverable.display) 237 | 238 | 239 | document.add_heading("Deliverables", level=SUB_HEADING) 240 | document.add_paragraph(cc["definitionOfDone"]) 241 | 242 | for deliverable in deliverables: 243 | document.add_heading(deliverable.display, level=SMALL_HEADING) 244 | document.add_paragraph(deliverable.text) 245 | 246 | document.add_heading("Place of Performance", level=SUB_HEADING) 247 | if cc['locationRequirement'] == "no": 248 | document.add_paragraph("The contractor is not required to have a full-time working staff presence on-site.") 249 | 250 | else: 251 | if len(cc['locationText']) > 0: 252 | location = cc["locationText"] 253 | else: 254 | location = "[LOCATION HERE]" 255 | 256 | location_text = "The contractor shall have a full-time working staff presence at " + location + ". The contractor shall have additional facilities to perform contract functions as necessary." 257 | document.add_paragraph(location_text) 258 | document.add_paragraph(cc["offSiteDevelopmentCompliance"]) 259 | 260 | document.add_heading("Kick Off Meeting", level=SUB_HEADING) 261 | kickoff_text = "" 262 | if cc["kickOffMeeting"] == "none": 263 | kickoff_text = "A formal kick-off meeting will not be required." 264 | if cc["kickOffMeeting"] == "in-person": 265 | kickoff_text = cc["kickOffMeetingInPerson"] 266 | if cc["kickOffMeeting"] == "remote": 267 | kickoff_text = cc["kickOffMeetingRemote"] 268 | 269 | document.add_paragraph(kickoff_text) 270 | 271 | return document 272 | 273 | 274 | def personnel(document, rfq): 275 | document.add_heading("4. Key Personnel", level=BIG_HEADING) 276 | content_components = session.query(ContentComponent).filter_by(document_id=rfq.id).filter_by(section=4).all() 277 | cc = make_dict(content_components) 278 | 279 | document.add_paragraph(cc["keyPersonnelIntro"]) 280 | 281 | document.add_heading("Security Clearance and Onsite Presence", level=SUB_HEADING) 282 | if cc["clearanceRequired"] == "None": 283 | document.add_paragraph("Contractor personnel will not be required to have a security clearance.") 284 | else: 285 | document.add_paragraph("Some contractor personnel will be required to have a clearance at the level of " + cc["clearanceRequired"] + ".") 286 | 287 | if cc["onSiteRequired"] == "yes": 288 | document.add_paragraph("An onsite presence by the contractor will be required.") 289 | else: 290 | document.add_paragraph("An onsite presence by the contractor will not be required.") 291 | 292 | document.add_heading("Key Personnel Evaluation", level=SUB_HEADING) 293 | 294 | if cc["evaluateKeyPersonnel"] == "yes": 295 | document.add_paragraph(cc["keyPersonnelRequirements"]) 296 | else: 297 | document.add_paragraph(cc["notEvaluateKeyPersonnel"]) 298 | 299 | document.add_heading("Performance Work Statement", level=SUB_HEADING) 300 | document.add_paragraph(cc["performanceWorkStatement"]) 301 | 302 | return document 303 | 304 | 305 | def invoicing(document, rfq): 306 | document.add_heading("5. Invoicing & Funding", level=BIG_HEADING) 307 | 308 | content_components = session.query(ContentComponent).filter_by(document_id=rfq.id).filter_by(section=5).all() 309 | cc = make_dict(content_components) 310 | 311 | document.add_paragraph(cc["invoicing"]) 312 | 313 | document.add_paragraph("The Contractor shall submit an original invoice for payment to the following office:") 314 | if len(cc['billingAddress']) < 1: 315 | document.add_paragraph("ADD BILLING ADDRESS HERE") 316 | else: 317 | document.add_paragraph(cc["billingAddress"]) 318 | document.add_paragraph(cc["duplicateInvoice"]) 319 | 320 | return document 321 | 322 | 323 | def inspection_and_delivery(document, rfq): 324 | document.add_heading("6. Inspection and Delivery", level=BIG_HEADING) 325 | 326 | content_components = session.query(ContentComponent).filter_by(document_id=rfq.id).filter_by(section=6).all() 327 | cc = make_dict(content_components) 328 | 329 | document.add_heading("Overview", level=SUB_HEADING) 330 | document.add_paragraph(cc["guidingPrinciples"]) 331 | 332 | document.add_heading("Delivery and Timing", level=SUB_HEADING) 333 | document.add_paragraph(cc["inspectionOverview"]) 334 | 335 | document.add_heading("Late Delivery", level=SUB_HEADING) 336 | document.add_paragraph(cc["lateDelivery"]) 337 | 338 | document.add_heading("Collaboration Environment", level=SUB_HEADING) 339 | 340 | if cc["workspaceExists"] == "yes": 341 | if len(cc["workspaceName"]) > 0: 342 | document.add_paragraph(rfq.agency + " is currently using " + cc["workspaceName"] + " as their primary collaborative workspace tool. The contractor is required to establish a collaborative workspace using either this tool or another that both the contractor and the CO can agree upon.") 343 | 344 | document.add_paragraph(cc["deliveringDeliverables"]) 345 | 346 | document.add_heading("Transition Activities", level=SUB_HEADING) 347 | document.add_paragraph(cc["transitionActivities"]) 348 | 349 | return document 350 | 351 | 352 | def government_roles(document, rfq): 353 | document.add_heading("7. Government Roles", level=BIG_HEADING) 354 | 355 | content_components = session.query(ContentComponent).filter_by(document_id=rfq.id).filter_by(section=7).all() 356 | cc = make_dict(content_components) 357 | 358 | custom_components = session.query(CustomComponent).filter_by(document_id=rfq.id).filter_by(section=7).order_by(CustomComponent.id).all() 359 | 360 | document.add_paragraph(cc["stakeholderIntro"]) 361 | 362 | component_list = make_custom_component_list(custom_components) 363 | 364 | for component in component_list: 365 | document.add_heading(component['title'], level=SUB_HEADING) 366 | document.add_paragraph(component['text']) 367 | 368 | return document 369 | 370 | 371 | def special_requirements(document, rfq): 372 | document.add_heading("8. Special Requirements", level=BIG_HEADING) 373 | 374 | custom_components = session.query(CustomComponent).filter_by(document_id=rfq.id).filter_by(section=8).order_by(CustomComponent.id).all() 375 | 376 | component_list = make_custom_component_list(custom_components) 377 | 378 | for component in component_list: 379 | document.add_heading(component['title'], level=SUB_HEADING) 380 | document.add_paragraph(component['text']) 381 | 382 | return document 383 | 384 | 385 | def contract_clauses(document, rfq): 386 | document.add_heading("9. Additional Contract Clauses", level=BIG_HEADING) 387 | contract_clauses = session.query(ContentComponent).filter_by(document_id=rfq.id).filter_by(section=9).first() 388 | document.add_paragraph(contract_clauses.text) 389 | 390 | return document 391 | 392 | def instructions_to_offerors(document, rfq): 393 | document.add_heading("10. Instructions to Offerors", level=BIG_HEADING) 394 | instructions = session.query(ContentComponent).filter_by(document_id=rfq.id).filter_by(section=10).first() 395 | document.add_paragraph(instructions.text) 396 | 397 | return document 398 | 399 | def evaluation_criteria(document, rfq): 400 | document.add_heading("11. Evaluation Criteria", level=BIG_HEADING) 401 | instructions = session.query(ContentComponent).filter_by(document_id=rfq.id).filter_by(section=11).first() 402 | document.add_paragraph(instructions.text) 403 | 404 | return document 405 | 406 | def appendix(document, rfq): 407 | 408 | document.add_heading("12. Appendix", level=BIG_HEADING) 409 | 410 | return document 411 | 412 | 413 | def create_document(rfq_id): 414 | rfq = session.query(RFQ).filter_by(id=rfq_id).first() 415 | 416 | document = Document() 417 | 418 | document = overview(document, rfq) 419 | document = definitions(document, rfq) 420 | document = services(document, rfq) 421 | document = objectives(document, rfq) 422 | document = personnel(document, rfq) 423 | document = invoicing(document, rfq) 424 | document = inspection_and_delivery(document, rfq) 425 | document = government_roles(document, rfq) 426 | document = special_requirements(document, rfq) 427 | document = contract_clauses(document, rfq) 428 | document = instructions_to_offerors(document, rfq) 429 | document = evaluation_criteria(document, rfq) 430 | document = appendix(document, rfq) 431 | 432 | return document 433 | -------------------------------------------------------------------------------- /app/src/questions/03_objectives.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var StateMixin = require("../state_mixin"); 3 | var EditBox = require("../edit_box"); 4 | 5 | var USER_RESEARCH = { 6 | "done": "Research has already been conducted, either internally or by another vendor. (proceed to product/program vision questionnaire)", 7 | "internal": "We intend to conduct user research internally prior to the start date of this engagement.", 8 | "vendor": "The vendor will be responsible for the user research." 9 | }; 10 | 11 | var KICK_OFF_MEETING = { 12 | "remote": "Remote Meeting", 13 | "in-person": "In-person Meeting", 14 | "none": "No Meeting", 15 | }; 16 | 17 | var USER_TYPES = { 18 | "internal_people": "Internal/Government Employees", 19 | "external_people": "External/The Public", 20 | "internal_it": "Internal Government IT", 21 | "external_it": "External IT", 22 | }; 23 | 24 | var DELIVERABLE_STATES = ["updates", "automatedTesting", "nativeMobile", "mobileWeb", "userTraining", "highTraffic", "devops", "legacySystems", "applicationDesign", "UXrequirements", "programManagement", "systemConfiguration", "helpDesk", "releaseManagement", "dataManagement"]; 25 | 26 | var STATES = [ 27 | "API_external", 28 | "API_internal", 29 | "agileIterativePractices", 30 | "dataDrivenDecisions", 31 | "defaultToOpen", 32 | "definitionOfDone", 33 | "documentationAndTraining", 34 | "external_it", 35 | "external_it_needs", 36 | "external_people", 37 | "external_people_needs", 38 | "generalBackground", 39 | "internal_it", 40 | "internal_it_needs", 41 | "internal_people", 42 | "internal_people_needs", 43 | "kickOffMeeting", 44 | "kickOffMeetingInPerson", 45 | "kickOffMeetingRemote", 46 | "languagesRequired", 47 | "locationRequirement", 48 | "locationText", 49 | "objectivesIntro", 50 | "objectivesSummary", 51 | "offSiteDevelopmentCompliance", 52 | "programHistory", 53 | "simpleAndIntuitive", 54 | "startToFinish", 55 | "userAccess", 56 | "userNeeds", 57 | "userResearchStrategy", 58 | "whatPeopleNeed", 59 | ]; 60 | 61 | var Objective = React.createClass({ 62 | mixins: [StateMixin], 63 | getInitialState: function() { 64 | var allStates = STATES.concat(DELIVERABLE_STATES).concat(["deliverables"]); 65 | for (var i=0; i < DELIVERABLE_STATES.length; i++){ 66 | var deliverable = DELIVERABLE_STATES[i]; 67 | allStates.push(deliverable + "text"); 68 | } 69 | var initialStates = getStates(allStates); 70 | return initialStates; 71 | }, 72 | componentDidMount: function() { 73 | var rfqId = getId(window.location.hash); 74 | get_data(3, rfqId, function(content){ 75 | var components = getComponents(content["data"]); 76 | this.setState( components ); 77 | }.bind(this)); 78 | getDeliverables(rfqId, function(content){ 79 | var states = { deliverables: content["data"]}; 80 | for (var i=0; i < content["data"].length; i++){ 81 | var deliverable = content["data"][i]; 82 | states[deliverable["name"]] = deliverable["value"]; 83 | states[deliverable["name"] + "text"] = deliverable["text"]; 84 | } 85 | this.setState( states ); 86 | }.bind(this)); 87 | }, 88 | handleCheck: function(key, event) { 89 | var newState = {}; 90 | var currentState = this.state[key]; 91 | if (currentState == "false"){ 92 | newState[key] = "true"; 93 | } 94 | else{ 95 | newState[key] = "false"; 96 | } 97 | if (DELIVERABLE_STATES.indexOf(key) >= 0){ 98 | for (var i=0; i < this.state.deliverables.length; i++){ 99 | var deliverable = this.state.deliverables[i]; 100 | if (deliverable['name'] == key){ 101 | this.state.deliverables[i]['value'] = newState[key]; 102 | } 103 | } 104 | } 105 | this.setState(newState); 106 | }, 107 | save: function(cb) { 108 | var data = {}; 109 | var deliverables_data = []; 110 | 111 | for (i=0; i < STATES.length; i++){ 112 | var stateName = STATES[i]; 113 | data[stateName] = this.state[stateName]; 114 | } 115 | 116 | // get states of deliverable content in addition to true/false value 117 | for (i=0; i < DELIVERABLE_STATES.length; i++){ 118 | var stateName = DELIVERABLE_STATES[i]; 119 | var deliverable = {} 120 | deliverable["name"] = stateName; 121 | deliverable["value"] = this.state[stateName]; 122 | deliverable["text"] = this.state[stateName+"text"]; 123 | deliverables_data.push(deliverable); 124 | } 125 | 126 | var rfqId = getId(window.location.hash); 127 | putDeliverables(rfqId, deliverables_data); 128 | put_data(3, "get_content", rfqId, data, cb); 129 | }, 130 | render: function() { 131 | 132 | var deliverables_options = []; 133 | var selected_deliverables = []; 134 | var selected_deliverables_strings = []; 135 | for (var i=0; i < this.state.deliverables.length; i++) { 136 | var deliverable = this.state.deliverables[i]; 137 | var key = deliverable["name"]; 138 | var contentStateName = key + "text"; 139 | if (deliverable["value"] == "true"){ 140 | selected_deliverables.push( 141 |
    142 |
    {deliverable['display']}
    143 | 144 | 149 | 150 |
    151 | ) 152 | selected_deliverables_strings.push(deliverable['display'].toLowerCase()); 153 | } 154 | deliverables_options.push( 155 |
  • 156 | 157 | 158 |
  • 159 | ); 160 | } 161 | deliverablesString = createString(selected_deliverables_strings); 162 | 163 | 164 | var userTypesOptions = []; 165 | for (var key in USER_TYPES){ 166 | userTypesOptions.push( 167 |
  • 168 | 169 | 170 |
  • 171 | ); 172 | } 173 | 174 | var userResearchOptions = []; 175 | for (var key in USER_RESEARCH) { 176 | userResearchOptions.push( 177 |
  • 178 | 179 | 180 |
  • 181 | ); 182 | } 183 | 184 | var kickOffMeetingOptions = []; 185 | for (var key in KICK_OFF_MEETING) { 186 | kickOffMeetingOptions.push( 187 |
  • 188 | 189 | 190 |
  • 191 | ); 192 | } 193 | 194 | var usersString = ""; 195 | var userTypes = []; 196 | for (var key in USER_TYPES){ 197 | if (this.state[key] == "true"){ 198 | userTypes.push(USER_TYPES[key]); 199 | } 200 | } 201 | usersString = createString(userTypes); 202 | 203 | return ( 204 |
    205 |
    Statement of Objectives
    206 |
    These questions are typically answered by the PM.
    207 | 208 | 213 | 214 | 215 |
    216 |
    General Background
    217 |
    Please provide several paragraphs about your project's history, mission, and current state.
    218 | 219 | 220 |
    221 | 222 |
    223 |
    Program History
    224 |
    If you have any information about the current vendors and specific technology being used please provide it here.
    225 | 226 | 227 |
    228 | 229 |
    Users
    230 | 231 |
    232 |
    Who will the primary users be?
    233 | 234 |
    235 | Who will the primary users be? 236 |
      237 | {userTypesOptions} 238 |
    239 |
    240 | 241 | {(usersString.length > 0)? 242 |
    243 |
    The users of the product will include {usersString}.
    244 | 245 |
    246 |
    What user needs will this service address?
    247 |
    Please list the user needs for each type of user selected above and how this service will address them.
    248 | 249 | {(this.state.internal_people == "true")? 250 |
    251 |

    Government Employee's Needs

    252 | 253 |
    : null} 254 | 255 | {(this.state.external_people == "true")? 256 |
    257 |

    The Public's Needs

    258 | 259 |
    : null} 260 | 261 | {(this.state.internal_it == "true")? 262 |
    263 |

    Internal IT Needs

    264 | 265 |
    : null} 266 | 267 | {(this.state.external_it == "true")? 268 |
    269 |

    External IT Needs

    270 | 271 |
    : null} 272 |
    273 |
    274 | : null} 275 |
    276 | 277 |
    278 |
    What languages is your service offered in?
    279 | 280 | 281 |
    282 | 283 |
    284 |
    What is your User Research Strategy?
    285 | 286 |
    287 | What is your User Research Strategy? 288 |
      289 | {userResearchOptions} 290 |
    291 |
    292 | 293 | 298 | 299 | 300 | {(this.state.userResearchStrategy === "vendor")? 301 |
    302 |
    303 |
    Understand what people need
    304 | 305 | 310 | 311 |
    312 |
    313 |
    Address the whole experience, from start to finish
    314 | 315 | 320 | 321 |
    322 |
    323 | : null } 324 |
    325 | 326 |
    General Requirements
    327 |

    All agile projects should follow these guidelines.

    328 | 329 |
    330 |
    Build the service using agile and iterative practices
    331 | 332 | 337 | 338 |
    339 | 340 |
    341 |
    Make it simple and intuitive
    342 | 343 | 348 | 349 |
    350 | 351 |
    352 |
    Use data to drive decisions
    353 | 354 | 359 | 360 |
    361 | 362 |
    Specific Tasks and Deliverables
    363 | 364 | 369 | 370 | 371 |
    372 |
    Which of the following do you anticipate your project will need?
    373 |
    We have already checked certain components that the USDS Playbook suggests be required for all projects.
    374 | 375 |
    376 | Which of the following do you anticipate your project will need? 377 |
      378 | {deliverables_options} 379 |
    380 |
    381 | 382 |
    The contractors are required to provide the following services: {deliverablesString}. Each deliverable has been described in more detail below. These functional Requirements will be translated into Epics and User Stories that will be used to populate the Product Backlog.
    383 | {selected_deliverables} 384 |
    385 | 386 |
    Location & Kick-Off Meeting
    387 | 388 |
    389 |
    Will you require the contractor to have a full-time working staff presence onsite at a specific location?
    390 |
    Ex: SBA headquarters in Washington, DC
    391 | 392 |
    393 | Will you require the contractor to have a full-time working staff presence onsite at a specific location? 394 |
      395 |
    • 396 | 397 | 398 |
    • 399 |
    • 400 | 401 | 402 |
    • 403 |
    404 |
    405 | 406 | {(this.state.locationRequirement === "yes")?
    : null} 407 | 408 | 409 | {(this.state.locationRequirement === "yes")? 410 |
    The contractor shall have a full-time working staff presence at {this.state.locationText}. Contractor shall have additional facilities to perform contract functions as necessary. 411 |
    : 412 |
    The contractor is not required to have a full-time working staff presence on-site. 413 |
    414 | } 415 | 416 | 421 | 422 |
    423 | 424 |
    425 |
    Will you require the contractor to attend a kick-off meeting?
    426 |
    427 | Will you require the contractor to attend a kick-off meeting? 428 |
      429 | {kickOffMeetingOptions} 430 |
    431 |
    432 | 433 | {(this.state.kickOffMeeting == "in-person")? 434 |
    435 | 440 | 441 |
    : null 442 | } 443 | 444 | {(this.state.kickOffMeeting == "remote")? 445 |
    446 | 451 | 452 |
    : null 453 | } 454 | 455 | {(this.state.kickOffMeeting === "none")? 456 |
    A formal kick-off meeting will not be required.
    : null 457 | } 458 |
    459 |
    460 | ); 461 | }, 462 | }); 463 | 464 | //
    Summary of Objectives
    465 | // 466 | // to ensure the system supports interoperability, must be followed. (see section). To ensure the user interface is X, Y (playbook language) 467 | module.exports = Objective; --------------------------------------------------------------------------------