Skip to content

Commit

Permalink
Merge pull request #244 from hackforla/dev
Browse files Browse the repository at this point in the history
Updating master with latest dev
  • Loading branch information
ryanmswan authored Feb 12, 2020
2 parents d47c26b + ecabb53 commit 1d1d5c1
Show file tree
Hide file tree
Showing 45 changed files with 972 additions and 1,046 deletions.
2 changes: 2 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*.json
*.test.js*
3 changes: 3 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,11 @@ module.exports = {
},
plugins: [
'react',
'react-hooks'
],
rules: {
'linebreak-style': 'off',
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
},
};
2 changes: 2 additions & 0 deletions .github/workflows/Continuous_Integration_Frontend.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ jobs:
node-version: ${{ matrix.node-version }}
- name: Install Packages
run: npm install
- name: Lint
run: npm run lint
- name: Build project
run: npm run build
- name: Run Tests
Expand Down
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
"axios": "^0.19.0",
"babel-jest": "^24.9.0",
"bulma": "^0.8.0",
"bulma-checkradio": "^1.1.1",
"bulma-switch": "^2.0.0",
"classnames": "^2.2.6",
"dataframe-js": "^1.4.3",
"dotenv-webpack": "^1.7.0",
"gh-pages": "^2.1.1",
Expand All @@ -14,6 +17,7 @@
"leaflet": "^1.5.1",
"proptypes": "^1.1.0",
"react": "^16.8.6",
"react-burger-menu": "^2.6.13",
"react-dom": "^16.8.6",
"react-leaflet": "^2.4.0",
"react-leaflet-choropleth": "^2.0.0",
Expand All @@ -29,7 +33,8 @@
"start": "npm run dev",
"dev": "webpack-dev-server --config webpack.dev.js --host 0.0.0.0",
"build": "webpack --config webpack.prod.js",
"test": "jest",
"lint": "eslint src/**/*.js*",
"test": "jest --passWithNoTests",
"predeploy": "npm run build",
"deploy": "gh-pages -d dist"
},
Expand Down
2 changes: 1 addition & 1 deletion public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
crossorigin=""/>
<title>311 Data</title>
</head>
<body>
<body class="has-navbar-fixed-bottom">
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script src="bundle.js"></script>
Expand Down
27 changes: 17 additions & 10 deletions server/src/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
from configparser import ConfigParser
from threading import Timer
from multiprocessing import cpu_count
from services.sqlIngest import DataHandler
from datetime import datetime


app = Sanic(__name__)
Expand Down Expand Up @@ -64,17 +66,22 @@ async def sample_route(request):

@app.route('/ingest', methods=["POST"])
async def ingest(request):
'''Accept POST requests with a list of datasets to import\
based on the YearMapping. Body parameter format is \
{"sets": ["YearMappingKey","YearMappingKey","YearMappingKey"]}'''

ingress_worker = ingress_service(config=app.config['Settings'])
"""Accept POST requests with a list of years to import.
Query parameter name is 'years', and parameter value is
a comma-separated list of years to import.
Ex. '/ingest?years=2015,2016,2017'
"""
current_year = datetime.now().year
ALLOWED_YEARS = [year for year in range(2015, current_year+1)]
if not request.args.get("years"):
return json({"error": "'years' parameter is required."})
years = set([int(year) for year in request.args.get("years").split(",")])
if not all(year in ALLOWED_YEARS for year in years):
return json({"error": f"'years' parameter values must be one of {ALLOWED_YEARS}"})
loader = DataHandler()
loader.loadConfig(configFilePath='./settings.cfg')
loader.populateFullDatabase(yearRange=years)
return_data = {'response': 'ingest ok'}

for dataSet in request.json.get("sets", None):
target_data = app.config["Settings"]["YearMapping"][dataSet]
return_data = await ingress_worker.ingest(from_dataset=target_data)

return json(return_data)


Expand Down
49 changes: 49 additions & 0 deletions server/src/services/dataCleaner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import sqlalchemy as db
import pandas as pd
from sqlIngest import DataHandler
import databaseOrm


class DataCleaner(DataHandler):
def __init__(self, config=None, configFilePath=None, separator=','):
self.data = None
self.config = config
self.dbString = None if not self.config \
else self.config['Database']['DB_CONNECTION_STRING']
self.filePath = None
self.configFilePath = configFilePath
self.separator = separator
self.fields = databaseOrm.tableFields
self.insertParams = databaseOrm.insertFields
self.readParams = databaseOrm.readFields

def fetchData(self):
'''Retrieve data from mySql database instance'''
engine = db.create_engine(self.dbString)
self.data = pd.read_sql('ingest_staging_table',
con=engine,
index_col='srnumber')

def formatData(self):
'''Perform changes to data formatting to ensure compatibility
with cleaning and frontend processes'''
pass

def groupData(self):
'''Cluster data by geographic area to remove repeat instances
of 311 reports'''
pass

def cleaningReport(self):
'''Write out cleaning report summarizing operations performed
on data as well as data characteristics'''
pass


if __name__ == "__main__":
'''Class DataHandler workflow from initial load to SQL population'''
cleaner = DataCleaner()
cleaner.loadConfig(configFilePath='../settings.cfg')
cleaner.fetchData()
# can use inherited ingestData method to write to table
cleaner.ingestData(tableName='clean_data')
50 changes: 25 additions & 25 deletions server/src/services/databaseOrm.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,40 +43,40 @@ class Ingest(Base):
policeprecinct = Column(String)


insertFields = {'srnumber': String,
insertFields = {'srnumber': String(50),
'createddate': DateTime,
'updateddate': DateTime,
'actiontaken': String,
'owner': String,
'requesttype': String,
'status': String,
'requestsource': String,
'createdbyuserorganization': String,
'mobileos': String,
'anonymous': String,
'assignto': String,
'servicedate': String,
'closeddate': String,
'addressverified': String,
'approximateaddress': String,
'address': String,
'housenumber': String,
'direction': String,
'streetname': String,
'suffix': String,
'actiontaken': String(30),
'owner': String(10),
'requesttype': String(30),
'status': String(20),
'requestsource': String(30),
'createdbyuserorganization': String(16),
'mobileos': String(10),
'anonymous': String(10),
'assignto': String(20),
'servicedate': String(30),
'closeddate': String(30),
'addressverified': String(16),
'approximateaddress': String(20),
'address': String(100),
'housenumber': String(10),
'direction': String(10),
'streetname': String(50),
'suffix': String(10),
'zipcode': Integer,
'latitude': Float,
'longitude': Float,
'location': String,
'location': String(100),
'tbmpage': Integer,
'tbmcolumn': String,
'tbmcolumn': String(10),
'tbmrow': Float,
'apc': String,
'apc': String(30),
'cd': Float,
'cdmember': String,
'cdmember': String(30),
'nc': Float,
'ncname': String,
'policeprecinct': String}
'ncname': String(100),
'policeprecinct': String(30)}


readFields = {'SRNumber': str,
Expand Down
25 changes: 13 additions & 12 deletions server/src/services/sqlIngest.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ def __init__(self, config=None, configFilePath=None, separator=','):
self.fields = databaseOrm.tableFields
self.insertParams = databaseOrm.insertFields
self.readParams = databaseOrm.readFields
self.dialect = None

def loadConfig(self, configFilePath):
'''Load and parse config data'''
Expand All @@ -33,6 +34,7 @@ def loadConfig(self, configFilePath):
config.read(configFilePath)
self.config = config
self.dbString = config['Database']['DB_CONNECTION_STRING']
self.dialect = self.dbString.split(':')[0]
self.token = None if config['Socrata']['TOKEN'] == 'None' \
else config['Socrata']['TOKEN']

Expand Down Expand Up @@ -82,21 +84,25 @@ def cleanData(self):
print('\tCleaning Complete: %.1f minutes' %
self.elapsedTimer(cleanTimer))

def ingestData(self, ingestMethod='replace'):
def ingestData(self, ingestMethod='replace',
tableName='ingest_staging_table'):
'''Set up connection to database'''
print('Inserting data into Postgres instance...')
asdf = 'Inserting data into ' + self.dialect + ' instance...'
print(asdf)
ingestTimer = time.time()
data = self.data.copy() # shard deepcopy for other endpoint operations
engine = db.create_engine(self.dbString)
newColumns = [column.replace(' ', '_').lower() for column in data]
data.columns = newColumns
# Ingest data
data.to_sql("ingest_staging_table",
# Schema is same as database in MySQL;
# schema here is set to db name in connection string
data.to_sql(tableName,
engine,
if_exists=ingestMethod,
schema='public',
index=False,
chunksize=10000,
chunksize=10,
dtype=self.insertParams)
print('\tIngest Complete: %.1f minutes' %
self.elapsedTimer(ingestTimer))
Expand Down Expand Up @@ -171,7 +177,7 @@ def populateFullDatabase(self, yearRange=range(2015, 2021)):
Default operation is to fetch data from 2015-2020
!!! Be aware that each fresh import will wipe the
existing staging table'''
print('Performing fresh Postgres population from Socrata data sources')
print('Performing fresh ' + self.dialect + ' population from Socrata data sources')
tableInit = False
globalTimer = time.time()
for y in yearRange:
Expand Down Expand Up @@ -238,11 +244,6 @@ def fix_nan_vals(resultDict):
'''Class DataHandler workflow from initial load to SQL population'''
loader = DataHandler()
loader.loadConfig(configFilePath='../settings.cfg')
loader.fetchSocrataFull(limit=10000)
loader.fetchSocrataFull()
loader.cleanData()
loader.ingestData()
loader.saveCsvFile('testfile.csv')
loader.dumpFilteredCsvFile(dataset="",
startDate='2018-05-01',
requestType='Bulky Items',
councilName='VOICES OF 90037')
loader.ingestData('ingest_staging_table')
2 changes: 1 addition & 1 deletion server/src/settings.example.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ HOST = 0.0.0.0
PORT = 5000

[Database]
DB_CONNECTION_STRING = postgres://REDACTED:REDACTED@localhost:5432/postgres
DB_CONNECTION_STRING = mysql://REDACTED:REDACTED@localhost:5432/public
DATA_DIRECTORY = static

[Api]
Expand Down
95 changes: 17 additions & 78 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,83 +1,22 @@
import React, { Component } from 'react';
import axios from 'axios';

import { getDataResources } from './Util/DataService';
import { REQUESTS, COUNCILS } from './components/common/CONSTANTS';
import React, { useEffect } from 'react';
import { connect } from 'react-redux';

import Header from './components/main/header/Header';
import Body from './components/main/body/Body';
import Footer from './components/main/footer/Footer';

class App extends Component {
constructor() {
super();

this.state = {
data: [],
year: '2015',
startMonth: '1',
endMonth: '12',
request: REQUESTS[0],
showMarkers: false,
showMarkersDropdown: true,
};
}

componentDidMount() {
this.fetchData();
}

updateState = (key, value, cb = () => null) => {
this.setState({ [key]: value }, () => {
this.fetchData(); // This is only for the dropdown component to fetch data on change
cb();
});
}

toggleShowMarkers = () => {
const { showMarkers } = this.state;
this.setState({ showMarkers: !showMarkers });
}

fetchData = () => {
const dataUrl = this.buildDataUrl();

axios.get(dataUrl)
.then(({ data }) => {
this.setState({ data });
})
.catch((error) => {
console.error(error);
});
}

buildDataUrl = () => {
const {
startMonth, endMonth, year, request,
} = this.state;
const dataResources = getDataResources();
return `https://data.lacity.org/resource/${dataResources[year]}.json?$select=location,zipcode,address,requesttype,status,ncname,streetname,housenumber&$where=date_extract_m(CreatedDate)+between+${startMonth}+and+${endMonth}+and+requesttype='${request}'`;
}

render() {
const { data, showMarkers, showMarkersDropdown } = this.state;

return (
<div className="main">
<Header
updateState={this.updateState}
toggleShowMarkers={this.toggleShowMarkers}
showMarkers={showMarkers}
showMarkersDropdown={showMarkersDropdown}
/>
<Body
data={data}
showMarkers={showMarkers}
/>
<Footer />
</div>
);
}
}

export default App;
const App = () => {
useEffect(() => {
// fetch data on load??
}, []);

return (
<div className="main">
<Header />
<Body />
<Footer />
</div>
);
};

export default connect(null, null)(App);
Loading

0 comments on commit 1d1d5c1

Please sign in to comment.