Skip to content

Commit 75702d9

Browse files
committed
Replaced local_settings.py with .env
Using environment variables is a more standard way of setting settings like these. (Removed the `NOTE: This must be changed in production!` comments in `settings.py`, as `env.py` now enforces that those settings have been set.) Also added a `channels_redis` logger from the current `local_settings_post.py` file on the prod server.
1 parent 38c1bcf commit 75702d9

File tree

14 files changed

+224
-60
lines changed

14 files changed

+224
-60
lines changed

.env.example

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# --- Core settings ---
2+
DEBUG='True'
3+
SECRET_KEY='INSECURE_SECRET'
4+
# This must be a JSON-encoded array of strings
5+
ALLOWED_HOSTS='["*"]'
6+
7+
# --- Databases ---
8+
# This is relative to the `REPO_DIR` setting - except if it starts with a `/`, of course
9+
SQLITE_DB_PATH='db.sqlite3'
10+
11+
# --- `django-hosts` ---
12+
PARENT_HOST='makentnu.localhost:8000'
13+
14+
# --- Cookies ---
15+
COOKIE_DOMAIN='.makentnu.localhost'
16+
COOKIE_SECURE='False'
17+
18+
# --- Static and media files ---
19+
# This is relative to the `REPO_DIR` setting - except if it starts with a `/`, of course
20+
STATIC_AND_MEDIA_FILES__PARENT_DIR='../'
21+
22+
# --- Emailing ---
23+
EMAIL_HOST='localhost'
24+
EMAIL_HOST_USER='test'
25+
EMAIL_PORT='25'
26+
EMAIL_USE_TLS='False'
27+
DEFAULT_FROM_EMAIL='test@localhost'
28+
SERVER_EMAIL='server@localhost'
29+
EMAIL_SUBJECT_PREFIX='[Django] ' # Note the trailing space
30+
# This must be a JSON-encoded array of 2-element arrays [<full name>, <email address>]
31+
# (see https://docs.djangoproject.com/en/stable/ref/settings/#admins)
32+
ADMINS='[]'
33+
34+
# --- Dataporten/Feide ---
35+
USE_DATAPORTEN_AUTH='False'
36+
SOCIAL_AUTH_DATAPORTEN_KEY='KEY'
37+
SOCIAL_AUTH_DATAPORTEN_SECRET='SECRET'
38+
39+
# --- Checkin ---
40+
CHECKIN_KEY='KEY'

.github/workflows/ci.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ jobs:
3636
- name: Install Python dependencies
3737
run: uv sync --locked --group ci
3838

39+
- name: Create `.env`
40+
shell: bash
41+
run: cp .env.example .env
42+
3943
- name: Run Django Tests
4044
run: |
4145
uv run manage.py collectstatic --no-input

.gitignore

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,4 @@ pip-selfcheck.json
473473

474474
# End of https://www.toptal.com/developers/gitignore/api/django,pycharm+all,venv,visualstudiocode,macos,node
475475

476-
477476
!src/web/static/lib/
478-
local_settings_post.py

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ Lastly, a new [release](https://github.com/MAKENTNU/web/releases) must be create
3232
- Prevented `compilemessages` from processing files outside the `src` folder (MAKENTNU/web#766)
3333
- Migrated project to use the more modern `pyproject.toml` and [uv](https://docs.astral.sh/uv/) instead of `requirements.txt` and pip
3434
(MAKENTNU/web#766)
35+
- Replaced `local_settings.py` with `.env` (MAKENTNU/web#767)
36+
- Developers must create a `.env` file locally - see the "Setup" section of the README
3537

3638

3739
## 2025-05-03 (MAKENTNU/web#757)

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
```shell
2626
uv sync --group dev
2727
```
28+
1. Create an empty `.env` file directly inside the repository folder, and fill it by
29+
copying the contents of [`.env.example`](.env.example)
2830

2931
#### PyCharm
3032

docker-compose.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,5 @@ services:
1010
- ./:/web
1111
ports:
1212
- "8000:8000"
13+
env_file:
14+
- .env

env.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import json
2+
import os
3+
from enum import StrEnum
4+
from typing import Final
5+
6+
from dotenv import load_dotenv
7+
8+
# Load the environment variables in the `.env` file
9+
load_dotenv()
10+
11+
_NOT_PROVIDED: Final = object()
12+
13+
14+
class DatabaseSystem(StrEnum):
15+
POSTGRESQL = "postgres"
16+
SQLITE = "sqlite"
17+
18+
19+
def get_envvar(name: str, *, default: str = _NOT_PROVIDED) -> str:
20+
"""Returns the environment variable with name ``name``.
21+
If it doesn't exist and ``default`` has been provided, ``default`` is returned."""
22+
value = os.environ.get(name)
23+
if value is None or value == "":
24+
if default is _NOT_PROVIDED:
25+
raise KeyError(f"Missing environment variable `{name}`.")
26+
value = default
27+
return value
28+
29+
30+
def get_bool_envvar(name: str, *, default: bool = _NOT_PROVIDED) -> bool:
31+
"""Returns the same as ``get_envvar()``, but with the value interpreted as a
32+
``bool``: ``True`` if the value equals ``"true"`` (case-insensitive), ``False``
33+
otherwise."""
34+
value = get_envvar(
35+
name,
36+
default=str(default) if type(default) is bool else default,
37+
)
38+
return value.lower() == "true"
39+
40+
41+
# DEV: If a setting *should* be specified in prod, it should *not* have a `default`!
42+
43+
# --- Core settings ---
44+
DEBUG: Final = get_bool_envvar("DEBUG")
45+
SECRET_KEY: Final = get_envvar("SECRET_KEY")
46+
ALLOWED_HOSTS: Final = list(json.loads(get_envvar("ALLOWED_HOSTS")))
47+
INTERNAL_IPS: Final = list(
48+
json.loads(get_envvar("INTERNAL_IPS", default='["127.0.0.1"]'))
49+
)
50+
51+
# --- Databases ---
52+
DATABASE_SYSTEM: Final = DatabaseSystem(
53+
get_envvar("DATABASE_SYSTEM", default=DatabaseSystem.SQLITE)
54+
)
55+
SQLITE_DB_PATH: Final = get_envvar("SQLITE_DB_PATH", default="db.sqlite3")
56+
POSTGRES_HOST: Final = get_envvar("POSTGRES_HOST", default="localhost")
57+
POSTGRES_DB_NAME: Final = get_envvar("POSTGRES_DB_NAME", default="make_web")
58+
POSTGRES_DB_USER: Final = get_envvar("POSTGRES_DB_USER", default="devuser")
59+
POSTGRES_DB_PASSWORD: Final = get_envvar("POSTGRES_DB_PASSWORD", default="devpassword")
60+
61+
# --- `django-hosts` ---
62+
PARENT_HOST: Final = get_envvar("PARENT_HOST")
63+
64+
# --- Cookies ---
65+
COOKIE_DOMAIN: Final = get_envvar("COOKIE_DOMAIN")
66+
COOKIE_SECURE: Final = get_bool_envvar("COOKIE_SECURE")
67+
68+
# --- Static and media files ---
69+
STATIC_AND_MEDIA_FILES__PARENT_DIR: Final = get_envvar(
70+
"STATIC_AND_MEDIA_FILES__PARENT_DIR"
71+
)
72+
# The max size in prod is 50 MiB (through Nginx)
73+
MEDIA_FILE_MAX_SIZE__MB: Final = int(get_envvar("MEDIA_FILE_MAX_SIZE__MB", default="25"))
74+
75+
# --- `channels` ---
76+
REDIS_HOST: Final = get_envvar("REDIS_HOST", default="localhost")
77+
REDIS_PORT: Final = int(get_envvar("REDIS_PORT", default="6379"))
78+
79+
# --- Emailing ---
80+
EMAIL_HOST: Final = get_envvar("EMAIL_HOST")
81+
EMAIL_HOST_USER: Final = get_envvar("EMAIL_HOST_USER")
82+
EMAIL_PORT: Final = int(get_envvar("EMAIL_PORT"))
83+
EMAIL_USE_TLS: Final = get_bool_envvar("EMAIL_USE_TLS")
84+
DEFAULT_FROM_EMAIL: Final = get_envvar("DEFAULT_FROM_EMAIL")
85+
SERVER_EMAIL: Final = get_envvar("SERVER_EMAIL")
86+
EMAIL_SUBJECT_PREFIX: Final = get_envvar("EMAIL_SUBJECT_PREFIX")
87+
ADMINS: Final = [
88+
(full_name, email) for full_name, email in json.loads(get_envvar("ADMINS"))
89+
]
90+
91+
# --- Dataporten/Feide ---
92+
USE_DATAPORTEN_AUTH: Final = get_bool_envvar("USE_DATAPORTEN_AUTH")
93+
SOCIAL_AUTH_DATAPORTEN_KEY: Final = get_envvar("SOCIAL_AUTH_DATAPORTEN_KEY")
94+
SOCIAL_AUTH_DATAPORTEN_SECRET: Final = get_envvar("SOCIAL_AUTH_DATAPORTEN_SECRET")
95+
96+
# --- Checkin ---
97+
CHECKIN_KEY: Final = get_envvar("CHECKIN_KEY")

manage.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88

99
def main():
1010
"""Run administrative tasks."""
11+
# IMPORTANT: Ensure this import is kept here, as it loads the envvars
12+
import env # noqa: F401
13+
1114
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'web.settings')
1215
try:
1316
from django.core.management import execute_from_command_line

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ dependencies = [
3737
"channels[daphne]==4.1.0",
3838
"channels-redis==4.2.0",
3939

40+
# Environment variables
41+
"python-dotenv==1.1.1",
42+
4043
# Misc. packages
4144
"bleach[css]==6.1.0",
4245
"Pillow==11.3.0",

src/util/url_utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ def logout_urls():
117117
from dataporten import views as dataporten_views # Avoids circular importing
118118

119119
# Both of these views log the user out, then redirects to the value of the `LOGOUT_REDIRECT_URL` setting
120-
if settings.USES_DATAPORTEN_AUTH:
120+
if settings.USE_DATAPORTEN_AUTH:
121121
logout_view = dataporten_views.Logout.as_view()
122122
else:
123123
logout_view = auth_views.LogoutView.as_view()

0 commit comments

Comments
 (0)