diff --git a/backend/src/protein.py b/backend/src/protein.py index 0fc3690f..ab314d66 100644 --- a/backend/src/protein.py +++ b/backend/src/protein.py @@ -43,20 +43,22 @@ def decode_base64(b64_header_and_data: str): return b64decode(b64_data_only).decode("utf-8") -def pdb_file_name(name: str): - return f"{os.path.join('src/data/pdbAlphaFold', name)}.pdb" +def pdb_file_name(protein_name: str): + return os.path.join("src/data/pdbAlphaFold", protein_name) + ".pdb" -def parse_protein_pdb(name: str, file_contents: str, encoding="str"): +def parse_protein_pdb(name: str, file_contents: str = "", encoding="str"): if encoding == "str": return PDB(file_contents, name) elif encoding == "b64": return PDB(decode_base64(file_contents), name) + elif encoding == "file": + return PDB(open(pdb_file_name(name), "r").read(), name) else: raise ValueError(f"Invalid encoding: {encoding}") -def protein_name_taken(name: str): +def protein_name_found(name: str): """Checks if a protein name already exists in the database Returns: True if exists | False if not exists """ @@ -96,3 +98,7 @@ def save_protein(pdb: PDB): ) except Exception as e: raise e + + +def pdb_to_fasta(pdb: PDB): + return ">{}\n{}".format(pdb.name, "".join(pdb.amino_acids())) diff --git a/backend/src/server.py b/backend/src/server.py index f9f198ba..f7a3c40a 100644 --- a/backend/src/server.py +++ b/backend/src/server.py @@ -1,15 +1,35 @@ import logging as log import os -from fastapi.staticfiles import StaticFiles +from io import BytesIO +from fastapi.responses import FileResponse, StreamingResponse from .api_types import ProteinEntry, UploadBody, UploadError, EditBody from .db import Database, bytea_to_str, str_to_bytea -from .protein import parse_protein_pdb, pdb_file_name, protein_name_taken +from .protein import parse_protein_pdb, pdb_file_name, protein_name_found, pdb_to_fasta from .setup import disable_cors, init_fastapi_app + app = init_fastapi_app() -disable_cors(app, origins=["http://0.0.0.0:5173", "http://localhost:5173"]) -# mount the data directory so we can easily access files through the url -app.mount("/data", StaticFiles(directory="src/data"), name="data") +disable_cors(app, origins=[os.environ["PUBLIC_FRONTEND_URL"]]) + + +@app.get("/pdb/{protein_name:str}") +def get_pdb_file(protein_name: str): + if protein_name_found(protein_name): + return FileResponse(pdb_file_name(protein_name), filename=protein_name + ".pdb") + + +@app.get("/fasta/{protein_name:str}") +def get_fasta_file(protein_name: str): + if protein_name_found(protein_name): + pdb = parse_protein_pdb(protein_name, encoding="file") + fasta = pdb_to_fasta(pdb) + return StreamingResponse( + BytesIO(fasta.encode()), + media_type="text/plain", + headers={ + "Content-Disposition": f"attachment; filename={protein_name}.fasta" + }, + ) # important to note the return type (response_mode) so frontend can generate that type through `./run.sh api` @@ -113,10 +133,8 @@ def delete_protein_entry(protein_name: str): # None return means success @app.post("/protein-upload", response_model=UploadError | None) def upload_protein_entry(body: UploadBody): - body.name = body.name.replace(" ", "_") - # check that the name is not already taken in the DB - if protein_name_taken(body.name): + if protein_name_found(body.name): return UploadError.NAME_NOT_UNIQUE # if name is unique, save the pdb file and add the entry to the database @@ -150,9 +168,6 @@ def upload_protein_entry(body: UploadBody): # TODO: add more edits, now only does name and content edits @app.put("/protein-edit", response_model=UploadError | None) def edit_protein_entry(body: EditBody): - body.new_name = body.new_name.replace(" ", "_") - body.old_name = body.old_name.replace(" ", "_") - # check that the name is not already taken in the DB # TODO: check if permission so we don't have people overriding other people's names diff --git a/docker-compose.yml b/docker-compose.yml index 4c517655..2877a16e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,6 +11,8 @@ services: - docker_node_modules:/app/node_modules/ ports: - "5173:5173" + environment: + PUBLIC_BACKEND_URL: http://localhost:8000 command: ["yarn", "dev", "--", "--host", "0.0.0.0"] backend: container_name: venome-backend @@ -25,6 +27,7 @@ services: depends_on: - postgres environment: + PUBLIC_FRONTEND_URL: http://localhost:5173 BACKEND_HOST: 0.0.0.0 BACKEND_PORT: 8000 DB_HOST: postgres diff --git a/frontend/src/lib/ListProteins.svelte b/frontend/src/lib/ListProteins.svelte new file mode 100644 index 00000000..50c7b437 --- /dev/null +++ b/frontend/src/lib/ListProteins.svelte @@ -0,0 +1,79 @@ + + + + + + + Protein name + Length + Mass (Da) + + + {#if allEntries} + {#each allEntries as entry} + { + goto(`/protein/${entry.name}`); + }} + > + {entry.name} + {entry.length} + {numberWithCommas(entry.mass)} + + {/each} + {/if} + +
+
+ +
+ {#if allEntries} + {#each allEntries as entry} + goto(`/protein/${entry.name}`)} + > +
+ {entry.name} +
+
+ Length: {entry.length}, Mass (Da): {numberWithCommas(entry.mass)} +
+
+ {/each} + {/if} +
+
+
+ + diff --git a/frontend/src/lib/backend.ts b/frontend/src/lib/backend.ts index 82696371..2146c417 100644 --- a/frontend/src/lib/backend.ts +++ b/frontend/src/lib/backend.ts @@ -1,7 +1,8 @@ export * from "../openapi"; export { DefaultService as Backend } from "../openapi"; import { OpenAPI } from "../openapi"; +import { env } from "$env/dynamic/public"; -const BACKEND_PORT = 8000; -const BACKEND_HOST = "localhost"; -OpenAPI.BASE = `http://${BACKEND_HOST}:${BACKEND_PORT}`; // backend server +export const BACKEND_URL = env["PUBLIC_BACKEND_URL"]; +if (!BACKEND_URL) throw new Error("PUBLIC_BACKEND_URL is not set in .env"); +OpenAPI.BASE = BACKEND_URL; diff --git a/frontend/src/lib/format.ts b/frontend/src/lib/format.ts index 93776499..b9e19c16 100644 --- a/frontend/src/lib/format.ts +++ b/frontend/src/lib/format.ts @@ -1,16 +1,11 @@ +import { goto } from "$app/navigation"; +import { writable, type Writable } from "svelte/store"; + export function numberWithCommas(x: number, round = 0) { const formatter = new Intl.NumberFormat("en-US"); return formatter.format(+x.toFixed(round)); } -export function formatProteinName(name: string) { - return name.replaceAll(" ", "_"); -} - -export function humanReadableProteinName(name: string) { - return name.replaceAll("_", " "); -} - export function fileToString(f: File): Promise { return new Promise((resolve, reject) => { const reader = new FileReader(); @@ -21,3 +16,34 @@ export function fileToString(f: File): Promise { reader.onerror = reject; }); } + +/** + * When I have a url parameter, I want to first set it to that + * Then, if the url parameter changes in svelte, I want to change the url in the browser parameter too + */ +export function writableUrlParams( + searchParams: URLSearchParams, + name: string +): Writable { + const param = writable(searchParams.get(name) ?? ""); + return { + subscribe: param.subscribe, + set(searchBy) { + searchParams.set(name, searchBy); // update url + goto(`?${searchParams.toString()}`, { + keepFocus: true, + replaceState: true, + noScroll: true, + }); // update browser top + }, + update() { + const searchBy = searchParams.get(name) ?? ""; + searchParams.set(name, searchBy); // update url + goto(`?${searchParams.toString()}`, { + keepFocus: true, + replaceState: true, + noScroll: true, + }); // update browser top + }, + }; +} diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index b38254ba..148b4855 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -1,98 +1,19 @@ - Home -
- - - - - Protein name - Length - Mass (Da) - - - {#if allEntries} - {#each allEntries as entry} - { - goto(`/protein/${entry.name}`); - }} - > - {humanReadableProteinName(entry.name)} - {entry.length} - {numberWithCommas(entry.mass)} - - {/each} - {/if} - -
-
- -
- {#if allEntries} - {#each allEntries as entry} - goto(`/protein/${entry.name}`)} - > -
- {humanReadableProteinName(entry.name)} -
-
- Length: {entry.length}, Mass (Da): {numberWithCommas( - entry.mass - )} -
-
- {/each} - {/if} -
-
-
-
+
Landing Page
diff --git a/frontend/src/routes/Header.svelte b/frontend/src/routes/Header.svelte index ba951f37..d99c4083 100644 --- a/frontend/src/routes/Header.svelte +++ b/frontend/src/routes/Header.svelte @@ -11,7 +11,7 @@ diff --git a/frontend/src/routes/edit/[proteinName]/+page.svelte b/frontend/src/routes/edit/[proteinName]/+page.svelte index a9aa8993..9a33f54b 100644 --- a/frontend/src/routes/edit/[proteinName]/+page.svelte +++ b/frontend/src/routes/edit/[proteinName]/+page.svelte @@ -1,9 +1,8 @@
@@ -48,7 +43,7 @@ {humanReadableProteinName(entry.name)}{entry.name} @@ -101,7 +96,11 @@
- PDB + {#each fileDownloadDropdown as fileType} + {fileType.toUpperCase()} + {/each}
- +
{:else if !error} diff --git a/frontend/src/routes/search/+page.svelte b/frontend/src/routes/search/+page.svelte new file mode 100644 index 00000000..d2c87b71 --- /dev/null +++ b/frontend/src/routes/search/+page.svelte @@ -0,0 +1,51 @@ + + + + Search + + +
+
{ + if (searchBy) { + allEntries = await Backend.searchEntries(searchBy); + $nameSearch = searchBy; // update url param + } else { + allEntries = await Backend.getAllEntries(); + } + }} + > + + + + +
diff --git a/frontend/src/routes/searchtest/[query]/+page.svelte b/frontend/src/routes/searchtest/[query]/+page.svelte deleted file mode 100644 index 6f9a1623..00000000 --- a/frontend/src/routes/searchtest/[query]/+page.svelte +++ /dev/null @@ -1,101 +0,0 @@ - - - - - - Home - - -
- - - - - Protein name - Length - Mass (Da) - - - {#if allEntries} - {#each allEntries as entry} - { - goto(`/protein/${entry.name}`); - }} - > - {humanReadableProteinName(entry.name)} - {entry.length} - {numberWithCommas(entry.mass)} - - {/each} - {/if} - -
-
- -
- {#if allEntries} - {#each allEntries as entry} - goto(`/protein/${entry.name}`)} - > -
- {humanReadableProteinName(entry.name)} -
-
- Length: {entry.length}, Mass (Da): {numberWithCommas( - entry.mass - )} -
-
- {/each} - {/if} -
-
-
-
- - diff --git a/frontend/src/routes/searchtest/[query]/+page.ts b/frontend/src/routes/searchtest/[query]/+page.ts deleted file mode 100644 index 1764b7d5..00000000 --- a/frontend/src/routes/searchtest/[query]/+page.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * This scrapes the data from the route - * ie. /protein/your_mom -> { proteinName: "your_mom" } - */ -export function load({ params }) { - return { query: params.query }; -} diff --git a/frontend/src/routes/upload/+page.svelte b/frontend/src/routes/upload/+page.svelte index 0403f57d..ff8e86c1 100644 --- a/frontend/src/routes/upload/+page.svelte +++ b/frontend/src/routes/upload/+page.svelte @@ -11,7 +11,7 @@ } from "flowbite-svelte"; import { ChevronDownSolid } from "flowbite-svelte-icons"; import { goto } from "$app/navigation"; - import { formatProteinName, fileToString } from "$lib/format"; + import { fileToString } from "$lib/format"; import ArticleEditor from "$lib/ArticleEditor.svelte"; type Organism = { @@ -97,7 +97,7 @@ console.log(uploadError); } else { // success, so we can go back! - goto(`/protein/${formatProteinName(name)}`); + goto(`/protein/${name}`); } } catch (e) { console.log(e);