Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# --- Core settings ---
DEBUG='True'
SECRET_KEY='INSECURE_SECRET'
# This must be a JSON-encoded array of strings
ALLOWED_HOSTS='["*"]'

# --- Databases ---
# This is relative to the `REPO_DIR` setting - except if it starts with a `/`, of course
SQLITE_DB_PATH='db.sqlite3'

# --- `django-hosts` ---
PARENT_HOST='makentnu.localhost:8000'

# --- Cookies ---
COOKIE_DOMAIN='.makentnu.localhost'
COOKIE_SECURE='False'

# --- Static and media files ---
# This is relative to the `REPO_DIR` setting - except if it starts with a `/`, of course
STATIC_AND_MEDIA_FILES__PARENT_DIR='../'

# --- Emailing ---
EMAIL_HOST='localhost'
EMAIL_HOST_USER='test'
EMAIL_PORT='25'
EMAIL_USE_TLS='False'
DEFAULT_FROM_EMAIL='test@localhost'
SERVER_EMAIL='server@localhost'
EMAIL_SUBJECT_PREFIX='[Django] ' # Note the trailing space
# This must be a JSON-encoded array of 2-element arrays [<full name>, <email address>]
# (see https://docs.djangoproject.com/en/stable/ref/settings/#admins)
ADMINS='[]'

# --- Dataporten/Feide ---
USE_DATAPORTEN_AUTH='False'
SOCIAL_AUTH_DATAPORTEN_KEY='KEY'
SOCIAL_AUTH_DATAPORTEN_SECRET='SECRET'

# --- Checkin ---
CHECKIN_KEY='KEY'
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ jobs:
- name: Install Python dependencies
run: uv sync --locked --group ci

- name: Create `.env`
shell: bash
run: cp .env.example .env

- name: Run Django Tests
run: |
uv run manage.py collectstatic --no-input
Expand Down
2 changes: 0 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,4 @@ pip-selfcheck.json

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


!src/web/static/lib/
local_settings_post.py
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ Lastly, a new [release](https://github.com/MAKENTNU/web/releases) must be create
- Prevented `compilemessages` from processing files outside the `src` folder (MAKENTNU/web#766)
- Migrated project to use the more modern `pyproject.toml` and [uv](https://docs.astral.sh/uv/) instead of `requirements.txt` and pip
(MAKENTNU/web#766)
- Replaced `local_settings.py` with `.env` (MAKENTNU/web#767)
- Developers must create a `.env` file locally - see the "Setup" section of the README


## 2025-05-03 (MAKENTNU/web#757)
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
```shell
uv sync --group dev
```
1. Create an empty `.env` file directly inside the repository folder, and fill it by
copying the contents of [`.env.example`](.env.example)

#### PyCharm

Expand Down
2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ services:
- ./:/web
ports:
- "8000:8000"
env_file:
- .env
97 changes: 97 additions & 0 deletions env.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import json
import os
from enum import StrEnum
from typing import Final

from dotenv import load_dotenv

# Load the environment variables in the `.env` file
load_dotenv()

_NOT_PROVIDED: Final = object()


class DatabaseSystem(StrEnum):
POSTGRESQL = "postgres"
SQLITE = "sqlite"


def get_envvar(name: str, *, default: str = _NOT_PROVIDED) -> str:
"""Returns the environment variable with name ``name``.
If it doesn't exist and ``default`` has been provided, ``default`` is returned."""
value = os.environ.get(name)
if value is None or value == "":
if default is _NOT_PROVIDED:
raise KeyError(f"Missing environment variable `{name}`.")
value = default
return value


def get_bool_envvar(name: str, *, default: bool = _NOT_PROVIDED) -> bool:
"""Returns the same as ``get_envvar()``, but with the value interpreted as a
``bool``: ``True`` if the value equals ``"true"`` (case-insensitive), ``False``
otherwise."""
value = get_envvar(
name,
default=str(default) if type(default) is bool else default,
)
return value.lower() == "true"


# DEV: If a setting *should* be specified in prod, it should *not* have a `default`!

# --- Core settings ---
DEBUG: Final = get_bool_envvar("DEBUG")
SECRET_KEY: Final = get_envvar("SECRET_KEY")
ALLOWED_HOSTS: Final = list(json.loads(get_envvar("ALLOWED_HOSTS")))
INTERNAL_IPS: Final = list(
json.loads(get_envvar("INTERNAL_IPS", default='["127.0.0.1"]'))
)

# --- Databases ---
DATABASE_SYSTEM: Final = DatabaseSystem(
get_envvar("DATABASE_SYSTEM", default=DatabaseSystem.SQLITE)
)
SQLITE_DB_PATH: Final = get_envvar("SQLITE_DB_PATH", default="db.sqlite3")
POSTGRES_HOST: Final = get_envvar("POSTGRES_HOST", default="localhost")
POSTGRES_DB_NAME: Final = get_envvar("POSTGRES_DB_NAME", default="make_web")
POSTGRES_DB_USER: Final = get_envvar("POSTGRES_DB_USER", default="devuser")
POSTGRES_DB_PASSWORD: Final = get_envvar("POSTGRES_DB_PASSWORD", default="devpassword")

# --- `django-hosts` ---
PARENT_HOST: Final = get_envvar("PARENT_HOST")

# --- Cookies ---
COOKIE_DOMAIN: Final = get_envvar("COOKIE_DOMAIN")
COOKIE_SECURE: Final = get_bool_envvar("COOKIE_SECURE")

# --- Static and media files ---
STATIC_AND_MEDIA_FILES__PARENT_DIR: Final = get_envvar(
"STATIC_AND_MEDIA_FILES__PARENT_DIR"
)
# The max size in prod is 50 MiB (through Nginx)
MEDIA_FILE_MAX_SIZE__MB: Final = int(get_envvar("MEDIA_FILE_MAX_SIZE__MB", default="25"))

# --- `channels` ---
REDIS_HOST: Final = get_envvar("REDIS_HOST", default="localhost")
REDIS_PORT: Final = int(get_envvar("REDIS_PORT", default="6379"))

# --- Emailing ---
EMAIL_HOST: Final = get_envvar("EMAIL_HOST")
EMAIL_HOST_USER: Final = get_envvar("EMAIL_HOST_USER")
EMAIL_PORT: Final = int(get_envvar("EMAIL_PORT"))
EMAIL_USE_TLS: Final = get_bool_envvar("EMAIL_USE_TLS")
DEFAULT_FROM_EMAIL: Final = get_envvar("DEFAULT_FROM_EMAIL")
SERVER_EMAIL: Final = get_envvar("SERVER_EMAIL")
EMAIL_SUBJECT_PREFIX: Final = get_envvar("EMAIL_SUBJECT_PREFIX")
ADMINS: Final = [
(full_name, email) for full_name, email in json.loads(get_envvar("ADMINS"))
]

# --- Dataporten/Feide ---
USE_DATAPORTEN_AUTH: Final = get_bool_envvar("USE_DATAPORTEN_AUTH")
SOCIAL_AUTH_DATAPORTEN_KEY: Final = get_envvar("SOCIAL_AUTH_DATAPORTEN_KEY")
SOCIAL_AUTH_DATAPORTEN_SECRET: Final = get_envvar("SOCIAL_AUTH_DATAPORTEN_SECRET")

# --- Checkin ---
CHECKIN_KEY: Final = get_envvar("CHECKIN_KEY")
3 changes: 3 additions & 0 deletions manage.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@

def main():
"""Run administrative tasks."""
# IMPORTANT: Ensure this import is kept here, as it loads the envvars
import env # noqa: F401

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'web.settings')
try:
from django.core.management import execute_from_command_line
Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ dependencies = [
"channels[daphne]==4.1.0",
"channels-redis==4.2.0",

# Environment variables
"python-dotenv==1.1.1",

# Misc. packages
"bleach[css]==6.1.0",
"Pillow==11.3.0",
Expand Down
2 changes: 1 addition & 1 deletion src/util/url_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ def logout_urls():
from dataporten import views as dataporten_views # Avoids circular importing

# Both of these views log the user out, then redirects to the value of the `LOGOUT_REDIRECT_URL` setting
if settings.USES_DATAPORTEN_AUTH:
if settings.USE_DATAPORTEN_AUTH:
logout_view = dataporten_views.Logout.as_view()
else:
logout_view = auth_views.LogoutView.as_view()
Expand Down
2 changes: 1 addition & 1 deletion src/web/admin_urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@

# Disable admin page login if Dataporten is configured,
# as in that case, all users would log in through Dataporten anyways
if settings.SOCIAL_AUTH_DATAPORTEN_SECRET:
if settings.USE_DATAPORTEN_AUTH:
urlpatterns.insert(0, path("login/", RedirectView.as_view(
url=f"{reverse('login', host='main')}?next=//admin.{settings.PARENT_HOST}"
)))
Loading