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