mirror of
https://github.com/bellingcat/datasheet-server.git
synced 2026-06-11 21:08:33 +03:00
Compare commits
1 Commits
feature/ad
...
v0.5.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9eb6ea6e2 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -14,3 +14,4 @@ tags
|
|||||||
tags.lock
|
tags.lock
|
||||||
tags.temp
|
tags.temp
|
||||||
src/config.js
|
src/config.js
|
||||||
|
src/local.config.js
|
||||||
|
|||||||
Binary file not shown.
1960
package-lock.json
generated
1960
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -26,16 +26,15 @@
|
|||||||
"express": "^4.13.3",
|
"express": "^4.13.3",
|
||||||
"express-graphql": "^0.6.12",
|
"express-graphql": "^0.6.12",
|
||||||
"express-handlebars": "^4.0.4",
|
"express-handlebars": "^4.0.4",
|
||||||
"googleapis": "^32.0.0",
|
"googleapis": "^39.1.0",
|
||||||
"graphql": "^0.13.2",
|
"graphql": "^0.13.2",
|
||||||
"moment": "^2.27.0",
|
|
||||||
"morgan": "^1.8.0",
|
"morgan": "^1.8.0",
|
||||||
"mz": "^2.7.0",
|
"mz": "^2.7.0",
|
||||||
"node-fetch": "^2.3.0",
|
"node-fetch": "^2.6.1",
|
||||||
"node-xlsx": "^0.15.0",
|
|
||||||
"object-hash": "^1.3.0",
|
"object-hash": "^1.3.0",
|
||||||
"ramda": "^0.25.0",
|
"ramda": "^0.25.0",
|
||||||
"resource-router-middleware": "^0.6.0"
|
"resource-router-middleware": "^0.6.0",
|
||||||
|
"xlsx": "^0.16.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/cli": "^7.1.2",
|
"@babel/cli": "^7.1.2",
|
||||||
|
|||||||
@@ -58,17 +58,6 @@ export default ({ config, controller }) => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
// api.get('/:sheet/:tab/:resource/validate', (req, res) => {
|
|
||||||
// const { sheet, tab, resource } = req.params
|
|
||||||
// controller
|
|
||||||
// .retrieve(sheet, tab, resource)
|
|
||||||
// .then(data => res.json(data))
|
|
||||||
// .catch(err =>
|
|
||||||
// res.status(err.status || 404)
|
|
||||||
// .send({ error: err.message })
|
|
||||||
// )
|
|
||||||
// })
|
|
||||||
|
|
||||||
// ERROR routes. Note that it is important that these come AFTER routes
|
// ERROR routes. Note that it is important that these come AFTER routes
|
||||||
// like /update, so that the regex does not greedily match these routes.
|
// like /update, so that the regex does not greedily match these routes.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { fmtObj } from '../lib/util'
|
import { fmtObj } from '../lib/util'
|
||||||
import { getColumnValidation } from '../lib/validation'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Each resource item is an object with values labelled according
|
* Each resource item is an object with values labelled according
|
||||||
@@ -36,6 +35,7 @@ export default (data) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// generate the value for deep labels using the structure created
|
// generate the value for deep labels using the structure created
|
||||||
data.forEach((row, idx) => {
|
data.forEach((row, idx) => {
|
||||||
if (idx === 0) return
|
if (idx === 0) return
|
||||||
@@ -61,11 +61,12 @@ export default (data) => {
|
|||||||
|
|
||||||
// move values for flat labels over from base
|
// move values for flat labels over from base
|
||||||
structure.__flat.forEach(label => {
|
structure.__flat.forEach(label => {
|
||||||
const validatedLabel = getColumnValidation(label, baseRow[label])
|
|
||||||
console.log(validatedLabel)
|
|
||||||
deepRow[label] = baseRow[label]
|
deepRow[label] = baseRow[label]
|
||||||
})
|
})
|
||||||
if (!Object.keys(deepRow).every(k => deepRow[k] === '')) {
|
if (!Object.keys(deepRow).every(k => (
|
||||||
|
(deepRow[k] === '') ||
|
||||||
|
(Array.isArray(deepRow[k]) && deepRow[k].length === 0)
|
||||||
|
))) {
|
||||||
output.push(deepRow)
|
output.push(deepRow)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,13 +1,7 @@
|
|||||||
import { timemap } from './lib'
|
import { timemap } from './lib'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
gsheets: [
|
gsheets: [],
|
||||||
{
|
|
||||||
name: 'us2020',
|
|
||||||
id: '1I_pgyTQJlIorTIEHBxw1mM1STn-SrIi66FKYxut61iM',
|
|
||||||
tabs: timemap.default
|
|
||||||
}
|
|
||||||
],
|
|
||||||
xlsx: [
|
xlsx: [
|
||||||
{
|
{
|
||||||
name: 'timemap_data',
|
name: 'timemap_data',
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import http from 'http'
|
import http from 'http'
|
||||||
|
import path from 'path'
|
||||||
import express from 'express'
|
import express from 'express'
|
||||||
import initialize from './initialize'
|
import initialize from './initialize'
|
||||||
import middleware from './middleware'
|
import middleware from './middleware'
|
||||||
@@ -40,7 +41,7 @@ initialize(controller => {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
app.use(express.static(__dirname + '/public'))
|
app.use(express.static(path.join(__dirname, 'public')))
|
||||||
|
|
||||||
app.server.listen(process.env.PORT || 4040, () => {
|
app.server.listen(process.env.PORT || 4040, () => {
|
||||||
console.log('===========================================')
|
console.log('===========================================')
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import StoreJson from './models/StoreJson'
|
import StoreJson from './models/StoreJson'
|
||||||
import fetchers from './lib/Fetcher'
|
import fetchers from './lib/Fetcher'
|
||||||
import Controller from './lib/Controller'
|
import Controller from './lib/Controller'
|
||||||
import config from './config'
|
|
||||||
import R from 'ramda'
|
import R from 'ramda'
|
||||||
|
|
||||||
const isntNull = n => n !== null
|
const isntNull = n => n !== null
|
||||||
@@ -9,6 +8,13 @@ const filterNull = ls => R.filter(isntNull, ls)
|
|||||||
const flattenfilterNull = ls => filterNull(R.flatten(ls))
|
const flattenfilterNull = ls => filterNull(R.flatten(ls))
|
||||||
let themFetchers
|
let themFetchers
|
||||||
|
|
||||||
|
let config
|
||||||
|
try {
|
||||||
|
config = require('./local.config.js').default
|
||||||
|
} catch (_) {
|
||||||
|
config = require('./config.js').default
|
||||||
|
}
|
||||||
|
|
||||||
export default callback => {
|
export default callback => {
|
||||||
return Promise.resolve().then(() => {
|
return Promise.resolve().then(() => {
|
||||||
return Object.keys(config).map(fType => {
|
return Object.keys(config).map(fType => {
|
||||||
|
|||||||
@@ -5,9 +5,7 @@ function prefixedTabs (prefix, cfg) {
|
|||||||
const prf = key => cfg[key] ? `${prefix}_` : ''
|
const prf = key => cfg[key] ? `${prefix}_` : ''
|
||||||
return {
|
return {
|
||||||
[`${prf('events')}export_events`]: BP.deeprows,
|
[`${prf('events')}export_events`]: BP.deeprows,
|
||||||
[`${prf('categories')}export_categories`]: [BP.groups, BP.rows],
|
[`${prf('associations')}export_associations`]: BP.deeprows,
|
||||||
[`${prf('filters')}export_filters`]: BP.tree,
|
|
||||||
[`${prf('narratives')}export_narratives`]: BP.rows,
|
|
||||||
[`${prf('sources')}export_sources`]: BP.deepids,
|
[`${prf('sources')}export_sources`]: BP.deepids,
|
||||||
[`${prf('sites')}export_sites`]: BP.rows
|
[`${prf('sites')}export_sites`]: BP.rows
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
// FetcherTwo class interfaces with Google Sheet, and saves to a specified db
|
import R from 'ramda'
|
||||||
import { google } from 'googleapis'
|
import { createHash } from 'crypto'
|
||||||
import { buildDesaturated } from './blueprinters'
|
import { buildDesaturated } from './blueprinters'
|
||||||
import {
|
import {
|
||||||
fmtName,
|
fmtName,
|
||||||
fmtBlueprinterTitles,
|
fmtBlueprinterTitles,
|
||||||
isFunction
|
isFunction
|
||||||
} from './util'
|
} from './util'
|
||||||
import { createHash } from 'crypto'
|
|
||||||
import R from 'ramda'
|
/* GsheetFetcher deps */
|
||||||
import xlsx from 'node-xlsx'
|
import { google } from 'googleapis'
|
||||||
import fs from 'fs'
|
/* LocalFetcher deps */
|
||||||
|
import X from 'xlsx'
|
||||||
|
|
||||||
class Fetcher {
|
class Fetcher {
|
||||||
constructor (db, name, bps) {
|
constructor (db, name, bps) {
|
||||||
@@ -138,7 +139,6 @@ class Fetcher {
|
|||||||
|
|
||||||
/** Run on startup. Should be overridden if explicit auth is required **/
|
/** Run on startup. Should be overridden if explicit auth is required **/
|
||||||
authenticate (env) {
|
authenticate (env) {
|
||||||
console.log(`Connected to ${this.sheetName}. No explicit authentication required for ${this.type}s.`)
|
|
||||||
return Promise.resolve(this)
|
return Promise.resolve(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -221,27 +221,22 @@ class GsheetFetcher extends Fetcher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class XlsxFetcher extends Fetcher {
|
class LocalFetcher extends Fetcher {
|
||||||
constructor (db, name, bps, path) {
|
constructor (db, name, bps, path) {
|
||||||
super(db, name, bps)
|
super(db, name, bps)
|
||||||
this.type = 'XLSX File'
|
|
||||||
this.path = path
|
this.path = path
|
||||||
this.isRemote = false
|
this.update().then(res =>
|
||||||
|
console.log(`${res ? 'Successful' : 'Couldn\'t'} update ${name}`)
|
||||||
if (this.path.startsWith('https')) {
|
)
|
||||||
this.isRemote = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
update () {
|
update () {
|
||||||
const data = xlsx.parse(fs.readFileSync(this.path))
|
const wb = X.readFile(this.path)
|
||||||
data.forEach(tab => {
|
wb.SheetNames.forEach(name => {
|
||||||
const stringyData = tab.data.map(row =>
|
const sh = wb.Sheets[name]
|
||||||
row.map(d =>
|
const csv = X.utils.sheet_to_csv(sh, { FS: '\t' })
|
||||||
typeof (d) === 'number' ? d.toString() : d
|
const ll = csv.split('\n').map(line => line.split('\t'))
|
||||||
)
|
this.save(name, ll)
|
||||||
)
|
|
||||||
this.save(tab.name, stringyData)
|
|
||||||
})
|
})
|
||||||
return Promise.resolve(true)
|
return Promise.resolve(true)
|
||||||
}
|
}
|
||||||
@@ -249,5 +244,7 @@ class XlsxFetcher extends Fetcher {
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
'gsheets': GsheetFetcher,
|
'gsheets': GsheetFetcher,
|
||||||
'xlsx': XlsxFetcher
|
'xlsx': LocalFetcher,
|
||||||
|
'ods': LocalFetcher,
|
||||||
|
'local': LocalFetcher
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import R from 'ramda';
|
import R from 'ramda'
|
||||||
|
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
String.prototype.replaceAll = function (search, replacement) {
|
String.prototype.replaceAll = function (search, replacement) {
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
import moment from 'moment';
|
|
||||||
|
|
||||||
const DATE_FORMAT = "MM/DD/YYYY";
|
|
||||||
const TIME_REGEX = new RegExp("^([01]\d|2[0-3]):?([0-5]\d)$")
|
|
||||||
|
|
||||||
|
|
||||||
export const getColumnValidation = (colName, value) => {
|
|
||||||
switch(colName) {
|
|
||||||
case 'longitude':
|
|
||||||
return isFinite(value) && Math.abs(value) <= 180;
|
|
||||||
case 'latitude':
|
|
||||||
return isFinite(value) && Math.abs(value) <= 90;
|
|
||||||
case 'date':
|
|
||||||
return moment(value, DATE_FORMAT, true).isValid();
|
|
||||||
case 'time':
|
|
||||||
return TIME_REGEX.test(value);
|
|
||||||
default:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -54,14 +54,7 @@ test('should launch', t => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const passUrls = [
|
const passUrls = [
|
||||||
// /
|
|
||||||
'/api/',
|
'/api/',
|
||||||
// /blueprints
|
|
||||||
'/api/blueprints',
|
|
||||||
// /:sheet/:tab/:resource
|
|
||||||
'/api/example/export_events/rows',
|
|
||||||
// /:sheet/:tab/:resource/:frag
|
|
||||||
'/api/example/export_events/rows/1'
|
|
||||||
]
|
]
|
||||||
|
|
||||||
const failUrls = [
|
const failUrls = [
|
||||||
@@ -77,6 +70,7 @@ passUrls.forEach(function (url) {
|
|||||||
.then(checkStatus)
|
.then(checkStatus)
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(json => {
|
.then(json => {
|
||||||
|
console.info('JSON: ', json)
|
||||||
t.pass()
|
t.pass()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{{#each urls}}
|
{{#each urls}}
|
||||||
<div><a target="_blank" href="http://localhost:4040{{ this }}">{{ this }}</a></div>
|
<div><a target="_blank" href="http://localhost:4040{{ this }}">{{ this }}</a></div>
|
||||||
<div class="bp-validate-button" target="_blank" href="http://localhost:4040{{ this }}">Validate</div>
|
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</div>
|
</div>
|
||||||
{{ else }}
|
{{ else }}
|
||||||
@@ -26,7 +25,6 @@
|
|||||||
:root {
|
:root {
|
||||||
--grey: #8a8a8a;
|
--grey: #8a8a8a;
|
||||||
--btn-width: 200px;
|
--btn-width: 200px;
|
||||||
--validate-btn-width: 150px;
|
|
||||||
}
|
}
|
||||||
.main-container {
|
.main-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -81,21 +79,6 @@
|
|||||||
color: black;
|
color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bp-validate-button {
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-size: 12pt;
|
|
||||||
font-weight: bold;
|
|
||||||
width: var(--validate-btn-width);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
border: 3px solid red;
|
|
||||||
padding: .5em;
|
|
||||||
text-decoration: none;
|
|
||||||
color: red;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bp-update-container:hover {
|
.bp-update-container:hover {
|
||||||
background-color: black;
|
background-color: black;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
|
|||||||
Reference in New Issue
Block a user