-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathscript.js
272 lines (227 loc) · 8.68 KB
/
script.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
// fetch complaint records from nyc opendata
// and convert to previous formats
import fetch from 'node-fetch'
import { parse } from 'csv-parse/sync'
import { stringify } from 'csv-stringify/sync'
import { promises as fs } from 'fs'
async function save({ officers, allegations, closingReports, departureLetters }) {
const csvOptions = {
header: true,
cast: {
boolean: (value) => value ? 'true' : 'false'
}
}
await fs.writeFile('officers.csv', stringify(officers, csvOptions))
await fs.writeFile('officers.json', JSON.stringify(officers.map(stripRecord), null, 2))
await fs.writeFile('complaints.csv', stringify(allegations, csvOptions))
await fs.writeFile('complaints.json', JSON.stringify(allegations.map(stripRecord), null, 2))
await fs.writeFile('closingreports.csv', stringify(closingReports, csvOptions))
await fs.writeFile('departureletters.csv', stringify(departureLetters, csvOptions))
closingReports = {
totalPosted: closingReports[0].totalPosted,
totalClosedCase: closingReports[0].totalClosedCase,
lastPublishDate: closingReports[0].LastPublishDate,
closingReports: closingReports.map(report => {
delete report.totalPosted
delete report.totalClosedCase
delete report.LastPublishDate
return report
})
}
departureLetters = {
lastPublishDate: departureLetters[0].LastPublishDate,
departureLetters: departureLetters.map(letter => {
delete letter.Id
delete letter.LastPublishDate
return letter
})
}
await fs.writeFile('closingreports.json', JSON.stringify(closingReports, null, 2))
await fs.writeFile('departureletters.json', JSON.stringify(departureLetters, null, 2))
let officerById = {}
officers.forEach(officer => { officerById[officer.id] = officer })
let closingReportsById = {}
closingReports.closingReports.forEach(report => { closingReportsById[report.ComplaintId] = report })
let departureLettersById = {}
departureLetters.departureLetters.forEach(letter => { departureLettersById[letter.CaseNumber] = letter })
const combined = allegations.map(allegation => {
const officer_id = allegation.officer_id
delete allegation.officer_id
const officer = officerById[officer_id]
const closingReport = closingReportsById[allegation.complaint_id]
let departureLetter = departureLettersById[allegation.complaint_id]
if ((departureLetter?.LastName.toUpperCase() !== officer?.last_name.replace('BRUCEWATSON', 'BRUCE').toUpperCase()) ||
(departureLetter?.FirstName.substr(0, 10).toUpperCase() !== officer?.first_name.substr(0, 10).toUpperCase())) {
departureLetter = null
}
let record = {
officer_id,
...officer,
...allegation,
closing_report_url: closingReport?.WebsiteDocumentFileName || null,
departure_letter_url: departureLetter?.FileLink || null
}
delete record.id
return record
})
await fs.writeFile('records.csv', stringify(combined, csvOptions))
await fs.writeFile('records.json', JSON.stringify(combined.map(stripRecord), null, 2))
}
async function fetchComplaints() {
const files = [
{ id: 'allegations', url: 'https://data.cityofnewyork.us/api/views/6xgr-kwjq/rows.csv?accessType=DOWNLOAD' },
{ id: 'complaints', url: 'https://data.cityofnewyork.us/api/views/2mby-ccnw/rows.csv?accessType=DOWNLOAD' },
{ id: 'officers', url: 'https://data.cityofnewyork.us/api/views/2fir-qns4/rows.csv?accessType=DOWNLOAD' },
{ id: 'penalties', url: 'https://data.cityofnewyork.us/api/views/keep-pkmh/rows.csv?accessType=DOWNLOAD' },
]
let results = {}
for (const file of files) {
console.info(file.url)
const response = await fetch(file.url)
const csv = await response.text()
let records = parse(csv, { columns: true })
records = records.map(record => {
delete record['As Of Date'] // strip date to allow useful diffs
delete record['Complaint Officer Number'] // strip officer number as they change
return record
})
records.sort((a,b) => {
const fields = [
'Allegation Record Identity',
'Complaint Id',
'Tax ID',
'Complaint Officer Number',
]
for (const field of fields) {
if (a[field] > b[field]) { return 1 }
if (a[field] < b[field]) { return -1 }
}
})
results[file.id] = records
await fs.writeFile(`ccrb-complaints-database-${file.id}.csv`, stringify(records, { header: true }))
}
results = convertComplaints(results)
return results
}
// convert to legacy formats
async function convertComplaints({ allegations, complaints, officers, penalties }) {
allegations = allegations.filter(allegation => allegation['Tax ID'])
let complaintsById = {}
complaints.forEach(complaint => { complaintsById[complaint['Complaint Id']] = complaint })
let penaltiesById = {}
penalties.forEach(penalty => {
if (!penalty['NYPD Officer Penalty']) return
const id = `${penalty['Complaint Id']}:${penalty['Tax ID']}`
penaltiesById[id] = penalty
})
allegations = allegations.map(record => {
let allegation = {
officer_id: record['Tax ID'],
complaint_id: record['Complaint Id'],
complaint_date: complaintsById[record['Complaint Id']]['Incident Date'],
fado_type: record['FADO Type'],
allegation: record['Allegation'],
board_disposition: record['CCRB Allegation Disposition'],
nypd_disposition: record['NYPD Allegation Disposition'],
penalty_desc: '',
}
const penaltyId = `${record['Complaint Id']}:${record['Tax ID']}`
if (penaltiesById[penaltyId] && allegation.board_disposition.startsWith('Substantiated')) {
allegation.penalty_desc = penaltiesById[penaltyId]['NYPD Officer Penalty']
}
const fixcase = ['Gun pointed', 'No penalty']
fixcase.forEach(entry => {
if (allegation.allegation?.toLowerCase() === entry.toLowerCase()) {
allegation.allegation = entry
}
if (allegation.penalty_desc?.toLowerCase() === entry.toLowerCase()) {
allegation.penalty_desc = entry
}
})
return allegation
})
officers = officers.map(record => {
return {
id: record['Tax ID'],
command: record['Current Command'],
last_name: record['Officer Last Name'].toUpperCase(),
first_name: record['Officer First Name'].toUpperCase(),
rank: record['Current Rank'],
shield_no: record['Shield No'],
active: (record['Active Per Last Reported Status'] === 'Yes') ? true : false,
}
})
allegations.sort((a,b) => {
const props = [
'complaint_id', 'officer_id', 'fado_type', 'allegation',
'board_disposition', 'nypd_disposition', 'penalty_desc'
]
for (let i = 0; i < props.length; i++) {
const prop = props[i]
if (a[prop] < b[prop]) { return -1 }
if (a[prop] > b[prop]) { return 1 }
}
return 0
})
officers.sort((a,b) => {
if (a.id < b.id) { return -1 }
if (a.id > b.id) { return 1 }
return 0
})
return {
allegations,
officers,
}
}
async function fetchClosingReports() {
let closingReports = await fetchCcrbCsv(
'https://www.nyc.gov/assets/ccrb/csv/closing-reports/redacted-closing-reports.csv',
'WebsiteDocumentFileName',
'https://www1.nyc.gov/assets/ccrb/downloads/pdf/closing-reports/')
closingReports.sort((a,b) => {
if (a.ComplaintId < b.ComplaintId) { return -1 }
if (a.ComplaintId > b.ComplaintId) { return 1 }
return 0
})
return closingReports
}
async function fetchDepartureLetters() {
let departureLetters = await fetchCcrbCsv(
'https://www.nyc.gov/assets/ccrb/csv/departure-letter/RedactedDepartureLetters.csv',
'FileLink',
'https://www1.nyc.gov/assets/ccrb/downloads/pdf/complaints/complaint-outcomes/redacted-departure-letters/')
departureLetters.sort((a,b) => {
if (a.CaseNumber < b.CaseNumber) { return -1 }
if (a.CaseNumber > b.CaseNumber) { return 1 }
if (a.LastName < b.LastName) { return -1 }
if (a.LastName > b.LastName) { return 1 }
return 0
})
return departureLetters
}
async function fetchCcrbCsv(url, docField, docPrefix) {
console.info(url)
const response = await fetch(url)
const buffer = await response.text()
let records = parse(buffer, { columns: true })
records.forEach(record => {
record[docField] = docPrefix + record[docField]
})
return records
}
function stripRecord(record) {
let stripped = JSON.parse(JSON.stringify(record))
Object.keys(stripped).forEach(key => {
if ((stripped[key] === null) || (stripped[key] === '')) {
delete stripped[key]
}
})
return stripped
}
async function start() {
let results = await fetchComplaints()
results.closingReports = await fetchClosingReports()
results.departureLetters = await fetchDepartureLetters()
save(results)
}
start()