diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 0000000..8d44f15
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,7 @@
+[run]
+omit =
+ opportune/test*
+ opportune/__init__.py
+ opportune/models/__init__.py
+ opportune/models/meta.py
+ opportune/scripts/*
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..54a6cd7
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,119 @@
+
+# Created by https://www.gitignore.io/api/python
+
+.DS_STORE
+.vscode
+
+Data.fs*
+*.sublime-project
+*.sublime-workspace
+.*.sw?
+.sw?
+
+### Python ###
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*.pyc
+*$py.class
+*~
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.coverage
+.coverage.*
+.cache
+.pytest_cache/
+nosetests.xml
+coverage.xml
+*.cover
+.hypothesis/
+tmp/
+
+# Translations
+*.mo
+*.pot
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# pyenv
+.python-version
+
+# celery beat schedule file
+celerybeat-schedule.*
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+env*/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+
+
+# End of https://www.gitignore.io/api/python
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..32d8cad
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,24 @@
+language: python
+python:
+ - "3.6"
+
+services:
+ - postgresql
+
+before_script:
+ - psql -c "create database opportune_test;" -U postgres
+
+#set some environment variables
+env:
+ - TEST_DATABASE_URL='postgres://127.0.0.1:5432/opportune_test'
+
+# command to install dependencies
+install:
+ - pip install -e .[testing]
+
+# commands to run tests
+script:
+ - pytest
+
+notifications:
+ email: false
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..988937c
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "python.pythonPath": "/usr/local/bin/python3"
+}
\ No newline at end of file
diff --git a/CHANGES.txt b/CHANGES.txt
new file mode 100644
index 0000000..14b902f
--- /dev/null
+++ b/CHANGES.txt
@@ -0,0 +1,4 @@
+0.0
+---
+
+- Initial version.
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..f206a8e
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,2 @@
+include *.txt *.ini *.cfg *.rst
+recursive-include opportune *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml *.jinja2
diff --git a/Procfile b/Procfile
new file mode 100644
index 0000000..e645050
--- /dev/null
+++ b/Procfile
@@ -0,0 +1 @@
+web: ./run
diff --git a/README.md b/README.md
index 886bff3..788cc3a 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,114 @@
-# Mildly-Sketchy
\ No newline at end of file
+# Opportune
+
+**Author**: Kat Cosgrove, Gene Pieterson, Patricia Raftery, Austin Matteson
+**Version**: 1.4.1
+
+## Overview
+
+Opportune is a web application that uses a web scraper and keywords from the user to find jobs in their area related to their chosen search terms. To use, the user must register or log in, choose their location and job keywords, and Opportune will email to them a list of 20 job postings within their specifications. Their results will also be displayed on their profile page. At any time the user can go back and search again, with different keywords/city, or if they use the same they will not get the same job postings again. The user has the ability to delete or add keywords, change their email, or fully delete their account. Data analysis is displayed to the user on salaries for different keyword searches.
+
+## How It Works
+
+This application returns 30 job postings to the user on submit of their location and keyword preferences. It does this by using Beautiful Soup to scrape Indeed.com. The results are stored in a csv file and emailed to the user, and also displayed on their profile page. Data analysis is done on common keyword searches in Seattle comparing salaries using Jupyter Notebook and Bokeh.
+
+## Getting Started
+
+To replicate this app, you would fork and clone our GitHub https://github.com/Mildly-Sketchy/Opportune. Then you would need to start a virtual environment, install the packages to get the dependencies used in this app, and have 3 empty databases set up.
+
+## Architecture
+
+Three SQL databases are used for persistence to store job postings and user data. Pyramid is used for the framework, and the web scraper used is Beautiful Soup. Other technologies used are Python3, Travis CI, Jupyter notebook, pandas, Bokeh, HTML, CSS, and JavaScript. Heroku is used for deployment.
+
+## Change Log
+
+#### Prework
+
+04/10/2018 1400 - Discussed project ideas
+
+04/11/2018 1400 - Selected project idea
+
+04/12/2018 1400 - Did wireframes and planning
+
+04/13/2018 1400 - Created repository
+
+04/13/2018 1400 - Created front-end mock-ups
+
+04/15/2018 1400 - Created Pyramid framework
+
+#### Day 1
+
+04/16/2018 1000 - Deployed scaffolded site with Heroku
+
+04/16/2018 1100 - Travis CI utilized
+
+04/16/2018 1200 - CSV with test data can be successfully emailed to the user
+
+04/16/2018 1300 - Databases dropped and rebuilt
+
+04/16/2018 1400 - Update gitignore
+
+04/16/2018 1430 - Basic web scraper functionality achieved within a jupyter notebook
+
+04/16/2018 1500 - Web scraper transfered to the proper file in Pyramid, still works
+
+#### Day 2
+
+04/17/2018 0900 - Keywords can be stored in the SQL database
+
+04/17/2018 0930 - Web scraper now accepts keywords stored in the database
+
+04/17/2018 1030 - Web scraper can accept a single city and multiple job keywords
+
+04/17/2018 1130 - Basic tests written for views, coverage at 54%
+
+04/17/2018 1245 - Keywords can be deleted from the database
+
+04/17/2018 1330 - Dummy data file created for testing scraper, using the data from Indeed with the python keyword
+
+04/17/2018 1430 - The user can add keywords to their account, they display on their profile page, and can be selectively deleted
+
+04/17/2018 1500 - Test for web scraper works. Test uses monkey patch to direct the scraper to the dummy data
+
+04/17/2018 1600 - User's results are written to a csv and displayed on the profile page
+
+04/17/2018 1700 - More styling and formatting done on the front end to make buttons look more appealing, improve organization
+
+#### Day 3
+
+04/18/2018 0900 - Merge developmenent branch to master branch, redeploy
+
+04/18/2018 1030 - The user can update their email address or delete their entire account
+
+04/18/2018 1130 - Front end styling for search page
+
+04/18/2018 1230 - Testing with fake authenticated user can post keywords, passes
+
+04/18/2018 1300 - Tests broken up in to different files
+
+04/18/2018 1400 - Data Analysis for different properties of dev related keywords
+
+#### Day 4
+
+04/19/2018 0900 - 401 redirect for bad login caught, scraper now returns job summaries
+
+04/19/2018 1000 - Small UI issues corrected, admin permissions for analytics page in place
+
+04/19/2018 1100 - Forbidden view and existing username tests added
+
+04/19/2018 1200 - Mass scraper notebook with CSV files added
+
+04/19/2018 1300 - Data analysis done on the user's keyword searches and is displayed to the user's profile
+
+04/19/2018 1400 - Test coverage is 85%
+
+04/19/2018 1600 - Test coverage is 95%
+
+04/19/2018 1800 - About Us page, pictures and descriptions added, styling done
+
+#### Day 5
+
+04/20/2018 0900 - Bug fixes with scraper and email
+
+04/20/2018 1000 - LinkedIn and GitHub links added to About Us page, better styling done
+
+04/20/2018 1400 - Presentation
diff --git a/README.txt b/README.txt
new file mode 100644
index 0000000..6fbc170
--- /dev/null
+++ b/README.txt
@@ -0,0 +1,33 @@
+opportune
+=========
+
+Getting Started
+---------------
+
+- Change directory into your newly created project.
+
+ cd opportune
+
+- Create a Python virtual environment.
+
+ python3 -m venv env
+
+- Upgrade packaging tools.
+
+ env/bin/pip install --upgrade pip setuptools
+
+- Install the project in editable mode with its testing requirements.
+
+ env/bin/pip install -e ".[testing]"
+
+- Configure the database.
+
+ env/bin/initialize_opportune_db development.ini
+
+- Run your project's tests.
+
+ env/bin/pytest
+
+- Run your project.
+
+ env/bin/pserve development.ini
diff --git a/development.ini b/development.ini
new file mode 100644
index 0000000..1988d44
--- /dev/null
+++ b/development.ini
@@ -0,0 +1,71 @@
+###
+# app configuration
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
+###
+
+[app:main]
+use = egg:opportune
+
+pyramid.reload_templates = true
+pyramid.debug_authorization = false
+pyramid.debug_notfound = false
+pyramid.debug_routematch = false
+pyramid.default_locale_name = en
+pyramid.includes =
+ pyramid_debugtoolbar
+
+sqlalchemy.url = postgres://localhost:5432/opportune_dev
+
+retry.attempts = 3
+
+# By default, the toolbar only appears for clients from IP addresses
+# '127.0.0.1' and '::1'.
+# debugtoolbar.hosts = 127.0.0.1 ::1
+
+###
+# wsgi server configuration
+###
+
+[server:main]
+use = egg:waitress#main
+listen = localhost:6543
+
+###
+# logging configuration
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
+###
+
+[loggers]
+keys = root, opportune, sqlalchemy
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = INFO
+handlers = console
+
+[logger_opportune]
+level = DEBUG
+handlers =
+qualname = opportune
+
+[logger_sqlalchemy]
+level = INFO
+handlers =
+qualname = sqlalchemy.engine
+# "level = INFO" logs SQL queries.
+# "level = DEBUG" logs SQL queries and results.
+# "level = WARN" logs neither. (Recommended for production systems.)
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
diff --git a/mass_scraper/DBAresults.csv b/mass_scraper/DBAresults.csv
new file mode 100644
index 0000000..35f89bb
--- /dev/null
+++ b/mass_scraper/DBAresults.csv
@@ -0,0 +1,1051 @@
+dba
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+93000.0
+70000.0
+77000.0
+54000.0
+76000.0
+77000.0
+97000.0
+45000.0
+59000.0
+73000.0
+103000.0
+94931.0
+67147.0
+84000.0
+39638.0
+96000.0
+78000.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+93000.0
+70000.0
+77000.0
+54000.0
+76000.0
+77000.0
+97000.0
+45000.0
+59000.0
+73000.0
+103000.0
+94931.0
+67147.0
+84000.0
+39638.0
+96000.0
+78000.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+73000.0
+94931.0
+67147.0
+39638.0
+84000.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+94931.0
+67147.0
+39638.0
+45000.0
+85000.0
+54869.0
+55704.0
+94931.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+55704.0
+94931.0
+45000.0
+43000.0
+77000.0
+94000.0
+45000.0
+70000.0
+40000.0
+35000.0
+59000.0
+85000.0
+54869.0
+55704.0
+43000.0
+94931.0
+45000.0
+43000.0
+54000.0
+25000.0
+45000.0
+85000.0
+54869.0
+94931.0
+55704.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+94931.0
+55704.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+94931.0
+55704.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+94931.0
+55704.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+94931.0
+55704.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+94931.0
+55704.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+55704.0
+94931.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+94931.0
+55704.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+94931.0
+55704.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+94931.0
+55704.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+55704.0
+94931.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+94931.0
+55704.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+55704.0
+94931.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+55704.0
+94931.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+43000.0
+94931.0
+55704.0
+45000.0
+43000.0
+45000.0
+45000.0
+85000.0
+54869.0
+94931.0
+55704.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+55704.0
+94931.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+55704.0
+94931.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+94931.0
+55704.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+55704.0
+94931.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+55704.0
+94931.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+55704.0
+94931.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+55704.0
+94931.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+55704.0
+94931.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+55704.0
+94931.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+55704.0
+94931.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+94931.0
+55704.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+55704.0
+94931.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+94931.0
+55704.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+94931.0
+55704.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+94931.0
+55704.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+55704.0
+94931.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+55704.0
+94931.0
+45000.0
+43000.0
+77000.0
+94000.0
+45000.0
+70000.0
+40000.0
+35000.0
+85000.0
+59000.0
+54869.0
+43000.0
+94931.0
+55704.0
+45000.0
+43000.0
+54000.0
+45000.0
+45000.0
+85000.0
+54869.0
+55704.0
+94931.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+94931.0
+55704.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+94931.0
+55704.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+94931.0
+55704.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+55704.0
+94931.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+94931.0
+55704.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+55704.0
+94931.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+55704.0
+94931.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+94931.0
+55704.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+94931.0
+55704.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+55704.0
+94931.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+94931.0
+55704.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+55704.0
+94931.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+94931.0
+55704.0
+45000.0
+43000.0
+84000.0
+45000.0
+85000.0
+54869.0
+55704.0
+94931.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+94931.0
+55704.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+94931.0
+55704.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+55704.0
+94931.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+55704.0
+43000.0
+94931.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+94931.0
+55704.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+94931.0
+55704.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+94931.0
+55704.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+94931.0
+55704.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+55704.0
+94931.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+55704.0
+94931.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+94931.0
+55704.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+55704.0
+94931.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+55704.0
+43000.0
+94931.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+94931.0
+55704.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+55704.0
+94931.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+55704.0
+43000.0
+94931.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+55704.0
+94931.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+55704.0
+94931.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+94931.0
+55704.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+94931.0
+55704.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+55704.0
+94931.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+55704.0
+94931.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+55704.0
+94931.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+55704.0
+94931.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+94931.0
+55704.0
+45000.0
+43000.0
+45000.0
+85000.0
+54869.0
+55704.0
+94931.0
+45000.0
+43000.0
+70000.0
+77000.0
+36000.0
+25000.0
+44000.0
+109000.0
+97000.0
+96000.0
+93000.0
+103000.0
+85000.0
+84000.0
+36000.0
+109000.0
+93000.0
+84000.0
+70000.0
+77000.0
+36000.0
+25000.0
+44000.0
+109000.0
+97000.0
+96000.0
+93000.0
+103000.0
+85000.0
+84000.0
+70000.0
+77000.0
+36000.0
+25000.0
+44000.0
+109000.0
+97000.0
+96000.0
+93000.0
+103000.0
+85000.0
+84000.0
+36000.0
+109000.0
+93000.0
+84000.0
+36000.0
+109000.0
+93000.0
+84000.0
+36000.0
+109000.0
+93000.0
+84000.0
diff --git a/mass_scraper/MassScraper.ipynb b/mass_scraper/MassScraper.ipynb
new file mode 100644
index 0000000..32aa0d2
--- /dev/null
+++ b/mass_scraper/MassScraper.ipynb
@@ -0,0 +1,569 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Kat's Scraper Notebook\n",
+ "## Code Fellows 401d8 Python Midterm\n",
+ "Scrapes Indeed for salary information for a given keyword in a given city. Keywords are a list and multiple arguments are acceptable, but note that adding additional keywords drastically increases the time it takes for the scrape to run, since it is currently searching OR, not AND."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import requests\n",
+ "import bs4\n",
+ "from bs4 import BeautifulSoup\n",
+ "import urllib3\n",
+ "import pandas as pd\n",
+ "import numpy as np\n",
+ "import matplotlib.pyplot as plt\n",
+ "%matplotlib inline"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Define city and keywords\n",
+ "Accepts one city and multiple keywords in a list. Multi-word keywords must be separated with a plus sign."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "city = 'Seattle'\n",
+ "keywords = ['UX+designer']"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### The scraper itself, using BeautifulSoup\n",
+ "Creates a dataframe from the results of the scrap. Also handles cleaning up of some data in the salary field, since Indeed salary fields come in a variety of formats."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {
+ "scrolled": true
+ },
+ "outputs": [],
+ "source": [
+ "url_template = 'https://www.indeed.com/jobs?q={}&l={}&fromage=any&limit=100'\n",
+ "max_results = 100\n",
+ "\n",
+ "df = pd.DataFrame(columns=['ux'])\n",
+ "requests.packages.urllib3.disable_warnings()\n",
+ "for keyword in keywords:\n",
+ " for start in range(0, max_results):\n",
+ " url = url_template.format(keyword, city)\n",
+ " http = urllib3.PoolManager()\n",
+ " response = http.request('GET', url)\n",
+ " soups = BeautifulSoup(response.data.decode('utf-8'), 'html.parser')\n",
+ " for b in soups.find_all('div', attrs={'class': ' row result'}):\n",
+ " try:\n",
+ " salary = b.find('span', attrs={'class': 'no-wrap'}).text\n",
+ " except AttributeError:\n",
+ " salary = 'NA'\n",
+ " df = df.append({'ux': salary}, ignore_index=True)\n",
+ "\n",
+ " df.ux.replace(regex=True,inplace=True,to_replace='\\n',value='')\n",
+ " df.ux.replace(regex=True,inplace=True,to_replace='$',value='')\n",
+ " df.ux.replace(regex=True,inplace=True,to_replace=' a year',value='')\n",
+ " df.ux.replace(regex=True,inplace=True,to_replace='(Indeed est.)',value='')"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Cleanup\n",
+ "The next three cells clean up some data for us. We eliminate rows where there is no salary, remove 'a year', comma separation, and dollar signs. We also eliminate any rows that contain 'a day,' 'an hour,' or 'a month,' since we only want to work with annual salaries."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "df = df.query('ux != \"NA\"')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "df = df[df.ux.str.contains('a day') == False]\n",
+ "df = df[df.ux.str.contains('an hour') == False]\n",
+ "df = df[df.ux.str.contains('a month') == False]"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "df.ux = df.ux.str.replace('a year', '').str.replace(',', '').str.replace('$', '')"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Here we just take a peek at the data to confirm the above reformatting is working correctly."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "
\n",
+ "\n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " ux \n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " 2891 \n",
+ " 74000 - 94000 () \n",
+ " \n",
+ " \n",
+ " 2892 \n",
+ " 82000 - 104000 () \n",
+ " \n",
+ " \n",
+ " 2893 \n",
+ " 86000 - 110000 () \n",
+ " \n",
+ " \n",
+ " 2895 \n",
+ " 79000 - 101000 () \n",
+ " \n",
+ " \n",
+ " 2896 \n",
+ " 83000 - 105000 () \n",
+ " \n",
+ " \n",
+ "
\n",
+ "
"
+ ],
+ "text/plain": [
+ " ux\n",
+ "2891 74000 - 94000 ()\n",
+ "2892 82000 - 104000 ()\n",
+ "2893 86000 - 110000 ()\n",
+ "2895 79000 - 101000 ()\n",
+ "2896 83000 - 105000 ()"
+ ]
+ },
+ "execution_count": 7,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "df.head()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Taking lowest in the range\n",
+ "Since most of the salaries are listed as a range, we assume the worst-case scenario by splitting the salary on the dash and assigning the first index as a float to a list."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "cleaned_salaries = []\n",
+ "for i in df.ux:\n",
+ " a = i.split('-')\n",
+ " cleaned_salaries.append(float(a[0]))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Reassigning salaries\n",
+ "This replaces the salary column in the dataframe with the values from the list we made above."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "df.ux = cleaned_salaries"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# df"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Write to CSV\n",
+ "We write our results to a CSV because this scrape is kind of large and it takes foreverrrrr. We want to do things with this data, but we don't want to have to run the scrapes repeatedly."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "df.to_csv('uxresults.csv', encoding='utf-8', index=False)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Let's chart some salaries!\n",
+ "First, read them into dataframes from the CSVs we made."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 22,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "c_plus = pd.read_csv('cplusresults.csv')\n",
+ "python = pd.read_csv('pythonresults.csv')\n",
+ "javascript = pd.read_csv('javascriptresults.csv')\n",
+ "java = pd.read_csv('javaresults.csv')\n",
+ "php = pd.read_csv('phpresults.csv')\n",
+ "csharp = pd.read_csv('csharpresults.csv')\n",
+ "datascience = pd.read_csv('datascienceresults.csv')\n",
+ "softwaredev = pd.read_csv('softwaredevresults.csv')\n",
+ "webdev = pd.read_csv('webdevresults.csv')\n",
+ "dba = pd.read_csv('DBAresults.csv')\n",
+ "ux = pd.read_csv('uxresults.csv')"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Concatenating them into relevant dataframes"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 35,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "languages = pd.concat([c_plus, python, javascript, java, php, csharp], axis=1)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 36,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " Cplus \n",
+ " python \n",
+ " javascript \n",
+ " java \n",
+ " php \n",
+ " csharp \n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " 0 \n",
+ " 150000.0 \n",
+ " 75000.0 \n",
+ " 70000.0 \n",
+ " 150000.0 \n",
+ " 90000.0 \n",
+ " 50000.0 \n",
+ " \n",
+ " \n",
+ " 1 \n",
+ " 150000.0 \n",
+ " 70000.0 \n",
+ " 70000.0 \n",
+ " 140000.0 \n",
+ " 70000.0 \n",
+ " 60000.0 \n",
+ " \n",
+ " \n",
+ " 2 \n",
+ " 150000.0 \n",
+ " 75000.0 \n",
+ " 65000.0 \n",
+ " 60000.0 \n",
+ " 70000.0 \n",
+ " 80000.0 \n",
+ " \n",
+ " \n",
+ " 3 \n",
+ " 150000.0 \n",
+ " 75000.0 \n",
+ " 50000.0 \n",
+ " 120000.0 \n",
+ " 60000.0 \n",
+ " 50000.0 \n",
+ " \n",
+ " \n",
+ " 4 \n",
+ " 150000.0 \n",
+ " 75000.0 \n",
+ " 50000.0 \n",
+ " 70000.0 \n",
+ " 45000.0 \n",
+ " 150000.0 \n",
+ " \n",
+ " \n",
+ "
\n",
+ "
"
+ ],
+ "text/plain": [
+ " Cplus python javascript java php csharp\n",
+ "0 150000.0 75000.0 70000.0 150000.0 90000.0 50000.0\n",
+ "1 150000.0 70000.0 70000.0 140000.0 70000.0 60000.0\n",
+ "2 150000.0 75000.0 65000.0 60000.0 70000.0 80000.0\n",
+ "3 150000.0 75000.0 50000.0 120000.0 60000.0 50000.0\n",
+ "4 150000.0 75000.0 50000.0 70000.0 45000.0 150000.0"
+ ]
+ },
+ "execution_count": 36,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "languages.head()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 14,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "jobs = pd.concat([datascience, softwaredev, webdev, dba, ux])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Get median values for each"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 15,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "median_languages = languages.median()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 16,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "median_jobs = jobs.median()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Let's just plot the distributions of some languages"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 30,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "Text(0,0.5,'Python')"
+ ]
+ },
+ "execution_count": 30,
+ "metadata": {},
+ "output_type": "execute_result"
+ },
+ {
+ "data": {
+ "image/png": "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\n",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "python = pd.read_csv('pythonresults.csv')\n",
+ "plt.hist(python.python, bins=20)\n",
+ "plt.ylabel('Python')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 32,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "Text(0,0.5,'Javascript')"
+ ]
+ },
+ "execution_count": 32,
+ "metadata": {},
+ "output_type": "execute_result"
+ },
+ {
+ "data": {
+ "image/png": "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\n",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "javascript = pd.read_csv('javascriptresults.csv')\n",
+ "plt.hist(javascript.javascript, bins=20)\n",
+ "plt.ylabel('Javascript')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 39,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "Text(0,0.5,'C++')"
+ ]
+ },
+ "execution_count": 39,
+ "metadata": {},
+ "output_type": "execute_result"
+ },
+ {
+ "data": {
+ "image/png": "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\n",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "c_plus = pd.read_csv('cplusresults.csv')\n",
+ "plt.hist(c_plus.Cplus, bins=20)\n",
+ "plt.ylabel('C++')"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.6.5"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/mass_scraper/cplusresults.csv b/mass_scraper/cplusresults.csv
new file mode 100644
index 0000000..34d427f
--- /dev/null
+++ b/mass_scraper/cplusresults.csv
@@ -0,0 +1,213 @@
+Cplus
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+87000.0
+105000.0
+119000.0
+120000.0
+121000.0
+121000.0
+78000.0
+83000.0
+126000.0
+111000.0
+90000.0
+92000.0
+117000.0
+102000.0
+59000.0
+109000.0
+102000.0
+110000.0
+94000.0
+150000.0
+142000.0
+120000.0
+115000.0
+108000.0
+124000.0
+81000.0
+132000.0
+83000.0
+122000.0
+108000.0
+111000.0
+116000.0
+110000.0
+121000.0
+111000.0
+97000.0
+90000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+87000.0
+81000.0
+105000.0
+119000.0
+81000.0
+120000.0
+121000.0
+121000.0
+78000.0
+83000.0
+126000.0
+111000.0
+90000.0
+92000.0
+117000.0
+102000.0
+116000.0
+59000.0
+109000.0
+102000.0
+110000.0
+94000.0
+150000.0
+142000.0
+120000.0
+115000.0
+108000.0
+124000.0
+81000.0
+132000.0
+83000.0
+122000.0
+108000.0
+111000.0
+116000.0
+110000.0
+121000.0
+111000.0
+97000.0
+90000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+87000.0
+105000.0
+119000.0
+120000.0
+121000.0
+121000.0
+83000.0
+78000.0
+126000.0
+111000.0
+90000.0
+92000.0
+117000.0
+102000.0
+59000.0
+109000.0
+150000.0
+102000.0
+83000.0
+94000.0
+90000.0
+142000.0
+120000.0
+110000.0
+124000.0
+108000.0
+132000.0
+115000.0
+122000.0
+108000.0
+111000.0
+116000.0
+110000.0
+121000.0
+111000.0
+97000.0
+95000.0
+81000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
diff --git a/mass_scraper/csharpresults.csv b/mass_scraper/csharpresults.csv
new file mode 100644
index 0000000..95bc2ce
--- /dev/null
+++ b/mass_scraper/csharpresults.csv
@@ -0,0 +1,736 @@
+csharp
+50000.0
+60000.0
+80000.0
+50000.0
+150000.0
+50000.0
+60000.0
+80000.0
+50000.0
+150000.0
+50000.0
+60000.0
+80000.0
+50000.0
+150000.0
+50000.0
+60000.0
+80000.0
+50000.0
+150000.0
+50000.0
+106000.0
+60000.0
+93000.0
+81000.0
+80000.0
+50000.0
+117000.0
+84000.0
+129000.0
+97000.0
+97000.0
+103000.0
+102000.0
+82000.0
+120000.0
+113000.0
+108000.0
+101000.0
+89000.0
+76000.0
+111000.0
+112000.0
+117000.0
+102000.0
+59000.0
+122000.0
+106000.0
+132000.0
+150000.0
+112000.0
+89000.0
+91000.0
+105000.0
+85000.0
+116000.0
+93000.0
+101000.0
+133000.0
+50000.0
+60000.0
+80000.0
+50000.0
+150000.0
+50000.0
+60000.0
+80000.0
+50000.0
+150000.0
+50000.0
+60000.0
+50000.0
+80000.0
+150000.0
+50000.0
+60000.0
+80000.0
+50000.0
+150000.0
+50000.0
+60000.0
+80000.0
+50000.0
+150000.0
+50000.0
+60000.0
+50000.0
+80000.0
+150000.0
+50000.0
+60000.0
+50000.0
+80000.0
+150000.0
+50000.0
+60000.0
+50000.0
+80000.0
+150000.0
+50000.0
+60000.0
+80000.0
+50000.0
+150000.0
+50000.0
+60000.0
+80000.0
+50000.0
+150000.0
+50000.0
+60000.0
+80000.0
+50000.0
+150000.0
+50000.0
+60000.0
+50000.0
+80000.0
+150000.0
+50000.0
+60000.0
+50000.0
+80000.0
+150000.0
+50000.0
+60000.0
+80000.0
+50000.0
+150000.0
+50000.0
+60000.0
+50000.0
+80000.0
+150000.0
+50000.0
+60000.0
+80000.0
+50000.0
+150000.0
+50000.0
+60000.0
+50000.0
+80000.0
+150000.0
+50000.0
+60000.0
+80000.0
+50000.0
+150000.0
+50000.0
+60000.0
+50000.0
+80000.0
+150000.0
+50000.0
+60000.0
+80000.0
+50000.0
+150000.0
+50000.0
+60000.0
+80000.0
+50000.0
+150000.0
+50000.0
+106000.0
+60000.0
+93000.0
+81000.0
+80000.0
+50000.0
+117000.0
+84000.0
+129000.0
+97000.0
+97000.0
+103000.0
+102000.0
+82000.0
+120000.0
+113000.0
+108000.0
+101000.0
+89000.0
+76000.0
+111000.0
+112000.0
+117000.0
+102000.0
+59000.0
+122000.0
+106000.0
+132000.0
+150000.0
+112000.0
+89000.0
+91000.0
+105000.0
+85000.0
+116000.0
+93000.0
+101000.0
+133000.0
+50000.0
+60000.0
+50000.0
+80000.0
+150000.0
+50000.0
+60000.0
+80000.0
+50000.0
+150000.0
+50000.0
+106000.0
+60000.0
+93000.0
+50000.0
+81000.0
+80000.0
+117000.0
+129000.0
+97000.0
+97000.0
+84000.0
+103000.0
+102000.0
+82000.0
+120000.0
+113000.0
+101000.0
+89000.0
+108000.0
+112000.0
+111000.0
+89000.0
+117000.0
+102000.0
+59000.0
+122000.0
+106000.0
+91000.0
+105000.0
+132000.0
+94000.0
+101000.0
+76000.0
+112000.0
+116000.0
+133000.0
+150000.0
+50000.0
+60000.0
+80000.0
+50000.0
+150000.0
+50000.0
+60000.0
+50000.0
+80000.0
+150000.0
+50000.0
+60000.0
+80000.0
+50000.0
+150000.0
+50000.0
+60000.0
+80000.0
+50000.0
+150000.0
+50000.0
+106000.0
+60000.0
+93000.0
+50000.0
+81000.0
+80000.0
+117000.0
+129000.0
+97000.0
+97000.0
+84000.0
+103000.0
+102000.0
+82000.0
+120000.0
+113000.0
+101000.0
+89000.0
+108000.0
+112000.0
+111000.0
+89000.0
+117000.0
+102000.0
+59000.0
+122000.0
+106000.0
+91000.0
+105000.0
+132000.0
+94000.0
+101000.0
+76000.0
+112000.0
+116000.0
+133000.0
+150000.0
+50000.0
+60000.0
+50000.0
+80000.0
+150000.0
+50000.0
+60000.0
+50000.0
+80000.0
+150000.0
+50000.0
+60000.0
+50000.0
+80000.0
+150000.0
+50000.0
+60000.0
+80000.0
+50000.0
+150000.0
+50000.0
+60000.0
+50000.0
+80000.0
+150000.0
+50000.0
+60000.0
+80000.0
+50000.0
+150000.0
+50000.0
+60000.0
+50000.0
+80000.0
+150000.0
+50000.0
+106000.0
+60000.0
+93000.0
+81000.0
+80000.0
+50000.0
+117000.0
+84000.0
+129000.0
+97000.0
+97000.0
+103000.0
+102000.0
+82000.0
+120000.0
+113000.0
+108000.0
+101000.0
+89000.0
+76000.0
+111000.0
+112000.0
+117000.0
+102000.0
+59000.0
+122000.0
+106000.0
+132000.0
+150000.0
+112000.0
+89000.0
+91000.0
+105000.0
+94000.0
+116000.0
+93000.0
+101000.0
+133000.0
+50000.0
+60000.0
+50000.0
+80000.0
+150000.0
+50000.0
+60000.0
+80000.0
+50000.0
+150000.0
+50000.0
+60000.0
+50000.0
+80000.0
+150000.0
+50000.0
+60000.0
+80000.0
+50000.0
+150000.0
+50000.0
+60000.0
+80000.0
+50000.0
+150000.0
+50000.0
+60000.0
+50000.0
+80000.0
+150000.0
+50000.0
+60000.0
+50000.0
+80000.0
+150000.0
+50000.0
+60000.0
+50000.0
+80000.0
+150000.0
+50000.0
+60000.0
+80000.0
+50000.0
+150000.0
+50000.0
+106000.0
+60000.0
+93000.0
+50000.0
+81000.0
+80000.0
+117000.0
+129000.0
+97000.0
+97000.0
+84000.0
+103000.0
+102000.0
+82000.0
+120000.0
+113000.0
+101000.0
+89000.0
+108000.0
+112000.0
+111000.0
+89000.0
+117000.0
+102000.0
+59000.0
+122000.0
+106000.0
+91000.0
+105000.0
+132000.0
+85000.0
+101000.0
+76000.0
+112000.0
+116000.0
+133000.0
+150000.0
+50000.0
+60000.0
+50000.0
+80000.0
+150000.0
+50000.0
+106000.0
+60000.0
+93000.0
+81000.0
+80000.0
+50000.0
+117000.0
+84000.0
+129000.0
+97000.0
+97000.0
+103000.0
+102000.0
+82000.0
+120000.0
+113000.0
+108000.0
+101000.0
+89000.0
+76000.0
+111000.0
+112000.0
+117000.0
+102000.0
+59000.0
+122000.0
+106000.0
+132000.0
+150000.0
+112000.0
+89000.0
+91000.0
+105000.0
+85000.0
+116000.0
+93000.0
+101000.0
+133000.0
+50000.0
+60000.0
+50000.0
+80000.0
+150000.0
+50000.0
+60000.0
+50000.0
+80000.0
+150000.0
+50000.0
+60000.0
+50000.0
+80000.0
+150000.0
+50000.0
+60000.0
+50000.0
+80000.0
+150000.0
+50000.0
+60000.0
+80000.0
+50000.0
+150000.0
+50000.0
+60000.0
+80000.0
+50000.0
+150000.0
+50000.0
+60000.0
+80000.0
+50000.0
+150000.0
+50000.0
+60000.0
+80000.0
+50000.0
+150000.0
+50000.0
+60000.0
+80000.0
+50000.0
+150000.0
+50000.0
+60000.0
+50000.0
+80000.0
+150000.0
+50000.0
+60000.0
+80000.0
+50000.0
+150000.0
+50000.0
+60000.0
+80000.0
+50000.0
+150000.0
+50000.0
+60000.0
+80000.0
+50000.0
+150000.0
+50000.0
+60000.0
+80000.0
+50000.0
+150000.0
+50000.0
+60000.0
+50000.0
+80000.0
+150000.0
+50000.0
+60000.0
+80000.0
+50000.0
+150000.0
+50000.0
+60000.0
+80000.0
+50000.0
+150000.0
+50000.0
+60000.0
+50000.0
+80000.0
+150000.0
+50000.0
+60000.0
+50000.0
+80000.0
+150000.0
+50000.0
+60000.0
+80000.0
+50000.0
+150000.0
+50000.0
+60000.0
+50000.0
+80000.0
+150000.0
+50000.0
+60000.0
+80000.0
+50000.0
+150000.0
+50000.0
+60000.0
+50000.0
+80000.0
+150000.0
+50000.0
+60000.0
+50000.0
+80000.0
+150000.0
+50000.0
+60000.0
+80000.0
+50000.0
+150000.0
+50000.0
+60000.0
+80000.0
+50000.0
+150000.0
+50000.0
+60000.0
+80000.0
+50000.0
+150000.0
+50000.0
+60000.0
+50000.0
+80000.0
+150000.0
+50000.0
+60000.0
+80000.0
+50000.0
+150000.0
+50000.0
+60000.0
+50000.0
+80000.0
+150000.0
+50000.0
+60000.0
+50000.0
+80000.0
+150000.0
+50000.0
+60000.0
+80000.0
+50000.0
+150000.0
+50000.0
+60000.0
+50000.0
+80000.0
+150000.0
+50000.0
+60000.0
+80000.0
+50000.0
+150000.0
+50000.0
+60000.0
+80000.0
+50000.0
+150000.0
+50000.0
+60000.0
+80000.0
+50000.0
+150000.0
+50000.0
+60000.0
+50000.0
+80000.0
+150000.0
+50000.0
+60000.0
+50000.0
+80000.0
+150000.0
+50000.0
+60000.0
+80000.0
+50000.0
+150000.0
+50000.0
+60000.0
+80000.0
+50000.0
+150000.0
+50000.0
+60000.0
+50000.0
+80000.0
+150000.0
+50000.0
+60000.0
+80000.0
+50000.0
+150000.0
+50000.0
+60000.0
+80000.0
+50000.0
+150000.0
+50000.0
+60000.0
+80000.0
+50000.0
+150000.0
+50000.0
+60000.0
+80000.0
+50000.0
+150000.0
diff --git a/mass_scraper/datascienceresults.csv b/mass_scraper/datascienceresults.csv
new file mode 100644
index 0000000..690dc42
--- /dev/null
+++ b/mass_scraper/datascienceresults.csv
@@ -0,0 +1,257 @@
+datascience
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+109000.0
+64000.0
+150000.0
+92000.0
+116000.0
+106000.0
+103000.0
+91000.0
+97000.0
+126000.0
+124000.0
+132000.0
+115000.0
+115000.0
+99000.0
+105000.0
+128000.0
+91000.0
+133000.0
+97000.0
+110000.0
+108000.0
+100000.0
+129000.0
+131000.0
+105000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+109000.0
+64000.0
+150000.0
+126000.0
+116000.0
+106000.0
+103000.0
+91000.0
+97000.0
+126000.0
+124000.0
+132000.0
+115000.0
+115000.0
+99000.0
+128000.0
+91000.0
+133000.0
+97000.0
+110000.0
+100000.0
+129000.0
+131000.0
+105000.0
+110000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+109000.0
+64000.0
+150000.0
+92000.0
+116000.0
+106000.0
+103000.0
+91000.0
+97000.0
+126000.0
+124000.0
+132000.0
+115000.0
+115000.0
+99000.0
+105000.0
+128000.0
+91000.0
+133000.0
+97000.0
+110000.0
+108000.0
+100000.0
+129000.0
+131000.0
+105000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+91000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+91000.0
+150000.0
+150000.0
+91000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+91000.0
+150000.0
+150000.0
+109000.0
+64000.0
+150000.0
+126000.0
+116000.0
+106000.0
+103000.0
+91000.0
+97000.0
+126000.0
+124000.0
+132000.0
+115000.0
+115000.0
+99000.0
+128000.0
+91000.0
+133000.0
+97000.0
+110000.0
+100000.0
+129000.0
+131000.0
+105000.0
+110000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+150000.0
+91000.0
+92000.0
+116000.0
+127000.0
+115000.0
+97000.0
+114000.0
+109000.0
+110000.0
+105000.0
+111000.0
+83000.0
+115000.0
+123000.0
+95000.0
+111000.0
+93000.0
+79000.0
+128000.0
+90000.0
+90000.0
+114000.0
+86000.0
+109000.0
+96000.0
+85000.0
+101000.0
+97000.0
+80000.0
+91000.0
+92000.0
+116000.0
+127000.0
+97000.0
+115000.0
+114000.0
+109000.0
+105000.0
+111000.0
+83000.0
+115000.0
+123000.0
+111000.0
+79000.0
+90000.0
+90000.0
+114000.0
+85000.0
+86000.0
+109000.0
+101000.0
+73000.0
+103000.0
+120000.0
diff --git a/mass_scraper/javaresults.csv b/mass_scraper/javaresults.csv
new file mode 100644
index 0000000..4a74d68
--- /dev/null
+++ b/mass_scraper/javaresults.csv
@@ -0,0 +1,910 @@
+java
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+93000.0
+89000.0
+81000.0
+88000.0
+106000.0
+94000.0
+81000.0
+140000.0
+100000.0
+122000.0
+60000.0
+120000.0
+104000.0
+94000.0
+110000.0
+143000.0
+106000.0
+119000.0
+70000.0
+120000.0
+109000.0
+109000.0
+100000.0
+78000.0
+94000.0
+102000.0
+97000.0
+90000.0
+34000.0
+109000.0
+97000.0
+34000.0
+100000.0
+110000.0
+97000.0
+100000.0
+111000.0
+120000.0
+92000.0
+70000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+93000.0
+81000.0
+89000.0
+88000.0
+106000.0
+94000.0
+140000.0
+100000.0
+122000.0
+60000.0
+120000.0
+104000.0
+110000.0
+119000.0
+143000.0
+106000.0
+70000.0
+120000.0
+78000.0
+109000.0
+109000.0
+94000.0
+102000.0
+100000.0
+97000.0
+90000.0
+109000.0
+34000.0
+120000.0
+34000.0
+97000.0
+97000.0
+70000.0
+110000.0
+84000.0
+150000.0
+100000.0
+111000.0
+92000.0
+100000.0
+150000.0
+93000.0
+89000.0
+81000.0
+88000.0
+106000.0
+94000.0
+140000.0
+100000.0
+122000.0
+60000.0
+120000.0
+104000.0
+110000.0
+143000.0
+106000.0
+119000.0
+70000.0
+120000.0
+109000.0
+109000.0
+100000.0
+78000.0
+94000.0
+102000.0
+97000.0
+90000.0
+34000.0
+109000.0
+97000.0
+34000.0
+100000.0
+110000.0
+97000.0
+100000.0
+111000.0
+120000.0
+92000.0
+70000.0
+102000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+93000.0
+89000.0
+81000.0
+88000.0
+106000.0
+94000.0
+81000.0
+140000.0
+100000.0
+122000.0
+60000.0
+120000.0
+104000.0
+94000.0
+110000.0
+143000.0
+106000.0
+119000.0
+70000.0
+120000.0
+109000.0
+109000.0
+100000.0
+78000.0
+94000.0
+102000.0
+97000.0
+90000.0
+34000.0
+109000.0
+97000.0
+34000.0
+100000.0
+110000.0
+97000.0
+100000.0
+111000.0
+120000.0
+92000.0
+70000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
+150000.0
+150000.0
+140000.0
+60000.0
+120000.0
+70000.0
+100000.0
+70000.0
diff --git a/mass_scraper/javascriptresults.csv b/mass_scraper/javascriptresults.csv
new file mode 100644
index 0000000..f4e9283
--- /dev/null
+++ b/mass_scraper/javascriptresults.csv
@@ -0,0 +1,690 @@
+javascript
+70000.0
+70000.0
+65000.0
+50000.0
+50000.0
+74866.0
+70000.0
+70000.0
+65000.0
+50000.0
+50000.0
+74866.0
+70000.0
+70000.0
+65000.0
+50000.0
+50000.0
+74866.0
+65000.0
+70000.0
+70000.0
+50000.0
+50000.0
+70000.0
+70000.0
+65000.0
+50000.0
+50000.0
+65000.0
+70000.0
+70000.0
+50000.0
+50000.0
+70000.0
+70000.0
+65000.0
+50000.0
+50000.0
+74866.0
+65000.0
+70000.0
+70000.0
+50000.0
+50000.0
+65000.0
+70000.0
+70000.0
+50000.0
+50000.0
+70000.0
+70000.0
+65000.0
+50000.0
+50000.0
+65000.0
+70000.0
+70000.0
+50000.0
+50000.0
+65000.0
+70000.0
+70000.0
+50000.0
+50000.0
+70000.0
+70000.0
+65000.0
+50000.0
+50000.0
+74866.0
+65000.0
+70000.0
+70000.0
+50000.0
+50000.0
+65000.0
+70000.0
+70000.0
+50000.0
+50000.0
+70000.0
+70000.0
+65000.0
+50000.0
+50000.0
+65000.0
+70000.0
+70000.0
+50000.0
+50000.0
+70000.0
+70000.0
+65000.0
+50000.0
+50000.0
+70000.0
+70000.0
+65000.0
+50000.0
+50000.0
+65000.0
+70000.0
+70000.0
+50000.0
+50000.0
+70000.0
+70000.0
+65000.0
+50000.0
+50000.0
+70000.0
+70000.0
+65000.0
+50000.0
+50000.0
+70000.0
+70000.0
+65000.0
+50000.0
+50000.0
+74866.0
+65000.0
+70000.0
+70000.0
+50000.0
+50000.0
+65000.0
+70000.0
+70000.0
+50000.0
+50000.0
+70000.0
+70000.0
+65000.0
+50000.0
+50000.0
+74866.0
+70000.0
+70000.0
+65000.0
+50000.0
+50000.0
+74866.0
+70000.0
+70000.0
+65000.0
+50000.0
+50000.0
+74866.0
+65000.0
+70000.0
+70000.0
+50000.0
+50000.0
+70000.0
+70000.0
+65000.0
+50000.0
+50000.0
+65000.0
+70000.0
+70000.0
+50000.0
+50000.0
+70000.0
+70000.0
+65000.0
+50000.0
+50000.0
+65000.0
+70000.0
+70000.0
+50000.0
+50000.0
+70000.0
+70000.0
+65000.0
+50000.0
+50000.0
+74866.0
+65000.0
+70000.0
+70000.0
+50000.0
+50000.0
+70000.0
+70000.0
+65000.0
+50000.0
+50000.0
+65000.0
+70000.0
+70000.0
+50000.0
+50000.0
+65000.0
+70000.0
+70000.0
+50000.0
+50000.0
+70000.0
+70000.0
+65000.0
+50000.0
+50000.0
+74866.0
+119000.0
+110000.0
+97000.0
+65000.0
+113000.0
+97000.0
+70000.0
+70000.0
+94000.0
+104000.0
+50000.0
+92000.0
+94000.0
+82000.0
+81000.0
+105000.0
+113000.0
+97000.0
+86000.0
+105000.0
+96000.0
+104000.0
+94000.0
+88000.0
+111000.0
+50000.0
+86000.0
+97000.0
+83000.0
+124000.0
+111000.0
+89000.0
+81000.0
+77000.0
+100000.0
+77000.0
+102000.0
+70000.0
+70000.0
+65000.0
+50000.0
+50000.0
+74866.0
+70000.0
+70000.0
+65000.0
+50000.0
+50000.0
+70000.0
+70000.0
+65000.0
+50000.0
+50000.0
+65000.0
+70000.0
+70000.0
+50000.0
+50000.0
+70000.0
+70000.0
+65000.0
+50000.0
+50000.0
+65000.0
+70000.0
+70000.0
+50000.0
+50000.0
+65000.0
+70000.0
+70000.0
+50000.0
+50000.0
+119000.0
+110000.0
+97000.0
+70000.0
+70000.0
+65000.0
+97000.0
+113000.0
+94000.0
+104000.0
+92000.0
+50000.0
+94000.0
+105000.0
+81000.0
+113000.0
+97000.0
+96000.0
+83000.0
+86000.0
+105000.0
+82000.0
+96000.0
+104000.0
+94000.0
+94000.0
+77000.0
+111000.0
+50000.0
+97000.0
+86000.0
+97000.0
+124000.0
+111000.0
+81000.0
+88000.0
+100000.0
+89000.0
+70000.0
+70000.0
+65000.0
+50000.0
+50000.0
+65000.0
+70000.0
+70000.0
+50000.0
+50000.0
+70000.0
+70000.0
+65000.0
+50000.0
+50000.0
+70000.0
+70000.0
+65000.0
+50000.0
+50000.0
+70000.0
+70000.0
+65000.0
+50000.0
+50000.0
+74866.0
+65000.0
+70000.0
+70000.0
+50000.0
+50000.0
+65000.0
+70000.0
+70000.0
+50000.0
+50000.0
+70000.0
+70000.0
+65000.0
+50000.0
+50000.0
+65000.0
+70000.0
+70000.0
+50000.0
+50000.0
+65000.0
+70000.0
+70000.0
+50000.0
+50000.0
+65000.0
+70000.0
+70000.0
+50000.0
+50000.0
+70000.0
+70000.0
+65000.0
+50000.0
+50000.0
+70000.0
+70000.0
+65000.0
+50000.0
+50000.0
+74866.0
+70000.0
+70000.0
+65000.0
+50000.0
+50000.0
+65000.0
+70000.0
+70000.0
+50000.0
+50000.0
+70000.0
+70000.0
+65000.0
+50000.0
+50000.0
+65000.0
+70000.0
+70000.0
+50000.0
+50000.0
+119000.0
+110000.0
+97000.0
+70000.0
+70000.0
+65000.0
+97000.0
+113000.0
+94000.0
+104000.0
+92000.0
+50000.0
+94000.0
+105000.0
+81000.0
+113000.0
+97000.0
+83000.0
+86000.0
+105000.0
+82000.0
+96000.0
+104000.0
+94000.0
+77000.0
+111000.0
+50000.0
+86000.0
+97000.0
+124000.0
+111000.0
+81000.0
+88000.0
+100000.0
+89000.0
+74866.0
+102000.0
+77000.0
+65000.0
+70000.0
+70000.0
+50000.0
+50000.0
+119000.0
+110000.0
+97000.0
+70000.0
+70000.0
+65000.0
+97000.0
+113000.0
+94000.0
+104000.0
+92000.0
+50000.0
+94000.0
+105000.0
+81000.0
+113000.0
+97000.0
+96000.0
+83000.0
+86000.0
+105000.0
+82000.0
+96000.0
+104000.0
+94000.0
+94000.0
+77000.0
+111000.0
+50000.0
+97000.0
+86000.0
+97000.0
+124000.0
+111000.0
+81000.0
+88000.0
+100000.0
+89000.0
+70000.0
+70000.0
+65000.0
+50000.0
+50000.0
+70000.0
+70000.0
+65000.0
+50000.0
+50000.0
+70000.0
+70000.0
+65000.0
+50000.0
+50000.0
+70000.0
+70000.0
+65000.0
+50000.0
+50000.0
+74866.0
+70000.0
+70000.0
+65000.0
+50000.0
+50000.0
+74866.0
+70000.0
+70000.0
+65000.0
+50000.0
+50000.0
+65000.0
+70000.0
+70000.0
+50000.0
+50000.0
+70000.0
+70000.0
+65000.0
+50000.0
+50000.0
+74866.0
+65000.0
+70000.0
+70000.0
+50000.0
+50000.0
+65000.0
+70000.0
+70000.0
+50000.0
+50000.0
+65000.0
+70000.0
+70000.0
+50000.0
+50000.0
+65000.0
+70000.0
+70000.0
+50000.0
+50000.0
+70000.0
+70000.0
+65000.0
+50000.0
+50000.0
+74866.0
+65000.0
+70000.0
+70000.0
+50000.0
+50000.0
+70000.0
+70000.0
+65000.0
+50000.0
+50000.0
+74866.0
+65000.0
+70000.0
+70000.0
+50000.0
+50000.0
+70000.0
+70000.0
+65000.0
+50000.0
+50000.0
+74866.0
+70000.0
+70000.0
+65000.0
+50000.0
+50000.0
+70000.0
+70000.0
+65000.0
+50000.0
+50000.0
+74866.0
+70000.0
+70000.0
+65000.0
+50000.0
+50000.0
+74866.0
+119000.0
+110000.0
+97000.0
+70000.0
+70000.0
+65000.0
+97000.0
+113000.0
+94000.0
+104000.0
+92000.0
+50000.0
+94000.0
+105000.0
+81000.0
+113000.0
+97000.0
+96000.0
+83000.0
+86000.0
+105000.0
+82000.0
+96000.0
+104000.0
+94000.0
+94000.0
+77000.0
+111000.0
+50000.0
+97000.0
+86000.0
+97000.0
+124000.0
+111000.0
+81000.0
+88000.0
+100000.0
+89000.0
+70000.0
+70000.0
+65000.0
+50000.0
+50000.0
+74866.0
+70000.0
+70000.0
+65000.0
+50000.0
+50000.0
+74866.0
+65000.0
+70000.0
+70000.0
+50000.0
+50000.0
+70000.0
+70000.0
+65000.0
+50000.0
+50000.0
+70000.0
+70000.0
+65000.0
+50000.0
+50000.0
+74866.0
+65000.0
+70000.0
+70000.0
+50000.0
+50000.0
+65000.0
+70000.0
+70000.0
+50000.0
+50000.0
+70000.0
+70000.0
+65000.0
+50000.0
+50000.0
+70000.0
+70000.0
+65000.0
+50000.0
+50000.0
+65000.0
+70000.0
+70000.0
+50000.0
+50000.0
+65000.0
+70000.0
+70000.0
+50000.0
+50000.0
diff --git a/mass_scraper/phpresults.csv b/mass_scraper/phpresults.csv
new file mode 100644
index 0000000..abf4446
--- /dev/null
+++ b/mass_scraper/phpresults.csv
@@ -0,0 +1,611 @@
+php
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+112000.0
+94000.0
+78000.0
+118000.0
+119000.0
+85000.0
+108000.0
+90000.0
+104000.0
+70000.0
+70000.0
+107000.0
+97000.0
+97000.0
+129000.0
+60000.0
+99000.0
+81000.0
+126000.0
+97000.0
+100000.0
+127000.0
+98000.0
+97000.0
+45000.0
+101000.0
+112000.0
+102000.0
+112000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+94000.0
+112000.0
+78000.0
+118000.0
+85000.0
+108000.0
+90000.0
+119000.0
+107000.0
+97000.0
+97000.0
+104000.0
+129000.0
+70000.0
+70000.0
+60000.0
+81000.0
+126000.0
+97000.0
+101000.0
+99000.0
+127000.0
+100000.0
+98000.0
+101000.0
+112000.0
+112000.0
+102000.0
+97000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+94000.0
+112000.0
+78000.0
+118000.0
+85000.0
+108000.0
+90000.0
+119000.0
+107000.0
+97000.0
+97000.0
+104000.0
+129000.0
+70000.0
+70000.0
+60000.0
+81000.0
+126000.0
+97000.0
+99000.0
+127000.0
+100000.0
+98000.0
+101000.0
+112000.0
+112000.0
+102000.0
+97000.0
+97000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+90000.0
+70000.0
+70000.0
+60000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+90000.0
+70000.0
+70000.0
+60000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+112000.0
+94000.0
+78000.0
+118000.0
+119000.0
+85000.0
+108000.0
+90000.0
+104000.0
+70000.0
+70000.0
+107000.0
+97000.0
+97000.0
+129000.0
+60000.0
+99000.0
+81000.0
+126000.0
+97000.0
+100000.0
+127000.0
+98000.0
+97000.0
+45000.0
+101000.0
+112000.0
+102000.0
+112000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+94000.0
+112000.0
+78000.0
+118000.0
+85000.0
+108000.0
+90000.0
+119000.0
+107000.0
+97000.0
+97000.0
+104000.0
+129000.0
+70000.0
+70000.0
+60000.0
+81000.0
+126000.0
+97000.0
+99000.0
+127000.0
+100000.0
+98000.0
+101000.0
+112000.0
+112000.0
+102000.0
+97000.0
+97000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
+45000.0
+90000.0
+70000.0
+70000.0
+60000.0
diff --git a/mass_scraper/pythonresults.csv b/mass_scraper/pythonresults.csv
new file mode 100644
index 0000000..7aaf2d3
--- /dev/null
+++ b/mass_scraper/pythonresults.csv
@@ -0,0 +1,238 @@
+python
+75000.0
+70000.0
+75000.0
+75000.0
+75000.0
+75000.0
+75000.0
+75000.0
+70000.0
+75000.0
+75000.0
+75000.0
+75000.0
+70000.0
+75000.0
+75000.0
+75000.0
+70000.0
+75000.0
+75000.0
+75000.0
+75000.0
+70000.0
+75000.0
+75000.0
+75000.0
+75000.0
+75000.0
+75000.0
+75000.0
+75000.0
+75000.0
+70000.0
+75000.0
+94000.0
+119000.0
+94000.0
+109000.0
+99000.0
+125000.0
+104000.0
+94000.0
+96000.0
+101000.0
+115000.0
+122000.0
+103000.0
+81000.0
+62000.0
+106000.0
+108000.0
+86000.0
+102000.0
+120000.0
+112000.0
+75000.0
+75000.0
+70000.0
+75000.0
+75000.0
+70000.0
+75000.0
+75000.0
+94000.0
+119000.0
+94000.0
+109000.0
+99000.0
+125000.0
+104000.0
+96000.0
+101000.0
+122000.0
+103000.0
+81000.0
+62000.0
+106000.0
+108000.0
+86000.0
+102000.0
+120000.0
+112000.0
+127000.0
+116000.0
+107000.0
+75000.0
+75000.0
+75000.0
+70000.0
+75000.0
+70000.0
+75000.0
+75000.0
+75000.0
+70000.0
+75000.0
+75000.0
+75000.0
+75000.0
+75000.0
+75000.0
+70000.0
+75000.0
+75000.0
+75000.0
+75000.0
+75000.0
+75000.0
+75000.0
+75000.0
+94000.0
+119000.0
+94000.0
+109000.0
+99000.0
+125000.0
+104000.0
+94000.0
+96000.0
+101000.0
+115000.0
+122000.0
+103000.0
+81000.0
+62000.0
+106000.0
+108000.0
+86000.0
+102000.0
+120000.0
+112000.0
+75000.0
+75000.0
+70000.0
+75000.0
+116000.0
+75000.0
+94000.0
+119000.0
+94000.0
+109000.0
+128000.0
+99000.0
+109000.0
+125000.0
+104000.0
+96000.0
+101000.0
+122000.0
+103000.0
+81000.0
+62000.0
+106000.0
+108000.0
+86000.0
+80000.0
+102000.0
+120000.0
+112000.0
+127000.0
+116000.0
+107000.0
+75000.0
+116000.0
+94000.0
+94000.0
+119000.0
+109000.0
+75000.0
+128000.0
+99000.0
+109000.0
+104000.0
+94000.0
+96000.0
+101000.0
+125000.0
+115000.0
+122000.0
+103000.0
+81000.0
+92000.0
+108000.0
+106000.0
+102000.0
+120000.0
+86000.0
+62000.0
+112000.0
+75000.0
+70000.0
+75000.0
+75000.0
+75000.0
+75000.0
+75000.0
+70000.0
+75000.0
+75000.0
+75000.0
+75000.0
+75000.0
+75000.0
+75000.0
+75000.0
+75000.0
+75000.0
+75000.0
+75000.0
+70000.0
+75000.0
+75000.0
+70000.0
+75000.0
+70000.0
+75000.0
+75000.0
+70000.0
+75000.0
+75000.0
+75000.0
+75000.0
+70000.0
+75000.0
+75000.0
+75000.0
+75000.0
+75000.0
+75000.0
+75000.0
+75000.0
+70000.0
+75000.0
+75000.0
+70000.0
+75000.0
+75000.0
diff --git a/mass_scraper/softwaredevresults.csv b/mass_scraper/softwaredevresults.csv
new file mode 100644
index 0000000..dd14f8d
--- /dev/null
+++ b/mass_scraper/softwaredevresults.csv
@@ -0,0 +1,749 @@
+softwaredev
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+70000.0
+80000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+70000.0
+80000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+70000.0
+80000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+70000.0
+80000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+70000.0
+80000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+70000.0
+80000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+70000.0
+80000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+70000.0
+80000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+70000.0
+80000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+70000.0
+80000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+70000.0
+80000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+70000.0
+80000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+70000.0
+80000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+70000.0
+80000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+70000.0
+80000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+70000.0
+80000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+70000.0
+80000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+70000.0
+80000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+70000.0
+80000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+126000.0
+116000.0
+100000.0
+109000.0
+102000.0
+102000.0
+64000.0
+82000.0
+77000.0
+80000.0
+48000.0
+86000.0
+94000.0
+87000.0
+99000.0
+115000.0
+108000.0
+83000.0
+82000.0
+107000.0
+82000.0
+102000.0
+104000.0
+77000.0
+119000.0
+80000.0
+65000.0
+93000.0
+114000.0
+94000.0
+70000.0
+126000.0
+106000.0
+99000.0
+87000.0
+96000.0
+105000.0
+89000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+70000.0
+80000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+70000.0
+80000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+70000.0
+80000.0
+60000.0
+126000.0
+116000.0
+100000.0
+109000.0
+102000.0
+102000.0
+64000.0
+82000.0
+77000.0
+80000.0
+48000.0
+86000.0
+94000.0
+87000.0
+103000.0
+99000.0
+115000.0
+108000.0
+83000.0
+82000.0
+107000.0
+82000.0
+102000.0
+81000.0
+104000.0
+94000.0
+119000.0
+80000.0
+65000.0
+93000.0
+114000.0
+94000.0
+70000.0
+126000.0
+106000.0
+99000.0
+60000.0
+126000.0
+116000.0
+100000.0
+109000.0
+102000.0
+102000.0
+82000.0
+77000.0
+80000.0
+48000.0
+86000.0
+94000.0
+87000.0
+99000.0
+115000.0
+108000.0
+83000.0
+82000.0
+107000.0
+82000.0
+102000.0
+104000.0
+77000.0
+119000.0
+80000.0
+65000.0
+93000.0
+114000.0
+94000.0
+70000.0
+126000.0
+106000.0
+99000.0
+87000.0
+96000.0
+105000.0
+89000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+70000.0
+80000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+70000.0
+80000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+70000.0
+80000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+70000.0
+80000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
+60000.0
+80000.0
+48000.0
+80000.0
+65000.0
+70000.0
diff --git a/mass_scraper/uxresults.csv b/mass_scraper/uxresults.csv
new file mode 100644
index 0000000..6110050
--- /dev/null
+++ b/mass_scraper/uxresults.csv
@@ -0,0 +1,30 @@
+ux
+74000.0
+82000.0
+86000.0
+79000.0
+83000.0
+93000.0
+64000.0
+96000.0
+78000.0
+86000.0
+95000.0
+88000.0
+88000.0
+92000.0
+93000.0
+73000.0
+70000.0
+87000.0
+81000.0
+85000.0
+71000.0
+97000.0
+84000.0
+111000.0
+82000.0
+89000.0
+97000.0
+83000.0
+65000.0
diff --git a/mass_scraper/webdevresults.csv b/mass_scraper/webdevresults.csv
new file mode 100644
index 0000000..990b844
--- /dev/null
+++ b/mass_scraper/webdevresults.csv
@@ -0,0 +1,469 @@
+webdev
+65000.0
+80000.0
+70000.0
+70000.0
+65000.0
+80000.0
+70000.0
+65000.0
+80000.0
+70000.0
+70000.0
+65000.0
+80000.0
+70000.0
+65000.0
+80000.0
+70000.0
+65000.0
+80000.0
+70000.0
+70000.0
+65000.0
+80000.0
+70000.0
+65000.0
+80000.0
+70000.0
+65000.0
+80000.0
+70000.0
+70000.0
+65000.0
+80000.0
+70000.0
+70000.0
+65000.0
+80000.0
+70000.0
+65000.0
+80000.0
+70000.0
+65000.0
+80000.0
+70000.0
+70000.0
+65000.0
+80000.0
+70000.0
+65000.0
+80000.0
+70000.0
+65000.0
+97000.0
+113000.0
+111000.0
+96000.0
+92000.0
+89000.0
+111000.0
+80000.0
+82000.0
+89000.0
+133000.0
+105000.0
+91000.0
+70000.0
+92000.0
+117000.0
+85000.0
+106000.0
+83000.0
+77000.0
+109000.0
+85000.0
+98000.0
+97000.0
+114000.0
+80000.0
+100000.0
+65000.0
+80000.0
+70000.0
+65000.0
+80000.0
+70000.0
+70000.0
+65000.0
+80000.0
+70000.0
+65000.0
+80000.0
+70000.0
+70000.0
+65000.0
+80000.0
+70000.0
+70000.0
+65000.0
+80000.0
+70000.0
+70000.0
+65000.0
+80000.0
+70000.0
+65000.0
+80000.0
+70000.0
+65000.0
+80000.0
+70000.0
+70000.0
+65000.0
+80000.0
+70000.0
+70000.0
+65000.0
+80000.0
+70000.0
+70000.0
+65000.0
+80000.0
+70000.0
+70000.0
+65000.0
+80000.0
+70000.0
+70000.0
+65000.0
+80000.0
+70000.0
+70000.0
+65000.0
+80000.0
+70000.0
+70000.0
+65000.0
+80000.0
+70000.0
+65000.0
+80000.0
+70000.0
+70000.0
+65000.0
+80000.0
+70000.0
+70000.0
+65000.0
+80000.0
+70000.0
+70000.0
+65000.0
+80000.0
+70000.0
+65000.0
+80000.0
+70000.0
+70000.0
+65000.0
+80000.0
+70000.0
+70000.0
+65000.0
+80000.0
+70000.0
+70000.0
+65000.0
+80000.0
+70000.0
+65000.0
+80000.0
+70000.0
+65000.0
+80000.0
+70000.0
+70000.0
+65000.0
+80000.0
+70000.0
+65000.0
+97000.0
+113000.0
+111000.0
+96000.0
+92000.0
+89000.0
+111000.0
+80000.0
+82000.0
+89000.0
+133000.0
+105000.0
+91000.0
+70000.0
+92000.0
+117000.0
+85000.0
+106000.0
+83000.0
+77000.0
+109000.0
+85000.0
+98000.0
+97000.0
+114000.0
+80000.0
+100000.0
+65000.0
+80000.0
+70000.0
+70000.0
+65000.0
+80000.0
+70000.0
+70000.0
+65000.0
+80000.0
+70000.0
+65000.0
+80000.0
+70000.0
+70000.0
+65000.0
+80000.0
+70000.0
+65000.0
+97000.0
+113000.0
+111000.0
+96000.0
+92000.0
+89000.0
+111000.0
+80000.0
+82000.0
+70000.0
+133000.0
+105000.0
+83000.0
+89000.0
+92000.0
+91000.0
+117000.0
+85000.0
+85000.0
+106000.0
+109000.0
+77000.0
+80000.0
+70000.0
+98000.0
+97000.0
+65000.0
+80000.0
+70000.0
+70000.0
+65000.0
+80000.0
+70000.0
+65000.0
+80000.0
+70000.0
+70000.0
+65000.0
+80000.0
+70000.0
+65000.0
+80000.0
+70000.0
+65000.0
+80000.0
+70000.0
+70000.0
+65000.0
+80000.0
+70000.0
+65000.0
+80000.0
+70000.0
+70000.0
+65000.0
+80000.0
+70000.0
+70000.0
+65000.0
+80000.0
+70000.0
+65000.0
+80000.0
+70000.0
+70000.0
+65000.0
+80000.0
+70000.0
+65000.0
+80000.0
+70000.0
+70000.0
+65000.0
+80000.0
+70000.0
+65000.0
+80000.0
+70000.0
+65000.0
+80000.0
+70000.0
+70000.0
+65000.0
+80000.0
+70000.0
+65000.0
+80000.0
+70000.0
+65000.0
+80000.0
+70000.0
+65000.0
+80000.0
+70000.0
+70000.0
+65000.0
+80000.0
+70000.0
+65000.0
+80000.0
+70000.0
+65000.0
+80000.0
+70000.0
+65000.0
+80000.0
+70000.0
+70000.0
+65000.0
+97000.0
+113000.0
+111000.0
+96000.0
+92000.0
+89000.0
+111000.0
+80000.0
+82000.0
+89000.0
+133000.0
+105000.0
+91000.0
+70000.0
+92000.0
+117000.0
+85000.0
+106000.0
+83000.0
+77000.0
+109000.0
+85000.0
+98000.0
+97000.0
+114000.0
+80000.0
+100000.0
+65000.0
+80000.0
+70000.0
+70000.0
+65000.0
+97000.0
+113000.0
+111000.0
+96000.0
+92000.0
+89000.0
+111000.0
+80000.0
+82000.0
+70000.0
+133000.0
+105000.0
+83000.0
+89000.0
+92000.0
+91000.0
+117000.0
+85000.0
+85000.0
+106000.0
+109000.0
+77000.0
+80000.0
+70000.0
+98000.0
+97000.0
+65000.0
+80000.0
+70000.0
+65000.0
+80000.0
+70000.0
+65000.0
+80000.0
+70000.0
+65000.0
+80000.0
+70000.0
+70000.0
+65000.0
+80000.0
+70000.0
+65000.0
+80000.0
+70000.0
+70000.0
+65000.0
+80000.0
+70000.0
+65000.0
+80000.0
+70000.0
+70000.0
+65000.0
+80000.0
+70000.0
+70000.0
+65000.0
+80000.0
+70000.0
+65000.0
+80000.0
+70000.0
+65000.0
+80000.0
+70000.0
+65000.0
+80000.0
+70000.0
+65000.0
+80000.0
+70000.0
+65000.0
+80000.0
+70000.0
+65000.0
+80000.0
+70000.0
+65000.0
+80000.0
+70000.0
+65000.0
+80000.0
+70000.0
+65000.0
+80000.0
+70000.0
+70000.0
+65000.0
+80000.0
+70000.0
+65000.0
+80000.0
+70000.0
+70000.0
+65000.0
+80000.0
+70000.0
+70000.0
+65000.0
+80000.0
+70000.0
diff --git a/mockups/about.html b/mockups/about.html
new file mode 100644
index 0000000..e69de29
diff --git a/mockups/admin.html b/mockups/admin.html
new file mode 100644
index 0000000..dbe2206
--- /dev/null
+++ b/mockups/admin.html
@@ -0,0 +1,53 @@
+
+
+
+
+ Opportune | Get Hired
+
+
+
+
+
+
+
+
+
+
+
+ Admin Analytics
+
+
+
+
diff --git a/mockups/base.css b/mockups/base.css
new file mode 100644
index 0000000..fa0c770
--- /dev/null
+++ b/mockups/base.css
@@ -0,0 +1,20 @@
+@import url('https://fonts.googleapis.com/css?family=Pacifico|Grand+Hotel');
+@import url('https://fonts.googleapis.com/css?family=Roboto');
+
+a {
+ text-decoration: none;
+}
+
+h1 {
+ font-family: 'Pacifico';
+ font-style: italic;
+}
+
+body {
+ background-image: url('whitebg.png')
+}
+
+h2, h3 {
+ font-family: 'Roboto';
+ font-style: italic;
+}
diff --git a/mockups/index.html b/mockups/index.html
new file mode 100644
index 0000000..2b3685a
--- /dev/null
+++ b/mockups/index.html
@@ -0,0 +1,76 @@
+
+
+
+
+ Opportune | Get Hired
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Gems only.
+
Opportune mines for you. Don't waste time combing through job descriptions. We only serve up jobs that are relevant to you, based on your keyword preferences.
+
+
+
+
+
+
Save time.
+
Gone are the days of searching four different job boards each day. Opportune does the work for you, with convenient email notifications, so you don't have to.
+
+
+
+
+
+
Hire smarter.
+
You might not be reaching people who want to work for you. Opportune can arm you with keyword statistics so you know what potential employees look for.
+
+
+
+
+
+
+
+
diff --git a/mockups/layout.css b/mockups/layout.css
new file mode 100644
index 0000000..b540155
--- /dev/null
+++ b/mockups/layout.css
@@ -0,0 +1,36 @@
+header {
+ background-color: rgb(0, 206, 252);
+ color: white;
+ list-style: none;
+ height: 8vw;
+ font-family: 'Pacifico';
+ font-size: 1.5vw;
+ font-style: italic;
+}
+
+header h1 {
+ position: absolute;
+ top: -5px;
+ left: 42.5%;
+}
+
+footer {
+ background-color: rgb(140, 140, 140);
+ color: white;
+ list-style: none;
+ height: 7vw;
+ font-family: 'Pacifico';
+ font-size: 1.75vw;
+ font-style: italic;
+ text-align: center;
+ left: 0;
+ bottom: 0;
+ width: 100%;
+}
+
+footer ul li {
+ list-style: none;
+ font-family: 'Roboto';
+ font-size: 1vw;
+ display: inline-block;
+}
diff --git a/mockups/login.html b/mockups/login.html
new file mode 100644
index 0000000..834d378
--- /dev/null
+++ b/mockups/login.html
@@ -0,0 +1,58 @@
+
+
+
+
+ Opportune | Get Hired
+
+
+
+
+
+
+
+
+
+
+
Log in to your account.
+
+
+
+
+
+
diff --git a/mockups/modules.css b/mockups/modules.css
new file mode 100644
index 0000000..a73b02d
--- /dev/null
+++ b/mockups/modules.css
@@ -0,0 +1,173 @@
+.columns {
+ list-style-type: none;
+ font-family: 'Roboto';
+ text-align: center;
+}
+
+.columns li {
+ display: inline-block;
+ margin: 4vw;
+ padding: 20px;
+ width: 300px;
+ background-color: white;
+ box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
+}
+
+.fa-gem, .fa-hourglass, .fa-laptop {
+ padding-top: 10px;
+ color: lightgrey;
+}
+
+.fa-gem:hover, .fa-hourglass:hover, .fa-laptop:hover {
+ padding-top: 10px;
+ color: rgb(0, 206, 252);
+}
+
+.columns h3, h2, h1 {
+ font-family: 'Pacifico';
+}
+
+button {
+ color: black;
+ font-family: 'Roboto';
+ background-color: rgb(255, 147, 7);
+ font-size: 1vw;
+ height: 40px;
+ width: 255px;
+}
+
+form input {
+ display: block;
+ width: 250px;
+ height: 40px;
+ margin-top: 5px;
+ margin-bottom: 5px;
+ margin-left: 41.25%;
+}
+
+.login, .register, .profile {
+ text-align: center;
+}
+
+#loginFooter {
+ position: fixed;
+}
+
+#registerFooter {
+ position: fixed;
+}
+
+/*************************************/
+/*Everything below is driving the menu*/
+/*************************************/
+a
+{
+ text-decoration: none;
+ color: #232323;
+ transition: color 0.3s ease;
+}
+
+a:hover
+{
+ color: tomato;
+}
+
+#menuToggle
+{
+ display: block;
+ position: relative;
+ top: 45px;
+ left: 40px;
+ width: 100px;
+ z-index: 1;
+ -webkit-user-select: none;
+ user-select: none;
+}
+
+#menuToggle input
+{
+ display: block;
+ width: 40px;
+ height: 32px;
+ position: absolute;
+ top: -7px;
+ left: -5px;
+ cursor: pointer;
+ opacity: 0;
+ z-index: 2;
+ -webkit-touch-callout: none;
+}
+
+#menuToggle span
+{
+ display: block;
+ width: 33px;
+ height: 4px;
+ margin-bottom: 5px;
+ position: relative;
+ background: white;
+ border-radius: 3px;
+ z-index: 1;
+ transform-origin: 4px 0px;
+ transition: transform 0.5s cubic-bezier(0.77,0.2,0.05,1.0),
+ background 0.5s cubic-bezier(0.77,0.2,0.05,1.0),
+ opacity 0.55s ease;
+}
+
+#menuToggle span:first-child
+{
+ transform-origin: 0% 0%;
+}
+
+#menuToggle span:nth-last-child(2)
+{
+ transform-origin: 0% 100%;
+}
+
+#menuToggle input:checked ~ span
+{
+ opacity: 1;
+ transform: rotate(45deg) translate(-2px, -1px);
+ background: #232323;
+}
+
+#menuToggle input:checked ~ span:nth-last-child(3)
+{
+ opacity: 0;
+ transform: rotate(0deg) scale(0.2, 0.2);
+}
+
+#menuToggle input:checked ~ span:nth-last-child(2)
+{
+ transform: rotate(-45deg) translate(0, -1px);
+}
+#menu
+{
+ font-family: 'Roboto';
+ font-style: italic;
+ position: absolute;
+ width: 180px;
+ margin: -100px 0 0 -50px;
+ padding: 50px;
+ padding-top: 125px;
+
+ background: #c9c9c9;
+ list-style-type: none;
+ -webkit-font-smoothing: antialiased;
+
+ transform-origin: 0% 0%;
+ transform: translate(-100%, 0);
+
+ transition: transform 0.5s cubic-bezier(0.77,0.2,0.05,1.0);
+}
+
+#menu li
+{
+ padding: 10px 0;
+ font-size: 22px;
+}
+
+#menuToggle input:checked ~ ul
+{
+ transform: none;
+}
diff --git a/mockups/normalize.css b/mockups/normalize.css
new file mode 100644
index 0000000..47b010e
--- /dev/null
+++ b/mockups/normalize.css
@@ -0,0 +1,341 @@
+/*! normalize.css v8.0.0 | MIT License | github.com/necolas/normalize.css */
+
+/* Document
+ ========================================================================== */
+
+/**
+ * 1. Correct the line height in all browsers.
+ * 2. Prevent adjustments of font size after orientation changes in iOS.
+ */
+
+html {
+ line-height: 1.15; /* 1 */
+ -webkit-text-size-adjust: 100%; /* 2 */
+}
+
+/* Sections
+ ========================================================================== */
+
+/**
+ * Remove the margin in all browsers.
+ */
+
+body {
+ margin: 0;
+}
+
+/**
+ * Correct the font size and margin on `h1` elements within `section` and
+ * `article` contexts in Chrome, Firefox, and Safari.
+ */
+
+h1 {
+ font-size: 2em;
+ margin: 0.67em 0;
+}
+
+/* Grouping content
+ ========================================================================== */
+
+/**
+ * 1. Add the correct box sizing in Firefox.
+ * 2. Show the overflow in Edge and IE.
+ */
+
+hr {
+ box-sizing: content-box; /* 1 */
+ height: 0; /* 1 */
+ overflow: visible; /* 2 */
+}
+
+/**
+ * 1. Correct the inheritance and scaling of font size in all browsers.
+ * 2. Correct the odd `em` font sizing in all browsers.
+ */
+
+pre {
+ font-family: monospace, monospace; /* 1 */
+ font-size: 1em; /* 2 */
+}
+
+/* Text-level semantics
+ ========================================================================== */
+
+/**
+ * Remove the gray background on active links in IE 10.
+ */
+
+a {
+ background-color: transparent;
+}
+
+/**
+ * 1. Remove the bottom border in Chrome 57-
+ * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
+ */
+
+abbr[title] {
+ border-bottom: none; /* 1 */
+ text-decoration: underline; /* 2 */
+ text-decoration: underline dotted; /* 2 */
+}
+
+/**
+ * Add the correct font weight in Chrome, Edge, and Safari.
+ */
+
+b,
+strong {
+ font-weight: bolder;
+}
+
+/**
+ * 1. Correct the inheritance and scaling of font size in all browsers.
+ * 2. Correct the odd `em` font sizing in all browsers.
+ */
+
+code,
+kbd,
+samp {
+ font-family: monospace, monospace; /* 1 */
+ font-size: 1em; /* 2 */
+}
+
+/**
+ * Add the correct font size in all browsers.
+ */
+
+small {
+ font-size: 80%;
+}
+
+/**
+ * Prevent `sub` and `sup` elements from affecting the line height in
+ * all browsers.
+ */
+
+sub,
+sup {
+ font-size: 75%;
+ line-height: 0;
+ position: relative;
+ vertical-align: baseline;
+}
+
+sub {
+ bottom: -0.25em;
+}
+
+sup {
+ top: -0.5em;
+}
+
+/* Embedded content
+ ========================================================================== */
+
+/**
+ * Remove the border on images inside links in IE 10.
+ */
+
+img {
+ border-style: none;
+}
+
+/* Forms
+ ========================================================================== */
+
+/**
+ * 1. Change the font styles in all browsers.
+ * 2. Remove the margin in Firefox and Safari.
+ */
+
+button,
+input,
+optgroup,
+select,
+textarea {
+ font-family: inherit; /* 1 */
+ font-size: 100%; /* 1 */
+ line-height: 1.15; /* 1 */
+ margin: 0; /* 2 */
+}
+
+/**
+ * Show the overflow in IE.
+ * 1. Show the overflow in Edge.
+ */
+
+button,
+input { /* 1 */
+ overflow: visible;
+}
+
+/**
+ * Remove the inheritance of text transform in Edge, Firefox, and IE.
+ * 1. Remove the inheritance of text transform in Firefox.
+ */
+
+button,
+select { /* 1 */
+ text-transform: none;
+}
+
+/**
+ * Correct the inability to style clickable types in iOS and Safari.
+ */
+
+button,
+[type="button"],
+[type="reset"],
+[type="submit"] {
+ -webkit-appearance: button;
+}
+
+/**
+ * Remove the inner border and padding in Firefox.
+ */
+
+button::-moz-focus-inner,
+[type="button"]::-moz-focus-inner,
+[type="reset"]::-moz-focus-inner,
+[type="submit"]::-moz-focus-inner {
+ border-style: none;
+ padding: 0;
+}
+
+/**
+ * Restore the focus styles unset by the previous rule.
+ */
+
+button:-moz-focusring,
+[type="button"]:-moz-focusring,
+[type="reset"]:-moz-focusring,
+[type="submit"]:-moz-focusring {
+ outline: 1px dotted ButtonText;
+}
+
+/**
+ * Correct the padding in Firefox.
+ */
+
+fieldset {
+ padding: 0.35em 0.75em 0.625em;
+}
+
+/**
+ * 1. Correct the text wrapping in Edge and IE.
+ * 2. Correct the color inheritance from `fieldset` elements in IE.
+ * 3. Remove the padding so developers are not caught out when they zero out
+ * `fieldset` elements in all browsers.
+ */
+
+legend {
+ box-sizing: border-box; /* 1 */
+ color: inherit; /* 2 */
+ display: table; /* 1 */
+ max-width: 100%; /* 1 */
+ padding: 0; /* 3 */
+ white-space: normal; /* 1 */
+}
+
+/**
+ * Add the correct vertical alignment in Chrome, Firefox, and Opera.
+ */
+
+progress {
+ vertical-align: baseline;
+}
+
+/**
+ * Remove the default vertical scrollbar in IE 10+.
+ */
+
+textarea {
+ overflow: auto;
+}
+
+/**
+ * 1. Add the correct box sizing in IE 10.
+ * 2. Remove the padding in IE 10.
+ */
+
+[type="checkbox"],
+[type="radio"] {
+ box-sizing: border-box; /* 1 */
+ padding: 0; /* 2 */
+}
+
+/**
+ * Correct the cursor style of increment and decrement buttons in Chrome.
+ */
+
+[type="number"]::-webkit-inner-spin-button,
+[type="number"]::-webkit-outer-spin-button {
+ height: auto;
+}
+
+/**
+ * 1. Correct the odd appearance in Chrome and Safari.
+ * 2. Correct the outline style in Safari.
+ */
+
+[type="search"] {
+ -webkit-appearance: textfield; /* 1 */
+ outline-offset: -2px; /* 2 */
+}
+
+/**
+ * Remove the inner padding in Chrome and Safari on macOS.
+ */
+
+[type="search"]::-webkit-search-decoration {
+ -webkit-appearance: none;
+}
+
+/**
+ * 1. Correct the inability to style clickable types in iOS and Safari.
+ * 2. Change font properties to `inherit` in Safari.
+ */
+
+::-webkit-file-upload-button {
+ -webkit-appearance: button; /* 1 */
+ font: inherit; /* 2 */
+}
+
+/* Interactive
+ ========================================================================== */
+
+/*
+ * Add the correct display in Edge, IE 10+, and Firefox.
+ */
+
+details {
+ display: block;
+}
+
+/*
+ * Add the correct display in all browsers.
+ */
+
+summary {
+ display: list-item;
+}
+
+/* Misc
+ ========================================================================== */
+
+/**
+ * Add the correct display in IE 10+.
+ */
+
+template {
+ display: none;
+}
+
+/**
+ * Add the correct display in IE 10.
+ */
+
+[hidden] {
+ display: none;
+}
diff --git a/mockups/profile.html b/mockups/profile.html
new file mode 100644
index 0000000..17629e9
--- /dev/null
+++ b/mockups/profile.html
@@ -0,0 +1,60 @@
+
+
+
+
+ Opportune | Get Hired
+
+
+
+
+
+
+
+
+
+
+
Profile
+ Update your profile
+
+ Edit your keywords
+
+
+
+
+
diff --git a/mockups/register.html b/mockups/register.html
new file mode 100644
index 0000000..5eff1cb
--- /dev/null
+++ b/mockups/register.html
@@ -0,0 +1,59 @@
+
+
+
+
+ Opportune | Get Hired
+
+
+
+
+
+
+
+
+
+
+
Register for an account.
+
+
+
+
+
+
diff --git a/mockups/whitebg.png b/mockups/whitebg.png
new file mode 100644
index 0000000..9549502
Binary files /dev/null and b/mockups/whitebg.png differ
diff --git a/opportune/__init__.py b/opportune/__init__.py
new file mode 100644
index 0000000..f915645
--- /dev/null
+++ b/opportune/__init__.py
@@ -0,0 +1,16 @@
+import os
+from pyramid.config import Configurator
+
+
+def main(global_config, **settings):
+ """ This function returns a Pyramid WSGI application.
+ """
+ if os.environ.get('DATABASE_URL', ''):
+ settings["sqlalchemy.url"] = os.environ["DATABASE_URL"]
+ config = Configurator(settings=settings)
+ config.include('pyramid_jinja2')
+ config.include('.models')
+ config.include('.routes')
+ config.include('.security')
+ config.scan()
+ return config.make_wsgi_app()
diff --git a/opportune/models/__init__.py b/opportune/models/__init__.py
new file mode 100644
index 0000000..26682f0
--- /dev/null
+++ b/opportune/models/__init__.py
@@ -0,0 +1,79 @@
+from sqlalchemy import engine_from_config
+from sqlalchemy.orm import sessionmaker
+from sqlalchemy.orm import configure_mappers
+import zope.sqlalchemy
+
+# import or define all models here to ensure they are attached to the
+# Base.metadata prior to any initialization routines
+from .accounts import Account # flake8: noqa
+from .keywords import Keyword # flake8: noqa
+from .association import Association # flake8: noqa
+
+# run configure_mappers after defining all of the models to ensure
+# all relationships can be setup
+configure_mappers()
+
+
+def get_engine(settings, prefix='sqlalchemy.'):
+ return engine_from_config(settings, prefix)
+
+
+def get_session_factory(engine):
+ factory = sessionmaker()
+ factory.configure(bind=engine)
+ return factory
+
+
+def get_tm_session(session_factory, transaction_manager):
+ """
+ Get a ``sqlalchemy.orm.Session`` instance backed by a transaction.
+
+ This function will hook the session to the transaction manager which
+ will take care of committing any changes.
+
+ - When using pyramid_tm it will automatically be committed or aborted
+ depending on whether an exception is raised.
+
+ - When using scripts you should wrap the session in a manager yourself.
+ For example::
+
+ import transaction
+
+ engine = get_engine(settings)
+ session_factory = get_session_factory(engine)
+ with transaction.manager:
+ dbsession = get_tm_session(session_factory, transaction.manager)
+
+ """
+ dbsession = session_factory()
+ zope.sqlalchemy.register(
+ dbsession, transaction_manager=transaction_manager)
+ return dbsession
+
+
+def includeme(config):
+ """
+ Initialize the model for a Pyramid app.
+
+ Activate this setup using ``config.include('opportune.models')``.
+
+ """
+ settings = config.get_settings()
+ settings['tm.manager_hook'] = 'pyramid_tm.explicit_manager'
+
+ # use pyramid_tm to hook the transaction lifecycle to the request
+ config.include('pyramid_tm')
+
+ # use pyramid_retry to retry a request when transient exceptions occur
+ config.include('pyramid_retry')
+
+ session_factory = get_session_factory(get_engine(settings))
+ config.registry['dbsession_factory'] = session_factory
+
+ # make request.dbsession available for use in Pyramid
+ config.add_request_method(
+ # r.tm is the transaction manager used by pyramid_tm
+ lambda r: get_tm_session(session_factory, r.tm),
+ 'dbsession',
+ reify=True
+ )
diff --git a/opportune/models/accounts.py b/opportune/models/accounts.py
new file mode 100644
index 0000000..0c08e08
--- /dev/null
+++ b/opportune/models/accounts.py
@@ -0,0 +1,48 @@
+from .meta import Base
+from sqlalchemy.exc import DBAPIError
+from cryptacular import bcrypt
+from sqlalchemy.orm import relationship
+from sqlalchemy import (
+ Column,
+ Integer,
+ String,
+ Boolean,
+)
+
+manager = bcrypt.BCRYPTPasswordManager()
+
+
+class Account(Base):
+ __tablename__ = 'accounts'
+ id = Column(Integer, primary_key=True)
+ username = Column(String, unique=True, nullable=False)
+ password = Column(String, nullable=False)
+ email = Column(String, nullable=False)
+ admin = Column(Boolean, nullable=False, default=False)
+
+ keywords = relationship(
+ 'Keyword',
+ secondary='user_keywords')
+
+ def __init__(self, username, email, password, admin=False):
+ """Initialize a new user with encoded password."""
+ self.username = username
+ self.email = email
+ self.password = manager.encode(password, 10)
+ self.admin = admin
+
+ @classmethod
+ def check_credentials(cls, request=None, username=None, password=None):
+ """Authenticate a user."""
+ if request.dbsession is None:
+ raise DBAPIError
+
+ is_authenticated = False
+
+ query = request.dbsession.query(cls).filter(cls.username == username).one_or_none()
+
+ if query is not None:
+ if manager.check(query.password, password):
+ is_authenticated = True
+
+ return (is_authenticated, username)
diff --git a/opportune/models/association.py b/opportune/models/association.py
new file mode 100644
index 0000000..26c7123
--- /dev/null
+++ b/opportune/models/association.py
@@ -0,0 +1,16 @@
+from sqlalchemy import (
+ Column,
+ Integer,
+ ForeignKey,
+ String
+)
+
+from .meta import Base
+
+
+class Association(Base):
+ """Association table for users and keywords."""
+ __tablename__ = 'user_keywords'
+ id = Column(Integer, primary_key=True)
+ user_id = Column(String, ForeignKey('accounts.username'), nullable=False)
+ keyword_id = Column(String, ForeignKey('keywords.keyword'), nullable=False)
diff --git a/opportune/models/keywords.py b/opportune/models/keywords.py
new file mode 100644
index 0000000..8dd4448
--- /dev/null
+++ b/opportune/models/keywords.py
@@ -0,0 +1,22 @@
+from sqlalchemy import (
+ Index,
+ Column,
+ Integer,
+ String,
+)
+from sqlalchemy.orm import relationship
+
+from .meta import Base
+
+
+class Keyword(Base):
+ __tablename__ = 'keywords'
+ id = Column(Integer, primary_key=True)
+ keyword = Column(String, nullable=False, unique=True)
+ accounts = relationship(
+ 'Account',
+ secondary='user_keywords')
+
+
+Index('entry_index', Keyword.id, unique=True, mysql_length=255)
+
\ No newline at end of file
diff --git a/opportune/models/meta.py b/opportune/models/meta.py
new file mode 100644
index 0000000..02285b3
--- /dev/null
+++ b/opportune/models/meta.py
@@ -0,0 +1,16 @@
+from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.schema import MetaData
+
+# Recommended naming convention used by Alembic, as various different database
+# providers will autogenerate vastly different names making migrations more
+# difficult. See: http://alembic.zzzcomputing.com/en/latest/naming.html
+NAMING_CONVENTION = {
+ "ix": "ix_%(column_0_label)s",
+ "uq": "uq_%(table_name)s_%(column_0_name)s",
+ "ck": "ck_%(table_name)s_%(constraint_name)s",
+ "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
+ "pk": "pk_%(table_name)s"
+}
+
+metadata = MetaData(naming_convention=NAMING_CONVENTION)
+Base = declarative_base(metadata=metadata)
diff --git a/opportune/routes.py b/opportune/routes.py
new file mode 100644
index 0000000..0959383
--- /dev/null
+++ b/opportune/routes.py
@@ -0,0 +1,20 @@
+def includeme(config):
+ config.add_static_view('static', 'static', cache_max_age=0)
+ config.add_route('home', '/')
+ config.add_route('auth', '/auth')
+ config.add_route('logout', '/logout')
+ config.add_route('profile', '/profile')
+ config.add_route('profile/delete', '/profile/delete')
+ config.add_route('profile/update', '/profile/update')
+ config.add_route('profile/keywords/delete', '/profile/keywords/delete')
+ config.add_route('profile/delete-account', '/profile/delete-account')
+ config.add_route('analytics', '/analytics')
+ config.add_route('about', '/about')
+ config.add_route('search', '/search')
+ config.add_route('search/results', '/search/results')
+ config.add_route('search/results/email', '/search/results/email')
+ config.add_route('search/results/download', '/search/results/download')
+ config.add_route('jobs', '/jobs')
+ config.add_route('stat', '/stat')
+ config.add_route('keywords', '/keywords')
+ config.add_route('keywords/delete', '/keywords/delete')
diff --git a/opportune/scripts/__init__.py b/opportune/scripts/__init__.py
new file mode 100644
index 0000000..5bb534f
--- /dev/null
+++ b/opportune/scripts/__init__.py
@@ -0,0 +1 @@
+# package
diff --git a/opportune/scripts/initializedb.py b/opportune/scripts/initializedb.py
new file mode 100644
index 0000000..9581022
--- /dev/null
+++ b/opportune/scripts/initializedb.py
@@ -0,0 +1,48 @@
+import os
+import sys
+import transaction
+
+from pyramid.paster import (
+ get_appsettings,
+ setup_logging,
+ )
+
+from pyramid.scripts.common import parse_vars
+
+from ..models.meta import Base
+from ..models import (
+ get_engine,
+ get_session_factory,
+ get_tm_session,
+ )
+
+from ..models import Account
+from ..models import Keyword
+from ..models import Association
+
+
+def usage(argv):
+ cmd = os.path.basename(argv[0])
+ print('usage: %s [var=value]\n'
+ '(example: "%s development.ini")' % (cmd, cmd))
+ sys.exit(1)
+
+
+def main(argv=sys.argv):
+ if len(argv) < 2:
+ usage(argv)
+ config_uri = argv[1]
+ options = parse_vars(argv[2:])
+ setup_logging(config_uri)
+ settings = get_appsettings(config_uri, options=options)
+
+ engine = get_engine(settings)
+ Base.metadata.create_all(engine)
+
+ # session_factory = get_session_factory(engine)
+
+ # with transaction.manager:
+ # dbsession = get_tm_session(session_factory, transaction.manager)
+
+ # model = MyModel(name='one', value=1)
+ # dbsession.add(model)
diff --git a/opportune/security.py b/opportune/security.py
new file mode 100644
index 0000000..7c1dce4
--- /dev/null
+++ b/opportune/security.py
@@ -0,0 +1,35 @@
+import os
+from pyramid.security import Allow, Everyone, Authenticated
+from pyramid.session import SignedCookieSessionFactory
+from pyramid.authentication import AuthTktAuthenticationPolicy
+from pyramid.authorization import ACLAuthorizationPolicy
+
+
+class MyRoot:
+ def __init__(self, request):
+ self.request = request # pragma: no cover
+
+ __acl__ = [
+ (Allow, Everyone, 'view'),
+ (Allow, Authenticated, 'secret'),
+ ]
+
+
+def includeme(config): # pragma: no cover
+ auth_secret = os.environ.get('AUTH_SECRET', '401d8')
+ authz_policy = ACLAuthorizationPolicy()
+ authn_policy = AuthTktAuthenticationPolicy(
+ secret=auth_secret,
+ hashalg='sha512',
+ )
+
+ config.set_authentication_policy(authn_policy)
+ config.set_authorization_policy(authz_policy)
+ config.set_default_permission('secret')
+ config.set_root_factory(MyRoot)
+
+ session_secret = os.environ.get('SESSION_SECRET', '401d8again')
+ session_factory = SignedCookieSessionFactory(session_secret)
+
+ config.set_session_factory(session_factory)
+ config.set_default_csrf_options(require_csrf=True)
diff --git a/opportune/static/austin-400x400.png b/opportune/static/austin-400x400.png
new file mode 100644
index 0000000..4e3c95e
Binary files /dev/null and b/opportune/static/austin-400x400.png differ
diff --git a/opportune/static/austin.png b/opportune/static/austin.png
new file mode 100644
index 0000000..651576a
Binary files /dev/null and b/opportune/static/austin.png differ
diff --git a/opportune/static/base.css b/opportune/static/base.css
new file mode 100644
index 0000000..6290db7
--- /dev/null
+++ b/opportune/static/base.css
@@ -0,0 +1,24 @@
+@import url('https://fonts.googleapis.com/css?family=Pacifico|Grand+Hotel');
+@import url('https://fonts.googleapis.com/css?family=Roboto');
+
+a {
+ text-decoration: none;
+}
+
+p {
+ font-family: 'Roboto';
+}
+
+h1 {
+ font-family: 'Pacifico';
+ font-style: italic;
+}
+
+body {
+ background-image: url('whitebg.png')
+}
+
+h2, h3 {
+ font-family: 'Roboto';
+ font-style: italic;
+}
diff --git a/opportune/static/gene-400x450.png b/opportune/static/gene-400x450.png
new file mode 100644
index 0000000..da99a04
Binary files /dev/null and b/opportune/static/gene-400x450.png differ
diff --git a/opportune/static/gene.png b/opportune/static/gene.png
new file mode 100644
index 0000000..98e1c17
Binary files /dev/null and b/opportune/static/gene.png differ
diff --git a/opportune/static/kat-450x400.jpg b/opportune/static/kat-450x400.jpg
new file mode 100644
index 0000000..f2b22ba
Binary files /dev/null and b/opportune/static/kat-450x400.jpg differ
diff --git a/opportune/static/kat.jpg b/opportune/static/kat.jpg
new file mode 100644
index 0000000..745d472
Binary files /dev/null and b/opportune/static/kat.jpg differ
diff --git a/opportune/static/layout.css b/opportune/static/layout.css
new file mode 100644
index 0000000..e4c2dd7
--- /dev/null
+++ b/opportune/static/layout.css
@@ -0,0 +1,38 @@
+header {
+ background-color: rgb(0, 206, 252);
+ color: white;
+ list-style: none;
+ height: 8vw;
+ font-family: 'Pacifico';
+ font-size: 1.5vw;
+ font-style: italic;
+ box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
+}
+
+header h1 {
+ position: absolute;
+ top: -2px;
+ left: 42.5%;
+}
+
+footer {
+ background-color: rgb(140, 140, 140);
+ color: white;
+ list-style: none;
+ height: 7vw;
+ font-family: 'Pacifico';
+/* position: fixed;*/
+ font-size: 1.75vw;
+ font-style: italic;
+ text-align: center;
+/* left: 0;
+ bottom: 0;*/
+ width: 100%;
+}
+
+footer ul li {
+ list-style: none;
+ font-family: 'Roboto';
+ font-size: 1vw;
+ display: inline-block;
+}
diff --git a/opportune/static/modules.css b/opportune/static/modules.css
new file mode 100644
index 0000000..c027fb2
--- /dev/null
+++ b/opportune/static/modules.css
@@ -0,0 +1,293 @@
+.columns {
+ list-style-type: none;
+ font-family: 'Roboto';
+ text-align: center;
+}
+
+.columns li {
+ display: inline-block;
+ margin: 4vw;
+ padding: 20px;
+ width: 300px;
+ background-color: white;
+ box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
+}
+
+.keywordList {
+ list-style-type: none;
+ text-align: center;
+}
+
+.keywordList li {
+ display: inline-block;
+}
+
+.fa-gem, .fa-hourglass, .fa-laptop {
+ padding-top: 10px;
+ color: lightgrey;
+ transition: all 0.4s ease-in-out;
+}
+
+.fa-gem:hover, .fa-hourglass:hover, .fa-laptop:hover {
+ padding-top: 10px;
+ color: rgb(0, 206, 252);
+ transition: all 0.4s ease-in-out;
+}
+
+.columns h3, h2, h1 {
+ font-family: 'Pacifico';
+}
+
+.profile {
+ margin-bottom: 8vw;
+}
+
+button {
+ color: black;
+ background-color: rgb(255, 147, 7);
+ font-size: 1vw;
+ height: 40px;
+ width: 255px;
+ transition: all 0.4s ease-in-out;
+}
+
+button:hover {
+ background-color: rgb(0, 206, 252);
+ transition: all 0.4s ease-in-out;
+}
+
+.keyword {
+ font-family: 'Roboto';
+ background-color: rgba(0, 206, 252, 0.4);
+ width: inherit;
+}
+
+.keyword::first-letter {
+ font-size: 150%;
+ color: rgb(255, 0, 0);
+}
+
+.forms {
+ list-style-type: none;
+ margin-left: 20%;
+ margin-bottom: 8vw;
+}
+
+.forms li {
+ display: inline-block;
+ margin: 50px;
+}
+
+.updateProfile input {
+ margin-left: 41.25%;
+}
+
+.scraper {
+ text-align: center;
+ margin-bottom: 8vw;
+}
+
+.scraper input {
+ margin-left: 41.25%;
+}
+
+.login, .register, .profile {
+ text-align: center;
+}
+
+.searchKeywords {
+ text-align:center;
+}
+
+.addKeyword {
+ text-align: center;
+}
+
+.addKeyword input {
+ margin-left: 41.25%;
+}
+
+form {
+ font-family: 'Roboto';
+}
+
+form input {
+ text-align: center;
+ display: block;
+ width: 250px;
+ height: 40px;
+ margin-top: 5px;
+ margin-bottom: 5px;
+ margin-left: 10%;
+}
+
+.resultsButtons {
+ margin-left: 38%;
+}
+
+.resultsButtons ul {
+ list-style-type: none;
+}
+
+.resultsButtons li {
+ display: inline-block;
+}
+
+#new-results {
+ list-style-type: none;
+ border-style: inset;
+ border-width: 3px 10px 3px 10px;
+ border-radius: 5px;
+ border-color: black;
+ padding-bottom: 25px;
+ padding-top: 10px;
+ margin: 0px 20px 0px 20px;
+}
+
+#abouth1 {
+ text-align: center;
+}
+
+#aboutlist {
+ list-style: none;
+ margin-left: 225px;
+}
+
+#aboutlist li {
+ display: inline-block;
+ padding: 15px;
+}
+
+#aboutlist a {
+ padding-left: 100px;
+}
+
+#gene img {
+ height: 420px;
+}
+
+#aboutlist p {
+ width: 450px;
+}
+
+#bokeh
+{
+ margin-left: 9%;
+}
+
+/*************************************/
+/*Everything below is driving the menu*/
+/*************************************/
+a
+{
+ text-decoration: none;
+ color: #232323;
+ transition: color 0.3s ease;
+}
+
+a:hover
+{
+ color: tomato;
+}
+
+#menuToggle
+{
+ display: block;
+ position: relative;
+ top: 45px;
+ left: 40px;
+ width: 100px;
+ z-index: 1;
+ -webkit-user-select: none;
+ user-select: none;
+}
+
+#menuToggle input
+{
+ display: block;
+ width: 40px;
+ height: 32px;
+ position: absolute;
+ top: -7px;
+ left: -5px;
+ cursor: pointer;
+ opacity: 0;
+ z-index: 2;
+ -webkit-touch-callout: none;
+}
+
+#menuToggle span
+{
+ display: block;
+ width: 33px;
+ height: 4px;
+ margin-bottom: 5px;
+ position: relative;
+ background: white;
+ border-radius: 3px;
+ z-index: 1;
+ transform-origin: 4px 0px;
+ transition: transform 0.5s cubic-bezier(0.77,0.2,0.05,1.0),
+ background 0.5s cubic-bezier(0.77,0.2,0.05,1.0),
+ opacity 0.55s ease;
+}
+
+#menuToggle span:first-child
+{
+ transform-origin: 0% 0%;
+}
+
+#menuToggle span:nth-last-child(2)
+{
+ transform-origin: 0% 100%;
+}
+
+#menuToggle input:checked ~ span
+{
+ opacity: 1;
+ transform: rotate(45deg) translate(-2px, -1px);
+ background: #232323;
+}
+
+#menuToggle input:checked ~ span:nth-last-child(3)
+{
+ opacity: 0;
+ transform: rotate(0deg) scale(0.2, 0.2);
+}
+
+#menuToggle input:checked ~ span:nth-last-child(2)
+{
+ transform: rotate(-45deg) translate(0, -1px);
+}
+#menu
+{
+ font-family: 'Roboto';
+ font-style: italic;
+ position: absolute;
+ width: 180px;
+ margin: -100px 0 0 -50px;
+ padding: 50px;
+ padding-top: 125px;
+
+ background: #c9c9c9;
+ list-style-type: none;
+ -webkit-font-smoothing: antialiased;
+
+ transform-origin: 0% 0%;
+ transform: translate(-100%, 0);
+
+ transition: transform 0.5s cubic-bezier(0.77,0.2,0.05,1.0);
+}
+
+#menu li
+{
+ padding: 10px 0;
+ font-size: 22px;
+}
+
+#menuToggle input:checked ~ ul
+{
+ transform: none;
+}
+
+
diff --git a/opportune/static/normalize.css b/opportune/static/normalize.css
new file mode 100644
index 0000000..47b010e
--- /dev/null
+++ b/opportune/static/normalize.css
@@ -0,0 +1,341 @@
+/*! normalize.css v8.0.0 | MIT License | github.com/necolas/normalize.css */
+
+/* Document
+ ========================================================================== */
+
+/**
+ * 1. Correct the line height in all browsers.
+ * 2. Prevent adjustments of font size after orientation changes in iOS.
+ */
+
+html {
+ line-height: 1.15; /* 1 */
+ -webkit-text-size-adjust: 100%; /* 2 */
+}
+
+/* Sections
+ ========================================================================== */
+
+/**
+ * Remove the margin in all browsers.
+ */
+
+body {
+ margin: 0;
+}
+
+/**
+ * Correct the font size and margin on `h1` elements within `section` and
+ * `article` contexts in Chrome, Firefox, and Safari.
+ */
+
+h1 {
+ font-size: 2em;
+ margin: 0.67em 0;
+}
+
+/* Grouping content
+ ========================================================================== */
+
+/**
+ * 1. Add the correct box sizing in Firefox.
+ * 2. Show the overflow in Edge and IE.
+ */
+
+hr {
+ box-sizing: content-box; /* 1 */
+ height: 0; /* 1 */
+ overflow: visible; /* 2 */
+}
+
+/**
+ * 1. Correct the inheritance and scaling of font size in all browsers.
+ * 2. Correct the odd `em` font sizing in all browsers.
+ */
+
+pre {
+ font-family: monospace, monospace; /* 1 */
+ font-size: 1em; /* 2 */
+}
+
+/* Text-level semantics
+ ========================================================================== */
+
+/**
+ * Remove the gray background on active links in IE 10.
+ */
+
+a {
+ background-color: transparent;
+}
+
+/**
+ * 1. Remove the bottom border in Chrome 57-
+ * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
+ */
+
+abbr[title] {
+ border-bottom: none; /* 1 */
+ text-decoration: underline; /* 2 */
+ text-decoration: underline dotted; /* 2 */
+}
+
+/**
+ * Add the correct font weight in Chrome, Edge, and Safari.
+ */
+
+b,
+strong {
+ font-weight: bolder;
+}
+
+/**
+ * 1. Correct the inheritance and scaling of font size in all browsers.
+ * 2. Correct the odd `em` font sizing in all browsers.
+ */
+
+code,
+kbd,
+samp {
+ font-family: monospace, monospace; /* 1 */
+ font-size: 1em; /* 2 */
+}
+
+/**
+ * Add the correct font size in all browsers.
+ */
+
+small {
+ font-size: 80%;
+}
+
+/**
+ * Prevent `sub` and `sup` elements from affecting the line height in
+ * all browsers.
+ */
+
+sub,
+sup {
+ font-size: 75%;
+ line-height: 0;
+ position: relative;
+ vertical-align: baseline;
+}
+
+sub {
+ bottom: -0.25em;
+}
+
+sup {
+ top: -0.5em;
+}
+
+/* Embedded content
+ ========================================================================== */
+
+/**
+ * Remove the border on images inside links in IE 10.
+ */
+
+img {
+ border-style: none;
+}
+
+/* Forms
+ ========================================================================== */
+
+/**
+ * 1. Change the font styles in all browsers.
+ * 2. Remove the margin in Firefox and Safari.
+ */
+
+button,
+input,
+optgroup,
+select,
+textarea {
+ font-family: inherit; /* 1 */
+ font-size: 100%; /* 1 */
+ line-height: 1.15; /* 1 */
+ margin: 0; /* 2 */
+}
+
+/**
+ * Show the overflow in IE.
+ * 1. Show the overflow in Edge.
+ */
+
+button,
+input { /* 1 */
+ overflow: visible;
+}
+
+/**
+ * Remove the inheritance of text transform in Edge, Firefox, and IE.
+ * 1. Remove the inheritance of text transform in Firefox.
+ */
+
+button,
+select { /* 1 */
+ text-transform: none;
+}
+
+/**
+ * Correct the inability to style clickable types in iOS and Safari.
+ */
+
+button,
+[type="button"],
+[type="reset"],
+[type="submit"] {
+ -webkit-appearance: button;
+}
+
+/**
+ * Remove the inner border and padding in Firefox.
+ */
+
+button::-moz-focus-inner,
+[type="button"]::-moz-focus-inner,
+[type="reset"]::-moz-focus-inner,
+[type="submit"]::-moz-focus-inner {
+ border-style: none;
+ padding: 0;
+}
+
+/**
+ * Restore the focus styles unset by the previous rule.
+ */
+
+button:-moz-focusring,
+[type="button"]:-moz-focusring,
+[type="reset"]:-moz-focusring,
+[type="submit"]:-moz-focusring {
+ outline: 1px dotted ButtonText;
+}
+
+/**
+ * Correct the padding in Firefox.
+ */
+
+fieldset {
+ padding: 0.35em 0.75em 0.625em;
+}
+
+/**
+ * 1. Correct the text wrapping in Edge and IE.
+ * 2. Correct the color inheritance from `fieldset` elements in IE.
+ * 3. Remove the padding so developers are not caught out when they zero out
+ * `fieldset` elements in all browsers.
+ */
+
+legend {
+ box-sizing: border-box; /* 1 */
+ color: inherit; /* 2 */
+ display: table; /* 1 */
+ max-width: 100%; /* 1 */
+ padding: 0; /* 3 */
+ white-space: normal; /* 1 */
+}
+
+/**
+ * Add the correct vertical alignment in Chrome, Firefox, and Opera.
+ */
+
+progress {
+ vertical-align: baseline;
+}
+
+/**
+ * Remove the default vertical scrollbar in IE 10+.
+ */
+
+textarea {
+ overflow: auto;
+}
+
+/**
+ * 1. Add the correct box sizing in IE 10.
+ * 2. Remove the padding in IE 10.
+ */
+
+[type="checkbox"],
+[type="radio"] {
+ box-sizing: border-box; /* 1 */
+ padding: 0; /* 2 */
+}
+
+/**
+ * Correct the cursor style of increment and decrement buttons in Chrome.
+ */
+
+[type="number"]::-webkit-inner-spin-button,
+[type="number"]::-webkit-outer-spin-button {
+ height: auto;
+}
+
+/**
+ * 1. Correct the odd appearance in Chrome and Safari.
+ * 2. Correct the outline style in Safari.
+ */
+
+[type="search"] {
+ -webkit-appearance: textfield; /* 1 */
+ outline-offset: -2px; /* 2 */
+}
+
+/**
+ * Remove the inner padding in Chrome and Safari on macOS.
+ */
+
+[type="search"]::-webkit-search-decoration {
+ -webkit-appearance: none;
+}
+
+/**
+ * 1. Correct the inability to style clickable types in iOS and Safari.
+ * 2. Change font properties to `inherit` in Safari.
+ */
+
+::-webkit-file-upload-button {
+ -webkit-appearance: button; /* 1 */
+ font: inherit; /* 2 */
+}
+
+/* Interactive
+ ========================================================================== */
+
+/*
+ * Add the correct display in Edge, IE 10+, and Firefox.
+ */
+
+details {
+ display: block;
+}
+
+/*
+ * Add the correct display in all browsers.
+ */
+
+summary {
+ display: list-item;
+}
+
+/* Misc
+ ========================================================================== */
+
+/**
+ * Add the correct display in IE 10+.
+ */
+
+template {
+ display: none;
+}
+
+/**
+ * Add the correct display in IE 10.
+ */
+
+[hidden] {
+ display: none;
+}
diff --git a/opportune/static/patricia-400x400.jpg b/opportune/static/patricia-400x400.jpg
new file mode 100644
index 0000000..86420de
Binary files /dev/null and b/opportune/static/patricia-400x400.jpg differ
diff --git a/opportune/static/patricia.jpg b/opportune/static/patricia.jpg
new file mode 100644
index 0000000..fe812af
Binary files /dev/null and b/opportune/static/patricia.jpg differ
diff --git a/opportune/static/whitebg.png b/opportune/static/whitebg.png
new file mode 100644
index 0000000..9549502
Binary files /dev/null and b/opportune/static/whitebg.png differ
diff --git a/opportune/templates/404.jinja2 b/opportune/templates/404.jinja2
new file mode 100644
index 0000000..aac1f93
--- /dev/null
+++ b/opportune/templates/404.jinja2
@@ -0,0 +1,8 @@
+{% extends "base.jinja2" %}
+
+{% block content %}
+
+
Pyramid Alchemy scaffold
+
404 Page Not Found
+
+{% endblock content %}
diff --git a/opportune/templates/about.jinja2 b/opportune/templates/about.jinja2
new file mode 100644
index 0000000..edd543d
--- /dev/null
+++ b/opportune/templates/about.jinja2
@@ -0,0 +1,40 @@
+{% extends "base.jinja2" %}
+{% block content %}
+About the Team
+
+
+
+
+ Kat Cosgrove
+
+ In a previous life, Kat worked as a helpdesk engineer for an offsite data backup software company and a human film encyclopedia at an independent video store (ask her about foreign or arthouse horror). Her hobbies include rock climbing, cosplay, building computers, and video games.
+ GitHub
+ LinkedIn
+
+
+
+ Patricia Raftery
+
+ Patricia likes snacking and coding and weird Canadian snack foods that her sister brings back from Vancouver. She lives in Issaquah and loves hiking with her black lab. She has really been enjoying software development because it lets her snack and code at the same time.
+ GitHub
+ LinkedIn
+
+
+
+ Gene Pieterson
+
+ After spending 6 beautiful years in San Diego working as a contractor for the Navy, Gene decided to set sail for yet another adventure. This time to learn computer programming. The plan is to become a python software developer, but setting his sights high for bigger roles.
+ GitHub
+ LinkedIn
+
+
+
+ Austin Matteson
+
+ Hailing from the Emerald City of Angels that Never Sleeps, Austin is excited to bring the perspectives of three corners of the US of A together in the software development space. He brings an eclectic nature, from hipster music tastes, to foreign films, to even tarot cards. Can you tell he has a background in theatre? Hit him up to collaborate!
+ GitHub
+ LinkedIn
+
+
+
+{% endblock content %}
\ No newline at end of file
diff --git a/opportune/templates/auth.jinja2 b/opportune/templates/auth.jinja2
new file mode 100644
index 0000000..39772a3
--- /dev/null
+++ b/opportune/templates/auth.jinja2
@@ -0,0 +1,27 @@
+{% extends "base.jinja2" %}
+{% block content %}
+
+{% endblock content %}
diff --git a/opportune/templates/base.jinja2 b/opportune/templates/base.jinja2
new file mode 100644
index 0000000..0c2bc73
--- /dev/null
+++ b/opportune/templates/base.jinja2
@@ -0,0 +1,71 @@
+
+
+
+
+ Opportune | Get Hired
+
+
+
+
+
+
+
+
+
+
+
+
+ {% block content %}
+
+ {% endblock content %}
+
+
+
+
+
diff --git a/opportune/templates/index.jinja2 b/opportune/templates/index.jinja2
new file mode 100644
index 0000000..4cb95de
--- /dev/null
+++ b/opportune/templates/index.jinja2
@@ -0,0 +1,26 @@
+{% extends "base.jinja2" %}
+{% block content %}
+
+
+
+
+
Gems only.
+
Opportune mines for you. Don't waste time combing through job descriptions. We only serve up jobs that are relevant to you, based on your keyword preferences.
+
+
+
+
+
+
Save time.
+
Gone are the days of manually searching multiple keywords each day. Opportune does the work for you, with convenient email notifications, so you don't have to.
+
+
+
+
+
+
Hire smarter.
+
You might not be reaching people who want to work for you. Opportune can arm you with keyword statistics so you know what potential employees look for.
+
+
+
+{% endblock content %}
\ No newline at end of file
diff --git a/opportune/templates/profile.jinja2 b/opportune/templates/profile.jinja2
new file mode 100644
index 0000000..dbfccb8
--- /dev/null
+++ b/opportune/templates/profile.jinja2
@@ -0,0 +1,34 @@
+{% extends "base.jinja2" %}
+{% block content %}
+
+
Profile
+
Update your profile
+ {% if alert %}
+ {{alert}}
+ {% endif %}
+
+
+
Edit your keywords
+ {% if message %}
+
{{message}}
+
Click here to add keywords and search.
+ {% endif %}
+ {% if keywords %}
+
Your current keywords:
+ {% for keyword in keywords %}
+
+ {% endfor %}
+ {% endif %}
+
+{% endblock content %}
diff --git a/opportune/templates/register.jinja2 b/opportune/templates/register.jinja2
new file mode 100644
index 0000000..ac3dfe9
--- /dev/null
+++ b/opportune/templates/register.jinja2
@@ -0,0 +1,12 @@
+{% extends "base.jinja2" %}
+{% block content %}
+
+
Register for an account.
+
+
+{% endblock content %}
\ No newline at end of file
diff --git a/opportune/templates/results.jinja2 b/opportune/templates/results.jinja2
new file mode 100644
index 0000000..efad259
--- /dev/null
+++ b/opportune/templates/results.jinja2
@@ -0,0 +1,29 @@
+{% extends "base.jinja2" %}
+{% block content %}
+
+
+ Results
+ {% for row in data %}
+
+ {{ row.job_title }}
+ Company: {{ row.company }}
+ Location: {{ row.location }}
+ Salary: {{ row.salary }}
+ Summary: {{ row.summary }}
+ Link to Posting
+
+
+ {% endfor %}
+
+{% endblock content %}
+
\ No newline at end of file
diff --git a/opportune/templates/search.jinja2 b/opportune/templates/search.jinja2
new file mode 100644
index 0000000..302a06f
--- /dev/null
+++ b/opportune/templates/search.jinja2
@@ -0,0 +1,38 @@
+{% extends "base.jinja2" %}
+{% block content %}
+
+
Search for Jobs
+
+{% if message %}
+
{{message}}
+{% endif %}
+{% if error %}
+
{{error}}
+{% endif %}
+{% if keywords %}
+
Your saved keywords:
+
+{% for keyword in keywords %}
+
+
+
+{% endfor %}
+
+{% endif %}
+
+
+
+{% endblock content %}
diff --git a/opportune/templates/stat.jinja2 b/opportune/templates/stat.jinja2
new file mode 100644
index 0000000..75b28d7
--- /dev/null
+++ b/opportune/templates/stat.jinja2
@@ -0,0 +1,5 @@
+{% extends "base.jinja2" %}
+{% block content %}
+{{div|safe}}
+{{script|safe}}
+{% endblock content %}
diff --git a/opportune/tests/__init__.py b/opportune/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/opportune/tests/conftest.py b/opportune/tests/conftest.py
new file mode 100644
index 0000000..305d186
--- /dev/null
+++ b/opportune/tests/conftest.py
@@ -0,0 +1,61 @@
+import pytest
+from pyramid import testing
+from ..models.meta import Base
+from ..models import Account
+
+
+@pytest.fixture
+def configuration(request):
+ """Setup a database for testing purposes."""
+ config = testing.setUp(settings={
+ 'sqlalchemy.url': 'postgres://localhost:5432/opportune_test'
+ # 'sqlalchemy.url': os.environ['TEST_DATABASE_URL']
+ })
+ config.include('opportune.models')
+ config.include('opportune.routes')
+
+ def teardown():
+ testing.tearDown()
+
+ request.addfinalizer(teardown)
+ return config
+
+
+@pytest.fixture
+def db_session(configuration, request):
+ """Create a database session for interacting with the test database."""
+ SessionFactory = configuration.registry['dbsession_factory']
+ session = SessionFactory()
+ engine = session.bind
+ Base.metadata.create_all(engine)
+
+ def teardown():
+ session.transaction.rollback()
+ Base.metadata.drop_all(engine)
+
+ request.addfinalizer(teardown)
+ return session
+
+
+@pytest.fixture
+def dummy_request(db_session):
+ """Create a dummy GET request with a dbsession."""
+ return testing.DummyRequest(dbsession=db_session)
+
+
+@pytest.fixture
+def test_user():
+ """Set up a test user"""
+ return Account(
+ username="testtest",
+ password="testpass",
+ email="test@testthis.com",
+ admin=True,
+ )
+
+
+@pytest.fixture
+def add_user(dummy_request, test_user):
+ """Add a user to database"""
+ dummy_request.dbsession.add(test_user)
+ return test_user
diff --git a/opportune/tests/dummy.txt b/opportune/tests/dummy.txt
new file mode 100644
index 0000000..437c44f
--- /dev/null
+++ b/opportune/tests/dummy.txt
@@ -0,0 +1,2183 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Python Jobs, Employment in Seattle, WA | Indeed.com
+
+
+
+
+
+
+
+
+
+
+:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
python jobs in Seattle, WA
+
+
+
+
+
+
Filter results by:
+
Sort by:
relevance -
+ date
+
+
+
+
+
+
+
+ Seattle, WA (2137)
+
+ Bellevue, WA (410)
+
+ Redmond, WA (392)
+
+ Kirkland, WA (82)
+
+ Kent, WA (49)
+
+ Bothell, WA (27)
+
+ Issaquah, WA (19)
+
+ Renton, WA (10)
+
+ Everett, WA (3)
+
+ Keyport, WA (3)
+
+ Gig Harbor, WA (3)
+
+ Mountlake Terrace, WA (3)
+
+ Adelaide, WA (2)
+
+ Snoqualmie, WA (2)
+
+ Woodinville, WA (1)
+
+ Python jobs nationwide
+
+
more »
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Site Reliability Engineer
+
+
+
+
+
+
+ Proficient in Java, Go, Python , or Ruby. As Revinate’s Site Reliability Engineer (SRE) you are responsible for ensuring Revinate continues to meet its SLA...
+
+
+
+
+
+
+
Backend Developer
+
+
+
+
+
+
+ Proficiency with Ruby/Rails, Python , Java/C#, and/or Go, and a demonstrated interest in learning new programming languages and environments....
+
+
+
+
+
+
+
+
+
+
+
+ Conversica
+
+ -
+
+5 reviews
+ -
Seattle, WA 98104 (First Hill area)
+
+
+
+
+
+ Advanced proficiency with at least one programming language for data analysis, preferably Python . One of the most commercially successful conversational AI...
+
+
+
+
+ Easily apply
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Attune Global
+
+ -
Seattle, WA
+
+
+
+
+
+ Python Script Developer*. Designing and Developing backend scripts/programs in Python . Learning and adopting to Test Plan – the existing Equities Automation...
+
+
+
+
+ Easily apply
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ POP
+
+ -
+
+23 reviews
+ -
Seattle, WA 98119 (Queen Anne area)
+
+
+
+
+
+ Use Python , Django, Django CMS and MySQL to develop new web application features. A successful Django CMS developer understands Python , Django and Django CMS at...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PitchBook Data, Inc.
+
+ -
Seattle, WA 98101 (Downtown area)
+
+
+
+
+
+ Experience with Python , Java or other relevant programming frameworks. In just 10 years, PitchBook has grown into a 600+-person organization stretching global...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Glowforge
+
+ -
Seattle, WA 98134 (Industrial Complex area)
+
+
+
+
+
+ You have at least two years of professional experience in an analytical function, working with common tools and languages of the trade, e.g. Tableau, Python ,...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Tableau
+
+ -
+
+37 reviews
+ -
Seattle, WA
+
+
+
+
+
+ Proficiency in at least one modern programming language such as Python , PHP, Java or Javascript. We have a collaborative team of engineers working in...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ iTrellis
+
+ -
Seattle, WA
+
+
+
+
+
+ At least 3 years of professional experience with Python . They use Python to build data pipelines, APIs, and data analytics....
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Fred Hutchinson Cancer Research Center
+
+ -
+
+71 reviews
+ -
Seattle, WA 98109 (Westlake area)
+
+
+
+
+
+ 3+ years of SAS and/or Python programming experience in a scientific or health-related field. At least 5 years of practical programming experience, with...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Shaker
+
+ -
Seattle, WA
+
+
+
+
+
+ AI Revolution: Employee Screening We are witnessing a pivotal moment in history where artificial intelligence has improved to the point where it can...
+
+
+
+
+ Easily apply
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Versive
+
+ -
Seattle, WA
+
+
+
+
+
+ You have 2+ years of professional software development experience with languages and systems such as Python , Scala, Java, and version control (git)....
+
+
+
+
+ Easily apply
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Machine Learning Engineer
+
+
+
+
+
+
+ Expertise with data analysis languages such as Python , Scala, or R. About the role....
+
+
+
+
+
+
+
Microservices Developer - AWS and Python
+
+
+
+
+
+
+ Strong programming skills in Python . Develop microservices and large scale web services leveraging internal and external API's for *IDM*....
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Be the first to see new python jobs in seattle
+
+
+
+
+
+
+
+
+
+
+
Boeing is the world's largest aerospace company. We’re engineers & technicians. Innovators & dreamers. Join us, and build something better.
+
+
+
+
+
+
+
+
+ Python Developer salaries in Seattle, WA
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/opportune/tests/test_account_model.py b/opportune/tests/test_account_model.py
new file mode 100644
index 0000000..173e225
--- /dev/null
+++ b/opportune/tests/test_account_model.py
@@ -0,0 +1,29 @@
+def test_constructed_account_added_to_database(db_session):
+ """Test adding a complete stock entry."""
+ from ..models import Account
+
+ assert len(db_session.query(Account).all()) == 0
+ account = Account(
+ username='TEST',
+ password='1234',
+ email='some@email.com',
+ )
+ db_session.add(account)
+ assert len(db_session.query(Account).all()) == 1
+
+
+def test_account_with_no_email_throws_error(db_session):
+ """Test adding stock with required field empty."""
+ from ..models import Account
+ import pytest
+ from sqlalchemy.exc import IntegrityError
+
+ assert len(db_session.query(Account).all()) == 0
+ account = Account(
+ username='Test2',
+ password='1234',
+ email=None
+ )
+ with pytest.raises(IntegrityError):
+ db_session.add(account)
+ assert db_session.query(Account).one_or_none() is None
diff --git a/opportune/tests/test_auth.py b/opportune/tests/test_auth.py
new file mode 100644
index 0000000..58bce5a
--- /dev/null
+++ b/opportune/tests/test_auth.py
@@ -0,0 +1,77 @@
+def test_get_auth_view(dummy_request):
+ """Test default auth page behavior."""
+ from ..views.auth import auth_view
+ response = auth_view(dummy_request)
+ assert isinstance(response, dict)
+
+
+def test_auth_signup_view(dummy_request):
+ """Test successful signup."""
+ from ..views.auth import auth_view
+ from pyramid.httpexceptions import HTTPFound
+
+ dummy_request.POST = {'username': 'kat', 'password': '1234', 'email': 'kat@kat.com'}
+ dummy_request.method = 'POST'
+ response = auth_view(dummy_request)
+ assert response.status_code == 302
+ assert isinstance(response, HTTPFound)
+
+
+def test_auth_signin_view(dummy_request):
+ """Test successful sign in."""
+ from ..views.auth import auth_view
+ from pyramid.httpexceptions import HTTPFound
+
+ dummy_request.POST = {'username': 'kat', 'password': '1234', 'email': 'kat@kat.com'}
+ dummy_request.method = 'POST'
+ auth_view(dummy_request)
+
+ dummy_request.GET = {'username': 'kat', 'password': '1234'}
+ dummy_request.method = 'GET'
+ response = auth_view(dummy_request)
+ assert response.status_code == 302
+ assert isinstance(response, HTTPFound)
+
+
+def test_logout_default(dummy_request):
+ """Test logout method returns HTTPFound"""
+ from ..views.auth import logout
+ from pyramid.httpexceptions import HTTPFound
+
+ response = logout(dummy_request)
+ assert isinstance(response, HTTPFound)
+
+
+def test_bad_request_auth_signup_view(dummy_request):
+ """Test bad signup."""
+ from ..views.auth import auth_view
+ from pyramid.httpexceptions import HTTPBadRequest
+
+ dummy_request.POST = {'password': 'test', 'email': 'test@test.com'}
+ dummy_request.method = 'POST'
+ response = auth_view(dummy_request)
+ assert response.status_code == 400
+ assert isinstance(response, HTTPBadRequest)
+
+
+def test_bad_request_method_auth_signup_view(dummy_request):
+ """Test bad method for signup."""
+ from ..views.auth import auth_view
+ from pyramid.httpexceptions import HTTPFound
+
+ dummy_request.POST = {'password': 'test', 'email': 'test@test.com'}
+ dummy_request.method = 'PUT'
+ response = auth_view(dummy_request)
+ assert response.status_code == 302
+ assert isinstance(response, HTTPFound)
+
+
+def test_username_already_in_use(dummy_request, db_session, test_user):
+ """test username already in use"""
+ from ..views.auth import auth_view
+ db_session.add(test_user)
+
+ dummy_request.POST = {'username': 'testtest', 'password': 'testpass', 'email': 'test@testthis.com'}
+ dummy_request.method = 'POST'
+ response = auth_view(dummy_request)
+ assert response == {'message': 'That username is already in use.'}
diff --git a/opportune/tests/test_default.py b/opportune/tests/test_default.py
new file mode 100644
index 0000000..74c1726
--- /dev/null
+++ b/opportune/tests/test_default.py
@@ -0,0 +1,24 @@
+from pyramid import testing
+# from pyramid.response import Response
+
+
+def test_default_behavior_of_base_view(dummy_request):
+ """Test default homepage behavior."""
+ from ..views.default import home_view
+ response = home_view(dummy_request)
+ assert type(response) == dict
+
+
+def test_default_behavior_of_about_view(dummy_request):
+ """Test default about behavior."""
+ from ..views.default import about_view
+ response = about_view(dummy_request)
+ assert len(response) == 0
+ assert type(response) == dict
+
+
+# def test_default_behavior_of_search(dummy_request):
+# """Test default for search behavior"""
+# from ..views.default import search_view
+# response = search_view(dummy_request)
+# assert user_keywords('Seattle') == Seattle
diff --git a/opportune/tests/test_keyword_model.py b/opportune/tests/test_keyword_model.py
new file mode 100644
index 0000000..6c2e012
--- /dev/null
+++ b/opportune/tests/test_keyword_model.py
@@ -0,0 +1,11 @@
+def test_constructed_keyword_added_to_database(db_session):
+ """Test adding a keyword."""
+ from ..models import Keyword
+
+ assert len(db_session.query(Keyword).all()) == 0
+ keyword = Keyword(
+ keyword='python'
+ )
+ db_session.add(keyword)
+ assert len(db_session.query(Keyword).all()) == 1
+
diff --git a/opportune/tests/test_notfound.py b/opportune/tests/test_notfound.py
new file mode 100644
index 0000000..cdcd0a5
--- /dev/null
+++ b/opportune/tests/test_notfound.py
@@ -0,0 +1,19 @@
+from pyramid import testing
+# from pyramid.response import Response
+
+
+def test_default_behavior_of_notfound_view(dummy_request):
+ """Test default notfound behavior."""
+ from ..views.notfound import notfound_view
+ response = notfound_view(dummy_request)
+ assert len(response) == 0
+ assert type(response) == dict
+
+
+def test_default_behavior_of_forbidden_view(dummy_request):
+ """Test default notfound behavior."""
+ from ..views.notfound import forbidden_view
+ from pyramid.httpexceptions import HTTPFound
+
+ response = forbidden_view(dummy_request)
+ assert isinstance(response, HTTPFound)
diff --git a/opportune/tests/test_profile.py b/opportune/tests/test_profile.py
new file mode 100644
index 0000000..bcfee1d
--- /dev/null
+++ b/opportune/tests/test_profile.py
@@ -0,0 +1,118 @@
+from pyramid import testing
+
+
+def test_default_behavior_of_profile_view(dummy_request):
+ """Test default profile behavior."""
+ from ..views.profile import profile_view
+ response = profile_view(dummy_request)
+ assert type(response) == dict
+
+
+def test_profile_view_with_no_keywords(dummy_request):
+ """Test default profile view with no keywords"""
+ from ..views.profile import profile_view
+ response = profile_view(dummy_request)
+ len(response) == 0
+ assert type(response) == dict
+ assert response == {'message': 'You do not have any keywords saved. Add one!'}
+
+
+def test_profile_view_gets_keywords(dummy_request):
+ '''Test profile view returns keywords with fake authenticated user'''
+ from ..views.profile import profile_view
+ from ..models.accounts import Account
+ from ..models.keywords import Keyword
+ from ..models.association import Association
+
+ config = testing.setUp()
+
+ config.testing_securitypolicy(
+ userid='codefellows', permissive=True
+ )
+ new_account = Account(
+ username='codefellows',
+ password='password',
+ email='myemail@gmail.com'
+ )
+ dummy_request.dbsession.add(new_account)
+
+ new_keyword = Keyword()
+ new_keyword.keyword = 'developer'
+ dummy_request.dbsession.add(new_keyword)
+
+ dummy_request.dbsession.commit()
+
+ new_association = Association()
+ new_association.user_id = 'codefellows'
+ new_association.keyword_id = 'developer'
+ dummy_request.dbsession.add(new_association)
+
+ dummy_request.dbsession.commit()
+
+ response = profile_view(dummy_request)
+
+ assert response['keywords'][0].keyword == 'developer'
+
+
+def test_profile_update_email(dummy_request):
+ '''Test bad attempt update email'''
+ from ..views.profile import update_email
+ from pyramid.httpexceptions import HTTPBadRequest
+
+ dummy_request.method = 'POST'
+ response = update_email(dummy_request)
+ assert response.status_code == 400
+ assert isinstance(response, HTTPBadRequest)
+
+
+def test_profile_delete_keyword_profile_works(dummy_request):
+ '''Test delete keyword behaviour'''
+ from ..views.profile import delete_keyword_profile
+ from ..models.accounts import Account
+ from ..models.keywords import Keyword
+ from ..models.association import Association
+ from pyramid.httpexceptions import HTTPFound
+
+ config = testing.setUp()
+ config.include('opportune.routes')
+
+ config.testing_securitypolicy(
+ userid='codefellows', permissive=True
+ )
+ new_account = Account(
+ username='codefellows',
+ password='password',
+ email='myemail@gmail.com'
+ )
+ dummy_request.dbsession.add(new_account)
+
+ new_keyword = Keyword()
+ new_keyword.keyword = 'developer'
+
+ dummy_request.dbsession.add(new_keyword)
+ dummy_request.dbsession.commit()
+
+ new_association = Association()
+ new_association.user_id = 'codefellows'
+ new_association.keyword_id = 'developer'
+ dummy_request.dbsession.add(new_association)
+
+ dummy_request.dbsession.commit()
+
+ dummy_request.method = 'POST'
+ dummy_request.POST = {'keyword': 'developer'}
+
+ response = delete_keyword_profile(dummy_request)
+ assert response.status_code == 302
+ assert isinstance(response, HTTPFound)
+
+
+def test_profile_delete_keyword_bad_request(dummy_request):
+ '''Test attempt delete keyword bad request'''
+ from ..views.profile import delete_keyword_profile
+ from pyramid.httpexceptions import HTTPBadRequest
+
+ dummy_request.method = 'POST'
+ response = delete_keyword_profile(dummy_request)
+ assert response.status_code == 400
+ assert isinstance(response, HTTPBadRequest)
diff --git a/opportune/tests/test_scraper.py b/opportune/tests/test_scraper.py
new file mode 100644
index 0000000..b824e1c
--- /dev/null
+++ b/opportune/tests/test_scraper.py
@@ -0,0 +1,73 @@
+from pyramid import testing
+import pytest
+import os
+
+
+def substitute_urllib_get_request(self, method, url):
+ # response.data.decode('utf-8')
+ class DummyResponse:
+ class Data:
+ def decode(self, encoding):
+ # path to the currently running file
+ curr_file = os.path.dirname(os.path.abspath(__file__))
+ dummy_file = os.path.join(curr_file, 'dummy.txt')
+ with open(dummy_file) as f:
+ result = f.read()
+ return result
+ data = Data()
+ return DummyResponse()
+
+
+def sub_requests_get(url):
+ class DummyResponse:
+ def __init__(self):
+ curr_file = os.path.dirname(os.path.abspath(__file__))
+ dummy_file = os.path.join(curr_file, 'dummy.txt')
+ with open(dummy_file) as f:
+ result = f.read()
+ self.text = result
+ return DummyResponse()
+
+
+def test_default_behavior_of_scraper(dummy_request, monkeypatch):
+ """Test default scraper behavior."""
+ from ..views.scraper import get_jobs
+ from urllib3 import PoolManager
+ monkeypatch.setattr(PoolManager, 'request', substitute_urllib_get_request)
+
+ dummy_request.method = 'POST'
+ dummy_request.POST = {'city': 'seattle', 'keyword': 'python'}
+ response = get_jobs(dummy_request)
+ assert type(response) == dict
+
+
+def test_scraper_bad_request(dummy_request):
+ from ..views.scraper import get_jobs
+ from pyramid.httpexceptions import HTTPBadRequest
+
+ dummy_request.method = 'POST'
+ response = get_jobs(dummy_request)
+ assert response.status_code == 400
+ assert isinstance(response, HTTPBadRequest)
+
+
+def test_default_behavior_of_email_view(dummy_request):
+ """Test default email view behavior."""
+ from ..views.scraper import email_view
+ from pyramid.httpexceptions import HTTPFound
+ response = email_view(dummy_request)
+ assert isinstance(response, HTTPFound)
+
+
+def test_file_download_status_code(dummy_request):
+ """Test file download function returns 200 status code."""
+ from ..views.scraper import download_results
+ response = download_results(dummy_request)
+ assert response.status_code == 200
+
+
+def test_file_download_type(dummy_request):
+ """Test file download returns a csv."""
+ from ..views.scraper import download_results
+ response = download_results(dummy_request)
+ assert response.content_type == 'text/csv'
diff --git a/opportune/tests/test_search.py b/opportune/tests/test_search.py
new file mode 100644
index 0000000..a0deff9
--- /dev/null
+++ b/opportune/tests/test_search.py
@@ -0,0 +1,104 @@
+from pyramid import testing
+
+def test_render_search_view(dummy_request):
+ """Test search view"""
+ from ..views.search import search_view
+ response = search_view(dummy_request)
+ assert type(response) == dict
+
+
+def test_search_view_no_keywords(dummy_request):
+ """Test search view response when the user does not give any keywords"""
+ from ..views.search import search_view
+ response = search_view(dummy_request)
+ len(response) == 0
+ assert type(response) == dict
+
+
+def test_search_view_with_no_keywords(dummy_request):
+ """Test search view with no keywords"""
+ from ..views.search import search_view
+
+ dummy_request.method = 'GET'
+ response = search_view(dummy_request)
+ assert response == {'message': 'You do not have any keywords saved. Add one!'}
+
+
+def test_search_view_gets_keywords(dummy_request):
+ '''Test search view returns keywords with fake authenticated user'''
+ from ..views.search import search_view
+ from ..models.accounts import Account
+ from ..models.keywords import Keyword
+ from ..models.association import Association
+
+ config = testing.setUp()
+
+ config.testing_securitypolicy(
+ userid='codefellows', permissive=True
+ )
+ new_account = Account(
+ username='codefellows',
+ password='password',
+ email='myemail@gmail.com'
+ )
+ dummy_request.dbsession.add(new_account)
+
+ new_keyword = Keyword()
+ new_keyword.keyword = 'developer'
+ dummy_request.dbsession.add(new_keyword)
+
+ dummy_request.dbsession.commit()
+
+ new_association = Association()
+ new_association.user_id = 'codefellows'
+ new_association.keyword_id = 'developer'
+ dummy_request.dbsession.add(new_association)
+
+ dummy_request.dbsession.commit()
+
+ response = search_view(dummy_request)
+
+ assert response['keywords'][0].keyword == 'developer'
+
+
+def test_handle_keywords_view_bad_request(dummy_request):
+ '''test handle keywords bad request'''
+ from ..views.search import handle_keywords
+ from pyramid.httpexceptions import HTTPBadRequest
+
+ dummy_request.method = 'POST'
+ response = handle_keywords(dummy_request)
+ assert response.status_code == 400
+ assert isinstance(response, HTTPBadRequest)
+
+
+def test_handle_keywords_gets_keyword(dummy_request):
+ '''test that it gets the key word'''
+ from ..views.search import handle_keywords
+ from pyramid.httpexceptions import HTTPFound
+
+ dummy_request.POST = {'keyword': 'web developer'}
+ dummy_request.method = 'POST'
+ response = handle_keywords(dummy_request)
+ assert isinstance(response, HTTPFound)
+
+
+def test_handle_keywords_number_as_a_keyword_throws_error(dummy_request):
+ '''test that a number throws the correct error'''
+ from ..views.search import handle_keywords
+
+ dummy_request.POST = {'keyword': '4'}
+ dummy_request.method = 'POST'
+ response = handle_keywords(dummy_request)
+ assert response == {'error': 'Search term cannot be a number.'}
+
+
+def test_delete_keyword_view_bad_request(dummy_request):
+ '''test delete keywords bad request'''
+ from ..views.search import delete_keyword
+ from pyramid.httpexceptions import HTTPBadRequest
+
+ dummy_request.method = 'POST'
+ response = delete_keyword(dummy_request)
+ assert response.status_code == 400
+ assert isinstance(response, HTTPBadRequest)
diff --git a/opportune/tests/test_security.py b/opportune/tests/test_security.py
new file mode 100644
index 0000000..956a332
--- /dev/null
+++ b/opportune/tests/test_security.py
@@ -0,0 +1,50 @@
+
+
+# check if the auth token is what is passed through
+
+# if key profile exists assert that key and token are what they should be
+
+
+
+
+# import unittest
+# from pyramid import testing
+
+
+# class MyTest(unittest.TestCase):
+# def setUp(self):
+# request = testing.DummyRequest()
+# self.config = testing.setUp(request=request)
+
+# def tearDown(self):
+# testing.tearDown()
+
+
+
+# import unittest
+# from pyramid import testing
+
+# class MyTest(unittest.TestCase):
+# def setUp(self):
+# self.config = testing.setUp()
+
+# def tearDown(self):
+# testing.tearDown()
+
+# def test_view_fn_forbidden(self):
+# from pyramid.httpexceptions import HTTPForbidden
+# from my.package import view_fn
+# self.config.testing_securitypolicy(userid='hank',
+# permissive=False)
+# request = testing.DummyRequest()
+# request.context = testing.DummyResource()
+# self.assertRaises(HTTPForbidden, view_fn, request)
+
+# def test_view_fn_allowed(self):
+# from my.package import view_fn
+# self.config.testing_securitypolicy(userid='hank',
+# permissive=True)
+# request = testing.DummyRequest()
+# request.context = testing.DummyResource()
+# response = view_fn(request)
+# self.assertEqual(response, {'greeting':'hello'})
diff --git a/opportune/tests/test_stat.py b/opportune/tests/test_stat.py
new file mode 100644
index 0000000..efd3846
--- /dev/null
+++ b/opportune/tests/test_stat.py
@@ -0,0 +1,33 @@
+def test_get_stat_view_error(dummy_request, db_session, test_user):
+ """Test default stat behavior."""
+ from ..views.stat import stat_view
+ from pyramid.httpexceptions import HTTPUnauthorized
+ response = stat_view(dummy_request)
+
+ assert isinstance(response, HTTPUnauthorized)
+
+
+def test_get_stat_view(dummy_request, db_session, test_user):
+ """Test default stat behavior."""
+ from ..views.stat import stat_view
+ from ..models.accounts import Account
+ from pyramid import testing
+
+ config = testing.setUp()
+
+ config.testing_securitypolicy(
+ userid='codefellows', permissive=True
+ )
+ new_account = Account(
+ username='codefellows',
+ password='password',
+ email='myemail@gmail.com',
+ admin=True
+ )
+ dummy_request.dbsession.add(new_account)
+
+ dummy_request.dbsession.commit()
+
+ response = stat_view(dummy_request)
+
+ assert type(response) == dict
diff --git a/opportune/views/__init__.py b/opportune/views/__init__.py
new file mode 100644
index 0000000..6ccfea8
--- /dev/null
+++ b/opportune/views/__init__.py
@@ -0,0 +1,3 @@
+DB_ERR_MSG = """
+ Hey something went wrong with the DB
+ """
diff --git a/opportune/views/auth.py b/opportune/views/auth.py
new file mode 100644
index 0000000..cf69b65
--- /dev/null
+++ b/opportune/views/auth.py
@@ -0,0 +1,69 @@
+from pyramid.httpexceptions import HTTPFound, HTTPBadRequest, HTTPUnauthorized
+from pyramid.security import NO_PERMISSION_REQUIRED, remember, forget
+from pyramid.response import Response
+from sqlalchemy.exc import DBAPIError
+from pyramid.view import view_config
+from ..models import Account
+from . import DB_ERR_MSG
+
+
+@view_config(
+ route_name='auth',
+ renderer='../templates/auth.jinja2',
+ permission=NO_PERMISSION_REQUIRED)
+def auth_view(request):
+ if request.authenticated_userid:
+ return HTTPFound(location=request.route_url('profile'))
+
+ if request.method == 'POST':
+ try:
+ username = request.POST['username']
+ email = request.POST['email']
+ password = request.POST['password']
+
+ except KeyError:
+ return HTTPBadRequest()
+
+ try:
+ instance = Account(
+ username=username,
+ email=email,
+ password=password,
+ )
+
+ query = request.dbsession.query(Account)
+ if query.filter(Account.username == username).first() is None:
+ headers = remember(request, userid=instance.username)
+ request.dbsession.add(instance)
+
+ else:
+ return {'message': 'That username is already in use.'}
+
+ return HTTPFound(location=request.route_url('search'), headers=headers)
+
+ except DBAPIError:
+ return Response(DB_ERR_MSG, content_type='text/plain', status=500)
+
+ if request.method == 'GET':
+ try:
+ username = request.GET['username']
+ password = request.GET['password']
+
+ except KeyError:
+ return {}
+
+ is_authenticated = Account.check_credentials(request, username, password)
+ if is_authenticated[0]:
+ headers = remember(request, userid=username)
+ return HTTPFound(location=request.route_url('search'), headers=headers)
+ else:
+ return HTTPUnauthorized()
+
+ return HTTPFound(location=request.route_url('home'))
+
+
+@view_config(route_name='logout')
+def logout(request):
+ """Log out of current account."""
+ headers = forget(request)
+ return HTTPFound(location=request.route_url('home'), headers=headers)
diff --git a/opportune/views/default.py b/opportune/views/default.py
new file mode 100644
index 0000000..d634a17
--- /dev/null
+++ b/opportune/views/default.py
@@ -0,0 +1,16 @@
+from pyramid.view import view_config
+from pyramid.security import NO_PERMISSION_REQUIRED
+
+
+@view_config(route_name='home', renderer='../templates/index.jinja2',
+ permission=NO_PERMISSION_REQUIRED)
+def home_view(request):
+ """Return homepage."""
+ return {}
+
+
+@view_config(route_name='about', renderer='../templates/about.jinja2',
+ permission=NO_PERMISSION_REQUIRED)
+def about_view(request):
+ """Return about page."""
+ return {}
diff --git a/opportune/views/notfound.py b/opportune/views/notfound.py
new file mode 100644
index 0000000..b3f50d1
--- /dev/null
+++ b/opportune/views/notfound.py
@@ -0,0 +1,15 @@
+from pyramid.view import notfound_view_config, forbidden_view_config
+from pyramid.httpexceptions import HTTPFound
+
+
+@notfound_view_config(renderer='../templates/404.jinja2')
+def notfound_view(request):
+ """Handle 404 response."""
+ request.response.status = 404
+ return {}
+
+
+@forbidden_view_config()
+def forbidden_view(request):
+ """Handle 401 response."""
+ return HTTPFound(location=request.route_url('auth'))
diff --git a/opportune/views/profile.py b/opportune/views/profile.py
new file mode 100644
index 0000000..7d92961
--- /dev/null
+++ b/opportune/views/profile.py
@@ -0,0 +1,92 @@
+from pyramid.view import view_config
+from pyramid.httpexceptions import HTTPFound, HTTPBadRequest
+from pyramid.security import forget
+from ..models import Keyword
+from ..models import Association
+from ..models import Account
+from sqlalchemy.exc import DBAPIError
+from pyramid.response import Response
+from . import DB_ERR_MSG
+
+
+@view_config(route_name='profile', renderer='../templates/profile.jinja2')
+def profile_view(request):
+ """Return profile settings page."""
+ if request.method == 'GET':
+ query = request.dbsession.query(Keyword)
+ user_keywords = query.filter(
+ Association.user_id == request.authenticated_userid,
+ Association.keyword_id == Keyword.keyword)
+
+ keywords = [keyword.keyword for keyword in user_keywords]
+ if len(keywords) < 1:
+ return{'message': 'You do not have any keywords saved. Add one!'}
+
+ return{'keywords': user_keywords}
+
+
+@view_config(
+ route_name='profile/update', renderer='../templates/profile.jinja2')
+def update_email(request):
+ """Allow user to update email address."""
+ try:
+ updated_email = request.POST['email']
+ except KeyError:
+ return HTTPBadRequest()
+ try: # pragma: no cover
+ query = request.dbsession.query(Account)
+ user = query.filter(
+ Account.username == request.authenticated_userid).one()
+ user.email = updated_email
+ except DBAPIError: # pragma: no cover
+ return Response(DB_ERR_MSG, content_type='text/plain', status=500)
+
+ return HTTPFound(location=request.route_url('profile'))
+
+
+@view_config(
+ route_name='profile/delete', renderer='../templates/profile.jinja2')
+def delete_account(request):
+ """Allow user to update email address."""
+ try: # pragma: no cover
+ query = request.dbsession.query(Account)
+ removed = query.filter(
+ Account.username == request.authenticated_userid).one()
+ request.dbsession.delete(removed)
+
+ return HTTPFound(location=request.route_url('logout'))
+ except KeyError: # pragma: no cover
+ return HTTPBadRequest()
+
+ return HTTPFound(location=request.route_url('home'))
+
+
+@view_config(
+ route_name='profile/keywords/delete',
+ renderer='../templates/profile.jinja2'
+ )
+def delete_keyword_profile(request):
+ """Delete a requested keyword from association table
+ for that particular user."""
+ if request.method == 'POST':
+ try:
+ keyword = request.POST['keyword']
+ user = request.authenticated_userid
+ except KeyError:
+ return HTTPBadRequest()
+
+ try:
+
+ query = request.dbsession.query(Association)
+ removed = query.filter(
+ Association.keyword_id == keyword,
+ Association.user_id == user).one()
+
+ request.dbsession.delete(removed)
+
+ return HTTPFound(location=request.route_url('profile'))
+
+ except DBAPIError: # pragma: no cover
+ return Response(
+ DB_ERR_MSG,
+ content_type='text/plain', status=500) # pragma: no cover
diff --git a/opportune/views/scraper.py b/opportune/views/scraper.py
new file mode 100644
index 0000000..fea0fbc
--- /dev/null
+++ b/opportune/views/scraper.py
@@ -0,0 +1,115 @@
+import requests
+from bs4 import BeautifulSoup
+from pyramid.view import view_config
+from ..models import Account
+from ..models import Keyword
+from ..models import Association
+from sqlalchemy.exc import DBAPIError
+from pyramid.response import FileResponse
+from pyramid.httpexceptions import HTTPBadRequest, HTTPFound
+from . import DB_ERR_MSG
+import urllib3
+import pandas as pd
+import csv
+import smtplib
+import os
+import csv
+
+
+@view_config(route_name='search/results', renderer='../templates/results.jinja2')
+def get_jobs(request): # pragma: no cover
+ if request.method == 'POST':
+
+ query = request.dbsession.query(Keyword)
+ keyword_query = query.filter(Association.user_id == request.authenticated_userid, Association.keyword_id == Keyword.keyword).all()
+ keywords = [keyword.keyword for keyword in keyword_query]
+
+ try:
+ city = request.POST['city']
+ except KeyError:
+ return HTTPBadRequest()
+
+ url_template = 'https://www.indeed.com/jobs?q={}&l={}'
+ max_results = 30
+
+ df = pd.DataFrame(columns=['location', 'company', 'job_title', 'salary', 'job_link', 'summary'])
+ requests.packages.urllib3.disable_warnings()
+ for keyword in keywords:
+ for start in range(0, max_results):
+ url = url_template.format(keyword, city)
+ http = urllib3.PoolManager()
+ response = http.request('GET', url)
+ soups = BeautifulSoup(response.data.decode('utf-8'), 'html.parser')
+ for b in soups.find_all('div', attrs={'class': ' row result'}):
+ location = b.find('span', attrs={'class': 'location'}).text
+ job_title = b.find('a', attrs={'data-tn-element': 'jobTitle'}).text
+ base_url = 'http://www.indeed.com'
+ href = b.find('a').get('href')
+ job_link = f'{base_url}{href}'
+ try:
+ company = b.find('span', attrs={'class': 'company'}).text
+ except AttributeError:
+ company = 'Not Listed'
+ try:
+ salary = b.find('span', attrs={'class': 'no-wrap'}).text
+ except AttributeError:
+ salary = 'Not Listed'
+ try:
+ summary = b.find('span', {'class':'summary'}).text
+ except AttributeError:
+ summary = 'Not Listed'
+ df = df.append({'location': location, 'company': company,
+ 'job_title': job_title,
+ 'salary': salary, 'summary': summary,
+ 'job_link': job_link}, ignore_index=True)
+
+ df.company.replace(regex=True,inplace=True,to_replace='\n',value='')
+ df.salary.replace(regex=True,inplace=True,to_replace='\n',value='')
+ df.summary.replace(regex=True, inplace=True, to_replace=u"\u2018", value="'")
+ df.summary.replace(regex=True, inplace=True, to_replace=u"\u2019", value="'")
+ df.summary.replace(regex=True, inplace=True, to_replace=u"\u2013", value="'")
+ cleaned = df.drop_duplicates(['job_link'])
+ output = cleaned.head(30)
+ output.to_csv('results.csv', index=False)
+ results = []
+ with open('./results.csv') as infile:
+ data = csv.DictReader(infile)
+ for row in data:
+ results.append(row)
+
+ return {'data': results}
+
+
+@view_config(route_name='search/results/email', renderer='../templates/search.jinja2')
+def email_view(request):
+ """Send email after scraper has run at user request."""
+
+ if request.method == 'POST': # pragma: no cover
+ with open('./results.csv') as input_file:
+ reader = csv.reader(input_file)
+ data = list(reader)
+ data.pop(0)
+ msg = 'Subject: Current Job Listings\n'
+ for posting in data:
+ msg += '\n'.join(posting) + '\n'*4
+ mail_from = os.environ.get('TEST_EMAIL')
+ log = os.environ.get('ZZZZZ')
+ query = request.dbsession.query(Account).filter(
+ Account.username == request.authenticated_userid).first()
+ smtpObj = smtplib.SMTP('smtp.gmail.com', 587)
+ smtpObj.ehlo()
+ smtpObj.starttls()
+ smtpObj.login(mail_from, log)
+ smtpObj.sendmail(mail_from, query.email, msg)
+ smtpObj.quit()
+ return HTTPFound(location=request.route_url('profile'))
+
+
+@view_config(route_name='search/results/download')
+def download_results(request):
+ """Send user their search results as a CSV."""
+ response = FileResponse(
+ './results.csv',
+ request=request,
+ content_type='text/csv')
+ return response
diff --git a/opportune/views/search.py b/opportune/views/search.py
new file mode 100644
index 0000000..aca7322
--- /dev/null
+++ b/opportune/views/search.py
@@ -0,0 +1,90 @@
+from pyramid.view import view_config
+from pyramid.httpexceptions import HTTPFound, HTTPBadRequest
+from sqlalchemy.exc import DBAPIError
+from . import DB_ERR_MSG
+from ..models import Keyword
+from ..models import Association
+from pyramid.response import Response
+
+
+@view_config(route_name='search', renderer='../templates/search.jinja2')
+def search_view(request):
+ """Get user's saved keywords from the database if they exist and render search page."""
+ if request.method == 'GET':
+ try:
+ query = request.dbsession.query(Keyword)
+ user_keywords = query.filter(Association.user_id == request.authenticated_userid, Association.keyword_id == Keyword.keyword)
+ except KeyError:
+ return HTTPFound
+ # except DBAPIError:
+ # raise DBAPIError(DB_ERR_MSG, content_type='text/plain', status=500)
+
+ keywords = [keyword.keyword for keyword in user_keywords]
+ if len(keywords) < 1:
+ return{'message': 'You do not have any keywords saved. Add one!'}
+
+ return{'keywords': user_keywords}
+
+
+@view_config(route_name='keywords', renderer='../templates/search.jinja2')
+def handle_keywords(request):
+ """Add and delete keywords in database."""
+
+ if request.method == 'POST':
+ try:
+ keyword = request.POST['keyword']
+ except KeyError:
+ return HTTPBadRequest()
+
+ try:
+ keyword = int(keyword)
+ return {'error': 'Search term cannot be a number.'}
+ except ValueError:
+ if len(keyword.split()) > 1:
+ keyword = keyword.split()
+ keyword = '+'.join(keyword)
+
+ instance = Keyword(
+ keyword=keyword
+ )
+ association = Association(
+ user_id=request.authenticated_userid,
+ keyword_id=instance.keyword
+ )
+
+ try:
+ keyword_query = request.dbsession.query(Keyword)
+ if keyword_query.filter(Keyword.keyword == instance.keyword).first() is None:
+ request.dbsession.add(instance)
+
+ if keyword_query.filter(instance.keyword == Association.keyword_id, association.user_id == Association.user_id).first() is None:
+ request.dbsession.add(association)
+ else:
+ return{'message': 'You have already saved that keyword.'}
+
+ except DBAPIError: # pragma: no cover
+ return Response(DB_ERR_MSG, content_type='text/plain', status=500)
+
+ return HTTPFound(location=request.route_url('search'))
+
+
+@view_config(route_name='keywords/delete', renderer='../templates/search.jinja2')
+def delete_keyword(request):
+ """Delete a requested keyword from association table for that particular user."""
+ if request.method == 'POST':
+ try:
+ keyword = request.POST['keyword']
+ user = request.authenticated_userid
+ except KeyError: # pragma: no cover
+ return HTTPBadRequest()
+
+ try: # pragma: no cover
+ query = request.dbsession.query(Association)
+ removed = query.filter(Association.keyword_id == keyword, Association.user_id == user).one()
+
+ request.dbsession.delete(removed)
+
+ return HTTPFound(location=request.route_url('search'))
+
+ except DBAPIError: # pragma: no cover
+ return Response(DB_ERR_MSG, content_type='text/plain', status=500)
diff --git a/opportune/views/stat.py b/opportune/views/stat.py
new file mode 100644
index 0000000..b022a6f
--- /dev/null
+++ b/opportune/views/stat.py
@@ -0,0 +1,146 @@
+from pyramid.view import view_config
+from pyramid.httpexceptions import HTTPUnauthorized
+from ..models import Association, Account
+from bokeh.models import ColumnDataSource
+from bokeh.plotting import figure
+from bokeh.layouts import gridplot
+from bokeh.embed import components
+from bokeh.palettes import Spectral6, Spectral5
+from bokeh.transform import factor_cmap
+import pandas as pd
+import numpy as np
+
+
+@view_config(route_name='stat', renderer='../templates/stat.jinja2',
+ request_method='GET')
+def stat_view(request):
+ """View statistics scraped from indeed."""
+ try:
+ query = request.dbsession.query(Account)
+ admin = query.filter(
+ Account.username == request.authenticated_userid).one_or_none()
+ if admin.admin is True:
+ # From here, down to next comment, is data we've tracked but
+ # decided not to render.
+ relationships = request.dbsession.query(Association)
+ count = {}
+ for each in relationships:
+ word = each.keyword_id
+ if word not in count:
+ count[word] = 1
+ else:
+ count[word] += 1
+ top = 1
+ for value in count.values():
+ if top <= value:
+ top = value * 1.5
+ users = list(count.values())
+ keywords = list(count.keys())
+ source = ColumnDataSource(
+ data=dict(keywords=keywords, users=users))
+ p = figure(x_range=keywords, y_range=(0, top), plot_height=500,
+ title="Current Stored Searches")
+ p.vbar(x='keywords', top='users', width=0.9, legend=False,
+ source=source)
+ p.xgrid.grid_line_color = None
+ p.legend.orientation = "horizontal"
+ p.legend.location = "top_center"
+ # End of unrendered tracking above.
+
+ lang = [
+ './mass_scraper/pythonresults.csv',
+ './mass_scraper/javascriptresults.csv',
+ './mass_scraper/csharpresults.csv',
+ './mass_scraper/javaresults.csv',
+ './mass_scraper/phpresults.csv',
+ './mass_scraper/cplusresults.csv']
+ lang_legend = [
+ 'python', 'javascript', 'csharp', 'java', 'php', 'Cplus'
+ ]
+ avg = []
+ place_count = 0
+ p1 = figure(
+ title="Salaries by Language", background_fill_color="#E8DDCB")
+ p1.xaxis[0].formatter.use_scientific = False
+ for lng in lang:
+ df = pd.read_csv(lng)
+ y = list(df[lang_legend[place_count]])
+ avg.append(np.mean(y))
+ hist, edges = np.histogram(y)
+ p1.quad(
+ top=hist,
+ bottom=0,
+ left=edges[:-1],
+ right=edges[1:],
+ fill_color=Spectral6[place_count],
+ fill_alpha=0.3,
+ line_color=Spectral6[place_count],
+ legend=lang_legend[place_count])
+ place_count += 1
+ p1.legend.location = "top_center"
+ p1.legend.click_policy = "hide"
+ p2 = figure(
+ x_range=lang_legend, y_range=(0, max(avg)), plot_height=500,
+ title="Average Salaries by Language")
+ source = ColumnDataSource(
+ data=dict(lang_legend=lang_legend, avg=avg))
+ p2.vbar(
+ x='lang_legend',
+ top='avg',
+ width=0.9,
+ legend=False,
+ source=source,
+ fill_color=factor_cmap(
+ 'lang_legend', palette=Spectral6, factors=lang_legend))
+ p2.yaxis[0].formatter.use_scientific = False
+
+ job = [
+ './mass_scraper/datascienceresults.csv',
+ './mass_scraper/DBAresults.csv',
+ './mass_scraper/softwaredevresults.csv',
+ './mass_scraper/uxresults.csv',
+ './mass_scraper/webdevresults.csv'
+ ]
+ job_legend = ['datascience', 'dba', 'softwaredev', 'ux', 'webdev']
+ avg1 = []
+ place_count = 0
+ p3 = figure(
+ title="Salaries by Job", background_fill_color="#E8DDCB")
+ p3.xaxis[0].formatter.use_scientific = False
+ for jab in job:
+ df = pd.read_csv(jab)
+ y = list(df[job_legend[place_count]])
+ avg1.append(np.mean(y))
+ hist, edges = np.histogram(y)
+ p3.quad(
+ top=hist,
+ bottom=0,
+ left=edges[:-1],
+ right=edges[1:],
+ fill_color=Spectral5[place_count],
+ fill_alpha=0.3,
+ line_color=Spectral5[place_count],
+ legend=job_legend[place_count])
+ place_count += 1
+ p3.legend.location = "top_center"
+ p3.legend.click_policy = "hide"
+ p4 = figure(x_range=job_legend, y_range=(0, max(avg1)),
+ plot_height=500, title="Average Salaries by Job")
+ source = ColumnDataSource(
+ data=dict(job_legend=job_legend, avg1=avg1))
+ p4.vbar(
+ x='job_legend',
+ top='avg1',
+ width=0.9,
+ legend=False,
+ source=source,
+ fill_color=factor_cmap(
+ 'job_legend', palette=Spectral5, factors=job_legend))
+ p4.yaxis[0].formatter.use_scientific = False
+
+ all_plots = gridplot([[p1, p3], [p2, p4]])
+ script, div = components(all_plots)
+ return {'script': script, 'div': div}
+
+ except AttributeError:
+ return HTTPUnauthorized()
diff --git a/production.ini b/production.ini
new file mode 100644
index 0000000..8bf6754
--- /dev/null
+++ b/production.ini
@@ -0,0 +1,63 @@
+###
+# app configuration
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
+###
+
+[app:main]
+use = egg:opportune
+
+pyramid.reload_templates = false
+pyramid.debug_authorization = false
+pyramid.debug_notfound = false
+pyramid.debug_routematch = false
+pyramid.default_locale_name = en
+
+retry.attempts = 3
+
+###
+# wsgi server configuration
+###
+
+[server:main]
+use = egg:waitress#main
+listen = localhost:6543
+
+###
+# logging configuration
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
+###
+
+[loggers]
+keys = root, opportune, sqlalchemy
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARN
+handlers = console
+
+[logger_opportune]
+level = WARN
+handlers =
+qualname = opportune
+
+[logger_sqlalchemy]
+level = WARN
+handlers =
+qualname = sqlalchemy.engine
+# "level = INFO" logs SQL queries.
+# "level = DEBUG" logs SQL queries and results.
+# "level = WARN" logs neither. (Recommended for production systems.)
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
diff --git a/pytest.ini b/pytest.ini
new file mode 100644
index 0000000..8055427
--- /dev/null
+++ b/pytest.ini
@@ -0,0 +1,3 @@
+[pytest]
+testpaths = opportune
+python_files = *.py
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..e6f20f8
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,63 @@
+attrs==17.4.0
+autopep8==1.3.5
+beautifulsoup4==4.6.0
+bokeh==0.12.15
+bs4==0.0.1
+certifi==2018.1.18
+chardet==3.0.4
+coverage==4.5.1
+cryptacular==1.4.1
+cycler==0.10.0
+flake8==3.5.0
+html5lib==1.0.1
+hupper==1.1
+idna==2.6
+Jinja2==2.10
+kiwisolver==1.0.1
+Mako==1.0.7
+MarkupSafe==1.0
+mccabe==0.6.1
+more-itertools==4.1.0
+numpy==1.14.2
+packaging==17.1
+pandas==0.22.0
+PasteDeploy==1.5.2
+pbkdf2==1.3
+pep8==1.7.1
+plaster==1.0
+plaster-pastedeploy==0.5
+pluggy==0.6.0
+psycopg2-binary==2.7.4
+py==1.5.3
+pycodestyle==2.4.0
+pyflakes==1.6.0
+Pygments==2.2.0
+pyparsing==2.2.0
+pyramid==1.9.1
+pyramid-debugtoolbar==4.4
+pyramid-jinja2==2.7
+pyramid-mako==1.0.2
+pyramid-retry==0.5
+pyramid-tm==2.2
+pytest==3.5.0
+pytest-cov==2.5.1
+python-dateutil==2.7.2
+pytz==2018.4
+PyYAML==3.12
+repoze.lru==0.7
+requests==2.18.4
+scipy==1.0.1
+six==1.11.0
+SQLAlchemy==1.2.6
+tornado==5.0.2
+transaction==2.2.1
+translationstring==1.3
+urllib3==1.22
+venusian==1.1.0
+waitress==1.1.0
+webencodings==0.5.1
+WebOb==1.8.1
+WebTest==2.0.29
+zope.deprecation==4.3.0
+zope.interface==4.4.3
+zope.sqlalchemy==1.0
diff --git a/results.csv b/results.csv
new file mode 100644
index 0000000..d40f5d5
--- /dev/null
+++ b/results.csv
@@ -0,0 +1 @@
+location,company,job_title,salary,job_link,summary
diff --git a/run b/run
new file mode 100755
index 0000000..64b09c1
--- /dev/null
+++ b/run
@@ -0,0 +1,4 @@
+#!/bin/bash
+set -e
+python setup.py develop
+python runapp.py
diff --git a/runapp.py b/runapp.py
new file mode 100644
index 0000000..df24540
--- /dev/null
+++ b/runapp.py
@@ -0,0 +1,10 @@
+import os
+
+from paste.deploy import loadapp
+from waitress import serve
+
+if __name__ == "__main__":
+ port = int(os.environ.get("PORT", 5000))
+ app = loadapp('config:production.ini', relative_to='.')
+
+ serve(app, host='0.0.0.0', port=port)
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..ba95bb5
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,68 @@
+import os
+
+from setuptools import setup, find_packages
+
+here = os.path.abspath(os.path.dirname(__file__))
+with open(os.path.join(here, 'README.md')) as f:
+ README = f.read()
+with open(os.path.join(here, 'CHANGES.txt')) as f:
+ CHANGES = f.read()
+
+requires = [
+ 'html5lib',
+ 'bs4',
+ 'urllib3',
+ 'pandas',
+ 'cryptacular',
+ 'plaster_pastedeploy',
+ 'psycopg2-binary',
+ 'pyramid >= 1.9a',
+ 'pyramid_debugtoolbar',
+ 'pyramid_jinja2',
+ 'pyramid_retry',
+ 'pyramid_tm',
+ 'requests',
+ 'SQLAlchemy',
+ 'transaction',
+ 'zope.sqlalchemy',
+ 'waitress',
+ 'bokeh',
+]
+
+tests_require = [
+ 'WebTest >= 1.3.1', # py3 compat
+ 'pytest',
+ 'pytest-cov',
+]
+
+setup(
+ name='opportune',
+ version='0.0',
+ description='opportune',
+ long_description=README + '\n\n' + CHANGES,
+ classifiers=[
+ 'Programming Language :: Python',
+ 'Framework :: Pyramid',
+ 'Topic :: Internet :: WWW/HTTP',
+ 'Topic :: Internet :: WWW/HTTP :: WSGI :: Application',
+ ],
+ author='',
+ author_email='',
+ url='',
+ keywords='web pyramid pylons',
+ packages=find_packages(),
+ include_package_data=True,
+ zip_safe=False,
+ extras_require={
+ 'testing': tests_require,
+ },
+ install_requires=requires,
+ entry_points={
+ 'paste.app_factory': [
+ 'main = opportune:main',
+ ],
+ 'console_scripts': [
+ 'initialize_opportune_db = opportune.scripts.initializedb:main',
+ ],
+ },
+)