Skip to content

Commit

Permalink
updates
Browse files Browse the repository at this point in the history
  • Loading branch information
dwreeves committed Nov 20, 2024
1 parent 8a8eac1 commit 9426f36
Show file tree
Hide file tree
Showing 23 changed files with 603 additions and 2,165 deletions.
8 changes: 8 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,12 @@ HOBOLINK_USERNAME=replace_me
HOBOLINK_PASSWORD=replace_me
HOBOLINK_TOKEN=replace_me

BASIC_AUTH_USERNAME=admin
BASIC_AUTH_PASSWORD=password

MAPBOX_ACCESS_TOKEN=replace_me

SENTRY_DSN=replace_me
SENTRY_ENVIRONMENT=replace_me

USE_MOCK_DATA=false
1 change: 1 addition & 0 deletions .flaskenv
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
FLASK_APP=app.main:create_app
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ docs/site/
pgadmin4/
mkdocs_env/
latest.dump
server.crt
server.key

# Created by https://www.toptal.com/developers/gitignore/api/python,pycharm,windows,osx,visualstudiocode
# Edit at https://www.toptal.com/developers/gitignore?templates=python,pycharm,windows,osx,visualstudiocode
Expand Down
11 changes: 11 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,14 @@ repos:
rev: v2.12.0
hooks:
- id: hadolint-docker

- repo: https://github.com/rhysd/actionlint
rev: v1.6.27
hooks:
- id: actionlint-docker

- repo: https://github.com/koalaman/shellcheck-precommit
rev: v0.10.0
hooks:
- id: shellcheck
files: ^(.*\.sh|run)$
2 changes: 1 addition & 1 deletion .python-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.12.6
3.12
16 changes: 8 additions & 8 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
FROM python:3.12

ADD --chmod=755 https://astral.sh/uv/install.sh /install.sh
RUN /install.sh && rm /install.sh
FROM python:3.12-slim
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/

WORKDIR /app
COPY requirements.txt ./requirements.txt
ENV UV_PROJECT_ENVIRONMENT=/usr/local

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

COPY ./ .
COPY . /app

EXPOSE 80
EXPOSE 80 443

CMD ["bash", "-c", "flask db migrate && gunicorn -c gunicorn_conf.py app.main:create_app\\(\\)"]
88 changes: 47 additions & 41 deletions alembic/versions/rev001.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@

import sqlalchemy as sa
from sqlalchemy import schema
from sqlalchemy.engine.reflection import Inspector

from alembic import op
from app.config import QUERIES_DIR
Expand All @@ -26,8 +25,6 @@

def upgrade():
conn = op.get_bind()
inspector = Inspector.from_engine(conn)
tables = inspector.get_table_names()

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

if "boathouses" not in tables:
op.execute(schema.CreateSequence(schema.Sequence("boathouses_id_seq")))
op.create_table(
"boathouses",
sa.Column(
"id",
sa.Integer(),
autoincrement=True,
nullable=False,
server_default=sa.text("nextval('boathouses_id_seq'::regclass)"),
),
sa.Column("boathouse", sa.String(length=255), nullable=False),
sa.Column("reach", sa.Integer(), nullable=True),
sa.Column("latitude", sa.Numeric(), nullable=True),
sa.Column("longitude", sa.Numeric(), nullable=True),
sa.Column("overridden", sa.Boolean(), nullable=True),
sa.Column("reason", sa.String(length=255), nullable=True),
sa.PrimaryKeyConstraint("boathouse"),
)
with open(QUERIES_DIR + "/override_event_triggers_v1.sql", "r") as f:
sql = sa.text(f.read())
conn.execute(sql)
if "live_website_options" not in tables:
op.create_table(
"live_website_options",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("flagging_message", sa.Text(), nullable=True),
sa.Column("boating_season", sa.Boolean(), nullable=False),
sa.PrimaryKeyConstraint("id"),
)
if "override_history" not in tables:
op.create_table(
"override_history",
sa.Column("time", sa.TIMESTAMP(), nullable=True),
sa.Column("boathouse", sa.TEXT(), nullable=True),
sa.Column("overridden", sa.BOOLEAN(), nullable=True),
sa.Column("reason", sa.TEXT(), nullable=True),
)
op.execute(schema.CreateSequence(schema.Sequence("boathouses_id_seq")))
op.create_table(
"boathouses",
sa.Column(
"id",
sa.Integer(),
autoincrement=True,
nullable=False,
server_default=sa.text("nextval('boathouses_id_seq'::regclass)"),
),
sa.Column("boathouse", sa.String(length=255), nullable=False),
sa.Column("reach", sa.Integer(), nullable=True),
sa.Column("latitude", sa.Numeric(), nullable=True),
sa.Column("longitude", sa.Numeric(), nullable=True),
sa.Column("overridden", sa.Boolean(), nullable=True),
sa.Column("reason", sa.String(length=255), nullable=True),
sa.PrimaryKeyConstraint("boathouse"),
)

with open(f"{QUERIES_DIR}/override_event_triggers_v1.sql", "r") as f:
sql = sa.text(f.read())
conn.execute(sql)

op.create_table(
"live_website_options",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("flagging_message", sa.Text(), nullable=True),
sa.Column("boating_season", sa.Boolean(), nullable=False),
sa.PrimaryKeyConstraint("id"),
)
op.create_table(
"override_history",
sa.Column("time", sa.TIMESTAMP(), nullable=True),
sa.Column("boathouse", sa.TEXT(), nullable=True),
sa.Column("overridden", sa.BOOLEAN(), nullable=True),
sa.Column("reason", sa.TEXT(), nullable=True),
)

with open(f"{QUERIES_DIR}/define_reach.sql", "r") as f:
sql = sa.text(f.read())
conn.execute(sql)
with open(f"{QUERIES_DIR}/define_boathouse.sql", "r") as f:
sql = sa.text(f.read())
conn.execute(sql)
with open(f"{QUERIES_DIR}/define_default_options.sql", "r") as f:
sql = sa.text(f.read())
conn.execute(sql)


def downgrade():
Expand Down
3 changes: 1 addition & 2 deletions app/admin/views/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ class LogoutView(BaseView):
@expose("/")
def index(self):
body = self.render("admin/logout.html")
status = 401
cache.clear()
status = 200
return body, status


Expand Down
10 changes: 1 addition & 9 deletions app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,15 +192,7 @@ def SQLALCHEMY_DATABASE_URI(self) -> str:

DEFAULT_WIDGET_VERSION: int = 2

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

SENTRY_DSN: str | None = os.getenv("SENTRY_DSN")
SENTRY_ENVIRONMENT: str | None = os.getenv("SENTRY_ENVIRONMENT")
Expand Down
4 changes: 3 additions & 1 deletion app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,8 @@ def register_jinja_env(app: Flask):
@app.template_filter("strftime")
def strftime(value: datetime.datetime, fmt: str = "%Y-%m-%d %I:%M:%S %p") -> str:
"""Render datetimes with a default format for the frontend."""
return value.strftime(fmt)
if value:
return value.strftime(fmt)

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

if async_:
Expand Down
7 changes: 4 additions & 3 deletions app/templates/admin/logout.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,19 @@
{% block tail %}
{{ super() }}
<script>
$(document).ready(function(){
$(document).ready(setTimeout(function(){
$.ajax({
async: false,
type: "GET",
url: "{{ url_for('admin.index') }}",
username: "logout"
username: "x-logout",
password: "x-logout"
})
.done(function(){
})
.fail(function(){
window.location.href = "{{ url_for('flagging.index') }}";
});
});
}, 500));
</script>
{% endblock %}
5 changes: 3 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ services:
web:
<<: *app-config
ports:
- 80:80
- "127.0.0.1:80:80"
- "127.0.0.1:443:443"
depends_on:
- postgres
links:
Expand Down Expand Up @@ -72,7 +73,7 @@ services:

celeryworker:
<<: *app-config
entrypoint: ["flask", "celery", "worker", "--uid", "nobody", "--gid", "nogroup"]
entrypoint: ["flask", "celery", "worker"]
ports:
- 5555:5555
depends_on:
Expand Down
74 changes: 33 additions & 41 deletions docs/src/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,68 +55,60 @@ cp -n .env.example .env
???+ danger
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.

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

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

If you do not have HOBOlink credentials, please turn on demo mode by setting `FLASK_ENV=demo`.
If you do not have HOBOlink credentials, please turn on demo mode by setting `USE_MOCK_DATA=true`.

**(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.

**(Very optional)** If you'd like, connect to Sentry via the `SENTRY_DSN` and `SENTRY_ENVIRONMENT` env vars: https://sentry.io/

**(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.

**(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.

## Run the Website Locally

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

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.
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).

To do this, run the following:

```
uv pip sync requirements.txt
```
=== "Windows (CMD)"
```shell
run_windows_dev
uv venv .venv
.\.venv\Scripts\activate.bat
uv pip sync requirements.txt
pre-commit install
```

=== "OSX (Bash)"
```shell
sh run_unix_dev.sh
uv venv .venv
source .venv/bin/activate
uv pip sync requirements.txt
pre-commit install
```

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

???+ tip
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.
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`.
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.
3. Now just wait for the database to start filling in and for the website to eventually run.
```shell
docker compose build
```

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

```
* Serving Flask app "app:create_app" (lazy loading)
* Environment: development
* Debug mode: on
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
* Restarting with stat
```
```shell
docker compose up
```

???+ error
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.
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:

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!
```shell
docker compose exec web flask update-db
```

![](img/successful_run.png)
Now visit the website at `http://localhost/` (note it's http, not https). And you should be all set!
4 changes: 4 additions & 0 deletions gunicorn_conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ def gen_config():
_host = os.getenv("HOST", "0.0.0.0")
_port = os.getenv("PORT", "80")
bind = os.getenv("BIND", f"{_host}:{_port}")
if os.getenv("CERTFILE"):
certfile = os.getenv("CERTFILE")
if os.getenv("KEYFILE"):
keyfile = os.getenv("KEYFILE")

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

Expand Down
Loading

0 comments on commit 9426f36

Please sign in to comment.