Skip to content

Commit

Permalink
Form Submission to Backend (#5)
Browse files Browse the repository at this point in the history
The focus of this PR was adding the backend server to the frontend so
that clicking the button submitted the form data somewhere (although
sending something more meaningful than 200 to the frontend will be part
of a separate work item). The frontend was also enhanced with additional
form field validation, error message text under the form fields when
applicable, and updated copy.

Initially submit button is disabled.
<img width="872" alt="image"
src="https://github.com/gt-sse-center/ramanujan-machine-web/assets/1794406/83ed946d-770b-4cb9-a0bc-3ba5047c0561">

Invalid entries display info from mathjs.
<img width="710" alt="image"
src="https://github.com/gt-sse-center/ramanujan-machine-web/assets/1794406/fcded698-0303-4680-9ee5-7a86410f5449">

Valid entries enable submit button.
<img width="737" alt="image"
src="https://github.com/gt-sse-center/ramanujan-machine-web/assets/1794406/84886e76-edc9-453e-95dc-fda5c4a13ff5">


Note: This PR seems really big because it includes a shift from
create-react-app / react-scripts to Vite because CRA is no longer
supported well and was causing some deeply nested outdated dependency
issues. This inspired two checks be added to the Actions workflow to
catch dependency issues (tested locally using `act`), as well as one
more for executing backend/python unit tests.
  • Loading branch information
krachwal committed Dec 1, 2023
2 parents 6d10860 + 5d15407 commit 41a2d12
Show file tree
Hide file tree
Showing 30 changed files with 5,018 additions and 10,298 deletions.
22 changes: 22 additions & 0 deletions .github/workflows/run-backend-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Commit Workflow
on:
push
jobs:
run-python-tests:
runs-on: ubuntu-latest
name: Python Unit Tests
steps:
- name: "Checkout repo"
uses: actions/checkout@v4
- name: "Setup Python"
uses: actions/setup-python@v4
with:
python-version: '3.11'

- name: "Install dependencies & run unit tests"
working-directory: ./python-backend
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pytest
pytest
25 changes: 21 additions & 4 deletions .github/workflows/run-frontend-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,35 @@ name: Commit Workflow
on:
push
jobs:
audit-deps:
runs-on: ubuntu-latest
name: "Check Node Dependencies for Known Security Vulnerabilities"
steps:
- name: "Checkout repo"
uses: actions/checkout@v4
- name: "Run npm audit"
working-directory: ./react-frontend
run: npm audit --omit dev
check-unused-deps:
runs-on: ubuntu-latest
name: "Check for Unused Node Dependencies"
steps:
- name: "Checkout repo"
uses: actions/checkout@v4
- name: "Install dependency and run dependency check"
working-directory: ./react-frontend
run: npm install depcheck -g && depcheck --ignores "typescript,@types/node,eslint,prettier,jest,@types/jest"
run-cypress-tests:
runs-on: ubuntu-latest
name: Run Cypress Tests
name: "Run Cypress Tests"
container:
image: cypress/browsers:node18.12.0-chrome107
steps:
- name: Checkout
- name: "Checkout repo"
uses: actions/checkout@v4
- name: Run Cypress
- name: "Run Cypress tests"
uses: cypress-io/github-action@v6
with:
working-directory: ./react-frontend
build: npm run build
start: npm start

2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,4 @@ react-frontend/cypress/screenshots/
react-frontend/cypress/videos/

#BACKEND
python_backend/.idea
.idea
53 changes: 47 additions & 6 deletions python-backend/main.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,62 @@
from fastapi import FastAPI
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
import re
from sympy.abc import _clash1
import uuid

from pydantic import BaseModel
from sympy import simplify, sympify

app = FastAPI()

origins = ["http://localhost:3000", "localhost:3000"]
origins = ["http://localhost:5173", "localhost:5173"]

# Only allow traffic from localhost and restrict methods to those in use
app.add_middleware(CORSMiddleware,
allow_origins=origins,
allow_methods=["GET", "POST", "OPTIONS"],
allow_credentials=True,
allow_headers=["*"])


@app.get("/")
def root():
content = {"message": "Hello"}
# The structure of the post body from the frontend
class Input(BaseModel):
p: str
q: str
i: int


def convert(polynomial: str):
"""
Take an acceptable math polynomial entered by a user and convert to one that Python can parse
:param polynomial: incoming polynomial entered by user in web frontend
:return: python parse-able polynomial
"""
return re.sub(r'([0-9.-])+([a-zA-Z])','\\1*\\2', polynomial.replace('^', '**'))


@app.post("/analyze")
async def analyze(request: Request):
"""
:param request: HTTP request
:return: HTTP response indicating success of parsing inputs with a 200 or a 500 to indicate failure parsing inputs
"""
# parse posted body as Input
data = Input(**(await request.json()))

# convert to math expression
try:
expression = sympify(convert(data.p), _clash1) / sympify(convert(data.q), _clash1)
# printing for the time being will process later on
print(simplify(expression))
except Exception as e:
print("p/q error", e)
response = JSONResponse(status_code=500, content={"error": "Failed to parse p / q"})
return response

content = {"message": "Results"}
response = JSONResponse(content=content)
response.set_cookie(key="trm-cookie", value="fake-cookie-session-value")
response.set_cookie(key="trm", value=uuid.uuid4().__str__())
return response
6 changes: 6 additions & 0 deletions python-backend/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Automatically generated by https://github.com/damnever/pigar.

fastapi==0.104.0
pydantic==2.4.2
pytest==7.4.3
sympy==1.12
51 changes: 51 additions & 0 deletions python-backend/test_main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import pytest
from sympy import sympify, simplify, SympifyError

from main import convert

TEST_INPUT_1 = "4^x"
CONVERSION_1 = "4**x"
TEST_INPUT_2 = "4x^2+3"
CONVERSION_2 = "4*x**2+3"
TEST_INPUT_3 = "4x^2+3x^5-1"
CONVERSION_3 = "4*x**2+3*x**5-1"
SYMPY_3 = "3*x**5+4*x**2-1"
TEST_INPUT_4 = "4x^2 + 3x^5 - 1"
CONVERSION_4 = "4*x**2 + 3*x**5 - 1"
SYMPY_4 = "3*x**5+4*x**2-1"
TEST_INPUT_5 = "2x^2"
TEST_INPUT_6 = "x"
SIMPLIFIED_5_6 = "2*x"
TEST_INPUT_7 = "6x^2 - 2 - 4x"
TEST_INPUT_8 = "x - 1"
SIMPLIFIED_7_8 = "6*x + 2"


def test_convert():
assert convert("") == ""
assert convert(TEST_INPUT_1) == CONVERSION_1
assert convert(TEST_INPUT_2) == CONVERSION_2
assert convert(TEST_INPUT_3) == CONVERSION_3
assert convert(TEST_INPUT_4) == CONVERSION_4


def test_sympify():
assert sympify(convert(TEST_INPUT_1))
with pytest.raises(SympifyError):
sympify(TEST_INPUT_2)
assert sympify(convert(TEST_INPUT_2)).__str__().replace(' ', '') == convert(TEST_INPUT_2)
with pytest.raises(SympifyError):
sympify(TEST_INPUT_3)
assert sympify(convert(TEST_INPUT_3)).__str__().replace(' ', '') == SYMPY_3
with pytest.raises(SympifyError):
sympify(TEST_INPUT_4)
assert sympify(convert(TEST_INPUT_4)).__str__().replace(' ', '') == SYMPY_4


def test_simplify():
assert simplify(sympify(convert(TEST_INPUT_1)))
assert simplify(sympify(convert(TEST_INPUT_2)))
assert simplify(sympify(convert(TEST_INPUT_3)))
assert simplify(sympify(convert(TEST_INPUT_4)))
assert simplify(sympify(convert(TEST_INPUT_5)) / sympify(convert(TEST_INPUT_6))).__str__() == SIMPLIFIED_5_6
assert simplify(sympify(convert(TEST_INPUT_7)) / sympify(convert(TEST_INPUT_8))).__str__() == SIMPLIFIED_7_8
2 changes: 2 additions & 0 deletions react-frontend/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
*.cy.js
8 changes: 8 additions & 0 deletions react-frontend/.eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": ["prettier", "react-app"],
"plugins": ["prettier"],
"rules": {
"prettier/prettier": ["error"]
},
"parser": "@typescript-eslint/parser"
}
30 changes: 14 additions & 16 deletions react-frontend/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
{
"env": {
"browser": true,
"es2021": true
},
"extends": [
"plugin:react/recommended"
],
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": [
"react"
],
"rules": {
}
"env": {
"browser": true,
"es2021": true
},
"extends": ["eslint:recommended", "plugin:react/recommended"],
"rules": {
"no-unused-vars": ["warn", { "vars": "all", "args": "after-used", "ignoreRestSiblings": false }]
},
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": ["react"]
}
19 changes: 10 additions & 9 deletions react-frontend/.prettierrc
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
{
"tabWidth": 2,
"useTabs": true,
"arrowParens": "always",
"bracketSameLine": true,
"bracketSpacing": true,
"singleQuote": true,
"trailingComma": "none",
"semi": true,
"printWidth": 100
"tabWidth": 2,
"useTabs": true,
"arrowParens": "always",
"bracketSameLine": true,
"bracketSpacing": true,
"singleQuote": true,
"trailingComma": "none",
"semi": true,
"printWidth": 100,
"ignore": ["cypress/*"]
}
19 changes: 10 additions & 9 deletions react-frontend/.prettierrc.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
module.exports = {
arrowParens: "always",
bracketSameLine: true,
bracketSpacing: true,
singleQuote: true,
trailingComma: "none",
tabWidth: 2,
useTabs: true,
semi: true,
printWidth: 100,
arrowParens: 'always',
bracketSameLine: true,
bracketSpacing: true,
singleQuote: true,
trailingComma: 'none',
tabWidth: 2,
useTabs: true,
semi: true,
printWidth: 100,
ignore: ['*.cy.js']
};
34 changes: 29 additions & 5 deletions react-frontend/cypress/e2e/test.cy.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
/// <reference types="cypress" />
/// <reference types="Cypress" />

describe('landing form', () => {
beforeEach(() => {
cy.visit('http://localhost:3000');
cy.visit('http://localhost:5173');
});

const invalidInput = `4..`;

it('displays two input fields for polynomials and one for iterations', () => {
cy.get('input').should('have.length', 3);
cy.get('input').first().should('have.text', '');
Expand All @@ -17,14 +19,36 @@ describe('landing form', () => {
});

it('validates math expressions', () => {
const newPolynomial = '4..\t';
cy.get('input').first().type(`${newPolynomial}`);
cy.get('input').first().type(invalidInput);
cy.get('input').first().blur();
cy.get('input').first().parent().should('have.class', 'invalid');
});

it('should display an error message when an input is invalid', () => {
cy.get('div.error-message').should('not.be.visible');
cy.get('input').first().type(invalidInput);
cy.get('input').first().blur();
cy.get('div.error-message').should('be.visible');
});

it('strips characters not used in math expressions', () => {
const newPolynomial = `4!@#$%&_={}[]|~\`<>\\?,+1
`;
cy.get('input').first().type(`${newPolynomial}`);
cy.get('input').first().blur();
cy.get('input').first().should('have.value', '4+1');
});

it('mangles script tags', () => {
const newPolynomial = '<script>console.log("hello")</script>';
cy.get('input').first().type(`${newPolynomial}`);
cy.get('input').first().blur();
cy.get('input').first().should('not.contain', '<script>');
cy.get('input').first().should('not.contain', '</script>');
});

it('sets the form as invalid if one of the polynomials is invalid', () => {
cy.get('div.form').first().should('have.class', 'invalid');
cy.get('form').first().should('have.class', 'invalid');
});

it('forces interation count to 10,000 if a larger value is entered', () => {
Expand Down
8 changes: 8 additions & 0 deletions react-frontend/cypress/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"compilerOptions": {
"allowJs": true,
"baseUrl": "../node_modules",
"types": ["cypress"]
},
"include": ["**/*.*"]
}
Loading

0 comments on commit 41a2d12

Please sign in to comment.