Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
724a399
switch crispy forms to the bootstrap5 template pack
BuczynskiRafal Sep 30, 2026
ce82031
document the units of simulated timeseries
BuczynskiRafal Sep 30, 2026
88032d3
keep session and CSRF cookies usable on the plain-HTTP dev server
BuczynskiRafal Sep 30, 2026
d82e10d
log in with a username or email and start signed in after sign-up
BuczynskiRafal Sep 30, 2026
3e316e5
rebuild the app shell with design tokens, dark mode and shared scripts
BuczynskiRafal Sep 30, 2026
a52a561
make charts theme-aware and add a hyetograph with hydrograph view
BuczynskiRafal Sep 30, 2026
82bfda4
restyle the upload zone and let uploads work without JavaScript
BuczynskiRafal Sep 30, 2026
080ea89
run simulations without page reloads in a workbench layout
BuczynskiRafal Sep 30, 2026
dac01ad
show timeseries as a hyetograph with hydrograph and summary metrics
BuczynskiRafal Sep 30, 2026
4abbde5
compare SWMM and ANN runoff with charts and error metrics
BuczynskiRafal Sep 30, 2026
8fa44a9
drop every tool's results when the model changes
BuczynskiRafal Sep 30, 2026
e901271
rebuild the home page around the bundled example model
BuczynskiRafal Sep 30, 2026
9252015
restyle content and account pages and fix the contact and profile forms
BuczynskiRafal Sep 30, 2026
0bcefd5
keep analysis results across restarts and same-model reloads
BuczynskiRafal Sep 30, 2026
759345d
centre the navigation links on wide screens
BuczynskiRafal Sep 30, 2026
fa9f5a4
add the UI modernization plan and design spec
BuczynskiRafal Sep 30, 2026
fd291dc
merge main into ui-modernization
BuczynskiRafal Sep 30, 2026
db1b109
let no-JS e2e tests wait for styles and skip smooth scrolling
BuczynskiRafal Sep 30, 2026
d3e8ca1
listen for the file chooser before pressing the key in the upload test
BuczynskiRafal Sep 30, 2026
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
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ __pycache__/
db.sqlite3
cs_app/db.sqlite3
cs_app/media/
cs_app/.cache/
cs_app/data/db/
docs/_build/
output_files/
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ model = FeaturesSimulation(subcatchment_id=subcatchment_id, raw_file=raw_file)
ts = model.calculate_timeseries()
# ts is a DataFrame with DatetimeIndex and columns:
# rainfall, runoff, infiltration_loss, evaporation_loss, runon
# rainfall and infiltration_loss are in mm/h (in/h for US-unit models),
# evaporation_loss in mm/day (in/day), runoff and runon in the model's FLOW_UNITS
```

#### Collect timeseries for varying subcatchment parameter values.
Expand Down
2 changes: 2 additions & 0 deletions cs_app/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ SECRET_KEY=change-me-for-local-dev
DEBUG=True
ALLOWED_HOSTS=localhost,127.0.0.1
DATABASE_URL=sqlite:///db.sqlite3
# Analysis results cache (file-based); defaults to cs_app/.cache/django
# DJANGO_CACHE_DIR=

# E2E test credentials (optional — defaults are baked into conftest.py)
# E2E_TEST_USERNAME=
Expand Down
29 changes: 25 additions & 4 deletions cs_app/cs_app/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"django.contrib.staticfiles",
"import_export",
"crispy_forms",
"crispy_bootstrap4",
"crispy_bootstrap5",
"storages",
"main.apps.MainConfig",
"register.apps.RegisterConfig",
Expand Down Expand Up @@ -94,6 +94,18 @@
"default": dj_database_url.parse(db_url, conn_max_age=600),
}

# Analysis results are kept in the cache between page visits. The file backend survives
# a server restart (runserver's autoreload, a deploy) and is shared by every worker
# process on the host; the default in-memory cache is neither. A deployment running
# several hosts needs a shared backend instead (Redis, database).
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.filebased.FileBasedCache",
"LOCATION": os.environ.get("DJANGO_CACHE_DIR", os.path.join(BASE_DIR, ".cache", "django")),
"OPTIONS": {"MAX_ENTRIES": 1000},
}
}

AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
Expand All @@ -109,6 +121,9 @@
},
]

# Replaces ModelBackend (not alongside it): login() without backend= needs exactly one.
AUTHENTICATION_BACKENDS = ["register.backends.UsernameOrEmailBackend"]

LOGIN_URL = "/accounts/login/"

LANGUAGE_CODE = "en-us"
Expand All @@ -121,7 +136,8 @@

DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

CRISPY_TEMPLATE_PACK = "bootstrap4"
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"
CRISPY_TEMPLATE_PACK = "bootstrap5"

LOGIN_REDIRECT_URL = "/"
LOGOUT_REDIRECT_URL = "/"
Expand All @@ -148,8 +164,13 @@

# Production Security Settings (Fail-Closed)
SECURE_SSL_REDIRECT = os.environ.get("SECURE_SSL_REDIRECT", "False").lower() == "true"
SESSION_COOKIE_SECURE = os.environ.get("SESSION_COOKIE_SECURE", "True").lower() == "true"
CSRF_COOKIE_SECURE = os.environ.get("CSRF_COOKIE_SECURE", "True").lower() == "true"
# Secure cookies unless DEBUG: the dev server speaks plain HTTP, and browsers
# that drop Secure cookies there (Safari) would reject every POST with a CSRF 403.
_SECURE_COOKIES_DEFAULT = "False" if DEBUG else "True"
SESSION_COOKIE_SECURE = (
os.environ.get("SESSION_COOKIE_SECURE", _SECURE_COOKIES_DEFAULT).lower() == "true"
)
CSRF_COOKIE_SECURE = os.environ.get("CSRF_COOKIE_SECURE", _SECURE_COOKIES_DEFAULT).lower() == "true"

SECURE_REFERRER_POLICY = "same-origin"

Expand Down
194 changes: 128 additions & 66 deletions cs_app/cs_app/templates/base.html
Original file line number Diff line number Diff line change
@@ -1,77 +1,125 @@
{% load static %}

<!DOCTYPE html>
<html lang="en">
<html lang="en" data-bs-theme="light">

<head>

<!-- Required meta tags -->
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">

<!-- Bootstrap CSS -->
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3"
crossorigin="anonymous"
>
<title>{% block title %}{% endblock %}</title>

<!-- Color snippet -->
<link href="https://cdn.jsdelivr.net/npm/prismjs@1.25.0/themes/prism.css" rel="stylesheet">
<link rel="stylesheet" href="https://unpkg.com/dropzone@5.9.3/dist/dropzone.css">
<!-- Sets data-bs-theme before first paint; must stay synchronous and first. -->
<script src="{% static 'main/js/core/theme-init.js' %}"></script>

<!-- Upload zone font -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600&display=swap" rel="stylesheet">

<link rel="stylesheet" href="{% static 'main/css/base.css' %}">

<title>
{% block title %}{% endblock %}
</title>
<link href="https://fonts.googleapis.com/css2?family=Archivo:wdth,wght@100..125,600..700&family=IBM+Plex+Mono:wght@400;500&family=Public+Sans:wght@400;500;600&display=swap" rel="stylesheet">

<!-- Vendor JavaScript -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.25.0/prism.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.25.0/components/prism-python.min.js"></script>
<script src="https://unpkg.com/dropzone@5.9.3/dist/dropzone.js"></script>
<script>Dropzone.autoDiscover = false;</script>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB"
crossorigin="anonymous"
>
<link rel="stylesheet" href="{% static 'main/css/tokens.css' %}">
<link rel="stylesheet" href="{% static 'main/css/app.css' %}">

{% block head_scripts %}{% endblock %}
{% block style %}{% endblock %}
</head>

<body data-login-url="{% url 'login' %}">
<header class="py-3 mb-4 border-bottom">
<div class="container">
<div class="d-flex flex-wrap align-items-center justify-content-center justify-content-md-between">
<a href="/" class="d-flex align-items-center col-md-3 mb-2 mb-md-0 text-dark text-decoration-none">
<img src="{% static '/img/logo.png' %}" alt="Logo" height="50px">
<a class="visually-hidden-focusable cs-skip-link" href="#main-content">Skip to content</a>

<header class="cs-header sticky-top">
<nav class="navbar navbar-expand-lg" aria-label="Main">
<div class="container">
{# The link is named after the visible wordmark (WCAG 2.5.3); alt="Logo" stays as a test hook. #}
<a class="navbar-brand cs-brand" href="{% url 'main:main_view' %}" aria-label="Catchment Simulation home">
<img src="{% static 'img/logo.png' %}" alt="Logo" width="30" height="36">
<span class="cs-brand__wordmark">Catchment Simulation</span>
</a>
<ul class="nav col-12 col-md-auto mb-2 justify-content-center mb-md-0">
<li><a href="{% url 'main:main_view' %}" class="nav-link px-2 link-dark">Home</a></li>
<li><a href="{% url 'main:simulation' %}" class="nav-link px-2 link-dark">Simulation</a></li>
<li><a href="{% url 'main:timeseries' %}" class="nav-link px-2 link-dark">Timeseries</a></li>
<li><a href="{% url 'main:calculations' %}" class="nav-link px-2 link-dark">Calculations</a></li>
<li><a href="{% url 'main:about' %}" class="nav-link px-2 link-dark">About</a></li>
</ul>
<div class="col-md-3 text-end">
{% if user.is_authenticated %}
<a type="button" class="btn btn-outline-primary me-2" href="{% url 'logout' %}">Logout</a>
{% else %}
<a type="button" class="btn btn-outline-primary me-2" href="{% url 'login' %}">Login</a>
<a type="button" class="btn btn-primary" href="{% url 'register:register' %}">Sign-up</a>
{% endif %}

<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#main-nav"
aria-controls="main-nav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>

<div class="collapse navbar-collapse" id="main-nav">
{% with current=request.resolver_match.view_name %}
<ul class="navbar-nav cs-nav-links mb-2 mb-lg-0">
<li class="nav-item"><a class="nav-link" href="{% url 'main:main_view' %}"{% if current == 'main:main_view' %} aria-current="page"{% endif %}>Home</a></li>
<li class="nav-item"><a class="nav-link" href="{% url 'main:simulation' %}"{% if current == 'main:simulation' %} aria-current="page"{% endif %}>Simulation</a></li>
<li class="nav-item"><a class="nav-link" href="{% url 'main:timeseries' %}"{% if current == 'main:timeseries' %} aria-current="page"{% endif %}>Timeseries</a></li>
<li class="nav-item"><a class="nav-link" href="{% url 'main:calculations' %}"{% if current == 'main:calculations' %} aria-current="page"{% endif %}>Calculations</a></li>
<li class="nav-item"><a class="nav-link" href="{% url 'main:about' %}"{% if current == 'main:about' %} aria-current="page"{% endif %}>About</a></li>
</ul>
{% endwith %}

<ul class="navbar-nav align-items-lg-center cs-nav-tools">
<li class="nav-item dropdown">
<button class="nav-link dropdown-toggle cs-theme-toggle" id="theme-menu" type="button"
data-bs-toggle="dropdown" aria-expanded="false">
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="8.5"/><path d="M12 3.5v17a8.5 8.5 0 0 0 0-17z" fill="currentColor" stroke="none"/></svg>
<span class="cs-theme-toggle__text">Theme: <span data-cs-theme-label>Auto</span></span>
</button>
<ul class="dropdown-menu dropdown-menu-lg-end cs-theme-menu" aria-labelledby="theme-menu">
<li>
<button type="button" class="dropdown-item" data-cs-theme-value="light" aria-pressed="false">
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>
Light
<svg class="cs-check" aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m5 12 5 5 9-10"/></svg>
</button>
</li>
<li>
<button type="button" class="dropdown-item" data-cs-theme-value="dark" aria-pressed="false">
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"><path d="M20 14.5A8.5 8.5 0 0 1 9.5 4a8.5 8.5 0 1 0 10.5 10.5z"/></svg>
Dark
<svg class="cs-check" aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m5 12 5 5 9-10"/></svg>
</button>
</li>
<li>
<button type="button" class="dropdown-item" data-cs-theme-value="auto" aria-pressed="false">
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"><rect x="3" y="4.5" width="18" height="12" rx="1.5"/><path d="M8.5 20h7M12 16.5V20"/></svg>
Auto (system)
<svg class="cs-check" aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m5 12 5 5 9-10"/></svg>
</button>
</li>
</ul>
</li>

{% if user.is_authenticated %}
<li class="nav-item dropdown">
<button class="nav-link dropdown-toggle" id="account-menu" type="button"
data-bs-toggle="dropdown" aria-expanded="false">
<span class="visually-hidden">Account: </span>{{ user.get_username }}
</button>
<ul class="dropdown-menu dropdown-menu-lg-end" aria-labelledby="account-menu">
<li><a class="dropdown-item" href="{% url 'main:userprofile' user.id %}">Profile</a></li>
<li><hr class="dropdown-divider"></li>
<li>
{# POST: logging out changes state, so it needs CSRF protection (GET logout is gone in Django 5). #}
<form method="post" action="{% url 'logout' %}">
{% csrf_token %}
<button type="submit" class="dropdown-item">Logout</button>
</form>
</li>
</ul>
</li>
{% else %}
<li class="nav-item"><a class="btn btn-sm btn-outline-primary" href="{% url 'login' %}">Log in</a></li>
<li class="nav-item"><a class="btn btn-sm btn-primary" href="{% url 'register:register' %}">Create account</a></li>
{% endif %}
</ul>
</div>
</div>
</div>
</nav>
</header>

<div class="container content-wrapper">
<main id="main-content" class="container content-wrapper" tabindex="-1">
{% if messages %}
<div class="mt-3">
<div class="cs-messages">
{% for message in messages %}
<div class="alert {% if 'error' in message.tags %}alert-danger{% elif 'success' in message.tags %}alert-success{% elif 'warning' in message.tags %}alert-warning{% else %}alert-info{% endif %} alert-dismissible fade show" role="alert">
{{ message }}
Expand All @@ -82,25 +130,39 @@
{% endif %}
{% block content %}
{% endblock %}
</div>


<footer class="py-3 my-4 border-top">
<div class="container">
<div class="d-flex flex-wrap justify-content-between align-items-center">
<p class="col-md-4 mb-0 text-muted">&copy; Rafał Buczyński 2023</p>
<a href="/" class="col-md-4 d-flex align-items-center justify-content-center mb-3 mb-md-0 me-md-autolink-dark text-decoration-none">
<img src="{% static '/img/logo.png' %}" alt="Logo" width="30px">
</a>
<ul class="nav col-md-4 justify-content-end">
<li class="nav-item"><a href="{% url 'main:about' %}" class="nav-link px-2 text-muted">About</a></li>
</ul>
</div>
</main>

<div class="toast-container position-fixed bottom-0 end-0 p-3" aria-live="polite"></div>

<footer class="cs-footer">
<div class="container d-flex flex-wrap align-items-center justify-content-between gap-3">
<p class="mb-0 text-muted">&copy; Rafał Buczyński {% now "Y" %}</p>
<a href="{% url 'main:main_view' %}" class="cs-footer__logo">
<img src="{% static 'img/logo.png' %}" alt="Logo" width="23" height="28">
</a>
<ul class="nav">
<li class="nav-item"><a href="{% url 'main:about' %}" class="nav-link">About</a></li>
<li class="nav-item"><a href="{% url 'main:contact' %}" class="nav-link">Contact</a></li>
<li class="nav-item"><a href="https://github.com/BuczynskiRafal/catchments_simulation" class="nav-link" rel="noopener">GitHub</a></li>
<li class="nav-item"><a href="https://pypi.org/project/catchment-simulation/" class="nav-link" rel="noopener">PyPI</a></li>
</ul>
</div>
</footer>

<script src="{% static 'main/js/base_ui.js' %}"></script>
<script src="{% static 'main/js/upload_zone.js' %}"></script>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"
integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI"
crossorigin="anonymous"
></script>
<script src="{% static 'main/js/core/theme.js' %}"></script>
<script src="{% static 'main/js/core/toast.js' %}"></script>
<script src="{% static 'main/js/core/format.js' %}"></script>
<script src="{% static 'main/js/core/clipboard.js' %}"></script>
<script src="{% static 'main/js/core/auth-guard.js' %}"></script>
<script src="{% static 'main/js/core/async-form.js' %}"></script>
<script src="{% static 'main/js/core/table.js' %}"></script>
<script src="{% static 'main/js/core/forms.js' %}"></script>
<script src="{% static 'main/js/core/workbench.js' %}"></script>

{% block additional_scripts %}{% endblock %}
</body>
Expand Down
24 changes: 18 additions & 6 deletions cs_app/cs_app/templates/registration/login.html
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
{% extends "base.html" %}
{% load crispy_forms_tags %}
{% load crispy_forms_tags static %}

{% block title %} Logowanie {% endblock %}
{% block title %}Log in — Catchment Simulation{% endblock %}

{% block style %}
<link rel="stylesheet" href="{% static 'main/css/content.css' %}">
{% endblock %}

{% block content %}
<form method="POST" class="form-group">
<section class="cs-card auth-card" aria-labelledby="auth-title">
<p class="cs-eyebrow">Welcome back</p>
<h1 class="auth-card__title" id="auth-title">Log in</h1>
<p class="auth-card__lead">Log in to run simulations on your own SWMM models.</p>

<form method="post" class="auth-card__form" data-cs-form>
{% csrf_token %}
{{ form|crispy }}
<br>
<button type="submit" class="btn btn-success">Login</button>
<input type="hidden" name="next" value="{{ next }}">
<button type="submit" class="btn btn-primary w-100">Log in</button>
</form>
{% endblock %}

<p class="auth-card__switch">New here? <a href="{% url 'register:register' %}">Create an account</a></p>
</section>
{% endblock %}
Loading
Loading