Skip to content

Commit 4835a81

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 45b47b0 commit 4835a81

File tree

14 files changed

+226
-60
lines changed

14 files changed

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

manage.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import os
44
import sys
55

6+
from dotenv import load_dotenv
7+
68
sys.path.append('src')
79

810

@@ -21,4 +23,5 @@ def main():
2123

2224

2325
if __name__ == '__main__':
26+
load_dotenv()
2427
main()

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)