Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# FPbase - Fluorescent Protein Database

Django web app for <https://www.fpbase.org> with React frontend. PostgreSQL database, REST + GraphQL APIs, Celery background tasks.

## Tech Stack

**Backend**: Django 5.2, Python 3.13, DRF, GraphQL (graphene-django), PostgreSQL 15, Celery + Redis
**Frontend**: React 16, Webpack, pnpm monorepo (packages: spectra, blast use Vite)
**Science**: BioPython, NumPy, Pandas, SciPy, Matplotlib

## Key Overrides

- **Line length: 119 chars** (not 88) - configured in pyproject.toml
- Frontend uses pnpm, not npm
- Python uses `uv` for virtualenv management and running commands

## Common Commands

```bash
# Setup
uv sync # Install/update Python deps
pnpm install # Install Node deps

# Development
pnpm dev # Start webpack + Django dev server
uv run backend/manage.py shell_plus # Interactive shell with auto-imports

# Testing
uv run pytest path/to/test.py # Run specific test
uv run pytest --cov # Run with coverage
pnpm --filter @fpbase/spectra test:ci # Frontend tests

# Code Quality
ruff format backend # Format Python
ruff check --fix backend # Auto-fix linting
uv run mypy backend # Type check
pre-commit run --all-files # Run all hooks

# Build
pnpm build # Build all frontend packages
```

## Project Structure

```
backend/
├── config/ # Django settings (base/local/production/test)
├── fpbase/ # Main app (users, site-wide)
├── proteins/ # Core app (models, APIs, views)
├── fpseq/ # Sequence analysis module
├── references/ # Citations
└── favit/ # Favorites

frontend/ # Main React app (Webpack)
packages/
├── spectra/ # Spectral viewer (Vite + React)
└── blast/ # BLAST interface (Vite + React)
```

## Testing Notes

- Tests colocated with apps: `proteins/tests/`, `fpbase/tests/`
- pytest with Django plugin, --reuse-db enabled
- factory-boy for test data
- Selenium + Playwright for browser tests
- Settings: `config.settings.test`
- Coverage target: 100% (soft), PRs: 5% for new code

## Important Tools

- **django-extensions**: `shell_plus` auto-imports models
- **django-debug-toolbar**: Available in local dev
- **django-reversion**: Model history tracking enabled
- **Algolia**: Full-text search integration
- **Sentry**: Error tracking (production)
- **Celery**: Background tasks (e.g., BLAST searches)

## Development Notes

- Django settings via django-environ (loads from .env)
- Pre-commit hooks auto-run (ruff format, ruff check --fix, django-upgrade)
- Database: PostgreSQL required (not SQLite)
- Dual APIs: REST (drf-spectacular docs) + GraphQL (JWT auth)
- Static files: WhiteNoise with Brotli compression
- Deployment: Heroku (Procfile: web=gunicorn, worker=celery)

## Gotchas

- Run `pnpm build` after frontend changes before Django tests if using `uses_frontend` fixture
- Migrations in proteins/ are extensive - review carefully before changing models
- React 16 is legacy version - upgrades planned but not yet done
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,10 @@ backend/blastdb/
.pdm-python
SemrockSpectra/

# Playwright
test-results/
playwright-report/
playwright/.cache/
# Memray profiling outputs
memray_*/
*.bin
Expand Down
2 changes: 1 addition & 1 deletion backend/fpbase/tests/test_end2end.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def _assert_no_console_errors(self):
logs = self.browser.get_log("browser")
for lg in logs:
if lg["level"] == "SEVERE":
raise AssertionError(f"Console errors occurred: {logs}")
raise AssertionError(f"Console errors occurred: {lg['message']}")

def test_spectra(self):
self._load_reverse("proteins:spectra")
Expand Down
7 changes: 4 additions & 3 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@
"nouislider": "^14.7.0",
"process": "^0.11.10",
"progressbar.js": "^1.1.0",
"react": "^16.14.0",
"react-dom": "^16.14.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"regenerator-runtime": "^0.13.5",
"select2": "^4.0.13",
"select2-theme-bootstrap4": "^0.2.0-beta.6",
Expand All @@ -39,7 +39,8 @@
"@babel/preset-react": "^7.7.4",
"@sentry/cli": "^2.21.2",
"@sentry/webpack-plugin": "^2.23.0",
"@types/react": "^16.14.42",
"@types/react": "^19",
"@types/react-dom": "^19",
"@welldone-software/why-did-you-render": "^6.2.3",
"autoprefixer": "^10.4.14",
"babel-eslint": "^10.0.3",
Expand Down
5 changes: 3 additions & 2 deletions frontend/src/blast-app.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from "react"
import { render } from "react-dom"
import { createRoot } from "react-dom/client"
import App from "@fpbase/blast"

render(<App />, document.getElementById("blast-app"))
const root = createRoot(document.getElementById("blast-app"))
root.render(<App />)
8 changes: 4 additions & 4 deletions frontend/src/simple-spectra-viewer.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import $ from "jquery"
import "./js/detect-touch"
import { createElement } from "react"
import { render } from "react-dom"
import { createRoot } from "react-dom/client"
import { SimpleSpectraViewer } from "@fpbase/spectra"

const elem = document.getElementById("spectra-viewer")

window.onload = function() {
render(
const root = createRoot(elem)
root.render(
createElement(SimpleSpectraViewer, {
ids: JSON.parse(elem.getAttribute("data-spectra")),
options: JSON.parse(elem.getAttribute("data-options")),
hidden: JSON.parse(elem.getAttribute("data-hidden")) || [],
}),
elem
})
)
}
const name = elem.getAttribute("data-name")
Expand Down
8 changes: 3 additions & 5 deletions frontend/src/spectra-viewer.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import "./js/detect-touch"
import { createElement } from "react"
import { render } from "react-dom"
import { createRoot } from "react-dom/client"
import App from "@fpbase/spectra"

render(
createElement(App, { uri: "/graphql/" }, null),
document.getElementById("spectra-viewer")
)
const root = createRoot(document.getElementById("spectra-viewer"))
root.render(createElement(App, { uri: "/graphql/" }, null))
7 changes: 6 additions & 1 deletion frontend/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,12 @@ const jsRule = {
corejs: 3,
},
],
"@babel/preset-react",
[
"@babel/preset-react",
{
runtime: "automatic",
},
],
],
plugins: ["@babel/plugin-syntax-dynamic-import"],
},
Expand Down
6 changes: 6 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,9 @@ pgpull:
dropdb fpbase
heroku pg:pull DATABASE_URL fpbase --exclude-table-data='public.django_session;public.proteins_ocfluoreff' -a fpbase || true
uv run backend/manage.py migrate

frontend:
pnpm --stream -r start

backend:
uv run backend/manage.py runserver
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"clean": "rm -rf node_modules && pnpm -r exec -- rm -rf node_modules && pnpm -r run clean"
},
"devDependencies": {
"@playwright/test": "^1.56.1",
"concurrently": "^8.2.0",
"playwright": "^1.56.1"
},
Expand All @@ -18,5 +19,5 @@
"engines": {
"node": "22.x"
},
"packageManager": "pnpm@10.18.3"
"packageManager": "pnpm@10.19.0"
}
13 changes: 8 additions & 5 deletions packages/blast/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,20 @@
"author": "Talley Lambert",
"license": "ISC",
"dependencies": {
"@material-ui/core": "^4.12.4",
"@material-ui/icons": "^4.11.3",
"js-cookie": "^3.0.5",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@mui/icons-material": "^5.16.10",
"@mui/material": "^5.16.10",
"@mui/styles": "^5.16.10",
"jquery": "^3.7.0",
"react-bootstrap": "^1.6.7"
"js-cookie": "^3.0.5"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.0.0",
"vite": "^4.3.9"
},
"peerDependencies": {
"react": "^16.14.0"
"react": "^19.2.0",
"react-dom": "^19.2.0"
}
}
57 changes: 30 additions & 27 deletions packages/blast/src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import React, { useState } from "react"
import InputForm from "./InputForm.jsx"
import BlastReport from "./ReportView.jsx"
import Form from "react-bootstrap/Form"
import Row from "react-bootstrap/Row"
import Col from "react-bootstrap/Col"
import { Box, Grid, FormControl, InputLabel, Select, MenuItem, Typography } from "@mui/material"
import $ from "jquery"

function ReportSelect({ reports, binary, index, onChange }) {
Expand All @@ -14,30 +12,35 @@ function ReportSelect({ reports, binary, index, onChange }) {
}

return (
<Form>
<Form.Group as={Row} controlId="reportSelect" className="mt-4">
<Form.Label
column
sm={3}
md={2}
className="font-weight-bold"
style={{ color: "#5b616b" }}
>
Results for:
</Form.Label>
<Col sm={9} md={10}>
<Form.Control as="select" onChange={handleChange} value={index}>
{reports.map((item, i) => (
<option key={i} value={i}>
{`${item.report.results.search.query_id}: ${
item.report.results.search.query_title
} (${item.report.results.search.query_len}${unit})`}
</option>
))}
</Form.Control>
</Col>
</Form.Group>
</Form>
<Box sx={{ mt: 4 }}>
<Grid container spacing={2} alignItems="center">
<Grid item xs={12} sm={3} md={2}>
<Typography variant="body1" sx={{ fontWeight: 'bold', color: '#5b616b' }}>
Results for:
</Typography>
</Grid>
<Grid item xs={12} sm={9} md={10}>
<FormControl fullWidth>
<InputLabel id="report-select-label">Select Report</InputLabel>
<Select
labelId="report-select-label"
id="report-select"
value={index}
label="Select Report"
onChange={handleChange}
>
{reports.map((item, i) => (
<MenuItem key={i} value={i}>
{`${item.report.results.search.query_id}: ${
item.report.results.search.query_title
} (${item.report.results.search.query_len}${unit})`}
</MenuItem>
))}
</Select>
</FormControl>
</Grid>
</Grid>
</Box>
)
}

Expand Down
35 changes: 21 additions & 14 deletions packages/blast/src/InputForm.jsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,37 @@
import React from "react"
import Cookies from "js-cookie"
import Form from "react-bootstrap/Form"
import Button from "react-bootstrap/Button"
import { Box, TextField, Button, FormHelperText } from "@mui/material"

function InputForm({ handleSubmit, initialValue }) {

return (
<Form onSubmit={handleSubmit}>
<Box component="form" onSubmit={handleSubmit}>
<input
type="hidden"
name="csrfmiddlewaretoken"
value={Cookies.get("csrftoken")}
/>
<Form.Group controlId="queryInput">
<Form.Label>Enter Query</Form.Label>
<Form.Control required defaultValue={initialValue} name="query" as="textarea" rows="4" />
<Form.Text className="text-muted">
Single sequence or multiple sequences in FASTA format. Accepts either
amino acid or nucleotide sequences, but all sequences in a query be of
the same type.
</Form.Text>
</Form.Group>
<Button variant="secondary" type="submit">
<TextField
id="queryInput"
required
fullWidth
multiline
rows={4}
label="Enter Query"
name="query"
defaultValue={initialValue}
variant="outlined"
margin="normal"
/>
<FormHelperText sx={{ mt: -1, mb: 2 }}>
Single sequence or multiple sequences in FASTA format. Accepts either
amino acid or nucleotide sequences, but all sequences in a query be of
the same type.
</FormHelperText>
<Button variant="contained" color="primary" type="submit">
Submit
</Button>
</Form>
</Box>
)
}

Expand Down
14 changes: 7 additions & 7 deletions packages/blast/src/ReportDescription.jsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import React from "react"
import Table from "@material-ui/core/Table"
import TableBody from "@material-ui/core/TableBody"
import TableCell from "@material-ui/core/TableCell"
import TableHead from "@material-ui/core/TableHead"
import TableRow from "@material-ui/core/TableRow"
import Paper from "@material-ui/core/Paper"
import { makeStyles } from "@material-ui/core/styles"
import Table from "@mui/material/Table"
import TableBody from "@mui/material/TableBody"
import TableCell from "@mui/material/TableCell"
import TableHead from "@mui/material/TableHead"
import TableRow from "@mui/material/TableRow"
import Paper from "@mui/material/Paper"
import { makeStyles } from "@mui/styles"

function fpbaseLink(accession) {
return (
Expand Down
Loading