Skip to content

Commit be36999

Browse files
Update documentation to reflect refactor
1 parent b960c56 commit be36999

File tree

18 files changed

+321
-432
lines changed

18 files changed

+321
-432
lines changed

.github/workflows/publish.yml

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -62,14 +62,14 @@ jobs:
6262
6363
- name: Set required env variables
6464
run: |
65-
echo "DB_USER=postgres" >> $GITHUB_ENV
66-
echo "DB_PASSWORD=postgres" >> $GITHUB_ENV
67-
echo "DB_HOST=127.0.0.1" >> $GITHUB_ENV
68-
echo "DB_PORT=5432" >> $GITHUB_ENV
69-
echo "DB_NAME=test_db" >> $GITHUB_ENV
70-
echo "SECRET_KEY=$(openssl rand -base64 32)" >> $GITHUB_ENV
71-
echo "BASE_URL=http://localhost:8000" >> $GITHUB_ENV
72-
echo "RESEND_API_KEY=resend_api_key" >> $GITHUB_ENV
65+
echo "DB_USER=postgres" > _environment
66+
echo "DB_PASSWORD=postgres" >> _environment
67+
echo "DB_HOST=127.0.0.1" >> _environment
68+
echo "DB_PORT=5432" >> _environment
69+
echo "DB_NAME=test_db" >> _environment
70+
echo "SECRET_KEY=$(openssl rand -base64 32)" >> _environment
71+
echo "BASE_URL=http://localhost:8000" >> _environment
72+
echo "RESEND_API_KEY=resend_api_key" >> _environment
7373
7474
- name: Setup Graphviz
7575
uses: ts-graphviz/setup-graphviz@v2

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
__pycache__
33
*.pyc
44
.env
5+
_environment
56
/.quarto/
67
_docs/
8+
*.ipynb
79
.pytest_cache/
810
.mypy_cache/
911
node_modules

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,15 +164,15 @@ Make sure the development database is running and tables and default
164164
permissions/roles are created first.
165165

166166
``` bash
167-
uvicorn main:app --host 0.0.0.0 --port 8000 --reload
167+
uv run uvicorn main:app --host 0.0.0.0 --port 8000 --reload
168168
```
169169

170170
Navigate to http://localhost:8000/
171171

172172
### Lint types with mypy
173173

174174
``` bash
175-
mypy .
175+
uv run mypy .
176176
```
177177

178178
## Developing with LLMs

docs/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/.quarto/

docs/architecture.qmd

Lines changed: 4 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ with dot.subgraph(name='cluster_client') as client:
2525
# Create server subgraph below
2626
with dot.subgraph(name='cluster_server') as server:
2727
server.attr(label='Server')
28-
server.node('C', 'Convert to Pydantic model', fillcolor='lightgreen', style='rounded,filled')
29-
server.node('D', 'Optional custom validation', fillcolor='lightgreen', style='rounded,filled')
28+
server.node('C', 'FastAPI request validation in route signature', fillcolor='lightgreen', style='rounded,filled')
29+
server.node('D', 'Business logic validation in route function body', fillcolor='lightgreen', style='rounded,filled')
3030
server.node('E', 'Update database', fillcolor='lightgreen', style='rounded,filled')
3131
server.node('F', 'Middleware error handler', fillcolor='lightgreen', style='rounded,filled')
3232
server.node('G', 'Render error template', fillcolor='lightgreen', style='rounded,filled')
@@ -59,103 +59,6 @@ dot.render('static/data_flow', format='png', cleanup=True)
5959

6060
![Data flow diagram](static/data_flow.png)
6161

62-
The advantage of the PRG pattern is that it is very straightforward to implement and keeps most of the rendering logic on the server side. The disadvantage is that it requires an extra round trip to the database to fetch the updated data, and re-rendering the entire page template may be less efficient than a partial page update on the client side.
62+
The advantage of the PRG pattern is that it is very straightforward to implement and keeps most of the rendering logic on the server side. One disadvantage is that it requires an extra round trip to the database to fetch the updated data, and re-rendering the entire page template may be less efficient than a partial page update on the client side. Another disadvantage is that it if the user makes an invalid form submission, they will see an error page and will have to click the browser's "back" button to get back to the form with their original form inputs.
6363

64-
## Form validation flow
65-
66-
We've experimented with several approaches to validating form inputs in the FastAPI endpoints.
67-
68-
### Objectives
69-
70-
Ideally, on an invalid input, we would redirect the user back to the form, preserving their inputs and displaying an error message about which input was invalid.
71-
72-
This would keep the error handling consistent with the PRG pattern described in the [Architecture](https://promptlytechnologies.com/fastapi-jinja2-postgres-webapp/docs/architecture) section of this documentation.
73-
74-
To keep the code DRY, we'd also like to handle such validation with Pydantic dependencies, Python exceptions, and exception-handling middleware as much as possible.
75-
76-
### Obstacles
77-
78-
One challenge is that if we redirect back to the page with the form, the page is re-rendered with empty form fields.
79-
80-
This can be overcome by passing the inputs from the request as context variables to the template.
81-
82-
But that's a bit clunky, because then we have to support form-specific context variables in every form page and corresponding GET endpoint.
83-
84-
Also, we have to:
85-
86-
1. access the request object (which is not by default available to our middleware), and
87-
2. extract the form inputs (at least one of which is invalid in this error case), and
88-
3. pass the form inputs to the template (which is a bit challenging to do in a DRY way since there are different sets of form inputs for different forms).
89-
90-
Solving these challenges is possible, but gets high-complexity pretty quickly.
91-
92-
### Approaches
93-
94-
The best solution, I think, is to use really robust client-side form validation to prevent invalid inputs from being sent to the server in the first place. That makes it less important what we do on the server side, although we still need to handle the server-side error case as a backup in the event that something slips past our validation on the client side.
95-
96-
Here are some patterns we've considered for server-side error handling:
97-
98-
<style>
99-
.styled-table, .styled-table th, .styled-table td {
100-
border: 1px solid black;
101-
padding: 8px;
102-
border-collapse: collapse;
103-
}
104-
105-
.styled-table th:nth-child(1) { width: 50%; }
106-
.styled-table th:nth-child(2),
107-
.styled-table th:nth-child(3),
108-
.styled-table th:nth-child(4) { width: 15%; }
109-
.styled-table th:nth-child(5) { width: 10%; }
110-
</style>
111-
112-
<table class="styled-table">
113-
<thead>
114-
<tr>
115-
<th>Approach</th>
116-
<th>Returns to same page</th>
117-
<th>Preserves form inputs</th>
118-
<th>Follows PRG pattern</th>
119-
<th>Complexity</th>
120-
</tr>
121-
</thead>
122-
<tbody>
123-
<tr>
124-
<td>Validate with Pydantic dependency, catch and redirect from middleware (with exception message as context) to an error page with "go back" button</td>
125-
<td>No</td>
126-
<td>Yes</td>
127-
<td>Yes</td>
128-
<td>Low</td>
129-
</tr>
130-
<tr>
131-
<td>Validate in FastAPI endpoint function body, redirect to origin page with error message query param</td>
132-
<td>Yes</td>
133-
<td>No</td>
134-
<td>Yes</td>
135-
<td>Medium</td>
136-
</tr>
137-
<tr>
138-
<td>Validate in FastAPI endpoint function body, redirect to origin page with error message query param and form inputs as context so we can re-render page with original form inputs</td>
139-
<td>Yes</td>
140-
<td>Yes</td>
141-
<td>Yes</td>
142-
<td>High</td>
143-
</tr>
144-
<tr>
145-
<td>Validate with Pydantic dependency, use session context to get form inputs from request, redirect to origin page from middleware with exception message and form inputs as context so we can re-render page with original form inputs</td>
146-
<td>Yes</td>
147-
<td>Yes</td>
148-
<td>Yes</td>
149-
<td>High</td>
150-
</tr>
151-
<tr>
152-
<td>Validate in either Pydantic dependency or function endpoint body and directly return error message or error toast HTML partial in JSON, then mount error toast with HTMX or some simple layout-level Javascript</td>
153-
<td>Yes</td>
154-
<td>Yes</td>
155-
<td>No</td>
156-
<td>Low</td>
157-
</tr>
158-
</tbody>
159-
</table>
160-
161-
Presently this template primarily uses option 1 but also supports option 2. Ultimately, I think option 5 will be preferable; support for that [is planned](https://github.com/Promptly-Technologies-LLC/fastapi-jinja2-postgres-webapp/issues/5) for a future update or fork of this template.
64+
A future iteration of this application will use HTMX to update the page in place, so that on an invalid submission an error toast is displayed without a page reload (thus preserving the user's scroll position and form inputs).

docs/contributing.qmd

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,17 @@ To contribute code to the project:
4343

4444
### Rendering the documentation
4545

46-
The README and documentation website are rendered with [Quarto](https://quarto.org/docs/). If you ,make changes to the `.qmd` files in the root folder and the `docs` folder, run the following commands to re-render the docs:
46+
The README and documentation website are rendered with [Quarto](https://quarto.org/docs/). If you make changes to the `.qmd` files in the root folder and the `docs` folder, you will need to re-render the docs with Quarto.
47+
48+
Quarto expects environment variables to be set in a file called `_environment`, so before running Quarto render commands, you should copy your `.env` file to `_environment`.
4749

4850
``` bash
51+
# To copy the .env file to _environment
52+
cp .env _environment
4953
# To render the documentation website
50-
quarto render
54+
uv run quarto render
5155
# To render the README
52-
quarto render index.qmd --output-dir . --output README.md --to gfm
56+
uv run quarto render index.qmd --output-dir . --output README.md --to gfm
5357
```
5458

5559
Due to a quirk of Quarto, an unnecessary `index.html` file is created in the root folder when the README is rendered. This file can be safely deleted.
@@ -74,5 +78,5 @@ When creating new features,
7478
To publish the documentation to GitHub Pages, run the following command:
7579

7680
``` bash
77-
quarto publish gh-pages
81+
uv run quarto publish gh-pages
7882
```

docs/customization.qmd

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,19 @@ The following fixtures, defined in `tests/conftest.py`, are available in the tes
3131
- `engine`: Creates a new SQLModel engine for the test database.
3232
- `set_up_database`: Sets up the test database before running the test suite by dropping all tables and recreating them to ensure a clean state.
3333
- `session`: Provides a session for database operations in tests.
34-
- `clean_db`: Cleans up the database tables before each test by deleting all entries in the `PasswordResetToken` and `User` tables.
34+
- `clean_db`: Cleans up the database tables before each test by deleting all entries in the `PasswordResetToken`, `EmailUpdateToken`, `User`, `Role`, `Organization`, and `Account` tables.
35+
- `test_account`: Creates a test account with a predefined email and hashed password.
36+
- `test_user`: Creates a test user in the database linked to the test account.
3537
- `auth_client`: Provides a `TestClient` instance with access and refresh token cookies set, overriding the `get_session` dependency to use the `session` fixture.
3638
- `unauth_client`: Provides a `TestClient` instance without authentication cookies set, overriding the `get_session` dependency to use the `session` fixture.
37-
- `test_user`: Creates a test user in the database with a predefined name, email, and hashed password.
39+
- `test_organization`: Creates a test organization for use in tests.
3840

3941
To run the tests, use these commands:
4042

41-
- Run all tests: `pytest`
42-
- Run tests in debug mode (includes logs and print statements in console output): `pytest -s`
43-
- Run particular test files by name: `pytest <test_file_name>`
44-
- Run particular tests by name: `pytest -k <test_name>`
43+
- Run all tests: `uv run pytest`
44+
- Run tests in debug mode (includes logs and print statements in console output): `uv run pytest -s`
45+
- Run particular test files by name: `uv run pytest <test_file_name>`
46+
- Run particular tests by name: `uv run pytest -k <test_name>`
4547

4648
### Type checking with mypy
4749

@@ -72,11 +74,13 @@ We also create POST endpoints, which accept form submissions so the user can cre
7274
#### Customizable folders and files
7375

7476
- FastAPI application entry point and homepage GET route: `main.py`
75-
- FastAPI POST routes: `routers/`
76-
- User authentication endpoints: `authentication.py`
77+
- FastAPI routes: `routers/`
78+
- Account and authentication endpoints: `account.py`
7779
- User profile management endpoints: `user.py`
7880
- Organization management endpoints: `organization.py`
7981
- Role management endpoints: `role.py`
82+
- Dashboard page: `dashboard.py`
83+
- Static pages (e.g., about, privacy policy, terms of service): `static_pages.py`
8084
- Jinja2 templates: `templates/`
8185
- Static assets: `static/`
8286
- Unit tests: `tests/`
@@ -86,8 +90,8 @@ We also create POST endpoints, which accept form submissions so the user can cre
8690
- Database helpers: `db.py`
8791
- FastAPI dependencies: `dependencies.py`
8892
- Enums: `enums.py`
89-
- Database models: `models.py`
9093
- Image helpers: `images.py`
94+
- Database models: `models.py`
9195
- Exceptions: `exceptions/`
9296
- HTTP exceptions: `http_exceptions.py`
9397
- Other custom exceptions: `exceptions.py`
@@ -99,7 +103,6 @@ We also create POST endpoints, which accept form submissions so the user can cre
99103
- Website source: `index.qmd` + `docs/`
100104
- Configuration: `_quarto.yml`
101105

102-
103106
Most everything else is auto-generated and should not be manually modified.
104107

105108
## Backend
@@ -108,7 +111,7 @@ Most everything else is auto-generated and should not be manually modified.
108111

109112
The GET route for the homepage is defined in the main entry point for the application, `main.py`. The entrypoint imports router modules from the `routers/` directory, which contain the other GET and POST routes for the application. In CRUD style, the router modules are named after the resource they manage, e.g., `account.py` for account management.
110113

111-
We name our GET routes using the convention `read_<name>`, where `<name>` is the name of the page, to indicate that they are read-only endpoints that do not modify the database. In POST routes that modify the database, you can use the `get_session` dependency as an argument to get a database session.
114+
We name our GET routes using the convention `read_<name>`, where `<name>` is the name of the resource, to indicate that they are read-only endpoints that do not modify the database. In POST routes that modify the database, you can use the `get_session` dependency as an argument to get a database session.
112115

113116
Routes that require authentication generally take the `get_authenticated_account` dependency as an argument. Unauthenticated GET routes generally take the `get_optional_user` dependency as an argument. If a route should *only* be seen by authenticated users (i.e., a login page), you can redirect to the dashboard if `get_optional_user` returns a `User` object.
114117

@@ -120,6 +123,7 @@ Context refers to Python variables passed to a template to populate the HTML. In
120123
@app.get("/welcome")
121124
async def welcome(request: Request):
122125
return templates.TemplateResponse(
126+
request,
123127
"welcome.html",
124128
{"username": "Alice"}
125129
)
@@ -176,11 +180,12 @@ SQLModel is an Object-Relational Mapping (ORM) library that allows us to interac
176180
Our database models are defined in `utils/models.py`. Each model is a Python class that inherits from `SQLModel` and represents a database table. The key models are:
177181

178182
- `Account`: Represents a user account with email and password hash
179-
- `User`: Represents a user profile with name, email, and avatar
183+
- `User`: Represents a user profile with details like name and avatar; the email and password hash are stored in the related `Account` model
180184
- `Organization`: Represents a company or team
181185
- `Role`: Represents a set of permissions within an organization
182186
- `Permission`: Represents specific actions a user can perform (defined by ValidPermissions enum)
183187
- `PasswordResetToken`: Manages password reset functionality with expiration
188+
- `EmailUpdateToken`: Manages email update confirmation functionality with expiration
184189

185190
Two additional models are used by SQLModel to manage many-to-many relationships; you generally will not need to interact with them directly:
186191

docs/static/data_flow.png

1.55 KB
Loading

0 commit comments

Comments
 (0)