Skip to content

Commit

Permalink
feat: add references in the article (#89)
Browse files Browse the repository at this point in the history
* feat: add bibtex parser

* feat: successfully parse the bibtex and display it

* refac: move the references to separate component

* Add tabs for the preview

* feat: preview

* fix: add error if parse doesn't work

* feat: replace cite with link

* feat: style references

* feat: move edit button and change download to dropdown

* feat: display the references too

* sh run.sh gen_api

* feat: upload page add refs

* sh run.sh gen_api

* feat: add edit functions to refs

* refac: move article editor in separate component

* sh run.sh gen_api

* feat: restyle so everything fits and references highlight

* fix: remove base64 encoding and use raw pdb text

* sh run.sh gen_api
  • Loading branch information
xnought authored Dec 1, 2023
1 parent f150536 commit 4e7b667
Show file tree
Hide file tree
Showing 16 changed files with 297 additions and 87 deletions.
12 changes: 8 additions & 4 deletions backend/init.sql
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ CREATE TABLE proteins (
name text NOT NULL UNIQUE PRIMARY KEY, -- user specified name of the protein (TODO: consider having a string limit)
length integer, -- length of amino acid sequence
mass numeric, -- mass in amu/daltons
content bytea -- stored markdown for the protein article (TODO: consider having a limit to how big this can be)
content bytea, -- stored markdown for the protein article (TODO: consider having a limit to how big this can be)
refs bytea -- bibtex references mentioned in the content/article
);

/*
Expand All @@ -45,24 +46,27 @@ CREATE TABLE species (
/*
* Inserts example proteins into proteins table
*/
INSERT INTO proteins (name, length, mass, content) VALUES (
INSERT INTO proteins (name, length, mass, content, refs) VALUES (
'Gh_comp271_c0_seq1',
0,
0.0,
null,
null
);

INSERT INTO proteins (name, length, mass, content) VALUES (
INSERT INTO proteins (name, length, mass, content, refs) VALUES (
'Lb17_comp535_c2_seq1',
0,
0.0,
null,
null
);

INSERT INTO proteins (name, length, mass, content) VALUES (
INSERT INTO proteins (name, length, mass, content, refs) VALUES (
'Lh14_comp2336_c0_seq1',
0,
0.0,
null,
null
);

Expand Down
5 changes: 4 additions & 1 deletion backend/src/api_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class ProteinEntry(CamelModel):
length: int
mass: float
content: str | None = None
refs: str | None = None


class AllEntries(CamelModel):
Expand All @@ -37,7 +38,8 @@ class AllEntries(CamelModel):
class UploadBody(CamelModel):
name: str
content: str # markdown content from user
pdb_file_base64: str
refs: str # references used in content (bibtex form)
pdb_file_str: str


class UploadError(str, enum.Enum):
Expand All @@ -55,3 +57,4 @@ class EditBody(CamelModel):
old_name: str # so we can identify the exact row we need to change
new_name: str
new_content: str | None = None
new_refs: str | None = None
40 changes: 28 additions & 12 deletions backend/src/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def get_protein_entry(protein_name: str):
with Database() as db:
try:
entry_sql = db.execute_return(
"""SELECT name, length, mass, content FROM proteins
"""SELECT name, length, mass, content, refs FROM proteins
WHERE name = %s""",
[protein_name],
)
Expand All @@ -53,13 +53,16 @@ def get_protein_entry(protein_name: str):
if entry_sql is not None and len(entry_sql) != 0:
# return the only entry
only_returned_entry = entry_sql[0]
name, length, mass, content = only_returned_entry
# if bytes are present, decode them into a string
name, length, mass, content, refs = only_returned_entry

# if byte arrays are present, decode them into a string
if content is not None:
content = bytea_to_str(content)
if refs is not None:
refs = bytea_to_str(refs)

return ProteinEntry(
name=name, length=length, mass=mass, content=content
name=name, length=length, mass=mass, content=content, refs=refs
)

except Exception as e:
Expand Down Expand Up @@ -95,7 +98,7 @@ def upload_protein_entry(body: UploadBody):
# if name is unique, save the pdb file and add the entry to the database
try:
# TODO: consider somehow sending the file as a stream instead of a b64 string or send as regular string
pdb = parse_protein_pdb(body.name, body.pdb_file_base64, encoding="b64")
pdb = parse_protein_pdb(body.name, body.pdb_file_str)
except Exception:
return UploadError.PARSE_ERROR

Expand All @@ -107,12 +110,13 @@ def upload_protein_entry(body: UploadBody):
# save to db
with Database() as db:
db.execute(
"""INSERT INTO proteins (name, length, mass, content) VALUES (%s, %s, %s, %s);""",
"""INSERT INTO proteins (name, length, mass, content, refs) VALUES (%s, %s, %s, %s, %s);""",
[
pdb.name,
pdb.num_amino_acids,
pdb.mass_daltons,
str_to_bytea(body.content),
str_to_bytea(body.refs),
],
)
except Exception:
Expand All @@ -133,21 +137,33 @@ def edit_protein_entry(body: EditBody):
os.rename(pdb_file_name(body.old_name), pdb_file_name(body.new_name))

with Database() as db:
# if we have content/markdown, then update it, otherwise just update the name
if body.new_content is not None:
if body.new_name != body.old_name:
db.execute(
"""UPDATE proteins SET name = %s, content = %s WHERE name = %s""",
"""UPDATE proteins SET name = %s WHERE name = %s""",
[
body.new_name,
body.old_name,
],
)

if body.new_content is not None:
db.execute(
"""UPDATE proteins SET content = %s WHERE name = %s""",
[
str_to_bytea(body.new_content),
body.old_name,
],
)
else:

if body.new_refs is not None:
db.execute(
"""UPDATE proteins SET name = %s WHERE name = %s""",
[body.new_name, body.old_name],
"""UPDATE proteins SET refs = %s WHERE name = %s""",
[
str_to_bytea(body.new_refs),
body.old_name,
],
)

except Exception:
return UploadError.WRITE_ERROR

Expand Down
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
},
"type": "module",
"dependencies": {
"bibtex": "^0.9.0",
"marked": "^10.0.0"
}
}
60 changes: 60 additions & 0 deletions frontend/src/lib/ArticleEditor.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<script lang="ts">
import {
Card,
Tabs,
TabItem,
Heading,
Label,
Textarea,
} from "flowbite-svelte";
import Markdown from "$lib/Markdown.svelte";
import References from "$lib/References.svelte";
export let refs = "";
export let content = "";
</script>

<Card
class="max-w-full"
style="height: 600px; overflow-y: scroll; padding: 0; padding-top: 4px; padding-left: 4px;"
>
<Tabs contentClass="bg-none p-5" style="underline">
<TabItem title="article content" open>
<div>
<Label for="content" class="block mb-2"
>Protein Article (Markdown)</Label
>
<Textarea
id="content"
placeholder="Enter markdown..."
rows={12}
bind:value={content}
/>
</div>

<div class="mt-3">
<Label for="refs" class="block mb-2">References (BibTeX)</Label>
<Textarea
id="refs"
placeholder="Enter bibtex with atleast an id, title, and author (optionally url and year)"
rows={4}
bind:value={refs}
/>
</div>
</TabItem>
<TabItem title="preview">
{#if content.length > 0 || refs.length > 0}
<Card class="max-w-full">
<Heading tag="h4">Article</Heading>
<Markdown text={content} />
</Card>

<Card class="max-w-full mt-5">
<Heading tag="h4">References</Heading>
<References bibtex={String.raw`${refs}`} />
</Card>
{:else}
No content to render/preview
{/if}
</TabItem>
</Tabs>
</Card>
16 changes: 15 additions & 1 deletion frontend/src/lib/Markdown.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,21 @@
export let text = ``;
// they recommend also sanitizing input text https://github.com/cure53/DOMPurify
$: mdToHTML = marked(text);
$: mdToHTML = marked(replaceCite(text));
/**
* @todo this is a hacky way to do this, but it works for now
* Instead use the builtin extensions https://marked.js.org/using_pro#extensions
*/
function replaceCite(str: string) {
// replace \cite{} with <a href="#ref-1">[1]</a>
const newStr = str.replaceAll(/\\cite{(.+?)}/g, (match, p1) => {
console.log(match, p1);
return `[<a href="#${p1}">${p1}</a>]`;
});
return newStr;
}
</script>

{@html mdToHTML}
68 changes: 68 additions & 0 deletions frontend/src/lib/References.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<script lang="ts">
/* Put stuff here */
import { parseBibFile, type BibEntry, BibFilePresenter } from "bibtex";
export let bibtex = String.raw``;
let bib: BibFilePresenter;
$: {
try {
bib = parseBibFile(bibtex);
} catch (e) {
console.log("error in syntax");
}
}
/**
* @returns string of authors
*/
function parseAuthors(entry: BibEntry) {
const authors = entry.getFieldAsString("author");
// if a number or not found, error in parsing, so do nothing
if (!authors || typeof authors === "number")
return "[error in parsing authors]";
const parsed = authors.split(" and ").map((author) =>
author
.split(",")
.map((d) => d.trim())
.reverse()
.join(" ")
);
return new Intl.ListFormat("en").format(parsed);
}
</script>

{#if bib}
{#each bib.entries_raw as entry, i}
<div class={i > 0 ? "mt-5" : ""} id={entry._id}>
<div class="bg-gray-50 text-gray-400">
[<span style="font-size: 15px;">{entry._id}</span>]
</div>
<div class="border-l-2 border-gray-400 pl-2">
<div style="font-size: 17px;">
{#if entry.getFieldAsString("url")}
<a href={`${entry.getFieldAsString("url")}`}>
<b>
{entry.getFieldAsString("title")}
</b>
</a>
{:else}
<b>
{entry.getFieldAsString("title")}
</b>
{/if}
</div>

<p>{parseAuthors(entry)}</p>
{#if entry.getFieldAsString("journal")}
<i>
{entry.getFieldAsString("journal")}
{entry.getFieldAsString("year")}
</i>
{/if}
</div>
</div>
{/each}
{:else}
<span class="text-red-700 font-bold"> BibTeX Syntax Error </span>
{/if}
11 changes: 11 additions & 0 deletions frontend/src/lib/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,14 @@ export function formatProteinName(name: string) {
export function humanReadableProteinName(name: string) {
return name.replaceAll("_", " ");
}

export function fileToString(f: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsText(f);
reader.onload = () => {
resolve(reader.result as string);
};
reader.onerror = reject;
});
}
1 change: 1 addition & 0 deletions frontend/src/openapi/models/EditBody.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ export type EditBody = {
oldName: string;
newName: string;
newContent?: (string | null);
newRefs?: (string | null);
};

1 change: 1 addition & 0 deletions frontend/src/openapi/models/ProteinEntry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ export type ProteinEntry = {
length: number;
mass: number;
content?: (string | null);
refs?: (string | null);
};

3 changes: 2 additions & 1 deletion frontend/src/openapi/models/UploadBody.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
export type UploadBody = {
name: string;
content: string;
pdbFileBase64: string;
refs: string;
pdbFileStr: string;
};

Loading

0 comments on commit 4e7b667

Please sign in to comment.