From 7da6d26687d10d93e3c74c6732dfb398755f50ef Mon Sep 17 00:00:00 2001 From: Anes Belfodil Date: Wed, 7 Oct 2020 10:47:01 -0400 Subject: [PATCH 1/4] Add file upload --- .prettierrc.yaml | 2 +- backend/app.py | 37 +++++++++++++++++++-- web/package.json | 2 ++ web/src/index.js | 4 --- web/src/requests/analayze-sleep.js | 9 +++++ web/src/requests/constants.js | 1 + web/src/requests/object-to-formdata.js | 7 ++++ web/src/requests/ping-server.js | 12 +++++++ web/src/views/analyze-sleep/index.js | 26 +++++++++++---- web/src/views/analyze-sleep/upload-form.js | 38 ++++++++++------------ web/yarn.lock | 7 +++- 11 files changed, 110 insertions(+), 35 deletions(-) create mode 100644 web/src/requests/analayze-sleep.js create mode 100644 web/src/requests/constants.js create mode 100644 web/src/requests/object-to-formdata.js create mode 100644 web/src/requests/ping-server.js diff --git a/.prettierrc.yaml b/.prettierrc.yaml index 3c7da0b7..a51674bd 100644 --- a/.prettierrc.yaml +++ b/.prettierrc.yaml @@ -2,4 +2,4 @@ trailingComma: "all" tabWidth: 2 semi: true singleQuote: true -printWidth: 80 +printWidth: 150 diff --git a/backend/app.py b/backend/app.py index 6e05e2ad..ef1131f8 100644 --- a/backend/app.py +++ b/backend/app.py @@ -1,9 +1,19 @@ -from flask import Flask +import os +from flask import Flask, flash, request +from werkzeug.utils import secure_filename from flask_cors import CORS from waitress import serve +from http import HTTPStatus + + +SERVER_FOLDER = os.path.dirname(os.path.realpath(__file__)) app = Flask(__name__) -cors = CORS(app) + + +def allowed_file(filename): + ALLOWED_EXTENSIONS = {'txt', 'csv'} + return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS @app.route("/") @@ -11,4 +21,27 @@ def hello(): return "Hello, World!" +@app.route('/analyze_sleep', methods=['POST']) +def analyze_sleep(): + if 'file' not in request.files: + return 'Missing file', HTTPStatus.BAD_REQUEST + file = request.files['file'] + + if file.filename == '': + return 'No selected file', HTTPStatus.BAD_REQUEST + + if not allowed_file(file.filename): + return 'File format not allowed', HTTPStatus.BAD_REQUEST + + file_content = file.read() + form_data = request.form.to_dict() + + return '' + + +CORS(app, + resources={r'/*': {"origins": '*'}}, + allow_headers='Content-Type') +app.config['CORS_HEADERS'] = 'Content-Type' + serve(app, host='0.0.0.0', port=8080) diff --git a/web/package.json b/web/package.json index 60a47fd7..c8dc2647 100644 --- a/web/package.json +++ b/web/package.json @@ -25,6 +25,7 @@ "dependencies": { "argon-design-system-react": "^1.1.0", "axios": "^0.20.0", + "axios-observable": "^1.1.3", "bootstrap": "^4.5.0", "classnames": "2.2.6", "d3": "^5.16.0", @@ -41,6 +42,7 @@ "react-scripts": "3.4.0", "react-waypoint": "^9.0.2", "reactstrap": "8.4.1", + "rxjs": "^6.6.3", "yarn": "^1.22.4" }, "devDependencies": { diff --git a/web/src/index.js b/web/src/index.js index afa6bd23..960a5894 100644 --- a/web/src/index.js +++ b/web/src/index.js @@ -16,10 +16,6 @@ import Performance from 'views/performance'; import AnalyzeSleep from 'views/analyze-sleep'; import ScrollToTop from 'components/scroll_to_top'; import Emoji from 'components/emoji'; -import * as axios from 'axios'; - -// Communication with server (test) -axios.get('http://localhost:8080').then(console.log); const underConstruction = () => { const text = { diff --git a/web/src/requests/analayze-sleep.js b/web/src/requests/analayze-sleep.js new file mode 100644 index 00000000..0ee32127 --- /dev/null +++ b/web/src/requests/analayze-sleep.js @@ -0,0 +1,9 @@ +import { SERVER_URL } from './constants'; +import Axios from 'axios-observable'; +import { objectToFormData } from './object-to-formdata'; + +export const analyzeSleep = (formData) => { + return Axios.post(`${SERVER_URL}/analyze_sleep`, objectToFormData(formData), { + headers: { 'Content-Type': 'multipart/form-data' }, + }); +}; diff --git a/web/src/requests/constants.js b/web/src/requests/constants.js new file mode 100644 index 00000000..e761c194 --- /dev/null +++ b/web/src/requests/constants.js @@ -0,0 +1 @@ +export const SERVER_URL = 'http://localhost:8080'; diff --git a/web/src/requests/object-to-formdata.js b/web/src/requests/object-to-formdata.js new file mode 100644 index 00000000..ed7d6cb7 --- /dev/null +++ b/web/src/requests/object-to-formdata.js @@ -0,0 +1,7 @@ +export const objectToFormData = (obj) => { + const formData = new FormData(); + for (const key in obj) { + formData.append(key, obj[key]); + } + return formData; +}; diff --git a/web/src/requests/ping-server.js b/web/src/requests/ping-server.js new file mode 100644 index 00000000..8b3265b7 --- /dev/null +++ b/web/src/requests/ping-server.js @@ -0,0 +1,12 @@ +import { SERVER_URL } from './constants'; +import Axios from 'axios-observable'; +import { catchError, exhaustMap, map } from 'rxjs/operators'; +import { timer, of } from 'rxjs'; + +const pingServer = () => + Axios.get(SERVER_URL).pipe( + map(() => true), + catchError(() => of(false)), + ); + +export const periodicPingServer = (period) => timer(0, period).pipe(exhaustMap(pingServer)); diff --git a/web/src/views/analyze-sleep/index.js b/web/src/views/analyze-sleep/index.js index d6d14810..429583ff 100644 --- a/web/src/views/analyze-sleep/index.js +++ b/web/src/views/analyze-sleep/index.js @@ -1,15 +1,22 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { Container } from 'reactstrap'; - import Header from 'components/header'; import WaitingForServerToBeReady from './waiting-for-server-to-be-ready'; import UploadForm from './upload-form'; - import text from './text.json'; - -const isServerReady = true; +import { periodicPingServer } from 'requests/ping-server'; const AnalyzeSleep = () => { + const [serverReady, setServerReady] = useState(false); + + useEffect(() => { + const subscription = periodicPingServer(200).subscribe( + (ready) => setServerReady(ready), + () => null, + ); + return () => subscription.unsubscribe(); + }); + return (
{ subtitle={text['header_subtitle']} description={text['header_description']} /> - {isServerReady ? : } + + + +
); }; diff --git a/web/src/views/analyze-sleep/upload-form.js b/web/src/views/analyze-sleep/upload-form.js index 11bf5635..ae666a7c 100644 --- a/web/src/views/analyze-sleep/upload-form.js +++ b/web/src/views/analyze-sleep/upload-form.js @@ -1,6 +1,7 @@ import React from 'react'; import { Button, Container, CustomInput, Form, FormGroup, Label, Input, InputGroup, Col, Row } from 'reactstrap'; import { useForm } from 'react-hook-form'; +import { analyzeSleep } from 'requests/analayze-sleep'; const dateFieldSuffix = '-date'; const timeFieldSuffix = '-time'; @@ -21,15 +22,22 @@ const mergeDateTimeFields = (data) => .filter(([fieldName, value]) => fieldName.startsWith(fieldPrefix)) .map(([fieldName, value]) => value) .join(' '), - ), + ).getTime(), ]); +const onSubmit = async (data) => { + let formData = Object.fromEntries([...filterOutDateTimeFields(data), ...mergeDateTimeFields(data)]); + formData = { ...formData, file: formData.file[0] }; + try { + const results = await analyzeSleep(formData).toPromise(); + console.log(results); + } catch (error) { + console.error(error); + } +}; + const UploadForm = () => { const { register, handleSubmit } = useForm(); - const onSubmit = (data) => { - const newFormData = Object.fromEntries([...filterOutDateTimeFields(data), ...mergeDateTimeFields(data)]); - console.log(newFormData); - }; return ( @@ -44,8 +52,8 @@ const UploadForm = () => { accept=".csv, text/plain" bsSize="lg" type="file" - id="openbci-file" - name="openbci-file" + id="file" + name="file" label="Upload an OpenBCI CSV file" /> @@ -62,18 +70,6 @@ const UploadForm = () => { - - - - - - - - - - - - @@ -96,8 +92,8 @@ const UploadForm = () => { - - + + diff --git a/web/yarn.lock b/web/yarn.lock index 70dd24f0..f4ba9f46 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -2202,6 +2202,11 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.10.1.tgz#e1e82e4f3e999e2cfd61b161280d16a111f86428" integrity sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA== +axios-observable@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/axios-observable/-/axios-observable-1.1.3.tgz#258055be37e5567c1c03be3860acf15645f658f9" + integrity sha512-XY0Q+D6bjiqmyLiXEmg2GXcwMfnbHDMbKNJkkADPOEUlCYsSmpISaZB8s9tZxUSY42S3RTuTHjLiLP6HuVxuRA== + axios@^0.20.0: version "0.20.0" resolved "https://registry.yarnpkg.com/axios/-/axios-0.20.0.tgz#057ba30f04884694993a8cd07fa394cff11c50bd" @@ -10048,7 +10053,7 @@ rw@1: resolved "https://registry.yarnpkg.com/rw/-/rw-1.3.3.tgz#3f862dfa91ab766b14885ef4d01124bfda074fb4" integrity sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q= -rxjs@^6.5.3, rxjs@^6.6.0: +rxjs@^6.5.3, rxjs@^6.6.0, rxjs@^6.6.3: version "6.6.3" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.3.tgz#8ca84635c4daa900c0d3967a6ee7ac60271ee552" integrity sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ== From 92acb8b60e86e17ce4513a65223ebdbbd32817d5 Mon Sep 17 00:00:00 2001 From: Anes Belfodil Date: Wed, 7 Oct 2020 14:20:16 -0400 Subject: [PATCH 2/4] cleanup --- backend/app.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/backend/app.py b/backend/app.py index ef1131f8..4f0b0a0c 100644 --- a/backend/app.py +++ b/backend/app.py @@ -1,13 +1,9 @@ -import os -from flask import Flask, flash, request -from werkzeug.utils import secure_filename +from flask import Flask, request from flask_cors import CORS from waitress import serve from http import HTTPStatus -SERVER_FOLDER = os.path.dirname(os.path.realpath(__file__)) - app = Flask(__name__) From ba1ff19e557071399afbc71b2d0c717c9864e5eb Mon Sep 17 00:00:00 2001 From: Anes Belfodil Date: Thu, 8 Oct 2020 11:40:49 -0400 Subject: [PATCH 3/4] Cleanup --- web/src/requests/analayze-sleep.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/web/src/requests/analayze-sleep.js b/web/src/requests/analayze-sleep.js index 0ee32127..b223f439 100644 --- a/web/src/requests/analayze-sleep.js +++ b/web/src/requests/analayze-sleep.js @@ -2,8 +2,7 @@ import { SERVER_URL } from './constants'; import Axios from 'axios-observable'; import { objectToFormData } from './object-to-formdata'; -export const analyzeSleep = (formData) => { - return Axios.post(`${SERVER_URL}/analyze_sleep`, objectToFormData(formData), { +export const analyzeSleep = (formData) => + Axios.post(`${SERVER_URL}/analyze_sleep`, objectToFormData(formData), { headers: { 'Content-Type': 'multipart/form-data' }, }); -}; From cc5b749a2b1ff4f1a38708312ea7b6bcf6a2b17c Mon Sep 17 00:00:00 2001 From: Anes Belfodil Date: Thu, 8 Oct 2020 11:53:27 -0400 Subject: [PATCH 4/4] max char per line => 120 --- .prettierrc.yaml | 2 +- web/package.json | 2 +- web/src/views/analyze-sleep/upload-form.js | 77 +++++++++++++++++++--- 3 files changed, 69 insertions(+), 12 deletions(-) diff --git a/.prettierrc.yaml b/.prettierrc.yaml index a51674bd..eb4e4040 100644 --- a/.prettierrc.yaml +++ b/.prettierrc.yaml @@ -2,4 +2,4 @@ trailingComma: "all" tabWidth: 2 semi: true singleQuote: true -printWidth: 150 +printWidth: 120 diff --git a/web/package.json b/web/package.json index c8dc2647..c97e43db 100644 --- a/web/package.json +++ b/web/package.json @@ -11,7 +11,7 @@ "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject", - "format": "yarn prettier --config ../.vscode/.prettierrc.yaml --write \"./**/*.{js,jsx,css,scss,json}\"" + "format": "yarn prettier --config ../.prettierrc.yaml --write \"./**/*.{js,jsx,css,scss,json}\"" }, "eslintConfig": { "extends": "react-app" diff --git a/web/src/views/analyze-sleep/upload-form.js b/web/src/views/analyze-sleep/upload-form.js index ae666a7c..e5bbf648 100644 --- a/web/src/views/analyze-sleep/upload-form.js +++ b/web/src/views/analyze-sleep/upload-form.js @@ -12,7 +12,7 @@ const filterInDateTimeFields = (data) => .map((field) => field.replace(timeFieldSuffix, '')); const filterOutDateTimeFields = (data) => - Object.entries(data).filter(([fieldName, value]) => !(fieldName.endsWith(dateFieldSuffix) || fieldName.endsWith(timeFieldSuffix))); + Object.entries(data).filter(([name, value]) => !(name.endsWith(dateFieldSuffix) || name.endsWith(timeFieldSuffix))); const mergeDateTimeFields = (data) => filterInDateTimeFields(data).map((fieldPrefix) => [ @@ -81,7 +81,16 @@ const UploadForm = () => { - + @@ -92,8 +101,20 @@ const UploadForm = () => { - - + + @@ -101,8 +122,20 @@ const UploadForm = () => { - - + + @@ -112,8 +145,20 @@ const UploadForm = () => { - - + + @@ -121,8 +166,20 @@ const UploadForm = () => { - - + +