Skip to content

Commit 9426f36

Browse files
committed
updates
1 parent 8a8eac1 commit 9426f36

23 files changed

+603
-2165
lines changed

.env.example

+8
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,12 @@ HOBOLINK_USERNAME=replace_me
55
HOBOLINK_PASSWORD=replace_me
66
HOBOLINK_TOKEN=replace_me
77

8+
BASIC_AUTH_USERNAME=admin
9+
BASIC_AUTH_PASSWORD=password
10+
11+
MAPBOX_ACCESS_TOKEN=replace_me
12+
13+
SENTRY_DSN=replace_me
14+
SENTRY_ENVIRONMENT=replace_me
15+
816
USE_MOCK_DATA=false

.flaskenv

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
FLASK_APP=app.main:create_app

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ docs/site/
55
pgadmin4/
66
mkdocs_env/
77
latest.dump
8+
server.crt
9+
server.key
810

911
# Created by https://www.toptal.com/developers/gitignore/api/python,pycharm,windows,osx,visualstudiocode
1012
# Edit at https://www.toptal.com/developers/gitignore?templates=python,pycharm,windows,osx,visualstudiocode

.pre-commit-config.yaml

+11
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,14 @@ repos:
2222
rev: v2.12.0
2323
hooks:
2424
- id: hadolint-docker
25+
26+
- repo: https://github.com/rhysd/actionlint
27+
rev: v1.6.27
28+
hooks:
29+
- id: actionlint-docker
30+
31+
- repo: https://github.com/koalaman/shellcheck-precommit
32+
rev: v0.10.0
33+
hooks:
34+
- id: shellcheck
35+
files: ^(.*\.sh|run)$

.python-version

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
3.12.6
1+
3.12

Dockerfile

+8-8
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1-
FROM python:3.12
2-
3-
ADD --chmod=755 https://astral.sh/uv/install.sh /install.sh
4-
RUN /install.sh && rm /install.sh
1+
FROM python:3.12-slim
2+
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
53

64
WORKDIR /app
7-
COPY requirements.txt ./requirements.txt
5+
ENV UV_PROJECT_ENVIRONMENT=/usr/local
86

9-
RUN /root/.cargo/bin/uv sync --system --no-cache
7+
RUN --mount=type=cache,target=/root/.cache/uv \
8+
--mount=type=bind,source=requirements.txt,target=requirements.txt \
9+
uv pip install -r requirements.txt --compile-bytecode --system
1010

11-
COPY ./ .
11+
COPY . /app
1212

13-
EXPOSE 80
13+
EXPOSE 80 443
1414

1515
CMD ["bash", "-c", "flask db migrate && gunicorn -c gunicorn_conf.py app.main:create_app\\(\\)"]

alembic/versions/rev001.py

+47-41
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111

1212
import sqlalchemy as sa
1313
from sqlalchemy import schema
14-
from sqlalchemy.engine.reflection import Inspector
1514

1615
from alembic import op
1716
from app.config import QUERIES_DIR
@@ -26,8 +25,6 @@
2625

2726
def upgrade():
2827
conn = op.get_bind()
29-
inspector = Inspector.from_engine(conn)
30-
tables = inspector.get_table_names()
3128

3229
# skip the following tables:
3330
# - hobolink
@@ -36,44 +33,53 @@ def upgrade():
3633
# - model_outputs
3734
# These are rewritten each time; their data doesn't need to be persisted.
3835

39-
if "boathouses" not in tables:
40-
op.execute(schema.CreateSequence(schema.Sequence("boathouses_id_seq")))
41-
op.create_table(
42-
"boathouses",
43-
sa.Column(
44-
"id",
45-
sa.Integer(),
46-
autoincrement=True,
47-
nullable=False,
48-
server_default=sa.text("nextval('boathouses_id_seq'::regclass)"),
49-
),
50-
sa.Column("boathouse", sa.String(length=255), nullable=False),
51-
sa.Column("reach", sa.Integer(), nullable=True),
52-
sa.Column("latitude", sa.Numeric(), nullable=True),
53-
sa.Column("longitude", sa.Numeric(), nullable=True),
54-
sa.Column("overridden", sa.Boolean(), nullable=True),
55-
sa.Column("reason", sa.String(length=255), nullable=True),
56-
sa.PrimaryKeyConstraint("boathouse"),
57-
)
58-
with open(QUERIES_DIR + "/override_event_triggers_v1.sql", "r") as f:
59-
sql = sa.text(f.read())
60-
conn.execute(sql)
61-
if "live_website_options" not in tables:
62-
op.create_table(
63-
"live_website_options",
64-
sa.Column("id", sa.Integer(), nullable=False),
65-
sa.Column("flagging_message", sa.Text(), nullable=True),
66-
sa.Column("boating_season", sa.Boolean(), nullable=False),
67-
sa.PrimaryKeyConstraint("id"),
68-
)
69-
if "override_history" not in tables:
70-
op.create_table(
71-
"override_history",
72-
sa.Column("time", sa.TIMESTAMP(), nullable=True),
73-
sa.Column("boathouse", sa.TEXT(), nullable=True),
74-
sa.Column("overridden", sa.BOOLEAN(), nullable=True),
75-
sa.Column("reason", sa.TEXT(), nullable=True),
76-
)
36+
op.execute(schema.CreateSequence(schema.Sequence("boathouses_id_seq")))
37+
op.create_table(
38+
"boathouses",
39+
sa.Column(
40+
"id",
41+
sa.Integer(),
42+
autoincrement=True,
43+
nullable=False,
44+
server_default=sa.text("nextval('boathouses_id_seq'::regclass)"),
45+
),
46+
sa.Column("boathouse", sa.String(length=255), nullable=False),
47+
sa.Column("reach", sa.Integer(), nullable=True),
48+
sa.Column("latitude", sa.Numeric(), nullable=True),
49+
sa.Column("longitude", sa.Numeric(), nullable=True),
50+
sa.Column("overridden", sa.Boolean(), nullable=True),
51+
sa.Column("reason", sa.String(length=255), nullable=True),
52+
sa.PrimaryKeyConstraint("boathouse"),
53+
)
54+
55+
with open(f"{QUERIES_DIR}/override_event_triggers_v1.sql", "r") as f:
56+
sql = sa.text(f.read())
57+
conn.execute(sql)
58+
59+
op.create_table(
60+
"live_website_options",
61+
sa.Column("id", sa.Integer(), nullable=False),
62+
sa.Column("flagging_message", sa.Text(), nullable=True),
63+
sa.Column("boating_season", sa.Boolean(), nullable=False),
64+
sa.PrimaryKeyConstraint("id"),
65+
)
66+
op.create_table(
67+
"override_history",
68+
sa.Column("time", sa.TIMESTAMP(), nullable=True),
69+
sa.Column("boathouse", sa.TEXT(), nullable=True),
70+
sa.Column("overridden", sa.BOOLEAN(), nullable=True),
71+
sa.Column("reason", sa.TEXT(), nullable=True),
72+
)
73+
74+
with open(f"{QUERIES_DIR}/define_reach.sql", "r") as f:
75+
sql = sa.text(f.read())
76+
conn.execute(sql)
77+
with open(f"{QUERIES_DIR}/define_boathouse.sql", "r") as f:
78+
sql = sa.text(f.read())
79+
conn.execute(sql)
80+
with open(f"{QUERIES_DIR}/define_default_options.sql", "r") as f:
81+
sql = sa.text(f.read())
82+
conn.execute(sql)
7783

7884

7985
def downgrade():

app/admin/views/misc.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,7 @@ class LogoutView(BaseView):
1212
@expose("/")
1313
def index(self):
1414
body = self.render("admin/logout.html")
15-
status = 401
16-
cache.clear()
15+
status = 200
1716
return body, status
1817

1918

app/config.py

+1-9
Original file line numberDiff line numberDiff line change
@@ -192,15 +192,7 @@ def SQLALCHEMY_DATABASE_URI(self) -> str:
192192

193193
DEFAULT_WIDGET_VERSION: int = 2
194194

195-
MAPBOX_ACCESS_TOKEN: str = os.getenv(
196-
"MAPBOX_ACCESS_TOKEN",
197-
# This is a token that's floating around the web in a lot of quickstart
198-
# examples for LeafletJS, and seems to work. ¯\_(ツ)_/¯
199-
#
200-
# You should not use it ideally, but as a default for very quick runs
201-
# and demos, it should be OK.
202-
"pk.eyJ1IjoibWFwYm94IiwiYSI6ImNpejY4NXVycTA2emYycXBndHRqcmZ3N3gifQ.rJcFIG214AriISLbB6B5aw",
203-
) # noqa
195+
MAPBOX_ACCESS_TOKEN: str = os.getenv("MAPBOX_ACCESS_TOKEN")
204196

205197
SENTRY_DSN: str | None = os.getenv("SENTRY_DSN")
206198
SENTRY_ENVIRONMENT: str | None = os.getenv("SENTRY_ENVIRONMENT")

app/main.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,8 @@ def register_jinja_env(app: Flask):
182182
@app.template_filter("strftime")
183183
def strftime(value: datetime.datetime, fmt: str = "%Y-%m-%d %I:%M:%S %p") -> str:
184184
"""Render datetimes with a default format for the frontend."""
185-
return value.strftime(fmt)
185+
if value:
186+
return value.strftime(fmt)
186187

187188
def _load_svg(file_name: str):
188189
"""Load an svg file from `static/images/`."""
@@ -372,6 +373,7 @@ def pip_compile(ctx: click.Context):
372373
)
373374
@mail_on_fail
374375
def email_90_day_data_command(async_: bool = False):
376+
"""Send email containing database dump."""
375377
from app.data.celery import send_database_exports_task
376378

377379
if async_:

app/templates/admin/logout.html

+4-3
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,19 @@
55
{% block tail %}
66
{{ super() }}
77
<script>
8-
$(document).ready(function(){
8+
$(document).ready(setTimeout(function(){
99
$.ajax({
1010
async: false,
1111
type: "GET",
1212
url: "{{ url_for('admin.index') }}",
13-
username: "logout"
13+
username: "x-logout",
14+
password: "x-logout"
1415
})
1516
.done(function(){
1617
})
1718
.fail(function(){
1819
window.location.href = "{{ url_for('flagging.index') }}";
1920
});
20-
});
21+
}, 500));
2122
</script>
2223
{% endblock %}

docker-compose.yml

+3-2
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ services:
2929
web:
3030
<<: *app-config
3131
ports:
32-
- 80:80
32+
- "127.0.0.1:80:80"
33+
- "127.0.0.1:443:443"
3334
depends_on:
3435
- postgres
3536
links:
@@ -72,7 +73,7 @@ services:
7273

7374
celeryworker:
7475
<<: *app-config
75-
entrypoint: ["flask", "celery", "worker", "--uid", "nobody", "--gid", "nogroup"]
76+
entrypoint: ["flask", "celery", "worker"]
7677
ports:
7778
- 5555:5555
7879
depends_on:

docs/src/setup.md

+33-41
Original file line numberDiff line numberDiff line change
@@ -55,68 +55,60 @@ cp -n .env.example .env
5555
???+ danger
5656
If you do any commits to the repo, _please make sure `.env` is properly gitignored!_ (`.env.example` does not need to be gitignored, only `.env`.) `.env` contains sensitive information.
5757

58-
5. The previous step created a file called `.env` (pronounced "dot env"). This file will contain our HOBOlink credentials and Twitter/X credentials.
58+
5. The previous step created a file called `.env` (pronounced "dot env"). This file will contain things like HOBOlink credentials and Twitter/X credentials.
5959

60-
Please update `.env` (**_NOT_** `.env.example`) to contain the correct credentials by replacing each `replace_me`. The Twitter/X credentials are optional.
60+
Please update `.env` (**_NOT_** `.env.example`) to contain the correct credentials by replacing each `replace_me`.
6161

62-
If you do not have HOBOlink credentials, please turn on demo mode by setting `FLASK_ENV=demo`.
62+
If you do not have HOBOlink credentials, please turn on demo mode by setting `USE_MOCK_DATA=true`.
63+
64+
**(Optional)** If you'd like, create a Mapbox access token and add it to your `.env`: https://www.mapbox.com/ If you don't do this, the map will not fully render.
65+
66+
**(Very optional)** If you'd like, connect to Sentry via the `SENTRY_DSN` and `SENTRY_ENVIRONMENT` env vars: https://sentry.io/
67+
68+
**(Very optional)** You can also set up `https` and run that way. Create a certfile and key via the command `./run ssl-cert`, and add `CERTFILE=server.crt`, `KEYFILE=server.key`, and `PORT=443` to your `.env`. However this will require some additional finagling as your browser will not by default trust self-signed certs, so it's not recommended for most users.
69+
70+
**(Very optional)** You can also set up Twitter/X credentials and send tweets. However, right now we do not use Twitter/X; this functionality is effectively deprecated.
6371

6472
## Run the Website Locally
6573

6674
After you get everything set up, you should run the website at least once.
6775

68-
1. Although not strictly required for running the website (as we will be using Docker Compose), it is recommended you install all the project dependencies into a virtual environment.
76+
1. Although not strictly required for running the website (as we will be using Docker Compose), it is recommended you install all the project dependencies into a virtual environment, and also enable `pre-commit` (which does checks of your code before you commit changes).
6977

7078
To do this, run the following:
7179

72-
```
73-
uv pip sync requirements.txt
74-
```
75-
76-
7780
=== "Windows (CMD)"
7881
```shell
79-
run_windows_dev
82+
uv venv .venv
83+
.\.venv\Scripts\activate.bat
84+
uv pip sync requirements.txt
85+
pre-commit install
8086
```
8187

8288
=== "OSX (Bash)"
8389
```shell
84-
sh run_unix_dev.sh
90+
uv venv .venv
91+
source .venv/bin/activate
92+
uv pip sync requirements.txt
93+
pre-commit install
8594
```
8695

87-
???+ note
88-
The script being run is doing the following, in order:
89-
90-
1. Set up a "virtual environment" (basically an isolated folder inside your project directory that we install the Python packages into),
91-
2. install the packages inside of `requirements/dev.txt`; this can take a while during your first time.
92-
3. Set up some environment variables that Flask needs.
93-
4. Prompts the user to set some options for the deployment. (See step 2 below.)
94-
5. Set up the Postgres database and update it with data.
95-
6. Run the actual website.
96+
2. Build the Docker images required to run the site:
9697

97-
???+ tip
98-
If you are receiving any errors related to the Postgres database and you are certain that Postgres is running on your computer, you can modify the `POSTGRES_USER` and `POSTGRES_PASSWORD` environment variables to connect to your local Postgres instance properly.
99-
100-
You can also save these Postgres environment variables inside of your `.env` file, but **do not** save your system password (the password you use to login to your computer) in a plain text file. If you need to save a `POSTGRES_PASSWORD` in `.env`, make sure it's a unique password, e.g. an admin user `POSTGRES_USER=flagging` and a password you randomly generated for that user `POSTGRES_PASSWORD=my_random_password_here`.
101-
102-
2. You will be prompted asking if you want to run the website with mock data. The `USE_MOCK_DATA` variable is a way to run the website with dummy data without accessing the credentials. It is useful for anyone who wants to run a demo of the website regardless of their affiliation with the CRWA or this project. It has also been useful for development purposes in the past for us.
103-
104-
3. Now just wait for the database to start filling in and for the website to eventually run.
98+
```shell
99+
docker compose build
100+
```
105101

106-
???+ success
107-
You should be good if you eventually see something like the following in your terminal:
102+
3. Spin the website up:
108103

109-
```
110-
* Serving Flask app "app:create_app" (lazy loading)
111-
* Environment: development
112-
* Debug mode: on
113-
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
114-
* Restarting with stat
115-
```
104+
```shell
105+
docker compose up
106+
```
116107

117-
???+ error
118-
If you get an error that says something like "`Microsoft Visual C++ 14.0 or greater is required.`," you need to follow the link provided by the error message, download and install it, then reboot your computer.
108+
4. If this is your first time running the website, you will need to populate the database by running the batch job that retrieves data and runs the model. To do this, **in a separate terminal** (while the other terminal is still running), run the following command:
119109

120-
4. Point your browser of choice to the URL shown in the terminal output. If everything worked out, the website should be running on your local computer!
110+
```shell
111+
docker compose exec web flask update-db
112+
```
121113

122-
![](img/successful_run.png)
114+
Now visit the website at `http://localhost/` (note it's http, not https). And you should be all set!

gunicorn_conf.py

+4
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ def gen_config():
1919
_host = os.getenv("HOST", "0.0.0.0")
2020
_port = os.getenv("PORT", "80")
2121
bind = os.getenv("BIND", f"{_host}:{_port}")
22+
if os.getenv("CERTFILE"):
23+
certfile = os.getenv("CERTFILE")
24+
if os.getenv("KEYFILE"):
25+
keyfile = os.getenv("KEYFILE")
2226

2327
return {k: v for k, v in locals().items() if not k.startswith("_")}
2428

0 commit comments

Comments
 (0)