From 88b0dba3b66741878adace8a0a67f3f0153cd1e4 Mon Sep 17 00:00:00 2001 From: Julia Friesel <julia.friesel@gmail.com> Date: Tue, 7 Aug 2018 18:18:11 +0200 Subject: [PATCH] Clean up file structure --- components/labels.js | 3 +- components/settings.js | 60 +++++---- db/index.js | 155 +--------------------- lib/{ => import-export}/export-to-csv.js | 4 +- lib/import-export/get-csv-column-names.js | 19 +++ lib/import-export/import-from-csv.js | 110 +++++++++++++++ 6 files changed, 166 insertions(+), 185 deletions(-) rename lib/{ => import-export}/export-to-csv.js (91%) create mode 100644 lib/import-export/get-csv-column-names.js create mode 100644 lib/import-export/import-from-csv.js diff --git a/components/labels.js b/components/labels.js index cd361c84..ec41af71 100644 --- a/components/labels.js +++ b/components/labels.js @@ -6,5 +6,6 @@ export const settings = { }, exportTitle: 'My Drip data export', exportSubject: 'My Drip data export', - buttonLabel: 'Export data' + exportLabel: 'Export data', + importLabel: 'Import data' } \ No newline at end of file diff --git a/components/settings.js b/components/settings.js index d6a4a6ec..3b1d7d81 100644 --- a/components/settings.js +++ b/components/settings.js @@ -7,12 +7,12 @@ import { } from 'react-native' import Share from 'react-native-share' -import getDataAsCsvDataUri from '../lib/export-to-csv' import { DocumentPicker, DocumentPickerUtil } from 'react-native-document-picker' import rnfs from 'react-native-fs' import styles from '../styles/index' import { settings as labels } from './labels' -import { importCsv } from '../db' +import getDataAsCsvDataUri from '../lib/import-export/export-to-csv' +import importCsv from '../lib/import-export/import-from-csv' export default class Settings extends Component { render() { @@ -21,38 +21,14 @@ export default class Settings extends Component { <View style={styles.homeButtons}> <View style={styles.homeButton}> <Button - onPress={async () => { - let data - try { - data = getDataAsCsvDataUri() - if (!data) { - return Alert.alert(labels.errors.noData) - } - } catch (err) { - console.error(err) - return Alert.alert(labels.errors.couldNotConvert) - } - - try { - await Share.open({ - title: labels.exportTitle, - url: data, - subject: labels.exportSubject, - type: 'text/csv', - showAppsToView: true - }) - } catch (err) { - console.error(err) - return Alert.alert(labels.errors.problemSharing) - } - }} - title={labels.buttonLabel}> + onPress={ openShareDialogAndExport } + title={labels.exportLabel}> </Button> </View> <View style={styles.homeButton}> <Button onPress={ getFileContentAndImport } - title="Import data"> + title={labels.importLabel}> </Button> </View> </View> @@ -61,6 +37,32 @@ export default class Settings extends Component { } } +async function openShareDialogAndExport() { + let data + try { + data = getDataAsCsvDataUri() + if (!data) { + return Alert.alert(labels.errors.noData) + } + } catch (err) { + console.error(err) + return Alert.alert(labels.errors.couldNotConvert) + } + + try { + await Share.open({ + title: labels.exportTitle, + url: data, + subject: labels.exportSubject, + type: 'text/csv', + showAppsToView: true + }) + } catch (err) { + console.error(err) + return Alert.alert(labels.errors.problemSharing) + } +} + async function getFileContentAndImport() { let fileInfo try { diff --git a/db/index.js b/db/index.js index 0b08149b..ea76bd67 100644 --- a/db/index.js +++ b/db/index.js @@ -1,9 +1,5 @@ import Realm from 'realm' import { LocalDate } from 'js-joda' -import { Base64 } from 'js-base64' -import objectPath from 'object-path' -import csvParser from 'csvtojson' -import isObject from 'isobject' import { cycleWithTempAndNoMucusShift, @@ -180,154 +176,9 @@ function getPreviousTemperature(cycleDay) { return winner.temperature.value } -function getCycleDaysAsCsvDataUri() { - if (!cycleDaysSortedByDate.length) return null - const csv = transformToCsv(cycleDaysSortedByDate) - const encoded = Base64.encodeURI(csv) - return `data:text/csv;base64,${encoded}` - - function transformToCsv() { - const columnNames = getColumnNamesForCsv() - const rows = cycleDaysSortedByDate - .map(day => { - return columnNames.map(column => { - return objectPath.get(day, column, '') - }) - }) - .map(row => row.join(',')) - - rows.unshift(columnNames.join(',')) - return rows.join('\n') - - } -} - -function getColumnNamesForCsv() { - return getPrefixedKeys('CycleDay') - - function getPrefixedKeys(schemaName, prefix) { - const schema = db.schema.find(x => x.name === schemaName).properties - return Object.keys(schema).reduce((acc, key) => { - const prefixedKey = prefix ? [prefix, key].join('.') : key - const childSchemaName = schema[key].objectType - if (!childSchemaName) { - acc.push(prefixedKey) - return acc - } - acc.push(...getPrefixedKeys(childSchemaName, prefixedKey)) - return acc - }, []) - } -} - -function getDbType(modelProperties, path) { - if (path.length === 1) return modelProperties[path[0]].type - const modelName = modelProperties[path[0]].objectType - const model = db.schema.find(x => x.name === modelName) - return getDbType(model.properties, path.slice(1)) -} - -async function importCsv(csv, deleteFirst) { - const cycleDayProperties = db.schema.find(x => x.name === 'CycleDay').properties - const parseFuncs = { - bool: val => { - if (val.toLowerCase() === 'true') return true - if (val.toLowerCase() === 'false') return false - return val - }, - int: parseNumberIfPossible, - float: parseNumberIfPossible, - double: parseNumberIfPossible, - string: val => val - } - - function parseNumberIfPossible(val) { - // Number and parseFloat catch different cases of weirdness, - // so we test them both - if (isNaN(Number(val)) || isNaN(parseFloat(val))) return val - return Number(val) - } - - const config = { - ignoreEmpty: true, - colParser: getColumnNamesForCsv().reduce((acc, colName) => { - const path = colName.split('.') - const dbType = getDbType(cycleDayProperties, path) - acc[colName] = item => { - if (item === '') return null - return parseFuncs[dbType](item) - } - return acc - }, {}) - } - - let cycleDays - try { - cycleDays = await csvParser(config) - .fromString(csv) - .on('header', validateHeaders) - } catch(err) { - // TODO - console.log(err) - } - - //remove symptoms where all fields are null - putNullForEmptySymptoms(cycleDays) - - if (deleteFirst) { - db.write(() => { - db.delete(db.objects('CycleDay')) - cycleDays.forEach(tryToCreateCycleDay) - }) - } else { - db.write(() => { - cycleDays.forEach((day, i) => { - const existing = getCycleDay(day.date) - if (existing) { - db.delete(existing) - } - tryToCreateCycleDay(day, i) - }) - }) - } -} - -function tryToCreateCycleDay(day, i) { - try { - db.create('CycleDay', day) - } catch (err) { - const msg = `Error for line ${i + 1}(${day.date}): ${err.message}` - throw new Error(msg) - } -} - -function validateHeaders(headers) { - const expectedHeaders = getColumnNamesForCsv() - if (!headers.every(header => { - return expectedHeaders.indexOf(header) > -1 - })) { - const msg = `Expected CSV column titles to be ${expectedHeaders.join()}` - throw new Error(msg) - } -} - -function putNullForEmptySymptoms(data) { - data.forEach(replaceWithNullIfAllPropertiesAreNull) - - function replaceWithNullIfAllPropertiesAreNull(obj) { - Object.keys(obj).forEach((key) => { - if (!isObject(obj[key])) return - if (Object.values(obj[key]).every(val => val === null)) { - obj[key] = null - return - } - replaceWithNullIfAllPropertiesAreNull(obj[key]) - }) - } - -} export { + db, saveSymptom, getOrCreateCycleDay, bleedingDaysSortedByDate, @@ -336,7 +187,5 @@ export { fillWithDummyData, deleteAll, getPreviousTemperature, - getCycleDay, - getCycleDaysAsCsvDataUri, - importCsv + getCycleDay } diff --git a/lib/export-to-csv.js b/lib/import-export/export-to-csv.js similarity index 91% rename from lib/export-to-csv.js rename to lib/import-export/export-to-csv.js index d3b13216..c5bca2b3 100644 --- a/lib/export-to-csv.js +++ b/lib/import-export/export-to-csv.js @@ -1,7 +1,7 @@ import objectPath from 'object-path' import { Base64 } from 'js-base64' - -import { getColumnNamesForCsv, cycleDaysSortedByDate } from '../db' +import { cycleDaysSortedByDate } from '../../db' +import getColumnNamesForCsv from './get-csv-column-names' export default function makeDataURI() { if (!cycleDaysSortedByDate.length) return null diff --git a/lib/import-export/get-csv-column-names.js b/lib/import-export/get-csv-column-names.js new file mode 100644 index 00000000..f637593e --- /dev/null +++ b/lib/import-export/get-csv-column-names.js @@ -0,0 +1,19 @@ +import { db } from '../../db' + +export default function getColumnNamesForCsv() { + return getPrefixedKeys('CycleDay') + + function getPrefixedKeys(schemaName, prefix) { + const schema = db.schema.find(x => x.name === schemaName).properties + return Object.keys(schema).reduce((acc, key) => { + const prefixedKey = prefix ? [prefix, key].join('.') : key + const childSchemaName = schema[key].objectType + if (!childSchemaName) { + acc.push(prefixedKey) + return acc + } + acc.push(...getPrefixedKeys(childSchemaName, prefixedKey)) + return acc + }, []) + } +} \ No newline at end of file diff --git a/lib/import-export/import-from-csv.js b/lib/import-export/import-from-csv.js new file mode 100644 index 00000000..50048fd7 --- /dev/null +++ b/lib/import-export/import-from-csv.js @@ -0,0 +1,110 @@ +import csvParser from 'csvtojson' +import isObject from 'isobject' +import { db, getCycleDay } from '../../db' +import getColumnNamesForCsv from './get-csv-column-names' + +export default async function importCsv(csv, deleteFirst) { + const parseFuncs = { + bool: val => { + if (val.toLowerCase() === 'true') return true + if (val.toLowerCase() === 'false') return false + return val + }, + int: parseNumberIfPossible, + float: parseNumberIfPossible, + double: parseNumberIfPossible, + string: val => val + } + + function parseNumberIfPossible(val) { + // Number and parseFloat catch different cases of weirdness, + // so we test them both + if (isNaN(Number(val)) || isNaN(parseFloat(val))) return val + return Number(val) + } + + const cycleDayDbSchema = db.schema.find(x => x.name === 'CycleDay').properties + const config = { + ignoreEmpty: true, + colParser: getColumnNamesForCsv().reduce((acc, colName) => { + const path = colName.split('.') + const dbType = getDbType(cycleDayDbSchema, path) + acc[colName] = item => { + if (item === '') return null + return parseFuncs[dbType](item) + } + return acc + }, {}) + } + + let cycleDays + try { + cycleDays = await csvParser(config) + .fromString(csv) + .on('header', validateHeaders) + } catch(err) { + // TODO + console.log(err) + } + + //remove symptoms where all fields are null + putNullForEmptySymptoms(cycleDays) + + if (deleteFirst) { + db.write(() => { + db.delete(db.objects('CycleDay')) + cycleDays.forEach(tryToCreateCycleDay) + }) + } else { + db.write(() => { + cycleDays.forEach((day, i) => { + const existing = getCycleDay(day.date) + if (existing) { + db.delete(existing) + } + tryToCreateCycleDay(day, i) + }) + }) + } +} + +function tryToCreateCycleDay(day, i) { + try { + db.create('CycleDay', day) + } catch (err) { + const msg = `Error for line ${i + 1}(${day.date}): ${err.message}` + throw new Error(msg) + } +} + +function validateHeaders(headers) { + const expectedHeaders = getColumnNamesForCsv() + if (!headers.every(header => { + return expectedHeaders.indexOf(header) > -1 + })) { + const msg = `Expected CSV column titles to be ${expectedHeaders.join()}` + throw new Error(msg) + } +} + +function putNullForEmptySymptoms(data) { + data.forEach(replaceWithNullIfAllPropertiesAreNull) + + function replaceWithNullIfAllPropertiesAreNull(obj) { + Object.keys(obj).forEach((key) => { + if (!isObject(obj[key])) return + if (Object.values(obj[key]).every(val => val === null)) { + obj[key] = null + return + } + replaceWithNullIfAllPropertiesAreNull(obj[key]) + }) + } +} + +function getDbType(modelProperties, path) { + if (path.length === 1) return modelProperties[path[0]].type + const modelName = modelProperties[path[0]].objectType + const model = db.schema.find(x => x.name === modelName) + return getDbType(model.properties, path.slice(1)) +} \ No newline at end of file -- GitLab