├── .circleci └── config.yml ├── .expo ├── packager-info.json └── settings.json ├── .gitignore ├── LICENSE ├── README.md ├── auth_server └── README.md ├── backend ├── .dockerignore ├── .expo │ ├── packager-info.json │ └── settings.json ├── .gitignore ├── Dockerfile ├── Pipfile ├── Pipfile.lock ├── Procfile ├── README.md ├── _init_.py ├── api │ ├── __init__.py │ ├── config.py │ ├── constants.py │ ├── core.py │ ├── models │ │ ├── BusStop.py │ │ ├── Business.py │ │ ├── Crime.py │ │ ├── EmergencyPhone.py │ │ ├── Location.py │ │ ├── OpenHours.py │ │ ├── PoliceStation.py │ │ ├── Streetlight.py │ │ ├── Tips.py │ │ ├── User.py │ │ └── __init__.py │ ├── scrapers │ │ ├── README_scrapers.md │ │ ├── __init__.py │ │ ├── bus_stops.py │ │ ├── crimes.py │ │ ├── emergency_phones.py │ │ ├── open_businesses.py │ │ ├── police_stations.py │ │ └── streetlights.py │ └── views │ │ ├── auth.py │ │ ├── busStop.py │ │ ├── business.py │ │ ├── crime.py │ │ ├── crime_duration.csv │ │ ├── emergencyPhone.py │ │ ├── main.py │ │ ├── policeStations.py │ │ ├── streetlight.py │ │ ├── tips.py │ │ └── user.py ├── manage.py ├── now.json ├── pytest.ini ├── runtime.txt ├── tests │ ├── README.md │ ├── __init__.py │ ├── bus_test_data.py │ ├── business_test_data.py │ ├── conftest.py │ ├── crime_test_data.py │ ├── phone_test_data.py │ ├── police_test_data.py │ ├── streetlight_test_data.py │ ├── test_basic.py │ ├── test_buses.py │ ├── test_business.py │ ├── test_crimes.py │ ├── test_lights.py │ ├── test_phone.py │ └── test_police.py └── yarn.lock ├── c2tc-mobile ├── .gitignore ├── .watchmanconfig ├── App.js ├── Pipfile ├── README.MD ├── Redux.js ├── __tests__ │ └── App-test.js ├── app.json ├── assets │ ├── data │ │ ├── light_locations.json │ │ └── police_locations.json │ ├── fonts │ │ └── SpaceMono-Regular.ttf │ └── images │ │ ├── back.png │ │ ├── bg-day.png │ │ ├── bg.png │ │ ├── bus.png │ │ ├── business.png │ │ ├── c2tc.png │ │ ├── crime.png │ │ ├── icon.png │ │ ├── phone.png │ │ ├── police.png │ │ ├── robot-dev.png │ │ ├── robot-prod.png │ │ ├── splash.png │ │ ├── streetlights.png │ │ └── welcome │ │ ├── 0-1.png │ │ ├── 0.png │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4-1.png │ │ ├── 4-2.png │ │ ├── 4-3.png │ │ ├── 5.png │ │ └── 6.png ├── components │ ├── API.js │ ├── Geocoding.js │ ├── Loader.js │ ├── MapRendering.js │ ├── NavigationComponents │ │ ├── ButtonInterface.js │ │ ├── CurrentLocationButton.js │ │ ├── Navigation.js │ │ ├── Panel.js │ │ ├── PhoneButtonInterface.js │ │ └── Tabs.js │ ├── StyledText.js │ ├── TabBarIcon.js │ ├── Tag.js │ ├── TipOverview.js │ └── __tests__ │ │ └── StyledText-test.js ├── constants │ ├── Colors.js │ └── Layout.js ├── package-lock.json ├── package.json ├── screens │ ├── AlertScreen.js │ ├── EditProfileScreen.js │ ├── IntroScreen.js │ ├── LiveLocation.js │ ├── LoginScreen.js │ ├── MapScreen.js │ ├── NonRegisteredScreen.js │ ├── NotificationScreen.js │ ├── PasswordResetScreen.js │ ├── PendingTipScreen.js │ ├── ProfileScreen.js │ ├── RegistrationScreen.js │ ├── SettingsScreen.js │ ├── TipCategories.js │ ├── TipDetailsScreen.js │ ├── TipForm.js │ ├── TipOverviewScreen.js │ ├── TipScreen.js │ ├── VerificationScreen.js │ └── WelcomeScreen.js └── yarn.lock ├── contributing.md ├── docs ├── api_docs.md └── endpoints │ ├── bus_stop.md │ ├── business.md │ ├── crime.md │ ├── emergency_phone.md │ ├── streetlight.md │ ├── tip.md │ └── users.md └── pull_request_template.md /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | backend_format: 4 | docker: 5 | - image: circleci/python:3.7 6 | steps: 7 | - checkout 8 | - run: 9 | command: | 10 | cd backend 11 | pipenv install --skip-lock 12 | pipenv install --skip-lock --dev 13 | pipenv run black . --check 14 | backend_test: 15 | docker: 16 | - image: circleci/python:3.7 17 | steps: 18 | - checkout 19 | - run: 20 | command: | 21 | cd backend 22 | pipenv install --skip-lock 23 | pipenv install --skip-lock --dev 24 | pipenv run pytest 25 | frontend_format: 26 | docker: 27 | - image: node:8.4.0 28 | steps: 29 | - checkout 30 | - run: 31 | command: | 32 | cd c2tc-mobile 33 | yarn 34 | yarn format 35 | workflows: 36 | version: 2 37 | build-test: 38 | jobs: 39 | - backend_format 40 | - backend_test 41 | - frontend_format 42 | -------------------------------------------------------------------------------- /.expo/packager-info.json: -------------------------------------------------------------------------------- 1 | { 2 | "devToolsPort": 19002, 3 | "expoServerPort": null 4 | } -------------------------------------------------------------------------------- /.expo/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "hostType": "lan", 3 | "lanType": "ip", 4 | "dev": true, 5 | "minify": false, 6 | "urlRandomness": null 7 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | 3 | 4 | yarn-error.log 5 | node_modules 6 | *.swp 7 | *.swo 8 | *~ 9 | .DS_Store 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Hack4Impact UIUC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SafeMaps [![CircleCI](https://circleci.com/gh/hack4impact-uiuc/safe-maps.svg?style=svg)](https://circleci.com/gh/hack4impact-uiuc/safe-maps) 2 | 3 | **Description:** Illini SafeMaps is your real-time safety companion. With Illini SafeMaps, you can navigate safely around the U of I campus by viewing the locations of past crimes, police stations, emergency phones, running buses, open businesses and streetlights. This app also enables you to be notified of the safety and health resources near you so you can stay safe and healthy at U of I. Lastly, students can access SafeWalks and SafeRides immediately through our app when they feel unsafe on campus. 4 | 5 | ## Product Resources 6 | 7 | - [Product Requirements Document](https://docs.google.com/document/d/1ZJVwFBKqaSK1ENXhDKrxS_6lCu60Nlf_htJgafS6m0w/edit?usp=sharing) 8 | 9 | ## Design Resources 10 | 11 | - [Wireframes](https://sketch.cloud/s/45Dzo) 12 | - [Prototype](https://sketch.cloud/s/AJ9Ky/PrjlrQ/play) 13 | 14 | ## Backend Resources 15 | 16 | - [Database Schema](https://github.com/hack4impact-uiuc/safe-maps/blob/master/docs/api_docs.md) 17 | 18 | ## Tech Stack 19 | 20 | We split this application into Frontend and Backend services. The backend is [Flask](http://flask.pocoo.org/) server using python 3.7 and pipenv with [MongoDB](https://docs.mongodb.com/), a NoSQL database, as our choice of data store. The Frontend is built with React Native and Expo, which enables our app to run on any type of mobile device. 21 | 22 | ## Application Structure 23 | 24 | - `c2tc-mobile`: frontend top directory 25 | - `backend`: backend top directory 26 | 27 | Specific Documentation is given inside the `c2tc-mobile` and `backend` folders. 28 | 29 | ## Development Setup 30 | 31 | ### Dependencies 32 | 33 | - Node.js 8.x.x 34 | - Python 3.7 35 | - Pipenv 36 | 37 | To run the flask server in the backend 38 | 39 | ```bash 40 | cd backend 41 | pipenv install # install dependencies 42 | pipenv shell 43 | python manage.py runserver 44 | ``` 45 | 46 | To run the frontend 47 | 48 | ```bash 49 | cd c2tc-mobile 50 | yarn # install dependencies 51 | yarn global add expo-cli # do this step if you have never used expo before. 52 | # Install the Expo Mobile App on phone if you have never used expo before. 53 | expo start 54 | # Scan QR code with phone (use camera if you have an iphone and use expo app if you have an android.) 55 | ``` 56 | 57 | Note: if you prefer using npm, use `npm` instead of `yarn` in commands provided above 58 | 59 | ## Team 60 | 61 | - **Product Manager:** Shreyas Mohan ([@shreyshrey1](https://github.com/shreyshrey1)) 62 | - **Technical Lead:** Megha Mallya ([@meghatron3000](https://github.com/meghatron3000)) 63 | - **Product Designer:** Philip Kuo ([@pkgamma](https://github.com/pkgamma)) 64 | - **User Research/External Relations:** Annie Wu ([@anniegw2](https://github.com/anniegw2)) 65 | 66 | ### Software Devs Fall 2018 67 | 68 | - Neeraj Aggarwal ([@n3a9](https://github.com/n3a9)) 69 | - Daniel Choi ([@choiboy98](https://github.com/choiboy98)) 70 | - Josh Burke ([@JoshBurke](https://github.com/JoshBurke)) 71 | - Anooj Lal([@anoojlal](https://github.com/anoojlal)) 72 | 73 | ### Software Devs Spring 2019 74 | 75 | - Neeraj Aggarwal ([@n3a9](https://github.com/n3a9)) 76 | - Albert Cao ([@abetco](https://github.com/abetco)) 77 | - Alice Fang ([@alicesf2](https://github.com/alicesf2)) 78 | - Michael Leon ([@micleo2](https://github.com/micleo2)) 79 | - Rebecca Xun([@rxun](https://github.com/rxun)) 80 | -------------------------------------------------------------------------------- /auth_server/README.md: -------------------------------------------------------------------------------- 1 | # H4i Infrastructure Authentication Server 2 | 3 | [https://docs.google.com/document/d/1K6e9jarVtAync-Bti6BN6bKI-8JnvwK4IZmhlTSn2pg/edit](Guide for integrating into your App) 4 | 5 | ## Links 6 | 7 | Documentation: [https://h4i-auth-infra-docs.now.sh/](https://h4i-auth-infra-docs.now.sh/) 8 | 9 | API: [https://github.com/hack4impact-uiuc/infra-authentication-api/](https://github.com/hack4impact-uiuc/infra-authentication-api/) 10 | 11 | Client Example: [https://github.com/hack4impact-uiuc/infra-authentication-client](https://github.com/hack4impact-uiuc/infra-authentication-client) 12 | 13 | ## To Run Locally 14 | 15 | ```bash 16 | yarn 17 | yarn start 18 | ``` 19 | -------------------------------------------------------------------------------- /backend/.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !api 3 | !manage.py 4 | !Pipfile 5 | !Pipfile.lock 6 | !creds.ini -------------------------------------------------------------------------------- /backend/.expo/packager-info.json: -------------------------------------------------------------------------------- 1 | { 2 | "devToolsPort": 19003, 3 | "expoServerPort": null 4 | } -------------------------------------------------------------------------------- /backend/.expo/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "hostType": "lan", 3 | "lanType": "ip", 4 | "dev": true, 5 | "minify": false, 6 | "urlRandomness": null 7 | } -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .vscode/ 3 | venv/ 4 | __pycache__ 5 | .DS_Store/ 6 | migrations/ 7 | postgres-data/ 8 | .mypy_cache 9 | .pytest_cache 10 | api.log 11 | test.db 12 | _connect.py 13 | creds.ini 14 | api/scrapers/api_constants.py -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7-alpine 2 | LABEL maintainer "Megha Mallya " 3 | 4 | COPY . /app 5 | WORKDIR /app 6 | RUN pip install pipenv 7 | RUN pipenv install --system 8 | 9 | EXPOSE 5000 10 | ENTRYPOINT [ "gunicorn", "-b", "0.0.0.0:5000", "--log-level", "INFO", "manage:app" ] -------------------------------------------------------------------------------- /backend/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.python.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | alembic = "==0.9.6" 8 | flask_script = "==2.0.6" 9 | gunicorn = "==19.7.1" 10 | pyflakes = "==1.6.0" 11 | requests = "==2.20.0" 12 | flask_cors = "*" 13 | flask = "*" 14 | mongoengine = "*" 15 | itsdangerous = "*" 16 | pip = "*" 17 | "urllib3" = "*" 18 | apscheduler = "*" 19 | geopy = "*" 20 | black = "*" 21 | 22 | [dev-packages] 23 | "flake8" = "*" 24 | black = "==18.6b4" 25 | mypy = "*" 26 | pytest = "*" 27 | -------------------------------------------------------------------------------- /backend/Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn api:app 2 | worker: python manage.py runworker 3 | -------------------------------------------------------------------------------- /backend/README.md: -------------------------------------------------------------------------------- 1 | # Backend Instructions 2 | 3 | ## Setting Up Instructions 4 | 5 | Set up a `creds.ini` with the following mongo creds `mongo_db_name` and `mongo_url`. 6 | 7 | ## How To Run Backend 8 | 9 | ```bash 10 | cd backend 11 | pipenv install #Install dependencies 12 | pipenv shell #Start server 13 | python manage.py runserver 14 | ``` 15 | 16 | ## How To Format Backend 17 | 18 | ```bash 19 | cd backend 20 | pipenv install #Install dependencies if you have not already 21 | pipenv run black . 22 | ``` 23 | 24 | ## How To Test Backend 25 | 26 | ```bash 27 | cd backend 28 | pipenv install #Install dependencies if you have not already 29 | pipenv run pytest 30 | ``` 31 | 32 | ## Backend Resources 33 | 34 | - [Database Schema](https://github.com/hack4impact-uiuc/c2tc-fall-2018/blob/master/docs/api_docs.md) 35 | -------------------------------------------------------------------------------- /backend/_init_.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hack4impact-uiuc/safe-maps/a64698ac47c35b244d32dc86019327b35d69e9ed/backend/_init_.py -------------------------------------------------------------------------------- /backend/api/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | 4 | from flask import Flask, request 5 | from flask_cors import CORS 6 | 7 | from api.config import config 8 | from api.core import get_mongo_credentials 9 | from mongoengine import connect 10 | from api.core import all_exception_handler 11 | import api.models 12 | 13 | 14 | class RequestFormatter(logging.Formatter): 15 | def format(self, record): 16 | record.url = request.url 17 | record.remote_addr = request.remote_addr 18 | return super().format(record) 19 | 20 | 21 | def create_app(test_config=None): 22 | app = Flask(__name__) 23 | 24 | CORS(app) # add CORS 25 | 26 | (db_name, mongo_url) = get_mongo_credentials() 27 | if test_config: 28 | if test_config.get("MONGO_TEST_URI"): 29 | mongo_url = test_config["MONGO_TEST_URI"] 30 | db_name = test_config["MONGO_TEST_DB"] 31 | connect(db_name, host=mongo_url) 32 | 33 | # check environment variables to see which config to load 34 | env = os.environ.get("FLASK_ENV", "dev") 35 | if test_config: 36 | # ignore environment variable config if config was given 37 | app.config.from_mapping(**test_config) 38 | else: 39 | app.config.from_object(config[env]) 40 | 41 | # logging 42 | formatter = RequestFormatter( 43 | "%(asctime)s %(remote_addr)s: requested %(url)s: %(levelname)s in [%(module)s: %(lineno)d]: %(message)s" 44 | ) 45 | if app.config.get("LOG_FILE"): 46 | fh = logging.FileHandler(app.config.get("LOG_FILE")) 47 | fh.setLevel(logging.DEBUG) 48 | fh.setFormatter(formatter) 49 | app.logger.addHandler(fh) 50 | 51 | strm = logging.StreamHandler() 52 | strm.setLevel(logging.DEBUG) 53 | strm.setFormatter(formatter) 54 | 55 | app.logger.addHandler(strm) 56 | app.logger.setLevel(logging.DEBUG) 57 | 58 | # import and register blueprints 59 | from api.views import ( 60 | main, 61 | business, 62 | crime, 63 | streetlight, 64 | emergencyPhone, 65 | busStop, 66 | policeStations, 67 | user, 68 | tips, 69 | auth, 70 | ) 71 | 72 | app.register_blueprint(main.main) 73 | app.register_blueprint(business.business) 74 | app.register_blueprint(crime.crime) 75 | app.register_blueprint(streetlight.streetlight) 76 | app.register_blueprint(emergencyPhone.emergencyPhone) 77 | app.register_blueprint(busStop.busStop) 78 | app.register_blueprint(policeStations.policeStation) 79 | app.register_blueprint(user.user) 80 | app.register_blueprint(tips.tips) 81 | app.register_blueprint(auth.auth) 82 | # register error Handler 83 | app.register_error_handler(Exception, all_exception_handler) 84 | 85 | return app 86 | -------------------------------------------------------------------------------- /backend/api/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | class Config: 5 | SECRET_KEY = "testkey" 6 | LOG_FILE = "api.log" 7 | 8 | 9 | class DevelopmentConfig(Config): 10 | DEBUG = True 11 | 12 | 13 | class ProductionConfig(Config): 14 | DEBUG = False 15 | 16 | 17 | config = {"dev": DevelopmentConfig, "prod": ProductionConfig} 18 | -------------------------------------------------------------------------------- /backend/api/constants.py: -------------------------------------------------------------------------------- 1 | UPVOTE = "UPVOTE" 2 | DOWNVOTE = "DOWNVOTE" 3 | -------------------------------------------------------------------------------- /backend/api/core.py: -------------------------------------------------------------------------------- 1 | import configparser 2 | import requests 3 | from typing import Tuple, List 4 | from pathlib import Path 5 | 6 | from werkzeug.local import LocalProxy 7 | from flask import current_app, jsonify, request 8 | from flask.wrappers import Response 9 | 10 | from bson import ObjectId 11 | from datetime import datetime 12 | import json 13 | 14 | import functools 15 | 16 | from api.models.User import User 17 | 18 | # logger object for all views to use 19 | logger = LocalProxy(lambda: current_app.logger) 20 | 21 | auth_server_host = "https://c2tc-auth-server.herokuapp.com/" 22 | # auth_server_host = "http://localhost:8000/" 23 | 24 | 25 | class Mixin: 26 | """Utility Base Class for SQLAlchemy Models. 27 | 28 | Adds `to_dict()` to easily serialize objects to dictionaries. 29 | """ 30 | 31 | def to_dict(self) -> dict: 32 | d_out = dict((key, val) for key, val in self.__dict__.items()) 33 | d_out.pop("_sa_instance_state", None) 34 | d_out["_id"] = d_out.pop("id", None) # rename id key to interface with response 35 | return d_out 36 | 37 | 38 | class JSONEncoder(json.JSONEncoder): 39 | def default(self, o): 40 | if isinstance(o, ObjectId): 41 | return str(o) 42 | if isinstance(o, datetime): 43 | return o.strftime("%m/%d/%Y, %H:%M:%S") 44 | return json.JSONEncoder.default(self, o) 45 | 46 | 47 | def create_response( 48 | data: dict = None, status: int = 200, message: str = "" 49 | ) -> Tuple[Response, int]: 50 | """ 51 | Wraps response in a consistent format throughout the API. 52 | 53 | Format inspired by https://medium.com/@shazow/how-i-design-json-api-responses-71900f00f2db 54 | Modifications included: 55 | - make success a boolean since there's only 2 values 56 | - make message a single string since we will only use one message per response 57 | 58 | IMPORTANT: data must be a dictionary where: 59 | - the key is the name of the type of data 60 | - the value is the data itself 61 | 62 | :param data optional data 63 | :param status optional status code, defaults to 200 64 | :param message optional message 65 | :returns tuple of Flask Response and int 66 | """ 67 | 68 | if type(data) is not dict and data is not None: 69 | raise TypeError("Data should be a dictionary 😞") 70 | # if data is None: 71 | # raise TypeError("Data is empty 😞") 72 | # for key in data: 73 | # if isinstance(data[key], ObjectId): 74 | # data[key] = str(data[key]) 75 | data = JSONEncoder().encode(data) 76 | response = { 77 | "success": 200 <= status < 300, 78 | "message": message, 79 | "result": json.loads(data), 80 | } 81 | return jsonify(response), status 82 | 83 | 84 | def serialize_list(items: List) -> List: 85 | """Serializes a list of SQLAlchemy Objects, exposing their attributes. 86 | 87 | :param items - List of Objects that inherit from Mixin 88 | :returns List of dictionaries 89 | """ 90 | if not items or items is None: 91 | return [] 92 | return [x.to_dict() for x in items] 93 | 94 | 95 | # add specific Exception handlers before this, if needed 96 | def all_exception_handler(error: Exception) -> Tuple[Response, int]: 97 | """Catches and handles all exceptions, add more specific error Handlers. 98 | :param Exception 99 | :returns Tuple of a Flask Response and int 100 | """ 101 | return create_response(message=str(error), status=500) 102 | 103 | 104 | def can_be_authenticated(route): 105 | @functools.wraps(route) 106 | def wrapper_wroute(*args, **kwargs): 107 | auth_server_res = get_auth_server_user() 108 | if auth_server_res.status_code != 200: 109 | return route(None, *args, **kwargs) 110 | auth_uid = auth_server_res.json()["user_id"] 111 | db_user = User.objects.get(auth_server_uid=auth_uid) 112 | return route(db_user, *args, **kwargs) 113 | 114 | return wrapper_wroute 115 | 116 | 117 | def authenticated_route(route): 118 | @functools.wraps(route) 119 | def wrapper_wroute(*args, **kwargs): 120 | auth_server_res = get_auth_server_user() 121 | if auth_server_res.status_code != 200: 122 | return create_response( 123 | message=auth_server_res.json()["message"], 124 | status=401, 125 | data={"status": "fail"}, 126 | ) 127 | auth_uid = auth_server_res.json()["user_id"] 128 | db_user = User.objects.get(auth_server_uid=auth_uid) 129 | return route(db_user, *args, **kwargs) 130 | 131 | return wrapper_wroute 132 | 133 | 134 | def get_auth_server_user(): 135 | token = request.headers.get("token") 136 | auth_server_res = requests.get( 137 | auth_server_host + "getUser/", 138 | headers={ 139 | "Content-Type": "application/json", 140 | "token": token, 141 | "google": "undefined", 142 | }, 143 | ) 144 | return auth_server_res 145 | 146 | 147 | def necessary_post_params(*important_properties): 148 | def real_decorator(route): 149 | @functools.wraps(route) 150 | def wrapper_wroute(*args, **kwargs): 151 | missing_fields = invalid_model_helper( 152 | request.get_json(), important_properties 153 | ) 154 | if missing_fields is not None: 155 | return create_response( 156 | message="Missing the following necesary field(s): " 157 | + ", ".join(missing_fields), 158 | status=422, 159 | data={"status": "fail"}, 160 | ) 161 | return route(*args, **kwargs) 162 | 163 | return wrapper_wroute 164 | 165 | return real_decorator 166 | 167 | 168 | def invalid_model_helper(user_data, props): 169 | missing_fields = [] 170 | for prop in props: 171 | if prop not in user_data: 172 | missing_fields.append(prop) 173 | if len(missing_fields) == 0: 174 | return None 175 | return missing_fields 176 | 177 | 178 | def get_mongo_credentials(file: str = "creds.ini") -> Tuple: 179 | config = configparser.ConfigParser() 180 | config.read(file) 181 | try: 182 | mongo_section = config["mongo_creds"] 183 | return (mongo_section["mongo_db_name"], mongo_section["mongo_url"]) 184 | except KeyError: 185 | print("Couldn't parse {file} for mongo creds... Check whether it exists.") 186 | return (None, None) 187 | -------------------------------------------------------------------------------- /backend/api/models/BusStop.py: -------------------------------------------------------------------------------- 1 | from mongoengine.fields import StringField, FloatField, DictField 2 | import mongoengine 3 | 4 | # DynamicDocument allows for unspecified fields to be put in as well 5 | class BusStop(mongoengine.DynamicDocument): 6 | """BusStop Document Schema""" 7 | 8 | stop_id = StringField(required=True, unique=True) 9 | stop_name = StringField(required=True) 10 | latitude = FloatField(required=True) 11 | longitude = FloatField(required=True) 12 | routes = DictField() 13 | -------------------------------------------------------------------------------- /backend/api/models/Business.py: -------------------------------------------------------------------------------- 1 | from mongoengine.fields import ( 2 | StringField, 3 | ListField, 4 | EmbeddedDocumentField, 5 | EmbeddedDocumentListField, 6 | FloatField, 7 | ) 8 | from api.models.Location import Location 9 | from api.models.OpenHours import OpenHours 10 | import mongoengine 11 | 12 | # DynamicDocument allows for unspecified fields to be put in as well 13 | class Business(mongoengine.DynamicDocument): 14 | """Business Document Schema""" 15 | 16 | name = StringField(required=True) 17 | yelp_id = StringField(required=True, unique=True) 18 | location = EmbeddedDocumentField(Location) 19 | latitude = FloatField() 20 | longitude = FloatField() 21 | image_url = StringField() 22 | display_phone = StringField() 23 | open_hours = EmbeddedDocumentListField(OpenHours) 24 | -------------------------------------------------------------------------------- /backend/api/models/Crime.py: -------------------------------------------------------------------------------- 1 | from mongoengine.fields import StringField, DateTimeField, FloatField, IntField 2 | from api.models.Location import Location 3 | from api.models.OpenHours import OpenHours 4 | import mongoengine 5 | from mongoengine import * 6 | import datetime 7 | 8 | # DynamicDocument allows for unspecified fields to be put in as well 9 | class Crime(mongoengine.DynamicDocument): 10 | """Crime Document Schema""" 11 | 12 | incident_id = StringField(required=True, unique=True) 13 | incident_datetime = StringField(required=True) 14 | incident_type_primary = StringField(required=True) 15 | incident_description = StringField(required=True) 16 | address_1 = StringField(required=True) 17 | city = StringField(required=True) 18 | state = StringField(required=True) 19 | latitude = FloatField(required=True) 20 | longitude = FloatField(required=True) 21 | hour_of_day = IntField(required=True) 22 | day_of_week = StringField(required=True) 23 | parent_incident_type = StringField(required=True) 24 | duration = IntField(required=False) 25 | -------------------------------------------------------------------------------- /backend/api/models/EmergencyPhone.py: -------------------------------------------------------------------------------- 1 | from mongoengine.fields import IntField, FloatField 2 | import mongoengine 3 | 4 | # DynamicDocument allows for unspecified fields to be put in as well 5 | class EmergencyPhone(mongoengine.DynamicDocument): 6 | """EmergencyPhone Document Schema""" 7 | 8 | emergencyPhone_id = IntField(required=True, unique=True) 9 | latitude = FloatField(required=True) 10 | longitude = FloatField(required=True) 11 | -------------------------------------------------------------------------------- /backend/api/models/Location.py: -------------------------------------------------------------------------------- 1 | from mongoengine.fields import StringField 2 | from mongoengine import EmbeddedDocument 3 | 4 | 5 | class Location(EmbeddedDocument): 6 | """Location Embedded Document Schema""" 7 | 8 | city = StringField() 9 | country = StringField() 10 | address1 = StringField() 11 | address2 = StringField() 12 | address3 = StringField() 13 | state = StringField() 14 | zip_code = StringField() 15 | -------------------------------------------------------------------------------- /backend/api/models/OpenHours.py: -------------------------------------------------------------------------------- 1 | from mongoengine.fields import StringField, BooleanField, IntField 2 | from mongoengine import EmbeddedDocument 3 | 4 | 5 | class OpenHours(EmbeddedDocument): 6 | """Hours Embedded Document Schema""" 7 | 8 | start = StringField(required=True) 9 | end = StringField(required=True) 10 | is_overnight = BooleanField(required=True) 11 | day = IntField(required=True) 12 | -------------------------------------------------------------------------------- /backend/api/models/PoliceStation.py: -------------------------------------------------------------------------------- 1 | from mongoengine.fields import StringField, FloatField 2 | import mongoengine 3 | 4 | # DynamicDocument allows for unspecified fields to be put in as well 5 | class PoliceStation(mongoengine.DynamicDocument): 6 | """Police Station Document Schema""" 7 | 8 | name = StringField(required=True) 9 | latitude = FloatField(required=True) 10 | longitude = FloatField(required=True) 11 | -------------------------------------------------------------------------------- /backend/api/models/Streetlight.py: -------------------------------------------------------------------------------- 1 | from mongoengine.fields import IntField, FloatField 2 | import mongoengine 3 | 4 | # DynamicDocument allows for unspecified fields to be put in as well 5 | class Streetlight(mongoengine.DynamicDocument): 6 | """Streetlight Document Schema""" 7 | 8 | latitude = FloatField(required=True) 9 | longitude = FloatField(required=True) 10 | -------------------------------------------------------------------------------- /backend/api/models/Tips.py: -------------------------------------------------------------------------------- 1 | from mongoengine.fields import ( 2 | StringField, 3 | IntField, 4 | DateTimeField, 5 | FloatField, 6 | ObjectIdField, 7 | ListField, 8 | BooleanField, 9 | ) 10 | import mongoengine 11 | from api.models.User import User 12 | 13 | 14 | # DynamicDocument allows for unspecified fields to be put in as well 15 | class Tips(mongoengine.DynamicDocument): 16 | """Tips Document Schema""" 17 | 18 | title = StringField(required=True) 19 | content = StringField(required=True) 20 | author = ObjectIdField(required=True) 21 | posted_time = DateTimeField(required=True) 22 | status = StringField(required=True) 23 | latitude = FloatField(required=True) 24 | longitude = FloatField(required=True) 25 | category = StringField(required=True) 26 | upvotes = ListField(ObjectIdField()) 27 | downvotes = ListField(ObjectIdField()) 28 | -------------------------------------------------------------------------------- /backend/api/models/User.py: -------------------------------------------------------------------------------- 1 | from mongoengine.fields import ( 2 | StringField, 3 | IntField, 4 | BooleanField, 5 | DateTimeField, 6 | ListField, 7 | ObjectIdField, 8 | ) 9 | import mongoengine 10 | 11 | # DynamicDocument allows for unspecified fields to be put in as well 12 | class User(mongoengine.DynamicDocument): 13 | """User Document Schema""" 14 | 15 | username = StringField(required=True) 16 | email = StringField(required=True) 17 | verified = BooleanField(required=True, default=False) 18 | trusted = BooleanField(required=True, default=False) 19 | anon = BooleanField(required=True, default=False) 20 | karma = IntField(required=True, default=0) 21 | posted_tips = ListField(ObjectIdField()) 22 | date_created = DateTimeField(required=True) 23 | pro_pic = StringField( 24 | required=True, 25 | default="https://pngimage.net/wp-content/uploads/2018/05/default-profile-image-png-5.png", 26 | ) 27 | auth_server_uid = StringField(required=True) 28 | -------------------------------------------------------------------------------- /backend/api/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hack4impact-uiuc/safe-maps/a64698ac47c35b244d32dc86019327b35d69e9ed/backend/api/models/__init__.py -------------------------------------------------------------------------------- /backend/api/scrapers/README_scrapers.md: -------------------------------------------------------------------------------- 1 | # Data Scrapers - *Cut to the Case* 2 | 3 | ### Data 4 | | Data | Sourced? | Implemented? | 5 | |--------------------------------|--------------------------------|----------------------------| 6 | | Open Businesses | Yes (Yelp API) | Scraper yes, db storage no | 7 | | Blue Lights (Emergency Phones) | In progress | no | 8 | | Streetlights | In progress | no | 9 | | Illini Alerts | Yes (twitter API) | no | 10 | | Police Blotter | In progress (CrimeReports API) | no | 11 | | VeoRides | No, not allowed :( | N/a | 12 | | Bus Stops | Yes (CUMTD API) | no | 13 | | SafeRides locations | In progress (CUMTD API) | no | 14 | 15 | ### Scheduling 16 | 17 | So probably going to use apscheduler package for scheduling scrapers to run. 18 | 19 | from apscheduler.schedulers.background import BackgroundScheduler 20 | 21 | Soon we will decide how often to run the various jobs. 22 | -------------------------------------------------------------------------------- /backend/api/scrapers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hack4impact-uiuc/safe-maps/a64698ac47c35b244d32dc86019327b35d69e9ed/backend/api/scrapers/__init__.py -------------------------------------------------------------------------------- /backend/api/scrapers/bus_stops.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | import sys 4 | 5 | api_keys = [ 6 | "95b24e883247444095625960a8bbee98", 7 | "901d92ed96ae44c280f3e3c7c48fc300", 8 | "80678f088271417aa6d0cdb898aa5624", 9 | ] 10 | stops_url = "https://developer.cumtd.com/api/v2.2/json/getstops" 11 | routes_url = "https://developer.cumtd.com/api/v2.2/json/getroutesbystop" 12 | stops_payload = {"key": api_keys[0]} 13 | routes_payload = {"key": api_keys[0], "stop_id": ""} # change stop_id to id of stop 14 | stops_req_fields = ["stop_id", "stop_name"] 15 | 16 | 17 | def get_qs_url(url, args): 18 | """ 19 | Accepts url string and dictionary of querystring parameters, returns properly 20 | formatted url. 21 | """ 22 | qs_url = url 23 | i = 0 24 | for k, v in args.items(): 25 | if i == 0: 26 | qs_url += "?" 27 | else: 28 | qs_url += "&" 29 | qs_url += str(k) + "=" + str(v) 30 | i += 1 31 | return qs_url 32 | 33 | 34 | def get_stops(payload, url, req_fields): 35 | """ 36 | Given the querystring payload, URL we are querying from, and list of fields 37 | to scrape, returns a list of all stops including stop points, stop name, 38 | stop ID. 39 | """ 40 | data = requests.get(get_qs_url(url, payload)) 41 | return_data = {} 42 | for stop in data.json()["stops"]: 43 | stop_data = {} 44 | for field in req_fields: 45 | stop_data[field] = stop.get(field) 46 | stop_data["stop_lat"] = stop.get("stop_points")[0].get("stop_lat") 47 | stop_data["stop_lon"] = stop.get("stop_points")[0].get("stop_lon") 48 | return_data[stop["stop_id"]] = stop_data 49 | return return_data 50 | 51 | 52 | def get_full_stop_info(stop_data, payload, url): 53 | """ 54 | Given a json structure containing all stops from get_stops(), the 55 | querystring payload and URL we are querying from, returns a modified 56 | dictionary of all stops including stop points, stop name, stop ID, AND 57 | routes that run through that stop. 58 | """ 59 | stop_counter = 0 # for debugging 60 | api_key_counter = 0 61 | total_stops = len(stop_data.keys()) # debugging 62 | print(total_stops, "stops to process.") # debugging 63 | for stop_id in list(stop_data.keys()): 64 | if stop_counter % 800 == 0: 65 | payload["key"] = api_keys[api_key_counter] 66 | if api_key_counter < len(api_keys) - 1: 67 | api_key_counter += 1 68 | payload["stop_id"] = stop_id 69 | single_stop_routes_raw = requests.get(get_qs_url(url, payload)).json() 70 | route_list = {} 71 | for stop_route in single_stop_routes_raw["routes"]: 72 | route_list[stop_route["route_short_name"]] = stop_route["route_color"] 73 | stop_data[stop_id]["routes"] = route_list 74 | stop_counter += 1 # debugging 75 | perc_complete = float(stop_counter) / total_stops * 100 # debugging 76 | print( 77 | stop_id, 78 | "finished.", 79 | stop_counter, 80 | "stops processed.", 81 | str(perc_complete)[:4] + "% complete.", 82 | ) # debugging 83 | return stop_data 84 | 85 | 86 | def scrape(): 87 | """ 88 | Wrapper function for get_stops and get_full_stop_info that returns a fully 89 | mined list of all bus stops. 90 | """ 91 | stop_data = get_stops(stops_payload, stops_url, stops_req_fields) 92 | stop_data = get_full_stop_info(stop_data, routes_payload, routes_url) 93 | return stop_data 94 | -------------------------------------------------------------------------------- /backend/api/scrapers/crimes.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | import time 4 | import datetime 5 | import re 6 | 7 | app_token = "3vAi4grxLm8Lql1sffmqGNo2o" # Effectively the api key 8 | api_url = "https://moto.data.socrata.com/resource/3h5f-6xbh.json" 9 | 10 | days_of_crime = 30 # How many days of crime data to pull from API 11 | 12 | # Columns to select from db 13 | req_fields = [ 14 | "incident_id", 15 | "incident_datetime", 16 | "incident_type_primary", 17 | "incident_description", 18 | "address_1", 19 | "city", 20 | "state", 21 | "latitude", 22 | "longitude", 23 | "hour_of_day", 24 | "day_of_week", 25 | "parent_incident_type", 26 | ] 27 | 28 | 29 | def get_qs_url(url, args): 30 | """ 31 | Accepts url string and dictionary of querystring parameters, returns properly 32 | formatted url. 33 | """ 34 | qs_url = url 35 | i = 0 36 | for k, v in args.items(): 37 | if i == 0: 38 | qs_url += "?" 39 | else: 40 | qs_url += "&" 41 | qs_url += str(k) + "=" + str(v) 42 | i += 1 43 | return qs_url 44 | 45 | 46 | def get_datetime(days_ago): 47 | """ 48 | Returns datetime as formatted floating datetime string from days_ago days 49 | ago. 50 | E.g.: 2015-01-10T14:00:00.000 51 | """ 52 | today = datetime.date.today() 53 | offset = datetime.timedelta(days=days_ago) 54 | bound = today - offset 55 | bound_str = str(bound) + "T06:00:00.000" 56 | return bound_str 57 | 58 | 59 | def pull_data(headers, payload, api_url): 60 | """ 61 | Pulls data from Socrata API, returns dict of all data. Could probably write a 62 | generic scraper class and this would take only a little tweaking to work for 63 | every scraper. 64 | """ 65 | return_data = {} 66 | data = requests.get(get_qs_url(api_url, payload), headers=headers) 67 | for raw_record in data.json(): 68 | record = {} 69 | for field in req_fields: 70 | if field == "incident_description": 71 | record[field] = format_string(raw_record.get(field)) 72 | elif field == "incident_type_primary": 73 | record[field] = raw_record.get(field).title() 74 | else: 75 | record[field] = raw_record.get(field) 76 | return_data[record["incident_id"]] = record 77 | return return_data 78 | 79 | 80 | def format_string(text): 81 | TAG_RE = re.compile(r"<[^>]+>") 82 | stripped = TAG_RE.sub("", text) 83 | return stripped.capitalize() 84 | 85 | 86 | def crime_scrape(): 87 | """ 88 | Wrapper function that calls all methods in this script in order to return 89 | a mined dictionary of the past ${days_of_crime} days of crime data. 90 | """ 91 | payload = {"$where": "incident_datetime > '" + get_datetime(days_of_crime) + "'"} 92 | headers = {"content-type": "application/json", "X-App-Token": app_token} 93 | mined_data = pull_data(headers, payload, api_url) 94 | return mined_data 95 | -------------------------------------------------------------------------------- /backend/api/scrapers/emergency_phones.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | 4 | """ 5 | This file is mostly just to host the hardcoded emergency phone data on a disk, 6 | but it also supports returning this data as a list of dicts. 7 | """ 8 | phones = [ 9 | {"latitude": 40.0957696644812, "longitude": -88.2405983758263, "id": 0}, 10 | {"latitude": 40.108900829088, "longitude": -88.230672676274, "id": 1}, 11 | {"latitude": 40.108153502015, "longitude": -88.2312650782796, "id": 2}, 12 | {"latitude": 40.1062696203638, "longitude": -88.227582555629, "id": 3}, 13 | {"latitude": 40.1053064152121, "longitude": -88.2332534323185, "id": 4}, 14 | {"latitude": 40.1040484113017, "longitude": -88.2305198505348, "id": 5}, 15 | {"latitude": 40.1011685523957, "longitude": -88.2200390954902, "id": 6}, 16 | {"latitude": 40.0963186128832, "longitude": -88.2255865263258, "id": 7}, 17 | {"latitude": 40.1153308841568, "longitude": -88.223901994968, "id": 8}, 18 | {"latitude": 40.1155293305783, "longitude": -88.2241735309661, "id": 9}, 19 | {"latitude": 40.1154860226732, "longitude": -88.2255966171525, "id": 10}, 20 | {"latitude": 40.1152541099075, "longitude": -88.2269993612424, "id": 11}, 21 | {"latitude": 40.1144443737128, "longitude": -88.2275677301718, "id": 12}, 22 | {"latitude": 40.1136422885451, "longitude": -88.2271974078571, "id": 13}, 23 | {"latitude": 40.1144351814401, "longitude": -88.2255421958797, "id": 14}, 24 | {"latitude": 40.1145134460554, "longitude": -88.2249901437491, "id": 15}, 25 | {"latitude": 40.1134327604323, "longitude": -88.2250744675027, "id": 16}, 26 | {"latitude": 40.1126178627756, "longitude": -88.2257416048253, "id": 17}, 27 | {"latitude": 40.1128212996944, "longitude": -88.2271199654827, "id": 18}, 28 | {"latitude": 40.111594063769, "longitude": -88.2276790695158, "id": 19}, 29 | {"latitude": 40.1115161572726, "longitude": -88.2257198928538, "id": 20}, 30 | {"latitude": 40.112021993743, "longitude": -88.2238296363227, "id": 21}, 31 | {"latitude": 40.1106590345767, "longitude": -88.2225725436292, "id": 22}, 32 | {"latitude": 40.1114123952184, "longitude": -88.2298200454636, "id": 23}, 33 | {"latitude": 40.1088889776135, "longitude": -88.227875549103, "id": 24}, 34 | {"latitude": 40.1085582367152, "longitude": -88.2290291258499, "id": 25}, 35 | {"latitude": 40.1088065061748, "longitude": -88.2257239688097, "id": 26}, 36 | {"latitude": 40.1064903286909, "longitude": -88.2256433940492, "id": 27}, 37 | {"latitude": 40.1053429588434, "longitude": -88.2268047853007, "id": 28}, 38 | {"latitude": 40.1053277643859, "longitude": -88.2249452207195, "id": 29}, 39 | {"latitude": 40.1060329957132, "longitude": -88.2248572858256, "id": 30}, 40 | {"latitude": 40.1079601489434, "longitude": -88.2244923651701, "id": 31}, 41 | {"latitude": 40.1042672981332, "longitude": -88.2239993835787, "id": 32}, 42 | {"latitude": 40.1053088556378, "longitude": -88.2214299304216, "id": 33}, 43 | {"latitude": 40.1071095282756, "longitude": -88.2206559952995, "id": 34}, 44 | {"latitude": 40.1081201551601, "longitude": -88.2214139121197, "id": 35}, 45 | {"latitude": 40.1091413296473, "longitude": -88.2202056480429, "id": 36}, 46 | {"latitude": 40.1030720915384, "longitude": -88.2280244527834, "id": 37}, 47 | {"latitude": 40.1038525703531, "longitude": -88.2291399902866, "id": 38}, 48 | {"latitude": 40.1013729107359, "longitude": -88.2278757999843, "id": 39}, 49 | {"latitude": 40.1026185952179, "longitude": -88.2255039661024, "id": 40}, 50 | {"latitude": 40.1030785755548, "longitude": -88.2246681930938, "id": 41}, 51 | {"latitude": 40.1034825199358, "longitude": -88.222604152514, "id": 42}, 52 | {"latitude": 40.1034837688445, "longitude": -88.2217432022013, "id": 43}, 53 | {"latitude": 40.1023221286569, "longitude": -88.2209867723194, "id": 44}, 54 | {"latitude": 40.1024538925846, "longitude": -88.2192835508928, "id": 45}, 55 | {"latitude": 40.1016051630037, "longitude": -88.220458668589, "id": 46}, 56 | {"latitude": 40.1014484580189, "longitude": -88.2217086955956, "id": 47}, 57 | {"latitude": 40.1007446592364, "longitude": -88.2229730766282, "id": 48}, 58 | {"latitude": 40.0979028839245, "longitude": -88.221126021137, "id": 49}, 59 | {"latitude": 40.092248279751, "longitude": -88.2207073420187, "id": 50}, 60 | {"latitude": 40.0923324893079, "longitude": -88.2217349916905, "id": 51}, 61 | {"latitude": 40.0941606201313, "longitude": -88.2278288347549, "id": 52}, 62 | {"latitude": 40.0957493753066, "longitude": -88.2276495295863, "id": 53}, 63 | {"latitude": 40.0962802725601, "longitude": -88.2282206081218, "id": 54}, 64 | {"latitude": 40.0951450620077, "longitude": -88.2320469284582, "id": 55}, 65 | {"latitude": 40.0968980603088, "longitude": -88.2396824215302, "id": 56}, 66 | {"latitude": 40.1011025875766, "longitude": -88.2387259850278, "id": 57}, 67 | {"latitude": 40.1036694056589, "longitude": -88.2416724682162, "id": 58}, 68 | {"latitude": 40.1079794065974, "longitude": -88.2416733651927, "id": 59}, 69 | {"latitude": 40.1029977057437, "longitude": -88.2343605660361, "id": 60}, 70 | {"latitude": 40.1015535758818, "longitude": -88.2335256439096, "id": 61}, 71 | {"latitude": 40.1040576884692, "longitude": -88.2315614763865, "id": 62}, 72 | {"latitude": 40.1028314113639, "longitude": -88.2301199128279, "id": 63}, 73 | {"latitude": 40.1006436158821, "longitude": -88.2299843043262, "id": 64}, 74 | {"latitude": 40.1067122819879, "longitude": -88.2304362253203, "id": 65}, 75 | {"latitude": 40.1080527825068, "longitude": -88.231199475882, "id": 66}, 76 | {"latitude": 40.10917256217, "longitude": -88.2298553902326, "id": 67}, 77 | ] 78 | 79 | 80 | def get_phones(): 81 | return phones 82 | -------------------------------------------------------------------------------- /backend/api/scrapers/open_businesses.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | import time 4 | 5 | api_key = "dD6vL4WCEnKquFTLsUWpWm2RaUCTiQlSgr-lw0m4DrDkbzmycTXIcOyPZUDttVeWkaRY1lr9WFD87ebLoy4sS6raIcPED-jiChg8KPLa9mGL3JAILiNtUZqnHOW3W3Yx" 6 | search_url = "https://api.yelp.com/v3/businesses/search" 7 | details_url = "https://api.yelp.com/v3/businesses/" # {id} after last backslash 8 | 9 | payload = { 10 | "latitude": 40.106689, # center of campus 11 | "longitude": -88.227326, 12 | "radius": 3000, # meters 13 | "limit": 50, # limit per page 14 | "sort_by": "distance", # closest first 15 | "offset": 0, # page offset 16 | } 17 | headers = {"content-type": "application/json", "Authorization": "Bearer " + api_key} 18 | 19 | 20 | def get_qs_url(url, args): 21 | """ 22 | Accepts url string and dictionary of querystring parameters, returns properly 23 | formatted url. 24 | """ 25 | qs_url = url 26 | i = 0 27 | for k, v in args.items(): 28 | if i == 0: 29 | qs_url += "?" 30 | else: 31 | qs_url += "&" 32 | qs_url += str(k) + "=" + str(v) 33 | i += 1 34 | return qs_url 35 | 36 | 37 | def business_scrape(): 38 | """ 39 | This method produces a dictionary containing business data from the Yelp 40 | API. 41 | """ 42 | mined_data = {} 43 | num_results = 1 44 | 45 | t0 = time.time() 46 | while num_results > 0: 47 | print("\n\nRequesting more data, offset = " + str(payload["offset"]) + "\n") 48 | data = requests.get(get_qs_url(search_url, payload), headers=headers) 49 | for k, v in data.json().items(): 50 | print("Retrieved " + k + " data.") 51 | if k == "businesses": 52 | num_results = len(v) 53 | print("Retrieving data for " + str(len(v)) + " businesses.") 54 | for biz in v: 55 | print("Retrieving " + biz["name"] + " data...") 56 | # get detailed info about each business, extract hours 57 | info_dict = {} 58 | info_dict["name"] = biz.get("name") 59 | info_dict["yelp_id"] = biz.get("id") 60 | info_dict["location"] = biz.get("location") 61 | info_dict["latitude"] = biz.get("coordinates").get("latitude") 62 | info_dict["longitude"] = biz.get("coordinates").get("longitude") 63 | info_dict["image_url"] = biz.get("image_url") 64 | info_dict["display_phone"] = biz.get("display_phone") 65 | biz_url = details_url + biz.get("id") 66 | biz_details = requests.get(biz_url, headers=headers) 67 | if "hours" in biz_details.json().keys(): 68 | info_dict["hours"] = biz_details.json()["hours"] 69 | mined_data[biz["id"]] = info_dict 70 | else: 71 | print(v) 72 | payload["offset"] += 50 73 | 74 | # with open("mined_raw_data.txt", "w") as file: 75 | # file.write(json.dumps(mined_data)) 76 | 77 | # Following lines are for timing the scrape 78 | t1 = time.time() 79 | total_time = t1 - t0 80 | print("\n\n") 81 | print(str(len(mined_data.keys())) + " businesses identified.") 82 | print("Data written to mined_raw_data.txt.") 83 | print( 84 | "Scraping took " 85 | + str(int(total_time / 60)) 86 | + "m" 87 | + str(int(total_time % 60)) 88 | + "s." 89 | ) 90 | print("\n\n") 91 | # End timing code 92 | return mined_data 93 | -------------------------------------------------------------------------------- /backend/api/scrapers/police_stations.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is mostly just to host the hardcoded police station data on a disk, 3 | but it also supports returning this data as a list of dicts. 4 | """ 5 | police_stations = [ 6 | {"place_name": "Champaign Police Department", "lat": 40.116739, "long": -88.239269}, 7 | { 8 | "place_name": "University of Illinois Police Department", 9 | "lat": 40.112997, 10 | "long": -88.223602, 11 | }, 12 | { 13 | "place_name": "Champaign County Sheriffs Office", 14 | "lat": 40.112961, 15 | "long": -88.205799, 16 | }, 17 | {"place_name": "Urbana Police Department", "lat": 40.109914, "long": -88.204599}, 18 | ] 19 | 20 | 21 | def get_stations(): 22 | return police_stations 23 | -------------------------------------------------------------------------------- /backend/api/scrapers/streetlights.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | 4 | url = "https://gisweb.champaignil.gov/cb/rest/services/Open_Data/Open_Data/MapServer/34/query?where=1%3D1&outFields=*&outSR=4326&f=json" 5 | 6 | 7 | def streetlight_scrape(): 8 | """ 9 | This method produces a dictionary containing streetlight data from the City of Champaign Streetlights API. 10 | """ 11 | mined_data = {} 12 | data = requests.get(url) 13 | 14 | for s in data.json()["features"]: 15 | insertion = {} 16 | insertion["id"] = s["attributes"]["OBJECTID"] 17 | insertion["latitude"] = s.get("geometry").get("y") 18 | insertion["longitude"] = s.get("geometry").get("x") 19 | mined_data[insertion["id"]] = insertion 20 | 21 | return mined_data 22 | -------------------------------------------------------------------------------- /backend/api/views/auth.py: -------------------------------------------------------------------------------- 1 | import pdb 2 | import requests 3 | from flask import Blueprint, request 4 | from api.core import create_response, serialize_list, logger, invalid_model_helper 5 | from api.core import necessary_post_params 6 | from api.models.User import User 7 | from datetime import datetime 8 | 9 | auth = Blueprint("auth", __name__) 10 | auth_server_host = "https://c2tc-auth-server.herokuapp.com/" 11 | # auth_server_host = "http://localhost:8000/" 12 | 13 | 14 | def invalid_email(email_address): 15 | return not email_address.endswith("@illinois.edu") 16 | 17 | 18 | @auth.route("/register", methods=["POST"]) 19 | @necessary_post_params("email", "password", "anon", "username", "role") 20 | def register(): 21 | client_data = request.get_json() 22 | 23 | if invalid_email(client_data["email"]): 24 | return create_response( 25 | message="Not a valid email to register with!", 26 | status=422, 27 | data={"status": "fail"}, 28 | ) 29 | 30 | our_response, code = post_and_expect_token("register", "email", "password", "role") 31 | if code == 200: 32 | res_data = our_response.get_json()["result"] 33 | auth_uid = res_data["auth_uid"] 34 | create_new_db_user(request.get_json(), auth_uid) 35 | return (our_response, code) 36 | 37 | 38 | @auth.route("/login", methods=["POST"]) 39 | @necessary_post_params("email", "password") 40 | def login(): 41 | return post_and_expect_token("login", "email", "password") 42 | 43 | 44 | @auth.route("/forgotPassword", methods=["POST"]) 45 | @necessary_post_params("email") 46 | def forgot_password(): 47 | return wrap_auth_server_response( 48 | forward_post_to_auth_server("forgotPassword", "email") 49 | ) 50 | 51 | 52 | @auth.route("/passwordReset", methods=["POST"]) 53 | @necessary_post_params("email", "pin", "password") 54 | def password_reset(): 55 | return wrap_auth_server_response( 56 | forward_post_to_auth_server("passwordReset", "email", "pin", "password") 57 | ) 58 | 59 | 60 | def wrap_auth_server_response(auth_server_response): 61 | response_body = auth_server_response.json() 62 | return create_response( 63 | message=response_body["message"], status=auth_server_response.status_code 64 | ) 65 | 66 | 67 | def post_and_expect_token(endpoint, *props_to_forward): 68 | auth_server_response = forward_post_to_auth_server(endpoint, *props_to_forward) 69 | response_body = auth_server_response.json() 70 | if "token" not in response_body: 71 | return wrap_auth_server_response(auth_server_response) 72 | else: 73 | jwt_token = response_body["token"] 74 | our_response_body = {"token": jwt_token, "auth_uid": response_body["uid"]} 75 | our_response, code = create_response( 76 | message=response_body["message"], 77 | status=auth_server_response.status_code, 78 | data=our_response_body, 79 | ) 80 | return (our_response, code) 81 | 82 | 83 | def forward_post_to_auth_server(endpoint, *props_to_forward): 84 | user_input = request.get_json() 85 | 86 | auth_post_data = {key: user_input[key] for key in props_to_forward} 87 | 88 | return requests.post(auth_server_host + endpoint, json=auth_post_data) 89 | 90 | 91 | def get_user_by_token(token): 92 | auth_server_res = requests.get( 93 | auth_server_host + "getUser/", 94 | headers={ 95 | "Content-Type": "application/json", 96 | "token": token, 97 | "google": "undefined", 98 | }, 99 | ) 100 | if auth_server_res.status_code != 200: 101 | return None 102 | auth_uid = auth_server_res.json()["user_id"] 103 | return User.objects.get(auth_server_uid=auth_uid) 104 | 105 | 106 | @auth.route("/verifyEmail", methods=["POST"]) 107 | @necessary_post_params("pin") 108 | def verifyEmail(): 109 | token = request.headers.get("token") 110 | post_body = {"pin": request.get_json()["pin"]} 111 | auth_server_res = requests.post( 112 | auth_server_host + "verifyEmail/", 113 | headers={ 114 | "Content-Type": "application/json", 115 | "token": token, 116 | "google": "undefined", 117 | }, 118 | json=post_body, 119 | ) 120 | 121 | response_body = auth_server_res.json() 122 | 123 | if auth_server_res.status_code == 200: 124 | db_user = get_user_by_token(token) 125 | db_user.update(verified=True) 126 | 127 | return create_response( 128 | message=response_body["message"], status=auth_server_res.status_code 129 | ) 130 | 131 | 132 | def create_new_db_user(client_data, auth_uid): 133 | user = User.objects.create( 134 | username=client_data["username"], 135 | email=client_data["email"], 136 | trusted=False, 137 | verified=False, 138 | anon=client_data["anon"], 139 | karma=0, 140 | posted_tips=[], 141 | date_created=datetime.now(), 142 | auth_server_uid=auth_uid, 143 | ) 144 | user.save() 145 | -------------------------------------------------------------------------------- /backend/api/views/busStop.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | from api.models.BusStop import BusStop 3 | from api.core import create_response, serialize_list, logger 4 | from api.scrapers.bus_stops import scrape 5 | import requests 6 | 7 | busStop = Blueprint("busStop", __name__) 8 | 9 | 10 | @busStop.route("/bus-stops", methods=["GET"]) 11 | def get_busStop(): 12 | """ 13 | GET function for retrieving BusStop objects 14 | """ 15 | response = [busStop.to_mongo() for busStop in BusStop.objects] 16 | response = {"busStops": response} 17 | logger.info("BUSSTOPS: %s", response) 18 | return create_response(data=response) 19 | 20 | 21 | @busStop.route("/bus-stops", methods=["POST"]) 22 | def scrape_stops(): 23 | """ 24 | POST function which scrapes data from scrape() method in bus_stops.py 25 | scraper and stores them in the busStops db collection. 26 | Should be run probably once a month or so, because bus routes only change 27 | once or twice a year. 28 | """ 29 | try: 30 | stop_data = scrape() 31 | delete_stop_collection() 32 | for stop_id in stop_data.keys(): 33 | save_stop_to_db(stop_data[stop_id]) 34 | return create_response(status=200, message="success!") 35 | except requests.exceptions.HTTPError: 36 | return create_response(status=500, message="HTTPError") 37 | except requests.exceptions.Timeout: 38 | return create_response(status=500, message="Connection timed out") 39 | except Exception as e: 40 | return create_response(status=500, message="Exception raised: " + repr(e)) 41 | 42 | 43 | def save_stop_to_db(stop_dict): 44 | """ 45 | Helper function to save python dict object representing a bus stop db entry 46 | to an actual mongoDB object. 47 | """ 48 | busStop = BusStop.objects.create( 49 | stop_id=stop_dict["stop_id"], 50 | stop_name=stop_dict["stop_name"], 51 | latitude=stop_dict["stop_lat"], 52 | longitude=stop_dict["stop_lon"], 53 | routes=stop_dict.get("routes"), 54 | ) 55 | busStop.save() 56 | 57 | 58 | @busStop.route("/bus-stops", methods=["DELETE"]) 59 | def clear_stops(): 60 | """ 61 | DELETE method which wraps the delete stops collection function as 62 | an API endpoint. 63 | """ 64 | try: 65 | count = delete_stop_collection() 66 | return create_response( 67 | status=200, message="Success! Deleted " + str(count) + " records." 68 | ) 69 | except Exception as e: 70 | return create_response( 71 | status=500, message="Could not clear collection: " + repr(e) 72 | ) 73 | 74 | 75 | def delete_stop_collection(): 76 | """ 77 | Helper function to delete stop collection in db. 78 | """ 79 | result = BusStop.objects().delete() 80 | return result 81 | -------------------------------------------------------------------------------- /backend/api/views/business.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, request 2 | from api.models.Business import Business 3 | from api.models.Location import Location 4 | from api.models.OpenHours import OpenHours 5 | from api.core import create_response, serialize_list, logger 6 | from api.scrapers.open_businesses import business_scrape 7 | 8 | business = Blueprint("business", __name__) 9 | 10 | 11 | @business.route("/businesses", methods=["GET"]) 12 | def open_businesses(): 13 | """ 14 | Querystring args: time= #### (time as 4 digit 24hr time, eg. 1430 = 2:30pm) 15 | day = # (integer 0-6, where 0 is Monday) 16 | 17 | Gets a list of businesses that are open at the time specified in the 18 | querystring. 19 | """ 20 | data = Business.objects() 21 | time = int(request.args.get("time", default=-1)) 22 | day = int(request.args.get("day", default=-1)) 23 | if time == -1 or day == -1: 24 | return get_business() 25 | p_day = day - 1 26 | if p_day == -1: 27 | p_day = 6 28 | open_businesses = [] 29 | for b in data: 30 | curr_day = get_open_business_day(b, day) 31 | prev_day = get_open_business_day(b, p_day) 32 | if curr_day == None: 33 | continue 34 | if int(curr_day.start) <= time and int(curr_day.end) >= time: 35 | # open 36 | open_businesses.append(b.to_mongo()) 37 | elif ( 38 | prev_day != None 39 | and prev_day.is_overnight 40 | and (int(prev_day.end) >= time or int(prev_day.end) == 0) 41 | ): 42 | open_businesses.append(b.to_mongo()) 43 | ret_data = {"businesses": open_businesses} 44 | return create_response(data=ret_data, message="Success", status=201) 45 | 46 | 47 | def get_open_business_day(business, day): 48 | """ 49 | Helper function which returns 'day' dictionary of corresponding day for 50 | given business dictionary. If the day is not found, returns None. 51 | """ 52 | if len(business.open_hours) == 0: 53 | return None 54 | for open_day in business.open_hours: 55 | if open_day.day == day: 56 | return open_day 57 | return None 58 | 59 | 60 | @business.route("/all_businesses", methods=["GET"]) 61 | def get_business(): 62 | """ 63 | GET function for retrieving Business objects 64 | """ 65 | response = [business.to_mongo() for business in Business.objects] 66 | response = {"businesses": response} 67 | logger.info("BUSINESSES: %s", response) 68 | return create_response( 69 | data=response, status=200, message="Returning all businesses." 70 | ) 71 | 72 | 73 | @business.route("/businesses", methods=["POST"]) 74 | def scrape_businesses(): 75 | """ 76 | POST function which scrapes data from business_scrape() method in 77 | open_businesses.py scraper and stores them in the businesses db collection. 78 | Should be run maybe once a month. 79 | """ 80 | try: 81 | data = business_scrape() 82 | delete_business_collection() 83 | for business_id in data.keys(): 84 | save_business_to_db(data[business_id]) 85 | return create_response(status=200, message="success!") 86 | except requests.exceptions.HTTPError: 87 | return create_response(status=500, message="HTTPError") 88 | except requests.exceptions.Timeout: 89 | return create_response(status=500, message="Connection timed out") 90 | except Exception as e: 91 | return create_response(status=500, message="Exception raised: " + repr(e)) 92 | 93 | 94 | def save_business_to_db(business_dict): 95 | """ 96 | Helper function to save python dict object representing a business db entry 97 | to an actual mongoDB object. Gracefully handles missing hours attribute by 98 | replacing it with an empty list.delete 99 | """ 100 | location = Location( 101 | city=business_dict["location"].get("city"), 102 | country=business_dict["location"].get("country"), 103 | address1=business_dict["location"].get("address1"), 104 | state=business_dict["location"].get("state"), 105 | zip_code=business_dict["location"].get("zip_code"), 106 | ) 107 | open_hours = [] 108 | hours_struct = business_dict.get("hours") 109 | if hours_struct != None: 110 | hours_data = hours_struct[0].get("open") 111 | if hours_data != None: 112 | for hours in hours_data: 113 | new_hours = OpenHours( 114 | start=hours["start"], 115 | end=hours["end"], 116 | is_overnight=hours["is_overnight"], 117 | day=hours["day"], 118 | ) 119 | open_hours.append(new_hours) 120 | business = Business.objects.create( 121 | name=business_dict.get("name"), 122 | yelp_id=business_dict.get("yelp_id"), 123 | image_url=business_dict.get("image_url"), 124 | display_phone=business_dict.get("display_hours"), 125 | location=location, 126 | open_hours=open_hours, 127 | latitude=business_dict.get("latitude"), 128 | longitude=business_dict.get("longitude"), 129 | ) 130 | business.save() 131 | 132 | 133 | @business.route("/businesses", methods=["DELETE"]) 134 | def clear_businesses(): 135 | """ 136 | DELETE method which wraps the delete business collection function as 137 | an API endpoint. 138 | """ 139 | try: 140 | count = delete_business_collection() 141 | return create_response( 142 | status=200, message="Success! Deleted " + str(count) + " records." 143 | ) 144 | except Exception as e: 145 | return create_response( 146 | status=500, message="Could not clear collection: " + repr(e) 147 | ) 148 | 149 | 150 | def delete_business_collection(): 151 | """ 152 | Helper function to delete business collection in db. 153 | """ 154 | result = Business.objects().delete() 155 | return result 156 | -------------------------------------------------------------------------------- /backend/api/views/crime.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import os 3 | from flask import Blueprint 4 | from api.models.Crime import Crime 5 | from api.core import create_response, serialize_list, logger 6 | from api.scrapers.crimes import crime_scrape 7 | import datetime 8 | import dateutil.parser 9 | 10 | crime = Blueprint("crime", __name__) 11 | important_crime = {} 12 | 13 | 14 | @crime.route("/crimes", methods=["GET"]) 15 | def get_crime(): 16 | """ 17 | GET function for retrieving Crime objects 18 | """ 19 | response = [crime.to_mongo() for crime in Crime.objects] 20 | response = {"crimes": response} 21 | logger.info("CRIMES: %s", response) 22 | return create_response(data=response) 23 | 24 | 25 | @crime.route("/crimes", methods=["POST"]) 26 | def scrape_crimes(): 27 | """ 28 | POST function which scrapes data from crime_scrape() method in crimes.py 29 | scraper and stores them in the crimes db collection. 30 | This should probably be run every day, or every hour at night. 31 | """ 32 | try: 33 | crime_data = crime_scrape() 34 | delete_crime_collection() 35 | for crime_id in crime_data.keys(): 36 | save_crime_to_db(crime_data[crime_id]) 37 | return create_response(status=200, message="success!") 38 | except requests.exceptions.HTTPError: 39 | return create_response(status=500, message="HTTPError") 40 | except requests.exceptions.Timeout: 41 | return create_response(status=500, message="Connection timed out") 42 | except Exception as e: 43 | return create_response(status=500, message="Exception raised: " + repr(e)) 44 | 45 | 46 | def save_crime_to_db(crime_dict): 47 | """ 48 | Helper function to save python dict object representing a crime db entry to 49 | an actual mongoDB object. 50 | """ 51 | date = dateutil.parser.parse(crime_dict.get("incident_datetime")) 52 | formatted_date = date.strftime("%b %d, %Y at %I:%M %p") 53 | crime = Crime.objects.create( 54 | incident_id=crime_dict.get("incident_id"), 55 | incident_type_primary=crime_dict.get("incident_type_primary"), 56 | incident_description=crime_dict.get("incident_description"), 57 | address_1=crime_dict.get("address_1"), 58 | city=crime_dict.get("city"), 59 | state=crime_dict.get("state"), 60 | latitude=float(crime_dict.get("latitude")), 61 | longitude=float(crime_dict.get("longitude")), 62 | hour_of_day=crime_dict.get("hour_of_day"), 63 | day_of_week=crime_dict.get("day_of_week"), 64 | parent_incident_type=crime_dict.get("parent_incident_type"), 65 | incident_datetime=formatted_date, 66 | ) 67 | crime.save() 68 | 69 | 70 | @crime.route("/crimes", methods=["DELETE"]) 71 | def clear_crimes(): 72 | """ 73 | DELETE method which wraps the clear crimes collection function as 74 | an API endpoint. 75 | """ 76 | try: 77 | count = delete_crime_collection() 78 | return create_response( 79 | status=200, message="Success! Deleted " + str(count) + " records." 80 | ) 81 | except Exception as e: 82 | return create_response( 83 | status=500, message="Could not clear collection: " + repr(e) 84 | ) 85 | 86 | 87 | def delete_crime_collection(): 88 | """ 89 | Helper function to delete crime collection in db. 90 | """ 91 | count = len(Crime.objects()) 92 | check_crime_duration() 93 | for crime in Crime.objects(): 94 | duration = check_filter(crime.incident_type_primary) 95 | if (crime.duration == None or crime.duration == 30) and duration == 30: 96 | crime.delete() 97 | else: 98 | crime.duration = duration - 30 99 | return count 100 | 101 | 102 | def check_crime_duration(): 103 | """ 104 | Helper function to get important crimes 105 | """ 106 | with open("./api/views/crime_duration.csv") as csv_file: 107 | csv_reader = csv.reader(csv_file, delimiter=",") 108 | for row in csv_reader: 109 | if row[0] not in important_crime: 110 | important_crime["[UIPD] " + row[0].upper()] = row[1] 111 | 112 | 113 | def check_filter(id): 114 | """ 115 | Helper function to determine if the current crime is in the dictionary 116 | """ 117 | if id not in important_crime: 118 | return 30 119 | else: 120 | return important_crime[id] * 30 121 | -------------------------------------------------------------------------------- /backend/api/views/crime_duration.csv: -------------------------------------------------------------------------------- 1 | Homicide,6 2 | Criminal Sexual Assault,6 3 | Robbery,3 4 | Assault,3 5 | Arson,2 6 | Human Trafficking,2 7 | Criminal Damage,2 8 | Weapons Offenses,2 9 | Sex Offenses,6 10 | Offenses Involving Children,3 11 | Driving Under the Influence,2 12 | Intimidation,3 13 | Kidnapping,6 14 | Burglary Tools,2 15 | Terrorism Offenses,2 16 | Accident,1 17 | Protective Custody - Juvenile,3 18 | Gang Activity,3 19 | Missing Person,6 20 | -------------------------------------------------------------------------------- /backend/api/views/emergencyPhone.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | from api.models.EmergencyPhone import EmergencyPhone 3 | from api.core import create_response, serialize_list, logger 4 | from api.scrapers.emergency_phones import get_phones 5 | 6 | emergencyPhone = Blueprint("emergencyPhone", __name__) 7 | 8 | 9 | @emergencyPhone.route("/emergency-phones", methods=["GET"]) 10 | def get_emergencyPhone(): 11 | """ 12 | GET function for retrieving EmergencyPhone objects 13 | """ 14 | response = [emergencyPhone.to_mongo() for emergencyPhone in EmergencyPhone.objects] 15 | response = {"emergencyPhones": response} 16 | logger.info("EMERGENCYPHONES: %s", response) 17 | return create_response(data=response) 18 | 19 | 20 | @emergencyPhone.route("/emergency-phones", methods=["POST"]) 21 | def scrape_phones(): 22 | """ 23 | POST function which calls get_phones() from the emergency_phones.py scraper 24 | and stores phone data to the database. 25 | This data is hardcoded and will probably never change, so this endpoint 26 | only needs to be called if the db is reset or the collection is lost. 27 | """ 28 | try: 29 | data = get_phones() 30 | delete_phone_collection() 31 | for phone in data: 32 | save_phone_to_db(phone) 33 | return create_response(status=200, message="success!") 34 | except Exception as e: 35 | return create_response(status=500, message="Exception raised: " + repr(e)) 36 | 37 | 38 | def save_phone_to_db(phone_dict): 39 | """ 40 | Helper function to save python dict object representing an emergency phone 41 | db entry to an actual mongoDB object. 42 | """ 43 | emergencyPhone = EmergencyPhone.objects.create( 44 | emergencyPhone_id=phone_dict.get("id"), 45 | latitude=phone_dict.get("latitude"), 46 | longitude=phone_dict.get("longitude"), 47 | ) 48 | emergencyPhone.save() 49 | 50 | 51 | @emergencyPhone.route("/emergency-phones", methods=["DELETE"]) 52 | def clear_phones(): 53 | """ 54 | DELETE method which wraps the clear emergency phones collection function as 55 | an API endpoint. 56 | """ 57 | try: 58 | count = delete_phone_collection() 59 | return create_response( 60 | status=200, message="Success! Deleted " + str(count) + " records." 61 | ) 62 | except Exception as e: 63 | return create_response( 64 | status=500, message="Could not clear collection: " + repr(e) 65 | ) 66 | 67 | 68 | def delete_phone_collection(): 69 | """ 70 | Helper function to delete phone collection in db. 71 | """ 72 | result = EmergencyPhone.objects().delete() 73 | return result 74 | -------------------------------------------------------------------------------- /backend/api/views/main.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | from api.core import create_response, serialize_list, logger 3 | 4 | main = Blueprint("main", __name__) 5 | 6 | 7 | # function that is called when you visit / 8 | @main.route("/") 9 | def index(): 10 | # access the logger with the logger from api.core and uses the standard logging module 11 | logger.info("Hello World!") 12 | return "

Hello World!

" 13 | 14 | 15 | # # function that is called when you visit /persons 16 | # @main.route("/users", methods=["GET"]) 17 | # def get_user(): 18 | # logger.info("USERS: %s", User.objects) # use log formatting 19 | # return create_response(data={"megha": ["is", "a", "weab"]}) 20 | 21 | 22 | # @main.route("/users", methods=["POST"]) 23 | # def create_user(): 24 | # User(net_id="tk2", first_name="Anooj", last_name="Ko").save() 25 | # return create_response(message="success!") 26 | -------------------------------------------------------------------------------- /backend/api/views/policeStations.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | from api.models.PoliceStation import PoliceStation 3 | from api.core import create_response, serialize_list, logger 4 | from api.scrapers.police_stations import get_stations 5 | 6 | policeStation = Blueprint("policeStations", __name__) 7 | 8 | 9 | @policeStation.route("/police-stations", methods=["GET"]) 10 | def get_police_stations(): 11 | """ 12 | GET function for retrieving Police Station objects 13 | """ 14 | response = [policeStation.to_mongo() for policeStation in PoliceStation.objects] 15 | response = {"policeStations": response} 16 | logger.info("POLICESTATIONS: %s", response) 17 | return create_response(data=response) 18 | 19 | 20 | @policeStation.route("/police-stations", methods=["POST"]) 21 | def scrape_station(): 22 | """ 23 | POST function which calls get_stations() from the police_station.py scraper 24 | and stores station data to the database. 25 | This data is hardcoded and will probably never change, so this endpoint 26 | only needs to be called if the db is reset or the collection is lost. 27 | """ 28 | try: 29 | stations = get_stations() 30 | delete_police_station_collection() 31 | for station in stations: 32 | save_station(station) 33 | return create_response(status=200, message="success!") 34 | except Exception as e: 35 | return create_response(status=500, message="Exception raised: " + repr(e)) 36 | 37 | 38 | def save_station(station_dict): 39 | """ 40 | Helper function to save python dict object representing an emergency phone 41 | db entry to an actual mongoDB object. 42 | """ 43 | police_station = PoliceStation.objects.create( 44 | name=station_dict.get("place_name"), 45 | latitude=station_dict.get("lat"), 46 | longitude=station_dict.get("long"), 47 | ) 48 | police_station.save() 49 | 50 | 51 | @policeStation.route("/police-stations", methods=["DELETE"]) 52 | def clear_stations(): 53 | """ 54 | DELETE method which wraps the clear station collection function as 55 | an API endpoint. 56 | """ 57 | try: 58 | count = delete_police_station_collection() 59 | return create_response( 60 | status=200, message="Success! Deleted " + str(count) + " records." 61 | ) 62 | except Exception as e: 63 | return create_response( 64 | status=500, message="Could not clear collection: " + repr(e) 65 | ) 66 | 67 | 68 | def delete_police_station_collection(): 69 | """ 70 | Helper function to delete station collection in db. 71 | """ 72 | result = PoliceStation.objects().delete() 73 | return result 74 | -------------------------------------------------------------------------------- /backend/api/views/streetlight.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, request 2 | from api.models.Streetlight import Streetlight 3 | from api.core import create_response, serialize_list, logger 4 | from api.scrapers.streetlights import streetlight_scrape 5 | import requests 6 | 7 | streetlight = Blueprint("streetlight", __name__) 8 | 9 | 10 | @streetlight.route("/streetlights", methods=["GET"]) 11 | def get_streetlight(): 12 | """ 13 | GET function for retrieving Streetlight objects 14 | """ 15 | response = [streetlight.to_mongo() for streetlight in Streetlight.objects] 16 | response = {"streetlights": response} 17 | logger.info("STREETLIGHTS: %s", response) 18 | return create_response(data=response) 19 | 20 | 21 | @streetlight.route("/streetlights", methods=["POST"]) 22 | def scrape_streetlights(): 23 | """ 24 | POST function which scrapes data from streetlight_scrape() method in 25 | streetlights.py scraper and stores them in the streetlight db collection. 26 | """ 27 | try: 28 | data = streetlight_scrape() 29 | delete_streetlight_collection() 30 | for streetlight_id in data.keys(): 31 | save_streetlight_to_db(data[streetlight_id]) 32 | return create_response(status=200, message="success!") 33 | except requests.exceptions.HTTPError: 34 | return create_response(status=500, message="HTTPError") 35 | except requests.exceptions.Timeout: 36 | return create_response(status=500, message="Connection timed out") 37 | except Exception as e: 38 | return create_response(status=500, message="Exception raised: " + repr(e)) 39 | 40 | 41 | def save_streetlight_to_db(streetlight_dict): 42 | """ 43 | Helper function to save python dict object representing a streetlight db entry 44 | to an actual mongoDB object. 45 | """ 46 | latitude = streetlight_dict.get("latitude") 47 | longitude = streetlight_dict.get("longitude") 48 | if latitude and longitude: 49 | streetlight = Streetlight.objects.create( 50 | latitude=streetlight_dict.get("latitude"), 51 | longitude=streetlight_dict.get("longitude"), 52 | ) 53 | streetlight.save() 54 | 55 | 56 | @streetlight.route("/streetlights", methods=["DELETE"]) 57 | def clear_streetlights(): 58 | """ 59 | DELETE method which wraps the delete streetlight collection function as 60 | an API endpoint. 61 | """ 62 | try: 63 | count = delete_streetlight_collection() 64 | return create_response( 65 | status=200, message="Success! Deleted " + str(count) + " records." 66 | ) 67 | except Exception as e: 68 | return create_response( 69 | status=500, message="Could not clear collection: " + repr(e) 70 | ) 71 | 72 | 73 | def delete_streetlight_collection(): 74 | """ 75 | Helper function to delete streetlight collection in db. 76 | """ 77 | result = len(Streetlight.objects()) 78 | count = 0 79 | for streetlight in Streetlight.objects(): 80 | streetlight.delete() 81 | count = count + 1 82 | return result 83 | -------------------------------------------------------------------------------- /backend/api/views/user.py: -------------------------------------------------------------------------------- 1 | import pdb 2 | from flask import Blueprint, request 3 | from datetime import datetime 4 | from api.models.User import User 5 | from api.core import create_response, serialize_list, logger, authenticated_route 6 | 7 | user = Blueprint("user", __name__) 8 | 9 | 10 | @user.route("/users", methods=["GET"]) 11 | def get_users(): 12 | """ 13 | GET function for retrieving User objects 14 | """ 15 | response = [user.to_mongo() for user in User.objects] 16 | response = {"users": response} 17 | logger.info("USERS: %s", response) 18 | return create_response(data=response) 19 | 20 | 21 | @user.route("/users", methods=["POST"]) 22 | def create_user(): 23 | """ 24 | POST function for creating a new User 25 | """ 26 | data = request.get_json() 27 | user = User.objects.create( 28 | username=data["username"], 29 | verified=False, 30 | anon=data["anon"], 31 | pro_pic=data["pro_pic"], 32 | karma=0, 33 | posted_tips=[], 34 | date_created=datetime.now(), 35 | ) 36 | user.save() 37 | return create_response(message="success!") 38 | 39 | 40 | @user.route("/userinfo", methods=["GET"]) 41 | @authenticated_route 42 | def current_user_info(db_user): 43 | return create_response( 44 | message="Success!", status=200, data=dict(db_user.to_mongo()) 45 | ) 46 | 47 | 48 | @user.route("/users/", methods=["GET"]) 49 | def get_user(id): 50 | """ 51 | GET function for retrieving a single User 52 | """ 53 | response = User.objects.get(id=id).to_mongo() 54 | return create_response(data=dict(response)) 55 | 56 | 57 | @user.route("/users", methods=["PUT"]) 58 | @authenticated_route 59 | def update_user(user): 60 | """ 61 | PUT function for updating a User 62 | """ 63 | data = request.get_json() 64 | if "username" in data: 65 | user.update(username=data["username"]) 66 | if "verified" in data: 67 | user.update(verified=data["verified"]) 68 | if "anon" in data: 69 | user.update(anon=data["anon"]) 70 | if "karma" in data: 71 | user.update(karma=data["karma"]) 72 | if "posted_tips" in data: 73 | user.update(posted_tips=data["posted_tips"]) 74 | if "pro_pic" in data: 75 | user.update(pro_pic=data["pro_pic"]) 76 | if "trusted" in data: 77 | user.update(trusted=data["trusted"]) 78 | return create_response(message="success!") 79 | 80 | 81 | @user.route("/users/", methods=["DELETE"]) 82 | def delete_user(id): 83 | """ 84 | DELETE function for deleting a user 85 | """ 86 | User.objects(id=id).delete() 87 | return create_response(message="success!") 88 | 89 | 90 | @user.route("/users//verify", methods=["PUT"]) 91 | def update_verified(id): 92 | """ 93 | PUT function for changing the user's verified status 94 | """ 95 | user = User.objects.get(id=id) 96 | if request.args.get("verified") == "True": 97 | user.update(verified=True) 98 | return create_response(message="success!") 99 | if request.args.get("verified") == "False": 100 | user.update(verified=False) 101 | return create_response(message="success!") 102 | return create_response( 103 | message="query string not recognized, it must be either True or False" 104 | ) 105 | -------------------------------------------------------------------------------- /backend/manage.py: -------------------------------------------------------------------------------- 1 | from flask_script import Manager 2 | from api import create_app 3 | 4 | # from apscheduler.schedulers.background import BackgroundScheduler 5 | from api.scrapers.bus_stops import scrape 6 | from api.scrapers.crimes import crime_scrape 7 | from api.scrapers.open_businesses import business_scrape 8 | from api.scrapers.streetlights import streetlight_scrape 9 | from flask import Blueprint, request, jsonify 10 | from api.core import create_response, serialize_list, logger 11 | 12 | app = create_app() 13 | 14 | manager = Manager(app) 15 | 16 | # scheduler = BackgroundScheduler() 17 | # scheduler.add_job(scrape, "interval", weeks=2, timezone="America/Indiana/Indianapolis") 18 | # scheduler.add_job( 19 | # crime_scrape, "interval", weeks=2, timezone="America/Indiana/Indianapolis" 20 | # ) 21 | # scheduler.add_job( 22 | # business_scrape, "interval", weeks=2, timezone="America/Indiana/Indianapolis" 23 | # ) 24 | # scheduler.add_job( 25 | # streetlight_scrape, "interval", weeks=2, timezone="America/Indiana/Indianapolis" 26 | # ) 27 | 28 | 29 | # @app.route("/schedule", methods=["GET"]) 30 | # def get_schedule(): 31 | # """ 32 | # GET function for retrieving all schedules for the scrapers 33 | # """ 34 | # jobs = [job.name for job in scheduler.get_jobs()] 35 | # next_time = [ 36 | # job.next_run_time.strftime("%m/%d/%Y, %H:%M:%S") for job in scheduler.get_jobs() 37 | # ] 38 | # response = dict(zip(jobs, next_time)) 39 | # return create_response(data=response) 40 | 41 | 42 | @manager.command 43 | def runserver(): 44 | # scheduler.start() 45 | app.run(debug=True, host="0.0.0.0", port=5000) 46 | 47 | 48 | @manager.command 49 | def runworker(): 50 | app.run(debug=False) 51 | 52 | 53 | if __name__ == "__main__": 54 | manager.run() 55 | -------------------------------------------------------------------------------- /backend/now.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "name": "cut-to-the-case", 4 | "public": true, 5 | "type": "docker", 6 | "features": { 7 | "cloud": "v2" 8 | }, 9 | "alias": "h4i-cut-to-the-case-backend.now.sh" 10 | } -------------------------------------------------------------------------------- /backend/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = -v --color=yes -------------------------------------------------------------------------------- /backend/runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.7 2 | -------------------------------------------------------------------------------- /backend/tests/README.md: -------------------------------------------------------------------------------- 1 | # TESTS WONT WORK 2 | -------------------------------------------------------------------------------- /backend/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hack4impact-uiuc/safe-maps/a64698ac47c35b244d32dc86019327b35d69e9ed/backend/tests/__init__.py -------------------------------------------------------------------------------- /backend/tests/bus_test_data.py: -------------------------------------------------------------------------------- 1 | busStops = [ 2 | { 3 | "_id": "5be88546ed396d3ebf7299f2", 4 | "stop_lat": 40.116158, 5 | "stop_lon": -88.197342, 6 | "routes": { 7 | "1": "fcee1f", 8 | "10": "c7994a", 9 | "100": "fcee1f", 10 | "11": "eb008b", 11 | "110": "eb008b", 12 | "12": "006991", 13 | "120": "006991", 14 | "13": "cccccc", 15 | "130": "cccccc", 16 | "14": "2b3088", 17 | "16": "ffbfff", 18 | "180": "b2d235", 19 | "2": "ed1c24", 20 | "20": "ed1c24", 21 | "21": "000000", 22 | "22": "5a1d5a", 23 | "220": "5a1d5a", 24 | "3": "a78bc0", 25 | "30": "a78bc0", 26 | "4": "355caa", 27 | "5": "008063", 28 | "50": "008063", 29 | "6": "f99f2a", 30 | "7": "808285", 31 | "70": "808285", 32 | "8": "9e8966", 33 | "9": "825622", 34 | }, 35 | "stop_id": "DEPOT", 36 | "stop_name": "MTD Garage", 37 | }, 38 | { 39 | "_id": "5be8858ded396d3ebf729d0b", 40 | "stop_lat": 40.107025, 41 | "stop_lon": -88.22892, 42 | "routes": { 43 | "1": "fcee1f", 44 | "10": "c7994a", 45 | "100": "fcee1f", 46 | "13": "cccccc", 47 | "130": "cccccc", 48 | "22": "5a1d5a", 49 | "220": "5a1d5a", 50 | "4": "355caa", 51 | "5": "008063", 52 | "9": "825622", 53 | }, 54 | "stop_id": "WRTCHAL", 55 | "stop_name": "Wright and Chalmers", 56 | }, 57 | ] 58 | 59 | 60 | def get_buses(): 61 | return busStops 62 | -------------------------------------------------------------------------------- /backend/tests/conftest.py: -------------------------------------------------------------------------------- 1 | # conftest.py is used by pytest to share fixtures 2 | # https://docs.pytest.org/en/latest/fixture.html#conftest-py-sharing-fixture-functions 3 | import pytest 4 | from api import create_app 5 | 6 | 7 | @pytest.fixture(scope="session") 8 | def client(): 9 | config_dict = { 10 | "DEBUG": True, 11 | "MONGO_TEST_URI": "mongodb://megha:megha9000@ds217864.mlab.com:17864/meghalab", 12 | "MONGO_TEST_DB": "meghalab", 13 | } 14 | app = create_app(config_dict) 15 | app.app_context().push() 16 | client = app.test_client() 17 | 18 | yield client 19 | -------------------------------------------------------------------------------- /backend/tests/crime_test_data.py: -------------------------------------------------------------------------------- 1 | crimes = [ 2 | { 3 | "_id": "5bdbba8fed396daf6e72d1d9", 4 | "address_1": "RANDOLPH ST N & CHURCH ST", 5 | "city": "URBANA", 6 | "day_of_week": "Monday", 7 | "hour_of_day": 23, 8 | "incident_datetime": "11/01/2018, 21:43:35", 9 | "incident_description": "WARRANT-IN STATE

OFFICER CONDUCTED A TRAFFIC STOP FOR FAILURE TO SIGNAL. A PA SSENGER WAS ARRESTED ON A VALID CHAMPAIGN COUNTY WARRANT. DR IVER WAS RELEASED WITH A WRITTEN WARNING.", 10 | "incident_id": "882600354", 11 | "incident_type_primary": "[UIPD] WARRANT-IN STATE", 12 | "latitude": 40.1183, 13 | "longitude": -88.2452, 14 | "parent_incident_type": "Other", 15 | "state": "IL", 16 | }, 17 | { 18 | "_id": "5bdbba8fed396daf6e72d1de", 19 | "address_1": "800 Block WRIGHT ST S", 20 | "city": "URBANA", 21 | "day_of_week": "Thursday", 22 | "hour_of_day": 20, 23 | "incident_datetime": "11/01/2018, 21:43:35", 24 | "incident_description": "THEFT-RETAIL

RETAIL THEFT OCCURRED AT IUB. THE SUSPECT WAS APPREHENDED AND THE PROPERTY RETURNED.", 25 | "incident_id": "883832978", 26 | "incident_type_primary": "[UIPD] THEFT-RETAIL", 27 | "latitude": 40.1083, 28 | "longitude": -88.2292, 29 | "parent_incident_type": "Theft", 30 | "state": "IL", 31 | }, 32 | { 33 | "_id": "5bdbba8fed396daf6e72d1df", 34 | "address_1": "1000 Block GREGORY DR E", 35 | "city": "URBANA", 36 | "day_of_week": "Thursday", 37 | "hour_of_day": 23, 38 | "incident_datetime": "11/01/2018, 21:43:35", 39 | "incident_description": "OTHER TROUBLE/INFO RPT

RESIDENT OF ALLEN HALL DISPOSED OF HOT INCENSE IN GARBAGE CAN. GARBAGE STARTED TO BURN RESULTING IN FIRE ALARM.", 40 | "incident_id": "883832977", 41 | "incident_type_primary": "[UIPD] OTHER TROUBLE/INFO RPT", 42 | "latitude": 40.1041, 43 | "longitude": -88.221, 44 | "parent_incident_type": "Other", 45 | "state": "IL", 46 | }, 47 | ] 48 | 49 | 50 | def get_crimes(): 51 | return crimes 52 | -------------------------------------------------------------------------------- /backend/tests/phone_test_data.py: -------------------------------------------------------------------------------- 1 | phones = [ 2 | {"id": 6, "latitude": 40.1011685523957, "longitude": -88.2200390954902}, 3 | {"id": 7, "latitude": 40.0963186128832, "longitude": -88.2255865263258}, 4 | {"id": 8, "latitude": 40.1153308841568, "longitude": -88.223901994968}, 5 | ] 6 | 7 | 8 | def get_phones(): 9 | return phones 10 | 11 | 12 | bad_data = [ 13 | {"id": 6, "longitude": -88.2200390954902}, 14 | {"id": 7, "latitude": 40.0963186128832, "longitude": -88.2255865263258}, 15 | {"id": 8, "latitude": 40.1153308841568, "longitude": -88.223901994968}, 16 | ] 17 | 18 | 19 | def get_bad_data(): 20 | return bad_data 21 | -------------------------------------------------------------------------------- /backend/tests/police_test_data.py: -------------------------------------------------------------------------------- 1 | police_stations = [ 2 | {"place_name": "Champaign Police Department", "lat": 40.116739, "long": -88.239269}, 3 | { 4 | "place_name": "University of Illinois Police Department", 5 | "lat": 40.112997, 6 | "long": -88.223602, 7 | }, 8 | { 9 | "place_name": "Champaign County Sheriffs Office", 10 | "lat": 40.112961, 11 | "long": -88.205799, 12 | }, 13 | {"place_name": "Urbana Police Department", "lat": 40.109914, "long": -88.204599}, 14 | ] 15 | 16 | 17 | def get_stations(): 18 | return police_stations 19 | -------------------------------------------------------------------------------- /backend/tests/streetlight_test_data.py: -------------------------------------------------------------------------------- 1 | streetlights = [ 2 | { 3 | "_id": "5c33cb90633a6f0003bcb38b", 4 | "latitude": 40.109356050955824, 5 | "longitude": -88.23546712954632, 6 | }, 7 | { 8 | "_id": "5c33cb90633a6f0003bcb38c", 9 | "latitude": 40.10956288950609, 10 | "longitude": -88.23546931624688, 11 | }, 12 | { 13 | "_id": "5c33cb90633a6f0003bcb38d", 14 | "latitude": 40.11072693111868, 15 | "longitude": -88.23548184676547, 16 | }, 17 | { 18 | "_id": "5c33cb90633a6f0003bcb38e", 19 | "latitude": 40.11052593366689, 20 | "longitude": -88.23548345321224, 21 | }, 22 | { 23 | "_id": "5c33cb90633a6f0003bcb38f", 24 | "latitude": 40.1105317123791, 25 | "longitude": -88.23527189093869, 26 | }, 27 | ] 28 | 29 | 30 | def get_streetlights(): 31 | return streetlights 32 | -------------------------------------------------------------------------------- /backend/tests/test_basic.py: -------------------------------------------------------------------------------- 1 | # from api.models import Person 2 | import pytest 3 | from api.models.EmergencyPhone import EmergencyPhone 4 | 5 | # client passed from client - look into pytest for more info about fixtures 6 | # test client api: http://flask.pocoo.org/docs/1.0/api/#test-client 7 | 8 | 9 | def test_basic(): 10 | assert True 11 | -------------------------------------------------------------------------------- /backend/tests/test_buses.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from api.models.BusStop import BusStop 3 | from tests.bus_test_data import get_buses 4 | from api.views.busStop import save_stop_to_db 5 | 6 | # client passed from client - look into pytest for more info about fixtures 7 | # test client api: http://flask.pocoo.org/docs/1.0/api/#test-client 8 | 9 | 10 | def test_delete(client): 11 | """ 12 | Tests delete endpoint. 13 | """ 14 | rs = client.delete("/bus-stops") 15 | collection = BusStop.objects() 16 | assert len(collection) == 0 17 | assert rs.status_code == 200 18 | 19 | 20 | def test_update(client): 21 | """ 22 | Tests update endpoint. 23 | """ 24 | # rs = client.post("/bus-stops") 25 | # collection = BusStop.objects() 26 | # assert len(collection) > 0 27 | # assert rs.status_code == 200 28 | 29 | 30 | def insert_test_data(client): 31 | """ 32 | Puts test data in the db 33 | """ 34 | bus = get_buses() 35 | for bus_dict in bus: 36 | save_stop_to_db(bus_dict) 37 | 38 | collection = BusStop.objects() 39 | assert len(collection) == 2 40 | 41 | 42 | def test_get_basic(client): 43 | """ 44 | Tests get endpoint (all stops) 45 | """ 46 | client.delete("/bus-stops") 47 | insert_test_data(client) 48 | rs = client.get("/bus-stops") 49 | collection = rs.json["result"]["busStops"] 50 | assert len(collection) == 2 51 | -------------------------------------------------------------------------------- /backend/tests/test_business.py: -------------------------------------------------------------------------------- 1 | # from api.models import Person 2 | import pytest 3 | from api.models.Business import Business 4 | from tests.business_test_data import get_businesses 5 | from api.views.business import save_business_to_db 6 | 7 | # client passed from client - look into pytest for more info about fixtures 8 | # test client api: http://flask.pocoo.org/docs/1.0/api/#test-client 9 | 10 | 11 | def test_delete(client): 12 | """ 13 | Tests delete endpoint. 14 | """ 15 | rs = client.delete("/businesses") 16 | collection = Business.objects() 17 | assert len(collection) == 0 18 | assert rs.status_code == 200 19 | 20 | 21 | def test_update(client): 22 | """ 23 | Tests update endpoint. 24 | """ 25 | # rs = client.post("/businesses") 26 | # collection = Business.objects() 27 | # assert len(collection) > 0 28 | # assert rs.status_code == 200 29 | 30 | 31 | def insert_test_data(client): 32 | """ 33 | Puts test data in the db 34 | """ 35 | businesses = get_businesses() 36 | for business_dict in businesses: 37 | save_business_to_db(business_dict) 38 | 39 | collection = Business.objects() 40 | assert len(collection) == 12 41 | 42 | 43 | def test_get_basic(client): 44 | """ 45 | Tests get endpoint (all businesses) 46 | """ 47 | client.delete("/businesses") 48 | insert_test_data(client) 49 | rs = client.get("/businesses") 50 | collection = rs.json["result"]["businesses"] 51 | assert len(collection) == 12 52 | 53 | 54 | def test_get_weekday_afternoon(client): 55 | """ 56 | Tests get endpoint (Tuesday 3:43pm) 57 | """ 58 | client.delete("/businesses") 59 | insert_test_data(client) 60 | params = {"day": "1", "time": "1543"} 61 | rs = client.get("/businesses", query_string=params) 62 | # print(rs) 63 | print(rs.json) 64 | collection = rs.json["result"]["businesses"] 65 | print(collection) 66 | assert len(collection) == 9 67 | 68 | 69 | def test_get_weekday_morning(client): 70 | """ 71 | Tests get endpoint (Wednesday 10:00am) 72 | """ 73 | client.delete("/businesses") 74 | insert_test_data(client) 75 | rs = client.get("/businesses?day=2&time=1000") 76 | collection = rs.json["result"]["businesses"] 77 | assert len(collection) == 4 78 | 79 | 80 | def test_get_weekend_earlymorning(client): 81 | """ 82 | Tests get endpoint (Saturday 12:30am) 83 | """ 84 | client.delete("/businesses") 85 | insert_test_data(client) 86 | rs = client.get("/businesses?day=5&time=0030") 87 | collection = rs.json["result"]["businesses"] 88 | assert len(collection) == 2 89 | 90 | 91 | def test_get_weekday_earlymorning(client): 92 | """ 93 | Tests get endpoint (Thursday 3:12am) 94 | """ 95 | client.delete("/businesses") 96 | insert_test_data(client) 97 | rs = client.get("/businesses?day=3&time=0312") 98 | collection = rs.json["result"]["businesses"] 99 | # print(rs.json) 100 | assert len(collection) == 1 101 | -------------------------------------------------------------------------------- /backend/tests/test_crimes.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from api.models.Crime import Crime 3 | from tests.crime_test_data import get_crimes 4 | from api.views.crime import save_crime_to_db 5 | 6 | # client passed from client - look into pytest for more info about fixtures 7 | # test client api: http://flask.pocoo.org/docs/1.0/api/#test-client 8 | 9 | 10 | def test_delete(client): 11 | """ 12 | Tests delete endpoint. 13 | """ 14 | rs = client.delete("/crimes") 15 | collection = Crime.objects() 16 | assert len(collection) == 0 17 | assert rs.status_code == 200 18 | 19 | 20 | def test_update(client): 21 | """ 22 | Tests update endpoint. 23 | """ 24 | rs = client.post("/crimes") 25 | collection = Crime.objects() 26 | assert len(collection) > 0 27 | assert rs.status_code == 200 28 | 29 | 30 | def insert_test_data(client): 31 | """ 32 | Puts test data in the db 33 | """ 34 | crimes = get_crimes() 35 | for crime_dict in crimes: 36 | save_crime_to_db(crime_dict) 37 | 38 | collection = Crime.objects() 39 | assert len(collection) == 3 40 | 41 | 42 | def test_get_basic(client): 43 | """ 44 | Tests get endpoint (all crimes) 45 | """ 46 | client.delete("/crimes") 47 | insert_test_data(client) 48 | rs = client.get("/crimes") 49 | collection = rs.json["result"]["crimes"] 50 | assert len(collection) == 3 51 | -------------------------------------------------------------------------------- /backend/tests/test_lights.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from api.models.Streetlight import Streetlight 3 | from tests.streetlight_test_data import get_streetlights 4 | from api.views.streetlight import save_streetlight_to_db 5 | 6 | # client passed from client - look into pytest for more info about fixtures 7 | # test client api: http://flask.pocoo.org/docs/1.0/api/#test-client 8 | 9 | 10 | def test_delete(client): 11 | """ 12 | Tests delete endpoint. 13 | """ 14 | rs = client.delete("/streetlights") 15 | collection = Streetlight.objects() 16 | assert len(collection) == 0 17 | assert rs.status_code == 200 18 | 19 | 20 | def test_update(client): 21 | """ 22 | Tests update endpoint. 23 | """ 24 | rs = client.post("/streetlights") 25 | collection = Streetlight.objects() 26 | assert len(collection) > 0 27 | assert rs.status_code == 200 28 | 29 | 30 | def insert_test_data(client): 31 | """ 32 | Puts test data in the db 33 | """ 34 | streetlights = get_streetlights() 35 | for streetlight_dict in streetlights: 36 | save_streetlight_to_db(streetlight_dict) 37 | collection = Streetlight.objects() 38 | assert len(collection) == 5 39 | 40 | 41 | def test_get_basic(client): 42 | """ 43 | Tests get endpoint (all crimes) 44 | """ 45 | client.delete("/streetlights") 46 | insert_test_data(client) 47 | rs = client.get("/streetlights") 48 | collection = rs.json["result"]["streetlights"] 49 | assert len(collection) == 5 50 | -------------------------------------------------------------------------------- /backend/tests/test_phone.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from api.models.EmergencyPhone import EmergencyPhone 3 | from tests.phone_test_data import get_phones, get_bad_data 4 | from api.views.emergencyPhone import save_phone_to_db 5 | 6 | # client passed from client - look into pytest for more info about fixtures 7 | # test client api: http://flask.pocoo.org/docs/1.0/api/#test-client 8 | 9 | 10 | def test_delete(client): 11 | """ 12 | Tests delete endpoint. 13 | """ 14 | rs = client.delete("/emergency-phones") 15 | collection = EmergencyPhone.objects() 16 | assert len(collection) == 0 17 | assert rs.status_code == 200 18 | 19 | 20 | def test_update(client): 21 | """ 22 | Tests update endpoint. 23 | """ 24 | rs = client.post("/emergency-phones") 25 | collection = EmergencyPhone.objects() 26 | assert len(collection) > 0 27 | assert rs.status_code == 200 28 | 29 | 30 | def insert_test_data(client): 31 | """ 32 | Puts test data in the db 33 | """ 34 | phones = get_phones() 35 | for phone_dict in phones: 36 | save_phone_to_db(phone_dict) 37 | 38 | collection = EmergencyPhone.objects() 39 | assert len(collection) == 3 40 | 41 | 42 | def test_get_basic(client): 43 | """ 44 | Tests get endpoint (all phones) 45 | """ 46 | client.delete("/emergency-phones") 47 | insert_test_data(client) 48 | rs = client.get("/emergency-phones") 49 | collection = rs.json["result"]["emergencyPhones"] 50 | assert len(collection) == 3 51 | -------------------------------------------------------------------------------- /backend/tests/test_police.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from api.models.PoliceStation import PoliceStation 3 | from tests.police_test_data import get_stations 4 | from api.views.policeStations import save_station 5 | 6 | # client passed from client - look into pytest for more info about fixtures 7 | # test client api: http://flask.pocoo.org/docs/1.0/api/#test-client 8 | 9 | 10 | def test_delete(client): 11 | """ 12 | Tests delete endpoint. 13 | """ 14 | rs = client.delete("/police-stations") 15 | collection = PoliceStation.objects() 16 | assert len(collection) == 0 17 | assert rs.status_code == 200 18 | 19 | 20 | def test_update(client): 21 | """ 22 | Tests update endpoint. 23 | """ 24 | rs = client.post("/police-stations") 25 | collection = PoliceStation.objects() 26 | assert len(collection) > 0 27 | assert rs.status_code == 200 28 | 29 | 30 | def insert_test_data(client): 31 | """ 32 | Puts test data in the db 33 | """ 34 | stations = get_stations() 35 | for station_dict in stations: 36 | save_station(station_dict) 37 | 38 | collection = PoliceStation.objects() 39 | assert len(collection) == 4 40 | 41 | 42 | def test_get_basic(client): 43 | """ 44 | Tests get endpoint (all stations) 45 | """ 46 | client.delete("/police-stations") 47 | insert_test_data(client) 48 | rs = client.get("/police-stations") 49 | collection = rs.json["result"]["policeStations"] 50 | assert len(collection) == 4 51 | -------------------------------------------------------------------------------- /backend/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | -------------------------------------------------------------------------------- /c2tc-mobile/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/**/* 2 | .expo/* 3 | npm-debug.* 4 | -------------------------------------------------------------------------------- /c2tc-mobile/.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /c2tc-mobile/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | 8 | [dev-packages] 9 | 10 | [requires] 11 | python_version = "3.7" 12 | -------------------------------------------------------------------------------- /c2tc-mobile/README.MD: -------------------------------------------------------------------------------- 1 | # Frontend Instructions 2 | 3 | ## Setting Up Instructions 4 | 5 | ```bash 6 | yarn global add expo-cli 7 | ``` 8 | 9 | Install the Expo Mobile App on phone if you have never used expo before. 10 | 11 | ## How To Run The Frontend 12 | 13 | ```bash 14 | cd c2tc-mobile 15 | yarn # install dependencies 16 | expo start 17 | # scan QR code with phone (use camera if you have an iphone or use expo app if you have an android.) 18 | ``` 19 | 20 | ## How To Format The Frontend 21 | 22 | ```bash 23 | cd c2tc-mobile 24 | yarn # install dependencies if not already installed 25 | yarn format 26 | ``` 27 | 28 | ## Design Resources 29 | 30 | - [Wireframes](https://sketch.cloud/s/45Dzo) 31 | - [Mockups](https://philkuo.com/hack4impact/c2tc_mockup_current/) 32 | - [Prototype](https://sketch.cloud/s/AJ9Ky/PrjlrQ/play) 33 | -------------------------------------------------------------------------------- /c2tc-mobile/Redux.js: -------------------------------------------------------------------------------- 1 | import { createStore } from "redux"; 2 | 3 | const initialState = { 4 | markerClicked: false, 5 | markerTitle: "", 6 | markerDescription: "", 7 | page: "filter", 8 | markers: [], 9 | layerData: {}, 10 | colorData: {}, 11 | renderData: { 12 | busStop: false, 13 | crime: false, 14 | business: false, 15 | emergency: false, 16 | policeStations: false, 17 | streetLights: false 18 | }, 19 | mapRegion: null 20 | }; 21 | 22 | export default (reducer = (state = initialState, action) => { 23 | switch (action.type) { 24 | case "UPDATE_MAP_REGION": 25 | return { ...state, mapRegion: action.value }; 26 | case "UPDATE_MARKERS": 27 | return { ...state, markers: action.value }; 28 | case "UPDATE_RENDER_DATA": 29 | return { 30 | ...state, 31 | renderData: { 32 | ...state.renderData, 33 | [action.payload.field]: action.payload.value 34 | } 35 | }; 36 | case "UPDATE_COLOR_DATA": 37 | return { ...state, colorData: action.value }; 38 | case "UPDATE_LAYER_DATA": 39 | return { ...state, layerData: action.value }; 40 | case "UPDATE_DETAIL_VIEW": 41 | return { 42 | ...state, 43 | markerClicked: action.payload.clicked, 44 | markerTitle: action.payload.title, 45 | markerDescription: action.payload.desc 46 | }; 47 | case "UPDATE_PAGE": 48 | return { ...state, page: action.value }; 49 | default: 50 | return state; 51 | } 52 | }); 53 | 54 | export const updateMapRegion = value => ({ 55 | type: "UPDATE_MAP_REGION", 56 | value 57 | }); 58 | export const updateMarkers = value => ({ 59 | type: "UPDATE_MARKERS", 60 | value 61 | }); 62 | export const updateRenderData = (field, value) => ({ 63 | type: "UPDATE_RENDER_DATA", 64 | payload: { 65 | field, 66 | value 67 | } 68 | }); 69 | export const updateLayerData = value => ({ 70 | type: "UPDATE_LAYER_DATA", 71 | value 72 | }); 73 | export const updateColorData = value => ({ 74 | type: "UPDATE_COLOR_DATA", 75 | value 76 | }); 77 | 78 | export const updateDetailView = (clicked, title, desc) => ({ 79 | type: "UPDATE_DETAIL_VIEW", 80 | payload: { 81 | clicked, 82 | title, 83 | desc 84 | } 85 | }); 86 | 87 | export const updatePage = value => ({ 88 | type: "UPDATE_PAGE", 89 | value 90 | }); 91 | 92 | export const store = createStore(reducer, initialState); 93 | -------------------------------------------------------------------------------- /c2tc-mobile/__tests__/App-test.js: -------------------------------------------------------------------------------- 1 | import "react-native"; 2 | import React from "react"; 3 | import App from "../App"; 4 | import renderer from "react-test-renderer"; 5 | import NavigationTestUtils from "react-navigation/NavigationTestUtils"; 6 | 7 | describe("App snapshot", () => { 8 | jest.useFakeTimers(); 9 | beforeEach(() => { 10 | NavigationTestUtils.resetInternalState(); 11 | }); 12 | 13 | it("renders the loading screen", async () => { 14 | const tree = renderer.create().toJSON(); 15 | expect(tree).toMatchSnapshot(); 16 | }); 17 | 18 | it("renders the root without loading screen", async () => { 19 | const tree = renderer.create().toJSON(); 20 | expect(tree).toMatchSnapshot(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /c2tc-mobile/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "Cut To The Case", 4 | "description": "Snip. Snip.", 5 | "slug": "c2tc", 6 | "privacy": "public", 7 | "sdkVersion": "32.0.0", 8 | "platforms": ["ios", "android"], 9 | "version": "1.0.0", 10 | "orientation": "portrait", 11 | "icon": "./assets/images/icon.png", 12 | "splash": { 13 | "image": "./assets/images/splash.png", 14 | "resizeMode": "contain", 15 | "backgroundColor": "#ffffff" 16 | }, 17 | "updates": { 18 | "fallbackToCacheTimeout": 0 19 | }, 20 | "assetBundlePatterns": ["**/*"], 21 | "ios": { 22 | "supportsTablet": true 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /c2tc-mobile/assets/data/light_locations.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "place_name": "Lights", 4 | "lat": 40.216739, 5 | "long": -88.339272 6 | }, 7 | { 8 | "place_name": "Lights", 9 | "lat": 40.312997, 10 | "long": -88.323607 11 | }, 12 | { 13 | "place_name": "Lights", 14 | "lat": 40.312961, 15 | "long": -88.305792 16 | }, 17 | { 18 | "place_name": "Lights", 19 | "lat": 40.309914, 20 | "long": -88.304596 21 | } 22 | ] 23 | -------------------------------------------------------------------------------- /c2tc-mobile/assets/data/police_locations.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "place_name": "Champaign Police Department", 4 | "lat": 40.116739, 5 | "long": -88.239269 6 | }, 7 | { 8 | "place_name": "University of Illinois Police Department", 9 | "lat": 40.112997, 10 | "long": -88.223602 11 | }, 12 | { 13 | "place_name": "Champaign County Sheriffs Office", 14 | "lat": 40.112961, 15 | "long": -88.205799 16 | }, 17 | { 18 | "place_name": "Urbana Police Department", 19 | "lat": 40.109914, 20 | "long": -88.204599 21 | } 22 | ] 23 | -------------------------------------------------------------------------------- /c2tc-mobile/assets/fonts/SpaceMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hack4impact-uiuc/safe-maps/a64698ac47c35b244d32dc86019327b35d69e9ed/c2tc-mobile/assets/fonts/SpaceMono-Regular.ttf -------------------------------------------------------------------------------- /c2tc-mobile/assets/images/back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hack4impact-uiuc/safe-maps/a64698ac47c35b244d32dc86019327b35d69e9ed/c2tc-mobile/assets/images/back.png -------------------------------------------------------------------------------- /c2tc-mobile/assets/images/bg-day.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hack4impact-uiuc/safe-maps/a64698ac47c35b244d32dc86019327b35d69e9ed/c2tc-mobile/assets/images/bg-day.png -------------------------------------------------------------------------------- /c2tc-mobile/assets/images/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hack4impact-uiuc/safe-maps/a64698ac47c35b244d32dc86019327b35d69e9ed/c2tc-mobile/assets/images/bg.png -------------------------------------------------------------------------------- /c2tc-mobile/assets/images/bus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hack4impact-uiuc/safe-maps/a64698ac47c35b244d32dc86019327b35d69e9ed/c2tc-mobile/assets/images/bus.png -------------------------------------------------------------------------------- /c2tc-mobile/assets/images/business.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hack4impact-uiuc/safe-maps/a64698ac47c35b244d32dc86019327b35d69e9ed/c2tc-mobile/assets/images/business.png -------------------------------------------------------------------------------- /c2tc-mobile/assets/images/c2tc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hack4impact-uiuc/safe-maps/a64698ac47c35b244d32dc86019327b35d69e9ed/c2tc-mobile/assets/images/c2tc.png -------------------------------------------------------------------------------- /c2tc-mobile/assets/images/crime.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hack4impact-uiuc/safe-maps/a64698ac47c35b244d32dc86019327b35d69e9ed/c2tc-mobile/assets/images/crime.png -------------------------------------------------------------------------------- /c2tc-mobile/assets/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hack4impact-uiuc/safe-maps/a64698ac47c35b244d32dc86019327b35d69e9ed/c2tc-mobile/assets/images/icon.png -------------------------------------------------------------------------------- /c2tc-mobile/assets/images/phone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hack4impact-uiuc/safe-maps/a64698ac47c35b244d32dc86019327b35d69e9ed/c2tc-mobile/assets/images/phone.png -------------------------------------------------------------------------------- /c2tc-mobile/assets/images/police.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hack4impact-uiuc/safe-maps/a64698ac47c35b244d32dc86019327b35d69e9ed/c2tc-mobile/assets/images/police.png -------------------------------------------------------------------------------- /c2tc-mobile/assets/images/robot-dev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hack4impact-uiuc/safe-maps/a64698ac47c35b244d32dc86019327b35d69e9ed/c2tc-mobile/assets/images/robot-dev.png -------------------------------------------------------------------------------- /c2tc-mobile/assets/images/robot-prod.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hack4impact-uiuc/safe-maps/a64698ac47c35b244d32dc86019327b35d69e9ed/c2tc-mobile/assets/images/robot-prod.png -------------------------------------------------------------------------------- /c2tc-mobile/assets/images/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hack4impact-uiuc/safe-maps/a64698ac47c35b244d32dc86019327b35d69e9ed/c2tc-mobile/assets/images/splash.png -------------------------------------------------------------------------------- /c2tc-mobile/assets/images/streetlights.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hack4impact-uiuc/safe-maps/a64698ac47c35b244d32dc86019327b35d69e9ed/c2tc-mobile/assets/images/streetlights.png -------------------------------------------------------------------------------- /c2tc-mobile/assets/images/welcome/0-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hack4impact-uiuc/safe-maps/a64698ac47c35b244d32dc86019327b35d69e9ed/c2tc-mobile/assets/images/welcome/0-1.png -------------------------------------------------------------------------------- /c2tc-mobile/assets/images/welcome/0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hack4impact-uiuc/safe-maps/a64698ac47c35b244d32dc86019327b35d69e9ed/c2tc-mobile/assets/images/welcome/0.png -------------------------------------------------------------------------------- /c2tc-mobile/assets/images/welcome/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hack4impact-uiuc/safe-maps/a64698ac47c35b244d32dc86019327b35d69e9ed/c2tc-mobile/assets/images/welcome/1.png -------------------------------------------------------------------------------- /c2tc-mobile/assets/images/welcome/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hack4impact-uiuc/safe-maps/a64698ac47c35b244d32dc86019327b35d69e9ed/c2tc-mobile/assets/images/welcome/2.png -------------------------------------------------------------------------------- /c2tc-mobile/assets/images/welcome/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hack4impact-uiuc/safe-maps/a64698ac47c35b244d32dc86019327b35d69e9ed/c2tc-mobile/assets/images/welcome/3.png -------------------------------------------------------------------------------- /c2tc-mobile/assets/images/welcome/4-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hack4impact-uiuc/safe-maps/a64698ac47c35b244d32dc86019327b35d69e9ed/c2tc-mobile/assets/images/welcome/4-1.png -------------------------------------------------------------------------------- /c2tc-mobile/assets/images/welcome/4-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hack4impact-uiuc/safe-maps/a64698ac47c35b244d32dc86019327b35d69e9ed/c2tc-mobile/assets/images/welcome/4-2.png -------------------------------------------------------------------------------- /c2tc-mobile/assets/images/welcome/4-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hack4impact-uiuc/safe-maps/a64698ac47c35b244d32dc86019327b35d69e9ed/c2tc-mobile/assets/images/welcome/4-3.png -------------------------------------------------------------------------------- /c2tc-mobile/assets/images/welcome/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hack4impact-uiuc/safe-maps/a64698ac47c35b244d32dc86019327b35d69e9ed/c2tc-mobile/assets/images/welcome/5.png -------------------------------------------------------------------------------- /c2tc-mobile/assets/images/welcome/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hack4impact-uiuc/safe-maps/a64698ac47c35b244d32dc86019327b35d69e9ed/c2tc-mobile/assets/images/welcome/6.png -------------------------------------------------------------------------------- /c2tc-mobile/components/Geocoding.js: -------------------------------------------------------------------------------- 1 | export async function addressToLatLong(address) { 2 | api_latlong = 3 | "http://www.mapquestapi.com/geocoding/v1/address?key=6lJsB5kKwRsYYkkjhk4AXkPFn2DhGCiy&maxResults=5&outFormat=json&location=" + 4 | address + 5 | "&boundingBox=40.121581,-88.253981,40.098315,-88.205082"; 6 | 7 | const response = await fetch(api_latlong, {}); 8 | const responseJson = await response.json(); 9 | 10 | lat = responseJson["results"][0]["locations"][0]["latLng"]["lat"]; 11 | lng = responseJson["results"][0]["locations"][0]["latLng"]["lng"]; 12 | return [lat, lng]; 13 | } 14 | 15 | export async function latlongToAddress(lat, long) { 16 | api_address = 17 | "http://www.mapquestapi.com/geocoding/v1/reverse?key=6lJsB5kKwRsYYkkjhk4AXkPFn2DhGCiy&location=" + 18 | lat + 19 | "," + 20 | long + 21 | "&includeRoadMetadata=false&includeNearestIntersection=false" + 22 | "&boundingBox=40.121581,-88.253981,40.098315,-88.205082"; 23 | 24 | const response = await fetch(api_address, {}); 25 | const responseJson = await response.json(); 26 | 27 | street_address = responseJson["results"][0]["locations"][0]["street"]; 28 | city = responseJson["results"][0]["locations"][0]["adminArea5"]; 29 | state = responseJson["results"][0]["locations"][0]["adminArea3"]; 30 | country = responseJson["results"][0]["locations"][0]["adminArea1"]; 31 | postal_code = responseJson["results"][0]["locations"][0][ 32 | "postalCode" 33 | ].substring(0, 5); 34 | full_address = 35 | street_address + 36 | " " + 37 | city + 38 | " " + 39 | state + 40 | " " + 41 | country + 42 | " " + 43 | postal_code; 44 | return full_address; 45 | } 46 | -------------------------------------------------------------------------------- /c2tc-mobile/components/Loader.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { StyleSheet, View, Modal, ActivityIndicator } from "react-native"; 3 | 4 | const Loader = props => { 5 | const { loading, ...attributes } = props; 6 | 7 | return ( 8 | { 11 | Alert.alert("Modal has been closed."); 12 | }} 13 | animationType={"none"} 14 | visible={loading} 15 | > 16 | 17 | 18 | 19 | 20 | 21 | 22 | ); 23 | }; 24 | 25 | const styles = StyleSheet.create({ 26 | modalBackground: { 27 | flex: 1, 28 | alignItems: "center", 29 | flexDirection: "column", 30 | justifyContent: "space-around", 31 | backgroundColor: "white" 32 | }, 33 | activityIndicatorWrapper: { 34 | backgroundColor: "black", 35 | height: 100, 36 | width: 100, 37 | borderRadius: 10, 38 | display: "flex", 39 | alignItems: "center", 40 | justifyContent: "space-around" 41 | } 42 | }); 43 | 44 | export default Loader; 45 | -------------------------------------------------------------------------------- /c2tc-mobile/components/MapRendering.js: -------------------------------------------------------------------------------- 1 | let id = 0; 2 | 3 | const icons = { 4 | busStop: require("../assets/images/bus.png"), 5 | crime: require("../assets/images/crime.png"), 6 | business: require("../assets/images/business.png"), 7 | emergency: require("../assets/images/phone.png"), 8 | policeStations: require("../assets/images/police.png"), 9 | streetLights: require("../assets/images/streetlights.png") 10 | }; 11 | 12 | export default (renderLayerMarkers = ( 13 | layer, 14 | data, 15 | markerColor, 16 | layerData, 17 | colorData, 18 | markers, 19 | mapRegion 20 | ) => { 21 | data = layerData[layer]; 22 | let list = markers === undefined ? [] : markers; 23 | for (let i = 0; i < data.length; i++) { 24 | if (markerColor === colorData.busStop) { 25 | buses = ""; 26 | for (let key in data[i].routes) { 27 | if (key === data[i].routes[data[i].routes.length - 1]) { 28 | buses = buses + key + "."; 29 | } else { 30 | buses = buses + key + ", "; 31 | } 32 | } 33 | title = data[i].stop_name; 34 | description = "Buses come to this stop: " + buses; 35 | } else if (markerColor === colorData.emergency) { 36 | distance = getDistance( 37 | data[i].latitude, 38 | data[i].longitude, 39 | mapRegion.latitude, 40 | mapRegion.longitude 41 | ); 42 | title = distance + " miles away"; 43 | description = "Emergency Phone #" + data[i].emergencyPhone_id; 44 | } else if (markerColor === colorData.crime) { 45 | distance = getDistance( 46 | data[i].latitude, 47 | data[i].longitude, 48 | mapRegion.latitude, 49 | mapRegion.longitude 50 | ); 51 | title = data[i].incident_type_primary; 52 | description = 53 | data[i].incident_datetime + 54 | "\n" + 55 | distance + 56 | " miles away \n" + 57 | data[i].incident_description; 58 | } else if (markerColor === colorData.business) { 59 | address = ""; 60 | for (let key in data[i].location) { 61 | if ( 62 | data[i].location[key] == data[i].location[data[i].location.length - 1] 63 | ) { 64 | address = address + data[i].location[key] + "."; 65 | } else { 66 | address = address + data[i].location[key] + ", "; 67 | } 68 | } 69 | title = data[i].name; 70 | description = "Address: " + address; 71 | } else if (markerColor === colorData.policeStations) { 72 | title = data[i].name; 73 | description = data[i].name + " is located here"; 74 | } else if (markerColor === colorData.streetLights) { 75 | title = "Streetlight"; 76 | description = ""; 77 | } else { 78 | title = "Title"; 79 | description = "Description"; 80 | } 81 | list.push({ 82 | coordinate: { 83 | latitude: data[i].latitude, 84 | longitude: data[i].longitude 85 | }, 86 | key: id++, 87 | color: markerColor, 88 | image: icons[layer], 89 | title: title, 90 | description: description 91 | }); 92 | } 93 | return list; 94 | }); 95 | 96 | function getDistance(lat1, lon1, lat2, lon2) { 97 | let earthRadius = 6371; 98 | let deltaLat = toRad(lat2 - lat1); 99 | let deltaLong = toRad(lon2 - lon1); 100 | let currentLat = toRad(lat1); 101 | let finalLat = toRad(lat2); 102 | 103 | let pythag = 104 | Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2) + 105 | Math.sin(deltaLong / 2) * 106 | Math.sin(deltaLong / 2) * 107 | Math.cos(currentLat) * 108 | Math.cos(finalLat); 109 | let deriv = 2 * Math.atan2(Math.sqrt(pythag), Math.sqrt(1 - pythag)); 110 | let mult = earthRadius * deriv; 111 | kmToMiles = mult / 1.6; 112 | return Math.round(kmToMiles * 100) / 100; 113 | } 114 | function toRad(value) { 115 | return (value * Math.PI) / 180; 116 | } 117 | -------------------------------------------------------------------------------- /c2tc-mobile/components/NavigationComponents/ButtonInterface.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { FontAwesome } from "@expo/vector-icons"; 3 | import { 4 | Text, 5 | View, 6 | TouchableOpacity, 7 | StyleSheet, 8 | Dimensions 9 | } from "react-native"; 10 | import { connect } from "react-redux"; 11 | import { bindActionCreators } from "redux"; 12 | import { updateRenderData, updateMarkers } from "../../Redux"; 13 | import renderLayerMarkers from "../MapRendering"; 14 | 15 | const mapDispatchToProps = dispatch => { 16 | return bindActionCreators( 17 | { 18 | updateMarkers, 19 | updateRenderData 20 | }, 21 | dispatch 22 | ); 23 | }; 24 | 25 | const mapStateToProps = state => { 26 | return { 27 | renderData: state.renderData, 28 | layerData: state.layerData, 29 | colorData: state.colorData, 30 | markers: state.markers, 31 | mapRegion: state.mapRegion 32 | }; 33 | }; 34 | 35 | class ButtonInterface extends Component { 36 | constructor(props) { 37 | super(props); 38 | } 39 | 40 | _onPressToggleLayers = layer => { 41 | if (this.props.renderData[layer]) { 42 | this.props.updateRenderData(layer, false); 43 | let markers = this.props.markers.filter( 44 | marker => marker["color"] !== this.props.colorData[layer] 45 | ); 46 | this.props.updateMarkers(markers); 47 | } else { 48 | this.renderMarkers( 49 | layer, 50 | this.props.layerData[layer], 51 | this.props.colorData[layer] 52 | ); 53 | this.props.updateRenderData(layer, true); 54 | } 55 | }; 56 | 57 | async renderMarkers(layer, data, markerColor) { 58 | const { layerData, colorData, markers, mapRegion } = this.props; 59 | let markerList = await renderLayerMarkers( 60 | layer, 61 | data, 62 | markerColor, 63 | layerData, 64 | colorData, 65 | markers, 66 | mapRegion 67 | ); 68 | this.props.updateMarkers(markerList); 69 | } 70 | 71 | updateLayer = () => { 72 | this._onPressToggleLayers(this.props.type); 73 | }; 74 | 75 | render() { 76 | var isSelected = this.props.renderData[this.props.type]; 77 | return ( 78 | 79 | 87 | 92 | 95 | {this.props.name} 96 | 97 | 98 | 99 | ); 100 | } 101 | } 102 | 103 | export default connect( 104 | mapStateToProps, 105 | mapDispatchToProps 106 | )(ButtonInterface); 107 | 108 | const styles = StyleSheet.create({ 109 | selectedButton: { 110 | borderRadius: 9, 111 | alignContent: "center", 112 | flexDirection: "row", 113 | borderWidth: 1.5, 114 | borderColor: "rgba(142,142,147,0)", 115 | flexWrap: "wrap", 116 | padding: 10 117 | }, 118 | unselectedButton: { 119 | flexDirection: "row", 120 | alignContent: "center", 121 | flexWrap: "wrap", 122 | borderColor: "rgba(142,142,147,0.70)", 123 | borderWidth: 1.5, 124 | backgroundColor: "white", 125 | borderRadius: 9, 126 | padding: 10 127 | }, 128 | view: { 129 | width: Dimensions.get("window").width / 2 - 10, 130 | height: 60, 131 | padding: 3 132 | }, 133 | icon: { 134 | position: "relative", 135 | width: 35 136 | }, 137 | selectedText: { 138 | paddingLeft: 10, 139 | textAlign: "left", 140 | fontWeight: "400", 141 | width: Dimensions.get("window").width / 2 - 75, 142 | flexWrap: "wrap", 143 | height: 50, 144 | fontSize: 18, 145 | color: "white" 146 | }, 147 | unselectedText: { 148 | paddingLeft: 10, 149 | textAlign: "left", 150 | fontWeight: "400", 151 | flexWrap: "wrap", 152 | width: Dimensions.get("window").width / 2 - 75, 153 | height: 50, 154 | fontSize: 18, 155 | color: "#8e8e93" 156 | } 157 | }); 158 | -------------------------------------------------------------------------------- /c2tc-mobile/components/NavigationComponents/CurrentLocationButton.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { StyleSheet, TouchableOpacity } from "react-native"; 3 | import { FontAwesome } from "@expo/vector-icons"; 4 | 5 | export default class CurrentLocationButton extends Component { 6 | constructor(props) { 7 | super(props); 8 | } 9 | 10 | render() { 11 | return ( 12 | 16 | 17 | 18 | ); 19 | } 20 | } 21 | 22 | const styles = StyleSheet.create({ 23 | button: { 24 | flexWrap: "wrap", 25 | borderColor: "rgba(142,142,147,0.70)", 26 | borderWidth: 1.5, 27 | backgroundColor: "white", 28 | borderRadius: 25, 29 | padding: 5, 30 | margin: 10 31 | } 32 | }); 33 | -------------------------------------------------------------------------------- /c2tc-mobile/components/NavigationComponents/Navigation.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import Panel from "./Panel"; 3 | import TabBar from "./Tabs"; 4 | 5 | export default class Navigation extends Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = { 9 | page: "filter" 10 | }; 11 | } 12 | 13 | render() { 14 | return ( 15 | 16 | 17 | 18 | 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /c2tc-mobile/components/NavigationComponents/Panel.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { StyleSheet, Text, View, Animated, Dimensions } from "react-native"; 3 | import SlidingUpPanel from "rn-sliding-up-panel"; 4 | import ButtonInterface from "./ButtonInterface"; 5 | import PhoneButton from "./PhoneButtonInterface"; 6 | import Colors from "../../constants/Colors"; 7 | import { connect } from "react-redux"; 8 | 9 | const mapStateToProps = state => { 10 | return { 11 | page: state.page, 12 | markerClicked: state.markerClicked, 13 | markerTitle: state.markerTitle, 14 | markerDescription: state.markerDescription 15 | }; 16 | }; 17 | 18 | const { height, width } = Dimensions.get("window"); 19 | const draggableRange = { 20 | top: height / 1.75, 21 | bottom: 160 22 | }; 23 | 24 | class Panel extends Component { 25 | draggedValue = new Animated.Value(-120); 26 | 27 | constructor(props) { 28 | super(props); 29 | } 30 | 31 | setRef = reference => { 32 | this._panel = reference; 33 | }; 34 | 35 | setDrag = velocity => { 36 | this.draggedValue.setValue(velocity); 37 | }; 38 | 39 | render() { 40 | let filter = this.props.page === "filter"; 41 | return ( 42 | 50 | 51 | {this.props.markerClicked ? ( 52 | 53 | 54 | {this.props.markerTitle} 55 | {this.props.markerDescription} 56 | 57 | 58 | ) : ( 59 | [ 60 | filter ? ( 61 | 62 | Filters 63 | 64 | 71 | 78 | 79 | 80 | 87 | 94 | 95 | 96 | 103 | 110 | 111 | 112 | ) : ( 113 | 114 | Contacts 115 | 116 | 122 | 129 | 130 | 131 | ) 132 | ] 133 | )} 134 | 135 | 136 | ); 137 | } 138 | } 139 | 140 | export default connect(mapStateToProps)(Panel); 141 | 142 | const styles = StyleSheet.create({ 143 | panel: { 144 | shadowColor: "black", 145 | shadowOpacity: 0.1, 146 | shadowRadius: 2, 147 | flex: 1, 148 | flexDirection: "row", 149 | backgroundColor: "white", 150 | flexDirection: "column", 151 | opacity: 1, 152 | borderRadius: 10, 153 | flexWrap: "wrap" 154 | }, 155 | title: { 156 | height: 20, 157 | width: width, 158 | flex: 1, 159 | justifyContent: "center" 160 | }, 161 | subtitle: { 162 | fontSize: 15, 163 | color: "#000000", 164 | letterSpacing: 0.01, 165 | lineHeight: 20, 166 | textAlign: "left" 167 | }, 168 | row: { 169 | flexDirection: "row", 170 | marginBottom: 20, 171 | justifyContent: "center", 172 | alignItems: "center" 173 | }, 174 | filter: { 175 | borderRadius: 10, 176 | width: width, 177 | fontWeight: "700", 178 | fontSize: 25, 179 | padding: 15, 180 | color: "black", 181 | textAlign: "left", 182 | position: "relative" 183 | }, 184 | subtitle: { 185 | borderRadius: 10, 186 | width: width, 187 | fontWeight: "700", 188 | fontSize: 15, 189 | padding: 15, 190 | color: "black", 191 | textAlign: "left", 192 | position: "relative" 193 | }, 194 | text: { 195 | height: 5, 196 | flex: 1, 197 | justifyContent: "center", 198 | borderRadius: 10, 199 | width: width, 200 | fontWeight: "300", 201 | fontSize: 10, 202 | padding: 15, 203 | color: "black", 204 | textAlign: "left", 205 | position: "relative" 206 | } 207 | }); 208 | -------------------------------------------------------------------------------- /c2tc-mobile/components/NavigationComponents/PhoneButtonInterface.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { 3 | View, 4 | Text, 5 | TouchableOpacity, 6 | StyleSheet, 7 | Dimensions, 8 | Linking 9 | } from "react-native"; 10 | import { FontAwesome } from "@expo/vector-icons"; 11 | import call from "react-native-phone-call"; 12 | 13 | export default class PhoneButton extends Component { 14 | constructor(props) { 15 | super(props); 16 | } 17 | 18 | _onPress = () => { 19 | if (typeof this.props.url !== "undefined") { 20 | Linking.openURL( 21 | "https://itunes.apple.com/us/app/mtd-connect/id1445758206" 22 | ); 23 | } else { 24 | const args = { 25 | number: this.props.number, 26 | prompt: false 27 | }; 28 | call(args).catch(console.error); 29 | } 30 | }; 31 | 32 | render() { 33 | return ( 34 | 35 | 36 | 37 | 38 | {this.props.name} 39 | 40 | ); 41 | } 42 | } 43 | 44 | const styles = StyleSheet.create({ 45 | view: { 46 | width: Dimensions.get("window").width / 2 - 50, 47 | alignItems: "center" 48 | }, 49 | button: { 50 | alignItems: "center", 51 | backgroundColor: "#e5e5ea", 52 | borderRadius: 900, 53 | paddingTop: 17, 54 | width: 60, 55 | height: 60, 56 | margin: 13 57 | }, 58 | text: { 59 | textAlign: "center", 60 | fontWeight: "600", 61 | width: 150, 62 | height: 32, 63 | fontSize: 17, 64 | color: "black" 65 | } 66 | }); 67 | -------------------------------------------------------------------------------- /c2tc-mobile/components/NavigationComponents/Tabs.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import Tabs from "react-native-tabs"; 3 | import { StyleSheet, Text } from "react-native"; 4 | import { FontAwesome } from "@expo/vector-icons"; 5 | import Colors from "../../constants/Colors"; 6 | import { connect } from "react-redux"; 7 | import { bindActionCreators } from "redux"; 8 | import { updateDetailView, updatePage } from "../../Redux.js"; 9 | 10 | const mapDispatchToProps = dispatch => { 11 | return bindActionCreators( 12 | { 13 | updatePage, 14 | updateDetailView 15 | }, 16 | dispatch 17 | ); 18 | }; 19 | const mapStateToProps = state => { 20 | return { page: state.page }; 21 | }; 22 | 23 | class TabBar extends Component { 24 | constructor(props) { 25 | super(props); 26 | } 27 | 28 | _onSelect = tab => { 29 | this.props.updatePage(tab.props.name); 30 | this.props.updateDetailView(false, "", ""); 31 | }; 32 | 33 | render() { 34 | let filter = this.props.page; 35 | return ( 36 | this._onSelect(tab)} 41 | > 42 | 43 | 50 | 51 | 52 | 59 | 60 | 61 | 68 | 69 | 70 | ); 71 | } 72 | } 73 | export default connect( 74 | mapStateToProps, 75 | mapDispatchToProps 76 | )(TabBar); 77 | 78 | const styles = StyleSheet.create({ 79 | tabbg: { 80 | borderTopWidth: 0.5, 81 | borderTopColor: "rgba(142,142,147,0.70)", 82 | shadowColor: "black", 83 | shadowOpacity: 0.15, 84 | shadowRadius: 15, 85 | backgroundColor: "white", 86 | opacity: 1, 87 | padding: 38 88 | }, 89 | tab: { 90 | borderTopWidth: 0, 91 | borderTopColor: "#c7c7cc" 92 | } 93 | }); 94 | -------------------------------------------------------------------------------- /c2tc-mobile/components/StyledText.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Text } from "react-native"; 3 | 4 | export class MonoText extends React.Component { 5 | render() { 6 | return ( 7 | 11 | ); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /c2tc-mobile/components/TabBarIcon.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Icon } from "expo"; 3 | 4 | import Colors from "../constants/Colors"; 5 | 6 | export default class TabBarIcon extends React.Component { 7 | render() { 8 | return ( 9 | 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /c2tc-mobile/components/Tag.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Text, View, TouchableOpacity, StyleSheet } from "react-native"; 3 | import ButtonInterface from "../components/NavigationComponents/ButtonInterface"; 4 | import Colors from "../constants/Colors"; 5 | class Tag extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | } 9 | 10 | render() { 11 | return ( 12 | 15 | {this.props.category.toUpperCase()} 16 | 17 | ); 18 | } 19 | } 20 | 21 | const styles = StyleSheet.create({ 22 | tag: { 23 | minWidth: 50, 24 | maxWidth: 100, 25 | borderRadius: 5, 26 | padding: 5, 27 | alignItems: "center", 28 | marginRight: 10 29 | }, 30 | text: { 31 | color: "white", 32 | fontSize: 13 33 | } 34 | }); 35 | 36 | export default Tag; 37 | -------------------------------------------------------------------------------- /c2tc-mobile/components/__tests__/StyledText-test.js: -------------------------------------------------------------------------------- 1 | import "react-native"; 2 | import React from "react"; 3 | import { MonoText } from "../StyledText"; 4 | import renderer from "react-test-renderer"; 5 | 6 | it("renders correctly", () => { 7 | const tree = renderer.create(Snapshot test!).toJSON(); 8 | 9 | expect(tree).toMatchSnapshot(); 10 | }); 11 | -------------------------------------------------------------------------------- /c2tc-mobile/constants/Colors.js: -------------------------------------------------------------------------------- 1 | const base = "#242134"; 2 | const primary = "#428bca"; 3 | const success = "#5cb85c"; 4 | const info = "#5bc0de"; 5 | const warning = "#f0ad4e"; 6 | const danger = "#d9534f"; 7 | const tabUnselected = "#C7C7CB"; 8 | const tabSelected = "#8F44AC"; 9 | const busStop = "#F39C12"; 10 | const crime = "#C0382A"; 11 | const business = "#1A5E20"; 12 | const emergency = "#3498DB"; 13 | const police = "#925CB1"; 14 | const streetlights = "#ffc107"; 15 | const health = "#306919"; 16 | const transportation = "#88054E"; 17 | const financial = "#0B65C1"; 18 | 19 | export default { 20 | base, 21 | primary, 22 | success, 23 | info, 24 | warning, 25 | danger, 26 | tabSelected, 27 | tabUnselected, 28 | business, 29 | busStop, 30 | crime, 31 | emergency, 32 | police, 33 | streetlights, 34 | health, 35 | transportation, 36 | financial 37 | }; 38 | -------------------------------------------------------------------------------- /c2tc-mobile/constants/Layout.js: -------------------------------------------------------------------------------- 1 | import { Dimensions } from "react-native"; 2 | 3 | const width = Dimensions.get("window").width; 4 | const height = Dimensions.get("window").height; 5 | 6 | export default { 7 | window: { 8 | width, 9 | height 10 | }, 11 | isSmallDevice: width < 375 12 | }; 13 | -------------------------------------------------------------------------------- /c2tc-mobile/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cut-to-the-case", 3 | "version": "0.0.6", 4 | "description": "A mobile application that makes the students of UIUC feel safer on campus.", 5 | "main": "node_modules/expo/AppEntry.js", 6 | "private": true, 7 | "author": "Hack4Impact UIUC", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/hack4impact-uiuc/c2tc-spring-2019.git" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/hack4impact-uiuc/c2tc-spring-2019/issues" 14 | }, 15 | "homepage": "https://github.com/hack4impact-uiuc/c2tc-spring-2019#readme", 16 | "scripts": { 17 | "start": "expo start", 18 | "format": "prettier --write './**/*.{js,jsx,json,css}'", 19 | "format:check": "prettier --list-different \"./**/*.js\"", 20 | "android": "expo start --android", 21 | "ios": "expo start --ios", 22 | "eject": "expo eject" 23 | }, 24 | "dependencies": { 25 | "@expo/samples": "2.1.1", 26 | "@material-ui/core": "^3.9.2", 27 | "babel-plugin-module-resolver": "^3.2.0", 28 | "csvtojson": "^2.0.8", 29 | "expo": "^32.0.0", 30 | "fs": "0.0.1-security", 31 | "react": "16.5.0", 32 | "react-native": "https://github.com/expo/react-native/archive/sdk-32.0.0.tar.gz", 33 | "react-native-awesome-alerts": "^1.2.0", 34 | "react-native-geocoding": "^0.3.0", 35 | "react-native-google-places-autocomplete": "^1.3.9", 36 | "react-native-maps": "^0.21.0", 37 | "react-native-paper": "^2.12.0", 38 | "react-native-phone-call": "^1.0.9", 39 | "react-native-sliding-up-down-panels": "^1.0.0", 40 | "react-native-tabs": "^1.0.9", 41 | "react-navigation": "^2.18.3", 42 | "react-redux": "^6.0.1", 43 | "redux": "^4.0.1", 44 | "rn-sliding-up-panel": "^1.3.1", 45 | "toggle-switch-react-native": "^2.0.2" 46 | }, 47 | "devDependencies": { 48 | "jest-expo": "^32.0.0", 49 | "prettier": "^1.16.4" 50 | }, 51 | "peerDependencies": { 52 | "@babel/core": "^7.0.0-0", 53 | "expo-constants-interface": "~1.0.2", 54 | "expo-file-system": "~1.1.0", 55 | "expo-font-interface": "~1.0.0", 56 | "expo-permissions-interface": "~1.1.0", 57 | "prop-types": "^15.0.0", 58 | "react-native-vector-icons": "^6.4.2" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /c2tc-mobile/screens/AlertScreen.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | StyleSheet, 4 | Alert, 5 | View, 6 | Text, 7 | Dimensions, 8 | TouchableOpacity, 9 | AsyncStorage 10 | } from "react-native"; 11 | 12 | import { FontAwesome } from "@expo/vector-icons"; 13 | 14 | import { Appbar, Button } from "react-native-paper"; 15 | import API from "../components/API"; 16 | 17 | const { width, height } = Dimensions.get("window"); 18 | 19 | export default class AlertScreen extends React.Component { 20 | constructor(props) { 21 | super(props); 22 | this.state = { 23 | token: null 24 | }; 25 | } 26 | 27 | async componentDidMount() { 28 | let token = await AsyncStorage.getItem("token"); 29 | this.setState({ 30 | token: token 31 | }); 32 | } 33 | 34 | handleBackPress = e => { 35 | this.props.navigation.navigate("TipOverview"); 36 | }; 37 | 38 | render() { 39 | return ( 40 | 41 | 42 | this.props.navigation.navigate("TipOverview")} 44 | style={styles.backButton} 45 | > 46 | 47 | Tip 48 | Overview 49 | 50 | 51 | 52 | 53 | 54 | 55 | Sorry, in order to access this feature you must login! 56 | 57 | 58 | {!this.state.token && ( 59 | 60 | 61 | 68 | 69 | 76 | 83 | 84 | 85 | )} 86 | {this.state.token ? ( 87 | 94 | 101 | 102 | ) : null} 103 | 104 | 105 | ); 106 | } 107 | } 108 | 109 | const styles = StyleSheet.create({ 110 | alert: { 111 | backgroundColor: "white", 112 | height: Dimensions.get("window").height 113 | }, 114 | reason: { 115 | marginHorizontal: 22, 116 | marginBottom: 20, 117 | flexDirection: "row", 118 | justifyContent: "center" 119 | }, 120 | reason_text: { 121 | fontSize: 20, 122 | fontWeight: "500" 123 | }, 124 | button: { 125 | alignItems: "center", 126 | backgroundColor: "#8E44AD", 127 | borderRadius: 7, 128 | width: "50%", 129 | fontSize: 17, 130 | paddingVertical: 5 131 | }, 132 | navBar: { 133 | paddingTop: 37, 134 | flexDirection: "row", 135 | justifyContent: "flex-start", 136 | width: Dimensions.get("window").width, 137 | backgroundColor: "#9041AF", 138 | paddingBottom: 15, 139 | marginBottom: 10 140 | }, 141 | backButton: { 142 | paddingLeft: 20, 143 | marginRight: Dimensions.get("window").width - 220 144 | }, 145 | headerText: { 146 | color: "white", 147 | fontSize: 20, 148 | fontWeight: "500" 149 | }, 150 | alert: { 151 | position: "absolute" 152 | } 153 | }); 154 | -------------------------------------------------------------------------------- /c2tc-mobile/screens/EditProfileScreen.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import API from "../components/API"; 3 | 4 | import { 5 | View, 6 | Text, 7 | StyleSheet, 8 | Image, 9 | Dimensions, 10 | TouchableOpacity, 11 | Button, 12 | Modal, 13 | AsyncStorage 14 | } from "react-native"; 15 | import { FontAwesome } from "@expo/vector-icons"; 16 | import { Appbar, TextInput } from "react-native-paper"; 17 | 18 | export default class EditProfileScreen extends React.Component { 19 | constructor(props) { 20 | super(props); 21 | this.state = { 22 | username: this.props.navigation.getParam("user", "no user").username, 23 | password: this.props.navigation.getParam("user", "no user").password, 24 | user: this.props.navigation.getParam("user", "no user"), 25 | url: this.props.navigation.getParam("user", "no user").pro_pic, 26 | modalVisible: false, 27 | token: "" 28 | }; 29 | } 30 | 31 | async onChangePassword(password) { 32 | this.setState({ password }); 33 | let data = { 34 | password 35 | }; 36 | await API.updateUser(this.state.token, data); 37 | let currentUser = this.state.user; 38 | currentUser.password = password; 39 | this.setState({ 40 | user: currentUser 41 | }); 42 | } 43 | async onChangeUserName(username) { 44 | this.setState({ username }); 45 | let data = { 46 | username 47 | }; 48 | await API.updateUser(this.state.token, data); 49 | let currentUser = this.state.user; 50 | currentUser.username = username; 51 | this.setState({ user: currentUser }); 52 | } 53 | 54 | async onChangePicture(picture) { 55 | this.setState({ 56 | url: picture 57 | }); 58 | } 59 | 60 | openModal = () => { 61 | this.setState({ modalVisible: true }); 62 | }; 63 | 64 | closeModal = async () => { 65 | let data = { 66 | pro_pic: this.state.url 67 | }; 68 | await API.updateUser(this.state.token, data); 69 | let currentUser = this.state.user; 70 | this.setState({ user: currentUser }); 71 | this.setState({ modalVisible: false }); 72 | }; 73 | 74 | async componentDidMount() { 75 | let token = await AsyncStorage.getItem("token"); 76 | this.setState({ token }); 77 | } 78 | 79 | render() { 80 | return ( 81 | 82 | 87 | 88 | 89 | Enter Image URL for New Profile Picture: 90 | 91 | this.onChangePicture(e)} 94 | value={this.state.url} 95 | /> 96 | 97 | Save 98 | 99 | 100 | 101 | 102 | 104 | this.props.navigation.navigate("Settings", { 105 | user: this.state.user 106 | }) 107 | } 108 | style={styles.backButton} 109 | > 110 | 111 | Save 112 | Changes 113 | 114 | 115 | 116 | 117 | 123 | 124 | Change Picture 125 | 126 | 127 | this.onChangeUserName(e)} 130 | value={this.state.username} 131 | /> 132 | this.onChangePassword(e)} 136 | value={this.state.password} 137 | /> 138 | 139 | ); 140 | } 141 | } 142 | 143 | const styles = StyleSheet.create({ 144 | editProfile: { 145 | backgroundColor: "white", 146 | height: Dimensions.get("window").height 147 | }, 148 | profile: { 149 | flexDirection: "row", 150 | padding: 22 151 | }, 152 | changePicture: { 153 | flexDirection: "row", 154 | paddingLeft: 20, 155 | paddingTop: 10, 156 | fontSize: 19, 157 | fontWeight: "400" 158 | }, 159 | textInput: { 160 | marginVertical: 10, 161 | borderRadius: 5, 162 | backgroundColor: "white", 163 | marginHorizontal: 22 164 | }, 165 | modalText: { 166 | fontSize: 20, 167 | fontWeight: "500", 168 | padding: 30, 169 | marginTop: 20, 170 | alignSelf: "center" 171 | }, 172 | modalSave: { 173 | fontSize: 20, 174 | fontWeight: "500", 175 | alignSelf: "center", 176 | color: "white", 177 | marginTop: 30, 178 | padding: 10, 179 | borderRadius: 10, 180 | backgroundColor: "#9042AF" 181 | }, 182 | navBar: { 183 | paddingTop: 37, 184 | flexDirection: "row", 185 | justifyContent: "flex-start", 186 | width: Dimensions.get("window").width, 187 | backgroundColor: "#9041AF", 188 | paddingBottom: 15, 189 | marginBottom: 10 190 | }, 191 | backButton: { 192 | paddingLeft: 20, 193 | marginRight: Dimensions.get("window").width - 220 194 | }, 195 | headerText: { 196 | color: "white", 197 | fontSize: 20, 198 | fontWeight: "500" 199 | } 200 | }); 201 | -------------------------------------------------------------------------------- /c2tc-mobile/screens/IntroScreen.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Animated, 4 | Dimensions, 5 | Text, 6 | ImageBackground, 7 | TouchableOpacity, 8 | StyleSheet 9 | } from "react-native"; 10 | 11 | class FadeInView extends React.Component { 12 | state = { 13 | fadeAnim: new Animated.Value(0) 14 | }; 15 | 16 | componentDidMount() { 17 | Animated.timing(this.state.fadeAnim, { 18 | toValue: 1, 19 | duration: 1500 20 | }).start(); 21 | } 22 | 23 | render() { 24 | let { fadeAnim } = this.state; 25 | 26 | return ( 27 | 33 | {this.props.children} 34 | 35 | ); 36 | } 37 | } 38 | 39 | export default class IntroScreen extends React.Component { 40 | render() { 41 | return ( 42 | 43 | 47 | 52 | 55 | this.props.navigation.navigate("Welcome", { backPage: "Intro" }) 56 | } 57 | > 58 | Get Started 59 | 60 | 61 | 62 | ); 63 | } 64 | } 65 | 66 | const styles = StyleSheet.create({ 67 | text: { 68 | color: "white", 69 | fontSize: 19, 70 | fontWeight: "600" 71 | }, 72 | view: { 73 | width: "100%", 74 | height: "100%" 75 | }, 76 | image: { 77 | alignSelf: "center", 78 | width: "90%", 79 | height: "70%" 80 | }, 81 | button: { 82 | alignItems: "center", 83 | backgroundColor: "#8E44AD", 84 | borderRadius: 7, 85 | width: Dimensions.get("window").width - 40, 86 | justifyContent: "flex-end", 87 | marginHorizontal: 20, 88 | paddingVertical: 17, 89 | marginTop: 120 90 | } 91 | }); 92 | -------------------------------------------------------------------------------- /c2tc-mobile/screens/MapScreen.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { Provider } from "react-redux"; 3 | import LiveLocation from "./LiveLocation"; 4 | import { store } from "../Redux"; 5 | 6 | export default class MapScreen extends Component { 7 | constructor(props) { 8 | super(props); 9 | } 10 | render() { 11 | return ( 12 | 13 | 14 | 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /c2tc-mobile/screens/NonRegisteredScreen.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { FontAwesome } from "@expo/vector-icons"; 3 | import { NavigationEvents } from "react-navigation"; 4 | 5 | import { 6 | StyleSheet, 7 | View, 8 | Dimensions, 9 | Text, 10 | TouchableOpacity, 11 | Image 12 | } from "react-native"; 13 | 14 | export default class NonRegisteredScreen extends React.Component { 15 | constructor(props) { 16 | super(props); 17 | this.state = {}; 18 | } 19 | 20 | render() { 21 | return ( 22 | 23 | 24 | 25 | this.props.navigation.navigate("TipOverview")} 27 | style={styles.backButton} 28 | > 29 | 30 | Tip 31 | Overview 32 | 33 | 34 | 35 | this.props.navigation.navigate("Notifications")} 37 | > 38 | 39 | Notifications 40 | 46 | 47 | 48 | 49 | this.props.navigation.navigate("Login")} 51 | > 52 | 53 | Login 54 | 60 | 61 | 62 | 63 | this.props.navigation.navigate("Registration")} 65 | > 66 | 67 | Register 68 | 74 | 75 | 76 | 77 | 78 | ); 79 | } 80 | } 81 | 82 | const styles = StyleSheet.create({ 83 | nonRegistered: { 84 | height: Dimensions.get("window").height, 85 | backgroundColor: "white" 86 | }, 87 | name: { 88 | flexDirection: "row", 89 | paddingLeft: 30, 90 | fontSize: 20 91 | }, 92 | list: { 93 | height: 45, 94 | flexDirection: "row", 95 | alignItems: "flex-start" 96 | }, 97 | text: { 98 | paddingHorizontal: 30, 99 | paddingTop: 10, 100 | fontSize: 15, 101 | width: Dimensions.get("window").width - 40 102 | }, 103 | divider: { 104 | borderBottomColor: "#D2D2D7", 105 | borderBottomWidth: 1 106 | }, 107 | profileArrow: { 108 | paddingTop: 20, 109 | paddingLeft: 100 110 | }, 111 | arrow: { 112 | paddingTop: 15 113 | }, 114 | navBar: { 115 | paddingTop: 37, 116 | flexDirection: "row", 117 | justifyContent: "flex-start", 118 | width: Dimensions.get("window").width, 119 | backgroundColor: "#9041AF", 120 | paddingBottom: 15, 121 | marginBottom: 10 122 | }, 123 | backButton: { 124 | paddingLeft: 20, 125 | marginRight: Dimensions.get("window").width - 220 126 | }, 127 | headerText: { 128 | color: "white", 129 | fontSize: 20, 130 | fontWeight: "500" 131 | }, 132 | arrow: { 133 | paddingTop: 15 134 | } 135 | }); 136 | -------------------------------------------------------------------------------- /c2tc-mobile/screens/PasswordResetScreen.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { 3 | StyleSheet, 4 | KeyboardAvoidingView, 5 | Dimensions, 6 | View, 7 | TouchableOpacity, 8 | Text, 9 | ScrollView, 10 | ActivityIndicator 11 | } from "react-native"; 12 | import { FontAwesome } from "@expo/vector-icons"; 13 | import { TextInput } from "react-native-paper"; 14 | import { AsyncStorage } from "react-native"; 15 | import API from "../components/API"; 16 | 17 | export default class Registration extends Component { 18 | constructor(props) { 19 | super(props); 20 | } 21 | 22 | state = { 23 | pin: "", 24 | pswd: "", 25 | repswd: "", 26 | errors: [], 27 | loading: false 28 | }; 29 | 30 | handlePasswordReset = async () => { 31 | let errors = this.validate(); 32 | if (errors.length === 0) { 33 | let { email } = this.props.navigation.state.params; 34 | this.setState({ loading: true }); 35 | const response = await API.passwordReset( 36 | email, 37 | this.state.pin, 38 | this.state.pswd 39 | ); 40 | this.setState({ loading: false }); 41 | if (!response.success) { 42 | errors = [response.message]; 43 | } else { 44 | this.props.navigation.goBack(null); 45 | } 46 | } 47 | this.setState({ errors }); 48 | }; 49 | 50 | validate() { 51 | let errors = []; 52 | 53 | if (this.state.pswd.length === 0) { 54 | errors.push("Password cannot be empty"); 55 | } 56 | 57 | if (this.state.repswd.length === 0) { 58 | errors.push("Re-enter password cannot be empty"); 59 | } 60 | 61 | if (this.state.pswd !== this.state.repswd) { 62 | errors.push("Passwords do not match!"); 63 | } 64 | 65 | return errors; 66 | } 67 | 68 | render() { 69 | const { errors } = this.state; 70 | 71 | return ( 72 | 77 | 78 | this.props.navigation.navigate("NonRegistered")} 80 | style={styles.backButton} 81 | > 82 | 83 | {" "} 84 | Settings 85 | 86 | 87 | 88 | 93 | 98 | Reset Password 99 | 100 | {errors.map(error => ( 101 | Error: {error} 102 | ))} 103 | 104 | Pin (check your email) 105 | this.setState({ pin })} 112 | /> 113 | Password 114 | this.setState({ pswd })} 122 | /> 123 | Re-enter Password 124 | this.setState({ repswd })} 132 | /> 133 | 137 | Reset 138 | 139 | 140 | 141 | ); 142 | } 143 | } 144 | 145 | const styles = StyleSheet.create({ 146 | container: { 147 | flex: 1 148 | }, 149 | navBar: { 150 | paddingTop: 37, 151 | flexDirection: "row", 152 | justifyContent: "flex-start", 153 | width: Dimensions.get("window").width, 154 | backgroundColor: "#9041AF", 155 | paddingBottom: 15, 156 | marginBottom: 10 157 | }, 158 | backButton: { 159 | paddingLeft: 20, 160 | marginRight: Dimensions.get("window").width - 220 161 | }, 162 | headerText: { 163 | color: "white", 164 | fontSize: 20, 165 | fontWeight: "500" 166 | }, 167 | arrow: { 168 | paddingTop: 15 169 | }, 170 | wrapper: { 171 | flex: 1, 172 | height: Dimensions.get("window").height, 173 | backgroundColor: "white" 174 | }, 175 | inputContainerStyle: { 176 | marginHorizontal: 20, 177 | marginTop: 0 178 | }, 179 | inputBodyContainerStyle: { 180 | paddingBottom: 100, 181 | marginHorizontal: 20, 182 | marginTop: 0 183 | }, 184 | full_header: { 185 | fontWeight: "500", 186 | fontSize: 35, 187 | paddingHorizontal: 20, 188 | paddingTop: 20, 189 | paddingBottom: 10, 190 | color: "black", 191 | textAlign: "left", 192 | position: "relative" 193 | }, 194 | header: { 195 | fontWeight: "500", 196 | fontSize: 25, 197 | paddingHorizontal: 20, 198 | paddingTop: 20, 199 | paddingBottom: 10, 200 | color: "black", 201 | textAlign: "left", 202 | position: "relative" 203 | }, 204 | button_text: { 205 | color: "white", 206 | fontSize: 19, 207 | fontWeight: "600" 208 | }, 209 | login_btn: { 210 | alignItems: "center", 211 | backgroundColor: "#8E44AD", 212 | borderRadius: 7, 213 | width: Dimensions.get("window").width - 40, 214 | paddingVertical: 17, 215 | marginTop: 30, 216 | marginLeft: 20 217 | } 218 | }); 219 | -------------------------------------------------------------------------------- /c2tc-mobile/screens/PendingTipScreen.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | StyleSheet, 4 | View, 5 | Text, 6 | TouchableOpacity, 7 | Dimensions, 8 | ScrollView 9 | } from "react-native"; 10 | import { FontAwesome } from "@expo/vector-icons"; 11 | import TipOverview from "../components/TipOverview"; 12 | 13 | class PendingTipScreen extends React.Component { 14 | constructor(props) { 15 | super(props); 16 | this.state = { 17 | tips: this.props.navigation.getParam("tips", []) 18 | }; 19 | } 20 | 21 | render() { 22 | return ( 23 | 24 | 25 | this.props.navigation.navigate("TipOverview")} 27 | style={styles.backButton} 28 | > 29 | 30 | {" "} 31 | TipOverview 32 | 33 | 34 | 35 | 36 | All Pending Tips 37 | {this.state.tips.map(tip => ( 38 | 45 | ))} 46 | 47 | 48 | ); 49 | } 50 | } 51 | 52 | const styles = StyleSheet.create({ 53 | content: { 54 | paddingHorizontal: 22 55 | }, 56 | row: { 57 | flexDirection: "row", 58 | marginBottom: 10, 59 | justifyContent: "center", 60 | alignItems: "center" 61 | }, 62 | pendingTips: { 63 | backgroundColor: "white" 64 | }, 65 | title: { 66 | color: "black", 67 | fontSize: 22, 68 | fontWeight: "500", 69 | alignSelf: "center", 70 | marginBottom: 10 71 | }, 72 | navBar: { 73 | paddingTop: 37, 74 | flexDirection: "row", 75 | justifyContent: "flex-start", 76 | width: Dimensions.get("window").width, 77 | backgroundColor: "#C03303", 78 | paddingBottom: 15, 79 | marginBottom: 20 80 | }, 81 | backButton: { 82 | paddingLeft: 20, 83 | marginRight: 20 84 | }, 85 | headerText: { 86 | color: "white", 87 | fontSize: 20, 88 | fontWeight: "500" 89 | } 90 | }); 91 | 92 | export default PendingTipScreen; 93 | -------------------------------------------------------------------------------- /c2tc-mobile/screens/RegistrationScreen.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { 3 | StyleSheet, 4 | KeyboardAvoidingView, 5 | Dimensions, 6 | View, 7 | TouchableOpacity, 8 | Text, 9 | ScrollView 10 | } from "react-native"; 11 | import { FontAwesome } from "@expo/vector-icons"; 12 | import { TextInput } from "react-native-paper"; 13 | import { AsyncStorage } from "react-native"; 14 | import API from "../components/API"; 15 | 16 | export default class Registration extends Component { 17 | constructor(props) { 18 | super(props); 19 | } 20 | 21 | state = { 22 | email: "", 23 | pswd: "", 24 | repswd: "", 25 | username: "", 26 | errors: [] 27 | }; 28 | 29 | handleRegistration = async () => { 30 | let errors = this.validate(); 31 | if (errors.length === 0) { 32 | const response = await API.registerNewUser( 33 | this.state.email, 34 | this.state.pswd, 35 | this.state.username 36 | ); 37 | if (!response.success) { 38 | errors = [response.message]; 39 | this.setState({ errors }); 40 | } else { 41 | await AsyncStorage.setItem( 42 | "token", 43 | JSON.stringify(response.result.token) 44 | ); 45 | await AsyncStorage.setItem("token", response.result.token); 46 | this.setState({ successfulSubmit: true }); 47 | this.props.navigation.navigate("Verify"); 48 | } 49 | } else { 50 | this.setState({ errors }); 51 | } 52 | }; 53 | 54 | validate() { 55 | let errors = []; 56 | 57 | if (this.state.email.length === 0) { 58 | errors.push("Email cannot be empty"); 59 | } 60 | 61 | if (this.state.pswd.length === 0) { 62 | errors.push("Password cannot be empty"); 63 | } 64 | 65 | if (this.state.repswd.length === 0) { 66 | errors.push("Re-enter password cannot be empty"); 67 | } 68 | 69 | if (this.state.pswd !== this.state.repswd) { 70 | errors.push("Passwords do not match!"); 71 | } 72 | 73 | let emailParts = this.state.email.split("@"); 74 | if (emailParts.length != 2) { 75 | errors.push("Invalid amount of @'s"); 76 | } else { 77 | if (emailParts[1] != "illinois.edu") { 78 | errors.push("Have to have an illinois email to register with the app!"); 79 | } else { 80 | this.state.username = emailParts[0]; 81 | } 82 | } 83 | 84 | return errors; 85 | } 86 | 87 | render() { 88 | const { errors } = this.state; 89 | 90 | return ( 91 | 96 | 97 | this.props.navigation.navigate("NonRegistered")} 99 | style={styles.backButton} 100 | > 101 | 102 | {" "} 103 | Settings 104 | 105 | 106 | 107 | 112 | Create Account 113 | 114 | {errors.map(error => ( 115 | Error: {error} 116 | ))} 117 | 118 | Email 119 | this.setState({ email })} 126 | /> 127 | Password 128 | this.setState({ pswd })} 136 | /> 137 | Re-enter Password 138 | this.setState({ repswd })} 146 | /> 147 | 151 | Register 152 | 153 | 154 | 155 | ); 156 | } 157 | } 158 | 159 | const styles = StyleSheet.create({ 160 | container: { 161 | flex: 1 162 | }, 163 | navBar: { 164 | paddingTop: 37, 165 | flexDirection: "row", 166 | justifyContent: "flex-start", 167 | width: Dimensions.get("window").width, 168 | backgroundColor: "#9041AF", 169 | paddingBottom: 15, 170 | marginBottom: 10 171 | }, 172 | backButton: { 173 | paddingLeft: 20, 174 | marginRight: Dimensions.get("window").width - 220 175 | }, 176 | headerText: { 177 | color: "white", 178 | fontSize: 20, 179 | fontWeight: "500" 180 | }, 181 | arrow: { 182 | paddingTop: 15 183 | }, 184 | wrapper: { 185 | flex: 1, 186 | height: Dimensions.get("window").height, 187 | backgroundColor: "white" 188 | }, 189 | inputContainerStyle: { 190 | marginHorizontal: 20, 191 | marginTop: 0 192 | }, 193 | inputBodyContainerStyle: { 194 | paddingBottom: 100, 195 | marginHorizontal: 20, 196 | marginTop: 0 197 | }, 198 | full_header: { 199 | fontWeight: "500", 200 | fontSize: 35, 201 | paddingHorizontal: 20, 202 | paddingTop: 20, 203 | paddingBottom: 10, 204 | color: "black", 205 | textAlign: "left", 206 | position: "relative" 207 | }, 208 | header: { 209 | fontWeight: "500", 210 | fontSize: 25, 211 | paddingHorizontal: 20, 212 | paddingTop: 20, 213 | paddingBottom: 10, 214 | color: "black", 215 | textAlign: "left", 216 | position: "relative" 217 | }, 218 | button_text: { 219 | color: "white", 220 | fontSize: 19, 221 | fontWeight: "600" 222 | }, 223 | login_btn: { 224 | alignItems: "center", 225 | backgroundColor: "#8E44AD", 226 | borderRadius: 7, 227 | width: Dimensions.get("window").width - 40, 228 | paddingVertical: 17, 229 | marginTop: 30, 230 | marginLeft: 20 231 | } 232 | }); 233 | -------------------------------------------------------------------------------- /c2tc-mobile/screens/SettingsScreen.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { FontAwesome } from "@expo/vector-icons"; 3 | import { NavigationEvents } from "react-navigation"; 4 | 5 | import { 6 | StyleSheet, 7 | View, 8 | Dimensions, 9 | Text, 10 | TouchableOpacity, 11 | Image, 12 | AsyncStorage 13 | } from "react-native"; 14 | 15 | import { Appbar } from "react-native-paper"; 16 | 17 | export default class SettingsScreen extends React.Component { 18 | constructor(props) { 19 | super(props); 20 | this.state = { 21 | user: this.props.navigation.getParam("user", "no user"), 22 | username: this.props.navigation.getParam("user", "no user").username 23 | }; 24 | } 25 | 26 | handleBackPress = e => { 27 | this.props.navigation.navigate("Profile"); 28 | }; 29 | 30 | handleLogout = async () => { 31 | await AsyncStorage.removeItem("token"); 32 | await AsyncStorage.removeItem("verifiedPin"); 33 | this.props.navigation.navigate("Intro"); 34 | }; 35 | 36 | render() { 37 | return ( 38 | 39 | 40 | 41 | 43 | this.props.navigation.navigate("Profile", { 44 | user: this.state.user 45 | }) 46 | } 47 | style={styles.backButton} 48 | > 49 | 50 | {" "} 51 | Profile 52 | 53 | 54 | 58 | Logout 59 | 60 | 61 | 63 | this.props.navigation.navigate("EditProfile", { 64 | user: this.state.user 65 | }) 66 | } 67 | > 68 | 69 | 75 | 76 | {this.state.user.username} 77 | Edit Your Profile 78 | 79 | 85 | 86 | 87 | 88 | this.props.navigation.navigate("Notifications")} 90 | > 91 | 92 | Notifications 93 | 99 | 100 | 101 | 102 | 104 | this.props.navigation.navigate("Welcome", { backPage: "Settings" }) 105 | } 106 | > 107 | 108 | Show App Tutorials 109 | 115 | 116 | 117 | 118 | 119 | ); 120 | } 121 | } 122 | 123 | const styles = StyleSheet.create({ 124 | settings: { 125 | backgroundColor: "white", 126 | height: Dimensions.get("window").height 127 | }, 128 | profile: { 129 | flexDirection: "row", 130 | padding: 25 131 | }, 132 | name: { 133 | flexDirection: "row", 134 | paddingLeft: 30, 135 | fontSize: 20 136 | }, 137 | editProfile: { 138 | paddingLeft: 30, 139 | fontSize: 15, 140 | color: "gray" 141 | }, 142 | list: { 143 | height: 45, 144 | flexDirection: "row", 145 | alignItems: "flex-start" 146 | }, 147 | text: { 148 | paddingHorizontal: 30, 149 | paddingTop: 10, 150 | fontSize: 15, 151 | width: Dimensions.get("window").width - 40 152 | }, 153 | divider: { 154 | borderBottomColor: "#D2D2D7", 155 | borderBottomWidth: 1 156 | }, 157 | profileArrow: { 158 | paddingTop: 20, 159 | paddingLeft: 100 160 | }, 161 | navBar: { 162 | paddingTop: 37, 163 | flexDirection: "row", 164 | justifyContent: "flex-start", 165 | width: Dimensions.get("window").width, 166 | backgroundColor: "#9041AF", 167 | paddingBottom: 15, 168 | marginBottom: 10 169 | }, 170 | backButton: { 171 | paddingLeft: 20, 172 | marginRight: Dimensions.get("window").width - 220 173 | }, 174 | headerText: { 175 | color: "white", 176 | fontSize: 20, 177 | fontWeight: "500" 178 | }, 179 | arrow: { 180 | paddingTop: 15 181 | } 182 | }); 183 | -------------------------------------------------------------------------------- /c2tc-mobile/screens/TipCategories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | StyleSheet, 4 | View, 5 | Text, 6 | TouchableOpacity, 7 | Dimensions 8 | } from "react-native"; 9 | import { FontAwesome } from "@expo/vector-icons"; 10 | import Colors from "../constants/Colors"; 11 | import { Appbar } from "react-native-paper"; 12 | 13 | class TipCategories extends React.Component { 14 | constructor(props) { 15 | super(props); 16 | } 17 | 18 | render() { 19 | return ( 20 | 21 | 22 | this.props.navigation.navigate("TipOverview")} 24 | style={styles.backButton} 25 | > 26 | 27 | Tip 28 | Overview 29 | 30 | 31 | 32 | 33 | 35 | this.props.navigation.navigate("TipForm", { 36 | category: "crime" 37 | }) 38 | } 39 | style={[ 40 | styles.category, 41 | { marginRight: 10, backgroundColor: Colors.crime } 42 | ]} 43 | > 44 | 45 | 46 | 47 | {"\n"} 48 | Crimes 49 | 50 | 51 | 52 | 54 | this.props.navigation.navigate("TipForm", { 55 | category: "health" 56 | }) 57 | } 58 | style={[styles.category, { backgroundColor: Colors.health }]} 59 | > 60 | 61 | 62 | 63 | {"\n"} 64 | Health 65 | 66 | 67 | 68 | 69 | 70 | 72 | this.props.navigation.navigate("TipForm", { 73 | category: "transportation" 74 | }) 75 | } 76 | style={[ 77 | styles.category, 78 | { marginRight: 10, backgroundColor: Colors.transportation } 79 | ]} 80 | > 81 | 82 | 83 | 84 | {"\n"} 85 | Transportation 86 | 87 | 88 | 89 | 91 | this.props.navigation.navigate("TipForm", { 92 | category: "financial" 93 | }) 94 | } 95 | style={[styles.category, { backgroundColor: Colors.financial }]} 96 | > 97 | 98 | 99 | 105 | {"\n"} 106 | Financial 107 | 108 | 109 | 110 | 111 | 112 | ); 113 | } 114 | } 115 | 116 | const styles = StyleSheet.create({ 117 | row: { 118 | flexDirection: "row", 119 | marginBottom: 10, 120 | justifyContent: "center", 121 | alignItems: "center" 122 | }, 123 | category: { 124 | width: (Dimensions.get("window").width - 30) / 2, 125 | height: (Dimensions.get("window").width - 100) / 2, 126 | borderRadius: 10, 127 | flexDirection: "row", 128 | justifyContent: "center" 129 | }, 130 | categoryText: { 131 | color: "white", 132 | fontSize: 15, 133 | fontWeight: "500", 134 | textAlign: "center" 135 | }, 136 | categoryView: { 137 | flexDirection: "column", 138 | justifyContent: "center" 139 | }, 140 | header: { 141 | marginBottom: 20 142 | }, 143 | navBar: { 144 | paddingTop: 37, 145 | flexDirection: "row", 146 | justifyContent: "flex-start", 147 | width: Dimensions.get("window").width, 148 | backgroundColor: "#9041AF", 149 | paddingBottom: 15, 150 | marginBottom: 30 151 | }, 152 | backButton: { 153 | paddingLeft: 20, 154 | marginRight: Dimensions.get("window").width - 220 155 | }, 156 | headerText: { 157 | color: "white", 158 | fontSize: 20, 159 | fontWeight: "500" 160 | } 161 | }); 162 | 163 | export default TipCategories; 164 | -------------------------------------------------------------------------------- /c2tc-mobile/screens/TipScreen.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import TabBar from "../components/NavigationComponents/Tabs"; 4 | import { Provider } from "react-redux"; 5 | import { store } from "../Redux"; 6 | import TipOverviewScreen from "./TipOverviewScreen"; 7 | 8 | export default class TipScreen extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | } 12 | 13 | render() { 14 | return ( 15 | 16 | 17 | 18 | 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /c2tc-mobile/screens/VerificationScreen.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | StyleSheet, 4 | TextInput, 5 | View, 6 | Text, 7 | TouchableOpacity, 8 | Dimensions 9 | } from "react-native"; 10 | import API from "../components/API"; 11 | import { Appbar } from "react-native-paper"; 12 | import { FontAwesome } from "@expo/vector-icons"; 13 | import { AsyncStorage } from "react-native"; 14 | 15 | class VerificationScreen extends React.Component { 16 | constructor(props) { 17 | super(props); 18 | this.state = { 19 | pin: "0", 20 | errors: [] 21 | }; 22 | } 23 | 24 | handleVerification = async () => { 25 | const response = await API.verifyPin(this.state.pin); 26 | if (!response.success) { 27 | errors = ["Error: " + response.message]; 28 | this.setState({ errors }); 29 | } else { 30 | await AsyncStorage.setItem("verifiedPin", "yes"); 31 | errors = ["Congrats, you're verified!"]; 32 | this.setState({ errors }); 33 | this.setState({ successfulSubmit: true }); 34 | } 35 | }; 36 | 37 | render() { 38 | const { errors } = this.state; 39 | return ( 40 | 41 | 42 | this.props.navigation.navigate("TipOverview")} 44 | style={styles.backButton} 45 | > 46 | 47 | Tip 48 | Overview 49 | 50 | 51 | 52 | 53 | 54 | {errors.map(error => ( 55 | {error} 56 | ))} 57 | 58 | Enter Verification Pin 59 | this.setState({ pin })} 66 | /> 67 | 71 | Submit 72 | 73 | 74 | 75 | ); 76 | } 77 | } 78 | 79 | const styles = StyleSheet.create({ 80 | verify: { 81 | backgroundColor: "white", 82 | height: Dimensions.get("window").height 83 | }, 84 | header: { 85 | fontSize: 25, 86 | fontWeight: "500", 87 | marginBottom: 50 88 | }, 89 | submit: { 90 | alignItems: "center", 91 | backgroundColor: "#8E44AD", 92 | width: 150, 93 | borderRadius: 7, 94 | padding: 10, 95 | marginTop: 20 96 | }, 97 | submitText: { 98 | color: "white", 99 | fontSize: 20 100 | }, 101 | verificationText: { 102 | fontSize: 25, 103 | padding: 10, 104 | width: 110, 105 | borderRadius: 5, 106 | borderColor: "black", 107 | borderWidth: 1 108 | }, 109 | content: { 110 | height: Dimensions.get("window").height - 275, 111 | flexDirection: "column", 112 | justifyContent: "center", 113 | alignItems: "center" 114 | }, 115 | navBar: { 116 | paddingTop: 37, 117 | flexDirection: "row", 118 | justifyContent: "flex-start", 119 | width: Dimensions.get("window").width, 120 | backgroundColor: "#9041AF", 121 | paddingBottom: 15, 122 | marginBottom: 10 123 | }, 124 | backButton: { 125 | paddingLeft: 20, 126 | marginRight: Dimensions.get("window").width - 220 127 | }, 128 | headerText: { 129 | color: "white", 130 | fontSize: 20, 131 | fontWeight: "500" 132 | } 133 | }); 134 | 135 | export default VerificationScreen; 136 | -------------------------------------------------------------------------------- /c2tc-mobile/screens/WelcomeScreen.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Animated, 4 | Text, 5 | View, 6 | Image, 7 | StyleSheet, 8 | ImageBackground, 9 | Dimensions, 10 | TouchableOpacity 11 | } from "react-native"; 12 | 13 | const { width, height } = Dimensions.get("window"); 14 | 15 | class FadeInView extends React.Component { 16 | state = { 17 | fadeAnim: new Animated.Value(0) // Initial value for opacity: 0 18 | }; 19 | 20 | componentDidMount() { 21 | Animated.timing( 22 | // Animate over time 23 | this.state.fadeAnim, // The animated value to drive 24 | { 25 | toValue: 1, // Animate to opacity: 1 (opaque) 26 | duration: 1500 // Make it take a while 27 | } 28 | ).start(); // Starts the animation 29 | } 30 | 31 | render() { 32 | let { fadeAnim } = this.state; 33 | 34 | return ( 35 | 41 | {this.props.children} 42 | 43 | ); 44 | } 45 | } 46 | 47 | export default class WelcomeScreen extends React.Component { 48 | render() { 49 | return ( 50 | 51 | 53 | this.props.navigation.navigate( 54 | this.props.navigation.getParam("backPage") 55 | ) 56 | } 57 | style={styles.top_view} 58 | > 59 | 64 | 65 | 66 | 67 | 72 | 77 | 82 | 89 | 94 | 99 | 104 | 105 | 110 | 111 | 116 | 117 | 118 | this.props.navigation.navigate("Map")} 121 | > 122 | Continue 123 | 124 | 125 | 126 | ); 127 | } 128 | } 129 | 130 | const styles = StyleSheet.create({ 131 | overall: { 132 | backgroundColor: "white", 133 | height: Dimensions.get("window").height 134 | }, 135 | top_view: { 136 | backgroundColor: "#FFFFFF" 137 | }, 138 | back: { 139 | width: 56, 140 | height: height / 10, 141 | left: 15, 142 | marginTop: 20 143 | }, 144 | welcome_1: { 145 | alignSelf: "center", 146 | width: 236, 147 | marginTop: -10 148 | }, 149 | welcome_2: { 150 | alignSelf: "center", 151 | width: 310, 152 | marginTop: -150 153 | }, 154 | welcome_3: { 155 | alignSelf: "center", 156 | width: 244, 157 | marginTop: -110 158 | }, 159 | welcome_4: { 160 | alignSelf: "center", 161 | width: 30, 162 | marginLeft: 20, 163 | marginRight: 20, 164 | marginTop: 20 165 | }, 166 | welcome_5: { 167 | alignSelf: "center", 168 | width: 30, 169 | marginLeft: 20, 170 | marginRight: 20, 171 | marginTop: 20 172 | }, 173 | welcome_6: { 174 | alignSelf: "center", 175 | width: 30, 176 | marginLeft: 20, 177 | marginRight: 20, 178 | marginTop: 20 179 | }, 180 | welcome_7: { 181 | alignSelf: "center", 182 | width: 310, 183 | marginTop: -270 184 | }, 185 | welcome_8: { 186 | position: "absolute", 187 | top: 0, 188 | bottom: -550, 189 | left: 0, 190 | right: 0 191 | }, 192 | selectedButton: { 193 | alignItems: "center", 194 | backgroundColor: "#8E44AD", 195 | borderRadius: 7, 196 | width: Dimensions.get("window").width - 40, 197 | justifyContent: "flex-end", 198 | marginHorizontal: 20, 199 | height: 55, 200 | paddingVertical: 17, 201 | marginTop: -75 // -75 works perfectly on iPhones but not on Android, 0 works fine on android but it's too low for iPhones, relative values dont work either 202 | }, 203 | view: { 204 | height: Dimensions.get("window").height - (110 + width / 10), 205 | backgroundColor: "#FFFFFF" 206 | }, 207 | selectedText: { 208 | marginTop: -5, 209 | color: "white", 210 | fontSize: 19, 211 | fontWeight: "600" 212 | } 213 | }); 214 | -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Welcome to our contribution guide! 4 | 5 | ## Getting Started 6 | 7 | Follow the README instructions in both the `backend/` and `frontend/` folders to run this application. 8 | 9 | ## Issue Guidelines 10 | 11 | Issues are how we keep track of our work and the progress of our development. Use Issues to define features, Epics, submitting bugs, etc. They are also where general discussions are made. 12 | 13 | Remember to: 14 | 15 | - Write a clear title and description 16 | - Link other relevant issues with `#`. Ex: Blocks #13 17 | - Label each issue a priority 18 | - Add other relevant labels 19 | - Add it to the relevant Project Board 20 | - Bug Reports must have clear instructions on reproducing it 21 | 22 | Obvious Fixes that do not introduce any new functionality or creative thinking don't require creating Issues! Some examples are: 23 | 24 | - Spelling / grammar fixes 25 | - Typo correction, white space, and formatting changes 26 | - Comment clean up 27 | - Adding logging messages or debugging output 28 | 29 | Just make a PR that follows the guidelines in the next section. 30 | 31 | ## Pull Request Guidelines 32 | 33 | Pull Requests are the way concrete changes are made to the code, dependencies, documentation, and tools in `hack4impact-uiuc/childs-play-tool`. 34 | 35 | ### Process of Making Changes 36 | 37 | As a rule of thumb, try to keep your Pull Requests small instead of huge PRs with changes in 20 files. It is much easier to review smaller incremental changes. 38 | 39 | #### Step 1: Clone & Branch 40 | 41 | As a best practice, create local branches to work within. They should be created off of the `master` branch. 42 | 43 | #### Step 2: Code 44 | 45 | Please make sure to run `yarn format` in both `frontend/` and `pipenv run black .` in `backend/` folders before commiting to ensure that changes follow our code style. Our code style is [standard](https://github.com/standard/standard). Our frontend also uses Flow for prop typing, so remember to add those in if you are modifying props in components. 46 | 47 | ### Step 3: Commit 48 | 49 | Commits are how we keep track of code changes and thus should be explicit. 50 | 51 | Unless your changes are trivial (when it doesn't require an issue), our commits must include a reference to an issue. Use the `Fixes` prefix, `Resolves`, or `Refs` depending on the type of your changes. Then, include a description of your changes. 52 | 53 | Ex: 54 | 55 | - `Resolves #14 List Page now filters games by lead character` 56 | - `Fixes #15 HomePage no longer shows a blank page when the passcode is incorrect` 57 | 58 | You may reference multiple issues if needed. 59 | 60 | Once your PR is approved, you must squash your commits. Instructions are provided below. It is recommended to not squash your commits during the review process for easier review. 61 | 62 | ### Step 4: Rebase 63 | 64 | It is recommended that you rebase your branch with master frequently to synchronize your work to ensure you have the latest changes and that you will pass tests when it's merged in. 65 | 66 | ```bash 67 | git pull origin master 68 | ``` 69 | 70 | If your branch was already pushed to github, do this as well: 71 | 72 | ```bash 73 | git push --force-with-lease origin my-branch 74 | ``` 75 | 76 | ### Step 5: Test 77 | 78 | A test suite will be coming soon :) You must always write tests for your features and changes. 79 | 80 | ### Step 6: Opening your PR 81 | 82 | Push your branch to github and open a PR! From there, reference the issues related to your PR just like you did in your commits in the description section. Add the collaborators as reviewers, especially including the owner of the issue (unless it's yourself). Please fill out as many details and include Screenshots if your work is on the frontend. Then, add a `In Review` Label and add in into the relevant Projects Board. 83 | 84 | ### Step 7: Review 85 | 86 | Your reviewers will get then provide feedback or requests for changes to your Pull Reuqest. Make those changes, add a new commit, and push them to your remote branch in Github! Remember to still rebase your branch with `master`! Feel free to put a comment down when your ready for re-reviews. 87 | 88 | Whenever you're making changes, please label your PR as "WIP" 89 | 90 | Each PR requires at least two approvals from a collaborator and pass the CI run. 91 | 92 | ### Step 8: Approval! 93 | 94 | Once your PR is approved, please squash your commits. To do this: 95 | 96 | - Use the Github squash and merge option in your PR. 97 | Or you can do this from terminal: 98 | - `git rebase -i ` 99 | - An editor will pop out and you must delete `pick` and add `squash` in front of the PRs that you want to squash. Save. 100 | - Another editor will open and delete the commit messages you don't want to keep. Save. Remember your final commit message must oblige to the commits guidelines. 101 | 102 | Each logical change should only have one commit. 103 | 104 | Merge! 105 | -------------------------------------------------------------------------------- /docs/api_docs.md: -------------------------------------------------------------------------------- 1 | # C2TC Fall 2018 Backend Design 2 | 3 | ## Schema Design 4 | 5 | **BUSINESS** 6 | 7 | | name | yelp_id | location | image_url | display_phone | open_hours | 8 | | :--: | :-----: | :------: | :-------: | :-----------: | :--------: | 9 | 10 | 11 | **BUSSTOP** 12 | 13 | | stop_id | stop_name | latitude | longitude | routes | 14 | | :-----: | :-------: | :------: | :-------: | :----: | 15 | 16 | 17 | **CRIME** 18 | 19 | | incident_id | incident_datetime | incident_type_primary | incident_description | address_1 | city | state | latitude | longitude | hour_of_day | day_of_week | parent_incident_type | 20 | | :---------: | :---------------: | :-------------------: | :------------------: | :-------: | :--: | :---: | :------: | :-------: | :---------: | :---------: | :------------------: | 21 | 22 | 23 | **EMERGENCY PHONE** 24 | 25 | | emergencyPhone_id | latitude | longitude | 26 | | :---------------: | :------: | :-------: | 27 | 28 | 29 | **STREETLIGHT** 30 | 31 | | streetlight_id | latitude | longitude | 32 | | :------------: | :------: | :-------: | 33 | 34 | 35 | **TIP** 36 | 37 | | title | content | author | posted_time | latitude | longitude | category | upvotes | downvotes | verified | 38 | | :---: | :-----: | :----: | :---------: | :------: | :-------: | :------: | :-----: | :-------: | :------: | 39 | 40 | 41 | ## Endpoints Documentation 42 | 43 | ### Map Filters 44 | 45 | - [Bus Stop Endpoints](https://github.com/hack4impact-uiuc/c2tc-spring-2019/blob/master/docs/endpoints/bus_stop.md) 46 | 47 | - [Business Endpoints](https://github.com/hack4impact-uiuc/c2tc-spring-2019/blob/master/docs/endpoints/business.md) 48 | 49 | - [Crime Endpoints](https://github.com/hack4impact-uiuc/c2tc-spring-2019/blob/master/docs/endpoints/crime.md) 50 | 51 | - [Emergency Phone Endpoints](https://github.com/hack4impact-uiuc/c2tc-spring-2019/blob/master/docs/endpoints/emergency_phone.md) 52 | 53 | - [Streetlight Endpoints](https://github.com/hack4impact-uiuc/c2tc-spring-2019/blob/master/docs/endpoints/streetlight.md) 54 | 55 | ### Location Based Tips 56 | 57 | - [Tip Endpoints](https://github.com/hack4impact-uiuc/c2tc-spring-2019/blob/master/docs/endpoints/tip.md) 58 | - [User Endpoints](https://github.com/hack4impact-uiuc/c2tc-spring-2019/blob/master/docs/endpoints/user.md) 59 | -------------------------------------------------------------------------------- /docs/endpoints/bus_stop.md: -------------------------------------------------------------------------------- 1 | # Bus Stop Endpoint Documentation 2 | ### Endpoint 3 | 4 | GET /busStops 5 | 6 | **Description** 7 | 8 | GET function for retrieving BusStop objects 9 | 10 | **Response** 11 | 12 | { 13 | "message": "Success", 14 | "result": { 15 | "busStops": [ 16 | { 17 | "_id": "5bd64156ed396d1ee50955c3", 18 | "latitude": 40.114512, 19 | "longitude": -88.180673, 20 | "routes": { 21 | "6": "000000" 22 | }, 23 | "stop_id": "150DALE:1", 24 | "stop_name": "U.S. 150 & Dale (NE Corner)" 25 | } 26 | ] 27 | }, 28 | "success": true 29 | } 30 | 31 | ### Endpoint 32 | 33 | POST /busStops 34 | 35 | **Description** 36 | 37 | POST function for posting a hard-coded BusStop object for testing purposes 38 | 39 | **Response** 40 | 41 | ### Endpoint 42 | 43 | POST /scrape_stops 44 | 45 | **Description** 46 | 47 | POST function which scrapes data from scrape() method in bus_stops.py scraper and stores them in the busStops db collection. Should be run probably once a month or so, because bus routes only change once or twice a year. 48 | 49 | **Response** 50 | -------------------------------------------------------------------------------- /docs/endpoints/business.md: -------------------------------------------------------------------------------- 1 | # Open Business Endpoint Documentation 2 | 3 | ### Endpoint 4 | 5 | GET /open_businesses 6 | 7 | **Description** 8 | 9 | Gets a list of businesses that are open at the time specified in the querystring. 10 | 11 | **Parameters** 12 | 13 | | Name | Type | Required | Description | 14 | |:---------:|:------:|:-----------------------------:|:-------------------------:| 15 | | time | int | **Required** | #### (time as 4 digit 24hr time, eg. 1430 = 2:30pm) 16 | | day | int | **Required** | # (integer 0-6, where 0 is Monday) 17 | 18 | **Response** 19 | 20 | { 21 | "message": "Success", 22 | "result": { 23 | "businesses": [ 24 | { 25 | "_id": "5bd63a71ed396d1d5b198f8b", 26 | "image_url": "https://s3-media2.fl.yelpcdn.com/bphoto/5tn-zU6PGPvbZoXnPi4Ahw/o.jpg", 27 | "location": { 28 | "address1": "1209 W Oregon St", 29 | "city": "Urbana", 30 | "country": "US", 31 | "state": "IL", 32 | "zip_code": "61801" 33 | }, 34 | "name": "The Red Herring Vegetarian Restaurant", 35 | "open_hours": [ 36 | { 37 | "day": 0, 38 | "end": "1430", 39 | "is_overnight": false, 40 | "start": "1100" 41 | } 42 | ], 43 | "yelp_id": "kKCwp86xU9XKRnAALQDhrw" 44 | } 45 | ] 46 | }, 47 | "success": true 48 | } 49 | 50 | ### Endpoint 51 | 52 | GET /businesses 53 | 54 | **Description** 55 | 56 | GET function for retrieving Business objects 57 | 58 | **Response** 59 | 60 | { 61 | "message": "Success", 62 | "result": { 63 | "businesses": [ 64 | { 65 | "_id": "5bd63a71ed396d1d5b198f8b", 66 | "image_url": "https://s3-media2.fl.yelpcdn.com/bphoto/5tn-zU6PGPvbZoXnPi4Ahw/o.jpg", 67 | "location": { 68 | "address1": "1209 W Oregon St", 69 | "city": "Urbana", 70 | "country": "US", 71 | "state": "IL", 72 | "zip_code": "61801" 73 | }, 74 | "name": "The Red Herring Vegetarian Restaurant", 75 | "open_hours": [ 76 | { 77 | "day": 0, 78 | "end": "1430", 79 | "is_overnight": false, 80 | "start": "1100" 81 | } 82 | ], 83 | "yelp_id": "kKCwp86xU9XKRnAALQDhrw" 84 | } 85 | ] 86 | }, 87 | "success": true 88 | } 89 | 90 | ### Endpoint 91 | 92 | POST /businesses 93 | 94 | **Description** 95 | 96 | POST function for posting a hard-coded Business object for testing purposes 97 | 98 | **Response** 99 | 100 | ### Endpoint 101 | 102 | POST /scrape_businesses 103 | 104 | **Description** 105 | 106 | POST function which scrapes data from business_scrape() method in 107 | open_businesses.py scraper and stores them in the businesses db collection. Should be run maybe once a month. 108 | 109 | **Response** -------------------------------------------------------------------------------- /docs/endpoints/crime.md: -------------------------------------------------------------------------------- 1 | # Crime Endpoint Documentation 2 | 3 | ### Endpoint 4 | 5 | GET /crimes 6 | 7 | **Description** 8 | 9 | GET function for retrieving Crime objects 10 | 11 | **Response** 12 | 13 | { 14 | "message": "", 15 | "result": { 16 | "crimes": [ 17 | { 18 | "_id": "5bc916d53d336f0592ef45fa", 19 | "address_1": "Outside Shreyas's apartment", 20 | "city": "Champaign", 21 | "day_of_week": "Monday", 22 | "hour_of_day": 23, 23 | "incident_datetime": "10/18/2018, 18:27:09", 24 | "incident_description": "Self explanatory", 25 | "incident_id": "1", 26 | "incident_type_primary": "Peeing in public", 27 | "latitude": 100.1, 28 | "longitude": 200.2, 29 | "parent_incident_type": "Disturbing the peace", 30 | "state": "IL" 31 | } 32 | ] 33 | }, 34 | "success": true 35 | } 36 | 37 | ### Endpoint 38 | 39 | POST /crimes 40 | 41 | **Description** 42 | 43 | POST function for posting a hard-coded Crime object for testing purposes 44 | 45 | **Response** 46 | 47 | ### Endpoint 48 | 49 | POST /scrape_crimes 50 | 51 | **Description** 52 | 53 | POST function which scrapes data from crime_scrape() method in crimes.py scraper and stores them in the crimes db collection. This should probably be run every day, or every hour at night. 54 | 55 | **Response** -------------------------------------------------------------------------------- /docs/endpoints/emergency_phone.md: -------------------------------------------------------------------------------- 1 | # Emergency Phone Endpoint Documentation 2 | ### Endpoint 3 | 4 | GET /emergencyPhones 5 | 6 | **Description** 7 | 8 | GET function for retrieving EmergencyPhone objects 9 | 10 | **Response** 11 | 12 | { 13 | "message": "", 14 | "result": { 15 | "emergencyPhones": [ 16 | { 17 | "_id": "5bd634afed396d1a9001053c", 18 | "emergencyPhone_id": 0, 19 | "latitude": 40.0957696644812, 20 | "longitude": -88.2405983758263 21 | } 22 | ] 23 | }, 24 | "success": true 25 | } 26 | 27 | ### Endpoint 28 | 29 | POST /emergencyPhones 30 | 31 | **Description** 32 | 33 | POST function for posting a hard-coded EmergencyPhone object for testing purposes 34 | 35 | **Response** 36 | 37 | ### Endpoint 38 | 39 | POST /scrape_phones 40 | 41 | **Description** 42 | 43 | POST function which calls get_phones() from the emergency_phones.py scraper and stores phone data to the database. This data is hardcoded and will probably never change, so this endpoint only needs to be called if the db is reset or the collection is lost. 44 | 45 | **Response** -------------------------------------------------------------------------------- /docs/endpoints/streetlight.md: -------------------------------------------------------------------------------- 1 | # Streetlight Endpoint Documentation 2 | 3 | ### Endpoint 4 | 5 | GET /streetlights 6 | 7 | **Description** 8 | 9 | GET function for retrieving Streetlight objects 10 | 11 | **Response** 12 | 13 | { 14 | "message": "", 15 | "result": { 16 | "streetlights": [ 17 | { 18 | "_id": "5bd280ab3d336f02d2b06a18", 19 | "latitude": 200.2, 20 | "longitude": 300.3, 21 | "streetlight_id": 0 22 | } 23 | ] 24 | }, 25 | "success": true 26 | } 27 | 28 | ### Endpoint 29 | 30 | POST /streetlights 31 | 32 | **Description** 33 | 34 | POST function for posting a hard-coded Streetlight object for testing purposes 35 | 36 | **Response** -------------------------------------------------------------------------------- /docs/endpoints/tip.md: -------------------------------------------------------------------------------- 1 | # Tip Endpoint Documentation 2 | ### Endpoint 3 | 4 | GET /tips 5 | 6 | **Description** 7 | 8 | GET function for retrieving all tips objects 9 | 10 | **Response** 11 | 12 | ### Endpoint 13 | 14 | GET /tips/ 15 | 16 | **Description** 17 | 18 | GET function for retrieving all tips objects posted by a certain user 19 | 20 | **Response** 21 | 22 | ### Endpoint 23 | 24 | GET /tips_category/ 25 | 26 | **Description** 27 | 28 | GET function for retrieving all tips objects in a certain category 29 | 30 | **Response** 31 | 32 | 33 | ### Endpoint 34 | 35 | GET /tips_upvotes/ 36 | 37 | **Description** 38 | 39 | GET function for retrieving all tips objects upvoted by a certain user 40 | 41 | **Response** 42 | 43 | ### Endpoint 44 | 45 | GET /tips_downvotes/ 46 | 47 | **Description** 48 | 49 | GET function for retrieving all tips objects downvoted by a certain user 50 | 51 | **Response** 52 | 53 | ### Endpoint 54 | 55 | GET /tips/verified 56 | 57 | **Description** 58 | 59 | GET function for retrieving all tips objects that are verified 60 | 61 | **Response** 62 | 63 | ### Endpoint 64 | 65 | POST /tips 66 | 67 | **Description** 68 | 69 | POST function for a user to create a new tip 70 | 71 | The parameters are passed in as a JSON: 72 | ``` 73 | { 74 | "title": "be there or be squared 2", 75 | "content": "make sure to be there", 76 | "user_id": "5c6cd199fa676f00aec97ff2", 77 | "latitude": 0, 78 | "longitude": 0, 79 | "category": "test" 80 | } 81 | ``` 82 | 83 | **Response** 84 | 85 | ### Endpoint 86 | 87 | PUT /tips/ 88 | 89 | **Description** 90 | 91 | PUT function for a user to edit a tip that they already posted 92 | 93 | The parameters are passed in as a JSON: 94 | ``` 95 | { 96 | "title": "be there or be squared 2", 97 | "content": "make sure to be there", 98 | "latitude": 0, 99 | "longitude": 0, 100 | "category": "test" 101 | } 102 | ``` 103 | 104 | **Response** 105 | 106 | ### Endpoint 107 | 108 | PUT /tips_upvotes 109 | 110 | **Description** 111 | 112 | PUT function for a user to change their upvote or downvote on a post 113 | 114 | The parameter are passed in as a JSON: 115 | ``` 116 | { 117 | "tips_id": "5c6f6cc7fa676f0336bb4a8b" 118 | "user_id": "5c6cd199fa676f00aec97ff2" 119 | "vote_type": "UPVOTE" 120 | } 121 | ``` 122 | "vote_type" can be either UPVOTE or DOWNVOTE 123 | 124 | **Response** 125 | 126 | ### Endpoint 127 | 128 | PUT /tips//verify 129 | 130 | **Description** 131 | 132 | PUT function for changing the tip's verified status 133 | 134 | Takes in a query parameter "verified" which must be either "True" or "False" 135 | 136 | **Response** 137 | 138 | 139 | ### Endpoint 140 | 141 | DELETE /tips/ 142 | 143 | **Description** 144 | 145 | DELETE function to delete a specific tip object 146 | 147 | **Response** 148 | 149 | ### Endpoint 150 | 151 | DELETE /tips 152 | 153 | **Description** 154 | 155 | DELETE function to delete all tips objects 156 | 157 | **Response** 158 | -------------------------------------------------------------------------------- /docs/endpoints/users.md: -------------------------------------------------------------------------------- 1 | ### Endpoint 2 | 3 | GET /users 4 | 5 | **Description** 6 | 7 | GET function for retrieving all user objects 8 | 9 | **Response** 10 | 11 | { 12 | "message": "", 13 | "result": { 14 | "users": [ 15 | { 16 | "_id": "5bd280ab3d336f02d2b06a18", 17 | "net_id": "alicesf2", 18 | "username": "alslice", 19 | "verified": false, 20 | "anon": true, 21 | "karma": 0, 22 | "posted_tips" : [ 23 | "5c85688964804c00031c4e21", 24 | "5c85688964804c00031c4e21", 25 | "5c85688964804c00031c4e21" 26 | ], 27 | "date_created": 2019-02-24 14:04:57.156 28 | }, 29 | { 30 | "_id": "5bd280ab3d336f02d2b06a19", 31 | "net_id": "alicesf3", 32 | "username": "alslice2", 33 | "verified": false, 34 | "anon": true, 35 | "karma": 0, 36 | "posted_tips" : [ 37 | "5c85688964804c00031c4e21", 38 | "5c85688964804c00031c4e21", 39 | "5c85688964804c00031c4e21" 40 | ], 41 | "date_created": "2019-02-24 14:04:57.156" 42 | } 43 | ] 44 | }, 45 | "success": true 46 | } 47 | 48 | ### Endpoint 49 | 50 | GET /users/ 51 | 52 | **Description** 53 | 54 | GET function for retrieving a single user object 55 | 56 | **Response** 57 | 58 | { 59 | "message": "", 60 | "result": { 61 | "user": { 62 | "_id": "5bd280ab3d336f02d2b06a18", 63 | "net_id": "alicesf2", 64 | "username": "alslice", 65 | "verified": false, 66 | "anon": true, 67 | "karma": 0, 68 | "posted_tips" : [ 69 | "5c85688964804c00031c4e21", 70 | "5c85688964804c00031c4e21", 71 | "5c85688964804c00031c4e21" 72 | ], 73 | "date_created": "2019-02-24 14:04:57.156" 74 | } 75 | }, 76 | "success": true 77 | } 78 | 79 | ### Endpoint 80 | 81 | POST /users 82 | 83 | **Description** 84 | 85 | POST function for creating a new user object 86 | 87 | **Response** 88 | 89 | { 90 | "message": "success!", 91 | "result": null, 92 | "success": true 93 | } 94 | 95 | ### Endpoint 96 | 97 | PUT /users/ 98 | 99 | **Description** 100 | 101 | PUT function for updating an existing user object 102 | 103 | **Response** 104 | 105 | { 106 | "message": "success!", 107 | "result": null, 108 | "success": true 109 | } 110 | 111 | ### Endpoint 112 | 113 | DELETE /users/ 114 | 115 | **Description** 116 | 117 | DELETE function for deleting an existing user object 118 | 119 | **Response** 120 | 121 | { 122 | "message": "success!", 123 | "result": null, 124 | "success": true 125 | } 126 | -------------------------------------------------------------------------------- /pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Pull Request Template 2 | 3 | - Make a list of notable changes that you've made 4 | - If marked with issue, include `Resolves #` 5 | --------------------------------------------------------------------------------