aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel Schadt <kingdread@gmx.de>2026-11-07 23:52:53 +0100
committerDaniel Schadt <kingdread@gmx.de>2026-11-07 23:52:53 +0100
commitad3803e12e4ded69ae4bc948bee6e41c5089dbd5 (patch)
tree9408cbb7b77e5eacd5c69158fc1a621149c40a7d
parent3fb48b99ed6883154f4f0b9e3d3957ec9a23aef1 (diff)
downloadfietsboek-browse-climbs.tar.gz
fietsboek-browse-climbs.tar.bz2
fietsboek-browse-climbs.zip
starting point for climb browserbrowse-climbs
-rw-r--r--asset-sources/theme.scss25
-rw-r--r--fietsboek/routes.py1
-rw-r--r--fietsboek/static/theme.css20
-rw-r--r--fietsboek/static/theme.css.map2
-rw-r--r--fietsboek/templates/climb_browse.jinja2227
-rw-r--r--fietsboek/templates/details.jinja21
-rw-r--r--fietsboek/templates/layout.jinja23
-rw-r--r--fietsboek/views/climb.py17
8 files changed, 295 insertions, 1 deletions
diff --git a/asset-sources/theme.scss b/asset-sources/theme.scss
index fbb836e..684b075 100644
--- a/asset-sources/theme.scss
+++ b/asset-sources/theme.scss
@@ -241,6 +241,31 @@ strong {
}
/* </Climbs> */
+/* <Climb-Browse> */
+.climb-browse-card {
+ .card-body {
+ padding: 0;
+ display: grid;
+ grid-template-areas:
+ "map stats"
+ "map profile";
+ grid-template-rows: 190px 110px;
+ }
+
+ .climb-profile {
+ grid-area: profile;
+ width: 300px;
+ height: 110px;
+ }
+}
+
+.climb-browse-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 1rem;
+}
+/* </Climb-Browse> */
+
.count-field {
width: 50px;
display: inline-block;
diff --git a/fietsboek/routes.py b/fietsboek/routes.py
index da9ed04..636c62a 100644
--- a/fietsboek/routes.py
+++ b/fietsboek/routes.py
@@ -70,6 +70,7 @@ def includeme(config):
"/climb/{climb_id}/profile",
factory="fietsboek.models.Climb.factory",
)
+ config.add_route("climb-browse", "/climb/")
config.add_route("journey-list", "/journey/")
config.add_route(
diff --git a/fietsboek/static/theme.css b/fietsboek/static/theme.css
index 6b9e1c2..d1e63fb 100644
--- a/fietsboek/static/theme.css
+++ b/fietsboek/static/theme.css
@@ -232,6 +232,26 @@ strong {
}
/* </Climbs> */
+/* <Climb-Browse> */
+.climb-browse-card .card-body {
+ padding: 0;
+ display: grid;
+ grid-template-areas: "map stats" "map profile";
+ grid-template-rows: 190px 110px;
+}
+.climb-browse-card .climb-profile {
+ grid-area: profile;
+ width: 300px;
+ height: 110px;
+}
+
+.climb-browse-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 1rem;
+}
+
+/* </Climb-Browse> */
.count-field {
width: 50px;
display: inline-block;
diff --git a/fietsboek/static/theme.css.map b/fietsboek/static/theme.css.map
index 30b9235..b499ab1 100644
--- a/fietsboek/static/theme.css.map
+++ b/fietsboek/static/theme.css.map
@@ -1 +1 @@
-{"version":3,"sourceRoot":"","sources":["../../asset-sources/theme.scss"],"names":[],"mappings":"AAAA;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;;;AAGF;EACE;;AAEA;EACE;EACA;EACA;;;AAIJ;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAIA;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;AAlCJ;EAqCE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACI;;;AAGJ;EACE;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;;AAEA;EACE;EACA;;AAGF;EACE;;;AAIJ;EACE;;;AAGF;EACE;;;AAGF;AACA;EACE;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;;;AAIA;EACE;;;AAGJ;AAEA;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;EACE;;;AAEF;EACE;EACA;;;AAEF;EACE;;AAEA;EACE;EACA;;AAGF;EACE;EACA;;AAGF;EACE;EACA;;AAEA;EACE;EACA;;AAGF;EACE;EACA;;AAIJ;EACE;;AAEF;EACE;;AACA;EACE;;AAGJ;EACE;;AACA;EACE;;AAGJ;EACE;;AACA;EACE;;AAGJ;EACE;;AACA;EACE;;AAGJ;EACE;EACA;;AACA;EACE;;AAIJ;EACE;EACA;;;AAKF;EACE;EACA;EAEA;EACA;;;AAIJ;EACE;EACA;;;AAGF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAGF;EACE;;;AAGF;AACA;EACE;;;AAGF;EACE;EACA;;AAEA;EAJF;IAKI,eACE;;;AAIJ;EACE;;AAGF;EACE;;AAEA;EAHF;IAII;;;AAGF;EACE;EACA;;AAIJ;EACE;EACA;;;AAIJ;EACE;;;AAGF;AACA;EACE;EACA;AACA;AAAA;AAAA;EAGA;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;;AAEA;EACE;;;AAIJ;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE","file":"theme.css"} \ No newline at end of file
+{"version":3,"sourceRoot":"","sources":["../../asset-sources/theme.scss"],"names":[],"mappings":"AAAA;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;;;AAGF;EACE;;AAEA;EACE;EACA;EACA;;;AAIJ;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAIA;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;AAlCJ;EAqCE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACI;;;AAGJ;EACE;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;;AAEA;EACE;EACA;;AAGF;EACE;;;AAIJ;EACE;;;AAGF;EACE;;;AAGF;AACA;EACE;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;;;AAIA;EACE;;;AAGJ;AAEA;AAEE;EACE;EACA;EACA,qBACE;EAEF;;AAGF;EACE;EACA;EACA;;;AAIJ;EACE;EACA;EACA;;;AAEF;AAEA;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;EACE;;;AAEF;EACE;EACA;;;AAEF;EACE;;AAEA;EACE;EACA;;AAGF;EACE;EACA;;AAGF;EACE;EACA;;AAEA;EACE;EACA;;AAGF;EACE;EACA;;AAIJ;EACE;;AAEF;EACE;;AACA;EACE;;AAGJ;EACE;;AACA;EACE;;AAGJ;EACE;;AACA;EACE;;AAGJ;EACE;;AACA;EACE;;AAGJ;EACE;EACA;;AACA;EACE;;AAIJ;EACE;EACA;;;AAKF;EACE;EACA;EAEA;EACA;;;AAIJ;EACE;EACA;;;AAGF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAGF;EACE;;;AAGF;AACA;EACE;;;AAGF;EACE;EACA;;AAEA;EAJF;IAKI,eACE;;;AAIJ;EACE;;AAGF;EACE;;AAEA;EAHF;IAII;;;AAGF;EACE;EACA;;AAIJ;EACE;EACA;;;AAIJ;EACE;;;AAGF;AACA;EACE;EACA;AACA;AAAA;AAAA;EAGA;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;;AAEA;EACE;;;AAIJ;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE","file":"theme.css"} \ No newline at end of file
diff --git a/fietsboek/templates/climb_browse.jinja2 b/fietsboek/templates/climb_browse.jinja2
new file mode 100644
index 0000000..d754c46
--- /dev/null
+++ b/fietsboek/templates/climb_browse.jinja2
@@ -0,0 +1,227 @@
+{% extends "layout.jinja2" %}
+{% block content %}
+<div class="container">
+ <h1>{{ _("page.climb_browse.title") }}</h1>
+ <div class="mb-3">
+ <form id="browseFilter">
+ <div class="row g-3 mb-3">
+ <div class="col-12">
+ <div class="input-group">
+ <button type="button" class="btn btn-outline-secondary button-clear-input"><i class="bi bi-eraser-fill"></i></button>
+ <input name="search-terms" type="text" class="form-control ui-element" placeholder="{{ _("page.browse.filter.search_terms") }}" value="{{ request.params.get('search-terms', '') }}">
+ </div>
+ </div>
+ </div>
+
+ <div class="collapse row g-3 mb-3" id="advancedSearch">
+ <div class="col-md-6">
+ <div class="input-group">
+ <button type="button" class="btn btn-outline-secondary button-clear-input"><i class="bi bi-eraser-fill"></i></button>
+ <input name="tags" type="text" class="form-control ui-element" placeholder="{{ _("page.browse.filter.tags") }}" value="{{ request.params.get('tags', '') }}">
+ </div>
+ </div>
+
+ <div class="col-md-6">
+ <div class="input-group">
+ <button type="button" class="btn btn-outline-secondary button-clear-input"><i class="bi bi-eraser-fill"></i></button>
+ <input name="tagged-person" type="text" class="form-control ui-element" placeholder="{{ _("page.browse.filter.tagged_person") }}" value="{{ request.params.get('tagged-person', '') }}">
+ </div>
+ </div>
+
+ <div class="col-md-4">
+ <div class="input-group">
+ <button type="button" class="btn btn-outline-secondary button-clear-input"><i class="bi bi-eraser-fill"></i></button>
+ <input name="min-length" type="number" class="form-control ui-element" placeholder="{{ _("page.browse.filter.length_minimum") }}" value="{{ request.params.get('min-length', '') }}">
+ <span class="input-group-text ui-element">km</span>
+ </div>
+ </div>
+
+ <div class="col-md-4 fs-5 d-flex align-items-center justify-content-center ui-element">
+ {{ _("page.browse.filter.length_boundaries") }}
+ </div>
+
+ <div class="col-md-4">
+ <div class="input-group">
+ <button type="button" class="btn btn-outline-secondary button-clear-input"><i class="bi bi-eraser-fill"></i></button>
+ <input name="max-length" type="number" class="form-control ui-element" placeholder="{{ _("page.browse.filter.length_maximum") }}" value="{{ request.params.get('max-length', '') }}">
+ <span class="input-group-text ui-element">km</span>
+ </div>
+ </div>
+
+ <div class="col-md-4">
+ <div class="input-group">
+ <button type="button" class="btn btn-outline-secondary button-clear-input"><i class="bi bi-eraser-fill"></i></button>
+ <input name="min-uphill" type="number" class="form-control ui-element" placeholder="{{ _("page.browse.filter.uphill_minimum") }}" value="{{ request.params.get('min-uphill', '') }}">
+ <span class="input-group-text ui-element">m</span>
+ </div>
+ </div>
+
+ <div class="col-md-4 fs-5 d-flex align-items-center justify-content-center ui-element">
+ {{ _("page.browse.filter.uphill_boundaries") }}
+ </div>
+
+ <div class="col-md-4">
+ <div class="input-group">
+ <button type="button" class="btn btn-outline-secondary button-clear-input"><i class="bi bi-eraser-fill"></i></button>
+ <input name="max-uphill" type="number" class="form-control ui-element" placeholder="{{ _("page.browse.filter.uphill_maximum") }}" value="{{ request.params.get('max-uphill', '') }}">
+ <span class="input-group-text ui-element">m</span>
+ </div>
+ </div>
+
+ <div class="col-md-4">
+ <div class="input-group">
+ <button type="button" class="btn btn-outline-secondary button-clear-input"><i class="bi bi-eraser-fill"></i></button>
+ <input name="min-date" type="date" class="form-control ui-element" value="{{ request.params.get('min-date', '') }}">
+ </div>
+ </div>
+
+ <div class="col-md-4 fs-5 d-flex align-items-center justify-content-center ui-element">
+ {{ _("page.browse.filter.date_boundaries") }}
+ </div>
+
+ <div class="col-md-4">
+ <div class="input-group">
+ <button type="button" class="btn btn-outline-secondary button-clear-input"><i class="bi bi-eraser-fill"></i></button>
+ <input name="max-date" type="date" class="form-control ui-element" value="{{ request.params.get('max-date', '') }}">
+ </div>
+ </div>
+
+ <div class="col-md-6">
+ <div class="input-group">
+ <button type="button" class="btn btn-outline-secondary button-clear-input"><i class="bi bi-eraser-fill"></i></button>
+ <select class="form-select ui-element" size="2" name="type[]" multiple>
+ <option value="ORGANIC"{% if "ORGANIC" in request.params.getall("type[]") %} selected{% endif %}>{{ _("page.browse.filter.type.organic") }}</option>
+ <option value="SYNTHETIC"{% if "SYNTHETIC" in request.params.getall("type[]") %} selected{% endif %}>{{ _("page.browse.filter.type.synthetic") }}</option>
+ </select>
+ </div>
+
+ {% if request.identity %}
+ <div class="input-group mt-2">
+ <button type="button" class="btn btn-outline-secondary button-clear-input ui-element"><i class="bi bi-eraser-fill"></i></button>
+ <select class="form-select" name="favourite">
+ <option value="">{{ _("page.browse.filter.favourite.all") }}</option>
+ <option value="true"{% if request.params.get("favourite") == "true" %} selected{% endif %}>{{ _("page.browse.filter.favourite.yes") }}</option>
+ <option value="false"{% if request.params.get("favourite") == "false" %} selected{% endif %}>{{ _("page.browse.filter.favourite.no") }}</option>
+ </select>
+ </div>
+ {% endif %}
+ </div>
+ <div class="col-md-6">
+ {% if request.identity %}
+ {% macro render_switch(id, name, value, text) %}
+ <div class="form-check form-switch ui-element">
+ <input class="form-check-input" type="checkbox" role="switch" id="{{ id }}" name="{{ name }}" value="{{ value }}" {% if value in request.params.getall(name) %}checked{% endif %}>
+ <label class="form-check-label" for="{{ id }}">{{ text }}</label>
+ </div>
+ {% endmacro %}
+ {{ render_switch("switchOnlyMyTracks", "show-only[]", "mine", _("page.browse.filter.my_tracks.only")) }}
+ {{ render_switch("switchOnlyFriendsTracks", "show-only[]", "friends", _("page.browse.filter.friends_tracks_only")) }}
+ {{ render_switch("switchOnlyMeTagged", "show-only[]", "user-tagged", _("page.browse.filter.me_tagged_only")) }}
+ {% endif %}
+ </div>
+
+ </div>
+
+ <div class="row g-3 mb-3">
+ <div class="col">
+ <button type="submit" class="btn btn-primary ui-element">
+ <i class="bi bi-funnel-fill"></i>
+ {{ _("page.browse.filters.apply") }}
+ </button>
+ <a class="btn btn-info ui-element" href="{{ request.route_path('browse') }}">
+ <i class="bi bi-x-octagon"></i>
+ {{ _("page.browse.filters.clear_all") }}
+ </a>
+ <button class="btn btn-secondary ui-element" type="button" data-bs-toggle="collapse" data-bs-target="#advancedSearch" aria-expanded="false" aria-controls="advancedSearch">
+ {{ _("page.browse.filters.expand_advanced") }}
+ </button>
+ </div>
+ <div class="col">
+ <select class="form-select ui-element" name="sort">
+ <option value="DATE_DESC" {% if request.params.get("sort") == "DATE_DESC" %}selected{% endif %}>{{ _("page.browse.sort.date") }} &ShortDownArrow;</option>
+ <option value="DATE_ASC" {% if request.params.get("sort") == "DATE_ASC" %}selected{% endif %}>{{ _("page.browse.sort.date") }} &ShortUpArrow;</option>
+ <option value="LENGTH_DESC" {% if request.params.get("sort") == "LENGTH_DESC" %}selected{% endif %}>{{ _("page.browse.sort.length") }} &ShortDownArrow;</option>
+ <option value="LENGTH_ASC" {% if request.params.get("sort") == "LENGTH_ASC" %}selected{% endif %}>{{ _("page.browse.sort.length") }} &ShortUpArrow;</option>
+ <option value="UPHILL_DESC" {% if request.params.get("sort") == "UPHILL_DESC" %}selected{% endif %}>{{ _("page.browse.sort.uphill") }} &ShortDownArrow;</option>
+ <option value="UPHILL_ASC" {% if request.params.get("sort") == "UPHILL_ASC" %}selected{% endif %}>{{ _("page.browse.sort.uphill") }} &ShortUpArrow;</option>
+ <option value="DURATION_DESC" {% if request.params.get("sort") == "DURATION_DESC" %}selected{% endif %}>{{ _("page.browse.sort.duration") }} &ShortDownArrow;</option>
+ <option value="DURATION_ASC" {% if request.params.get("sort") == "DURATION_ASC" %}selected{% endif %}>{{ _("page.browse.sort.duration") }} &ShortUpArrow;</option>
+ </select>
+ </div>
+ </div>
+ </form>
+ </div>
+ {% if climbs %}
+ <div class="climb-browse-list mb-3">
+ {% for climb in climbs %}
+ <div class="card climb-browse-card">
+ <h5 class="card-header">
+ <a href="{{ request.route_url('details', track_id=climb.track_id) }}#climb-{{ climb.id }}">
+ {{ climb.display_name() }}
+ </a>
+ {% if climb.category is not none %}
+ <div class="climb-category climb-category-{{ climb.category }}">
+ {% if climb.category == 0 %}
+ HC
+ {% else %}
+ {{ climb.category }}
+ {% endif %}
+ </div>
+ {% endif %}
+ </h5>
+ <div class="card-body">
+ <img src="{{ request.route_url('climb-preview', climb_id=climb.id) }}" style="grid-area: map;">
+ <table class="table climb-details" style="grid-area: stats;">
+ <tr>
+ <th scope="row">{{ _("page.details.climb.length") }}</th>
+ <td>{{ (climb.length / 1000) | round(2) | format_decimal }} km</td>
+ </tr>
+ <tr>
+ <th scope="row">{{ _("page.details.climb.average_grade") }}</th>
+ <td>{{ (climb.avg_grade * 100) | round(2) | format_decimal }} %</td>
+ </tr>
+ <tr>
+ <th scope="row">{{ _("page.details.climb.lowest_elevation") }}</th>
+ <td>{{ climb.lowest_elevation | round(2) | format_decimal }} m</td>
+ </tr>
+ <tr>
+ <th scope="row">{{ _("page.details.climb.highest_elevation") }}</th>
+ <td>{{ climb.highest_elevation | round(2) | format_decimal }} m</td>
+ </tr>
+ </table>
+ <img class="climb-profile" src="{{ request.route_url('climb-profile', climb_id=climb.id) }}">
+ </div>
+ </div>
+ {% endfor %}
+ </div>
+
+ <nav aria-label="Page navigation">
+ <ul class="pagination justify-content-center">
+ {% if page_previous is none %}
+ <li class="page-item disabled">
+ <span class="page-link">{{ _("pagination.previous") }}</span>
+ </li>
+ {% else %}
+ <li class="page-item">
+ <a class="page-link" href="{{ page_previous | safe }}">{{ _("pagination.previous") }}</a>
+ </li>
+ {% endif %}
+ {% if page_next is none %}
+ <li class="page-item disabled">
+ <span class="page-link">{{ _("pagination.next") }}</span>
+ </li>
+ {% else %}
+ <li class="page-item">
+ <a class="page-link" href="{{ page_next | safe }}">{{ _("pagination.next") }}</a>
+ </li>
+ {% endif %}
+ </ul>
+ </nav>
+
+ {% elif used_filters %}
+ <p>{{ _("page.climb_browse.no_results") }}</p>
+ {% else %}
+ <p>{{ _("page.climb_browse.no_climbs") }}</p>
+ {% endif %}
+</div>
+{% endblock %}
diff --git a/fietsboek/templates/details.jinja2 b/fietsboek/templates/details.jinja2
index 210e301..0fd7d30 100644
--- a/fietsboek/templates/details.jinja2
+++ b/fietsboek/templates/details.jinja2
@@ -199,6 +199,7 @@
<div class="climbs">
{% for climb in track.climbs %}
<div class="climb card">
+ <a name="climb-{{ climb.id }}">
<h5 class="card-header">
{{ climb.display_name() | default(_("page.details.climbs.climb"), true) }}
{% if climb.category is not none %}
diff --git a/fietsboek/templates/layout.jinja2 b/fietsboek/templates/layout.jinja2
index 3058553..d7f8086 100644
--- a/fietsboek/templates/layout.jinja2
+++ b/fietsboek/templates/layout.jinja2
@@ -60,6 +60,9 @@ const Legende = false;
<li class="nav-item">
<a class="nav-link" href="{{ request.route_url('journey-list') }}">{{ _("page.navbar.journeys") }}</a>
</li>
+ <li class="nav-item">
+ <a class="nav-link" href="{{ request.route_url('climb-browse') }}">{{ _("page.navbar.climb-browse") }}</a>
+ </li>
{% if request.identity is not none %}
<li class="nav-item">
<a class="nav-link" href="{{ request.route_url('upload') }}">{{ _("page.navbar.upload") }}</a>
diff --git a/fietsboek/views/climb.py b/fietsboek/views/climb.py
index 1f5641f..e3594ef 100644
--- a/fietsboek/views/climb.py
+++ b/fietsboek/views/climb.py
@@ -9,11 +9,28 @@ from pyramid.httpexceptions import (
from pyramid.request import Request
from pyramid.response import Response
from pyramid.view import view_config
+from sqlalchemy import select
+from sqlalchemy.orm import aliased
from .. import models
LOGGER = logging.getLogger(__name__)
+@view_config(
+ route_name="climb-browse",
+ renderer="fietsboek:templates/climb_browse.jinja2",
+ request_method="GET",
+)
+def climb_browse(request: Request):
+ tracks_qry = aliased(models.Track, models.User.visible_tracks_query(request.identity).subquery())
+ climbs_qry = select(models.Climb).filter(models.Climb.track_id.in_(select(tracks_qry.id)))
+ climbs = list(request.dbsession.execute(climbs_qry).scalars())
+ return {
+ "climbs": climbs,
+ "page_previous": None,
+ "page_next": None,
+ }
+
@view_config(route_name="climb-preview", permission="track.view")
def climb_preview(request: Request):