Partial refactor of tags functionality
Still need to move tags db logic to BP and move workout tag logic to BP as well
This commit is contained in:
33
app.py
33
app.py
@@ -13,6 +13,7 @@ from routes.workout import workout_bp # Import the new workout blueprint
|
||||
from routes.sql_explorer import sql_explorer_bp # Import the new SQL explorer blueprint
|
||||
from routes.endpoints import endpoints_bp # Import the new endpoints blueprint
|
||||
from routes.export import export_bp # Import the new export blueprint
|
||||
from routes.tags import tags_bp # Import the new tags blueprint
|
||||
from extensions import db
|
||||
from utils import convert_str_to_date, generate_plot
|
||||
from flask_htmx import HTMX
|
||||
@@ -46,6 +47,7 @@ app.register_blueprint(workout_bp) # Register the workout blueprint
|
||||
app.register_blueprint(sql_explorer_bp) # Register the SQL explorer blueprint (prefix defined in blueprint file)
|
||||
app.register_blueprint(endpoints_bp) # Register the endpoints blueprint (prefix defined in blueprint file)
|
||||
app.register_blueprint(export_bp) # Register the export blueprint (prefix defined in blueprint file)
|
||||
app.register_blueprint(tags_bp) # Register the tags blueprint (prefix defined in blueprint file)
|
||||
|
||||
@app.after_request
|
||||
def response_minify(response):
|
||||
@@ -212,36 +214,7 @@ def settings():
|
||||
return render_template('settings.html', people=people, exercises=exercises)
|
||||
|
||||
|
||||
@ app.route("/tag/redirect", methods=['GET'])
|
||||
def goto_tag():
|
||||
person_id = request.args.get("person_id")
|
||||
tag_filter = request.args.get('filter')
|
||||
if person_id:
|
||||
return redirect(url_for('person_overview', person_id=int(person_id)) + tag_filter)
|
||||
return redirect(url_for('dashboard') + tag_filter)
|
||||
|
||||
|
||||
@ app.route("/tag/add", methods=['GET'])
|
||||
def add_tag():
|
||||
person_id = request.args.get("person_id")
|
||||
tag = request.args.get('tag')
|
||||
tag_filter = request.args.get('filter')
|
||||
if person_id:
|
||||
db.add_or_update_tag_for_person(person_id, tag, tag_filter)
|
||||
else:
|
||||
db.add_or_update_tag_for_dashboard(tag, tag_filter)
|
||||
return ""
|
||||
|
||||
|
||||
@ app.route("/tag/<int:tag_id>/delete", methods=['GET'])
|
||||
def delete_tag(tag_id):
|
||||
person_id = request.args.get("person_id")
|
||||
tag_filter = request.args.get("filter")
|
||||
if person_id:
|
||||
db.delete_tag_for_person(person_id=person_id, tag_id=tag_id)
|
||||
return redirect(url_for('get_person', person_id=person_id) + tag_filter)
|
||||
db.delete_tag_for_dashboard(tag_id)
|
||||
return redirect(url_for('dashboard') + tag_filter)
|
||||
# Routes moved to routes/tags.py blueprint
|
||||
|
||||
@ app.route("/person/<int:person_id>/exercise/<int:exercise_id>/sparkline", methods=['GET'])
|
||||
def get_exercise_progress_for_user(person_id, exercise_id):
|
||||
|
||||
103
routes/tags.py
Normal file
103
routes/tags.py
Normal file
@@ -0,0 +1,103 @@
|
||||
from flask import Blueprint, request, redirect, url_for, render_template, current_app
|
||||
from urllib.parse import urlencode, parse_qs, unquote_plus
|
||||
from flask_login import current_user
|
||||
from extensions import db
|
||||
from jinja2_fragments import render_block
|
||||
|
||||
tags_bp = Blueprint('tags', __name__, url_prefix='/tag')
|
||||
|
||||
# Helper function to get tags (assuming similar logic exists in dashboard/person_overview)
|
||||
# NOTE: This is a placeholder and might need adjustment based on actual data fetching logic
|
||||
def _get_tags_data(person_id=None):
|
||||
if person_id:
|
||||
# Logic to fetch tags for a specific person
|
||||
tags_raw = db.get_tags_for_person(person_id)
|
||||
# Assuming get_tags_for_person returns list like [{'tag_id': 1, 'tag_name': 'Bulk', 'tag_filter': '?tag=Bulk'}]
|
||||
return tags_raw # Adjust based on actual return format
|
||||
else:
|
||||
tags_raw = db.get_tags_for_dashboard()
|
||||
return tags_raw
|
||||
|
||||
|
||||
@tags_bp.route("/redirect", methods=['GET'])
|
||||
def goto_tag():
|
||||
"""Redirects or loads content based on tag filter."""
|
||||
person_id = request.args.get("person_id")
|
||||
tag_filter = request.args.get("filter", "") # Default to empty string
|
||||
|
||||
if person_id:
|
||||
# Assuming person_overview handles HTMX requests to render blocks
|
||||
# Corrected endpoint name for person overview
|
||||
target_url = url_for('person_overview', person_id=person_id) + tag_filter
|
||||
# Check if it's an HTMX request targeting #container
|
||||
if request.headers.get('HX-Target') == 'container' or request.headers.get('HX-Target') == '#container':
|
||||
# Need the actual function that renders person_overview content block
|
||||
# Placeholder: Re-render the person overview block with the filter
|
||||
# This requires knowing how person_overview fetches its data based on filters
|
||||
# return render_block('person_overview.html', 'content_block', person_id=person_id, filter=tag_filter)
|
||||
# For now, let's assume a full redirect might be simpler if block rendering is complex
|
||||
return redirect(target_url)
|
||||
else:
|
||||
return redirect(target_url)
|
||||
else:
|
||||
# Assuming dashboard handles HTMX requests to render blocks
|
||||
target_url = url_for('dashboard') + tag_filter
|
||||
if request.headers.get('HX-Target') == 'container' or request.headers.get('HX-Target') == '#container':
|
||||
# Need the actual function that renders dashboard content block
|
||||
# Placeholder: Re-render the dashboard block with the filter
|
||||
# This requires knowing how dashboard fetches its data based on filters
|
||||
# return render_block('dashboard.html', 'content_block', filter=tag_filter)
|
||||
# For now, let's assume a full redirect might be simpler
|
||||
return redirect(target_url)
|
||||
else:
|
||||
return redirect(target_url)
|
||||
|
||||
|
||||
@tags_bp.route("/add", methods=['POST']) # Changed to POST
|
||||
def add_tag():
|
||||
"""Adds a tag and returns the updated tags partial."""
|
||||
person_id = request.form.get("person_id") # Get from form data
|
||||
tag_name = request.form.get('tag_name')
|
||||
current_filter_str = request.form.get('current_filter', '')
|
||||
|
||||
if not tag_name:
|
||||
# Handle error - maybe return an error message partial?
|
||||
# For now, just re-render tags without adding
|
||||
tags = _get_tags_data(person_id)
|
||||
return render_template('partials/tags.html', tags=tags, person_id=person_id)
|
||||
|
||||
|
||||
# Parse the current filter string, add the new tag, and re-encode
|
||||
parsed_params = parse_qs(current_filter_str)
|
||||
# parse_qs returns lists for values, handle potential existing 'tag' param
|
||||
parsed_params['tag'] = [tag_name] # Set/overwrite tag param with the new one
|
||||
# Re-encode, ensuring proper handling of multiple values if needed (though 'tag' is likely single)
|
||||
tag_filter_value = "?" + urlencode(parsed_params, doseq=True)
|
||||
|
||||
if person_id:
|
||||
db.add_or_update_tag_for_person(person_id, tag_name, tag_filter_value)
|
||||
else:
|
||||
db.add_or_update_tag_for_dashboard(tag_name, tag_filter_value)
|
||||
|
||||
# Fetch updated tags and render the partial
|
||||
tags = _get_tags_data(person_id)
|
||||
return render_template('partials/tags.html', tags=tags, person_id=person_id)
|
||||
|
||||
|
||||
@tags_bp.route("/<int:tag_id>/delete", methods=['DELETE']) # Changed to DELETE
|
||||
def delete_tag(tag_id):
|
||||
"""Deletes a tag and returns the updated tags partial."""
|
||||
# We might get person_id from request body/headers if needed, or assume context
|
||||
# For simplicity, let's try deleting based on tag_id only first, assuming tags are unique enough or context is handled elsewhere
|
||||
# If person_id is strictly required for deletion scope:
|
||||
person_id = request.form.get("person_id") # Or from headers/session
|
||||
|
||||
# Decide which delete function to call based on context (person page vs dashboard)
|
||||
if person_id:
|
||||
db.delete_tag_for_person(person_id=person_id, tag_id=tag_id)
|
||||
else:
|
||||
db.delete_tag_for_dashboard(tag_id=tag_id)
|
||||
|
||||
# Fetch updated tags and render the partial
|
||||
tags = _get_tags_data(person_id)
|
||||
return render_template('partials/tags.html', tags=tags, person_id=person_id)
|
||||
@@ -10,6 +10,21 @@
|
||||
<div class="prose max-w-none">
|
||||
<p>Updates and changes to the site will be documented here, with the most recent changes listed first.</p>
|
||||
|
||||
<!-- New Entry for SQL Explorer SVG Plots -->
|
||||
<hr class="my-6">
|
||||
<h2 class="text-xl font-semibold mb-2">April 19, 2025</h2>
|
||||
<ul class="list-disc pl-5 space-y-1">
|
||||
<li>Refactored tag management functionality:</li>
|
||||
<ul class="list-disc pl-5 space-y-1">
|
||||
<li>Moved tag-related routes (`add_tag`, `delete_tag`, `goto_tag`) from `app.py` to a new blueprint
|
||||
`routes/tags.py`.</li>
|
||||
<li>Changed `add_tag` endpoint to use `POST` and `delete_tag` to use `DELETE`.</li>
|
||||
<li>Updated `add_tag` and `delete_tag` to return the updated `tags.html` partial via HTMX swap.</li>
|
||||
<li>Wrapped the inclusion of `tags.html` in `dashboard.html` and `person_overview.html` with
|
||||
`div#container` for correct HTMX targeting.</li>
|
||||
</ul>
|
||||
</ul>
|
||||
|
||||
<!-- New Entry for SQL Explorer SVG Plots -->
|
||||
<hr class="my-6">
|
||||
<h2 class="text-xl font-semibold mb-2">April 15, 2025</h2>
|
||||
|
||||
@@ -92,7 +92,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ render_partial('partials/tags.html',person_id=None, tags=tags) }}
|
||||
<div id="tags-container">
|
||||
{{ render_partial('partials/tags.html', person_id=None, tags=tags) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hidden" hx-get="{{ url_for('get_people_graphs') }}"
|
||||
@@ -160,7 +162,7 @@
|
||||
<tbody class="bg-white">
|
||||
|
||||
{% for set in exercise.sets %}
|
||||
<tr hx-get="{{ url_for('goto_tag') }}"
|
||||
<tr hx-get="{{ url_for('tags.goto_tag') }}"
|
||||
hx-vals='{"filter": "?exercise_id={{ set.exercise_id }}", "person_id" : "{{ person.id }}" }'
|
||||
hx-target="#container" hx-swap="innerHTML" hx-push-url="true"
|
||||
class="cursor-pointer">
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
<div class="flex w-full flex-wrap justify-center animate-fadeIn">
|
||||
{# Container for the tags partial, needed for HTMX swapping #}
|
||||
<div id="tags-container" class="flex w-full flex-wrap justify-center items-center animate-fadeIn space-x-2">
|
||||
|
||||
{# Display Existing Tags #}
|
||||
{% for t in tags %}
|
||||
<div data-te-chip-init data-te-ripple-init
|
||||
class="[word-wrap: break-word] my-[5px] mr-4 flex h-[32px] cursor-pointer items-center justify-between rounded-[16px] border border-[#9fa6b2] bg-[#eceff1] bg-[transparent] py-0 px-[12px] text-[13px] font-normal normal-case leading-loose text-[#4f4f4f] shadow-none transition-[opacity] duration-300 ease-linear hover:border-[#9fa6b2] hover:!shadow-none dark:text-neutral-200"
|
||||
class="[word-wrap: break-word] my-1 flex h-8 cursor-pointer items-center justify-between rounded-full border border-gray-300 bg-gray-100 py-0 px-3 text-sm font-normal text-gray-700 shadow-none transition-opacity duration-300 ease-linear hover:border-gray-400 hover:shadow-sm dark:border-neutral-600 dark:bg-neutral-700 dark:text-neutral-200"
|
||||
data-te-ripple-color="dark">
|
||||
<span hx-get="{{ url_for('goto_tag') }}" {% if person_id %}
|
||||
hx-vals='{"filter": "{{ t["tag_filter"] }}", "person_id": "{{ person_id }}"}' {% else %}
|
||||
hx-vals='{"filter": "{{ t["tag_filter"] }}"}' {% endif%} hx-target="#container" hx-push-url="true">{{
|
||||
t['tag_name'] }}</span>
|
||||
|
||||
{# Tag Name (Clickable to filter) #}
|
||||
<span class="pr-2" hx-get="{{ url_for('tags.goto_tag') }}"
|
||||
hx-vals='{"filter": "{{ t.tag_filter }}", "person_id": "{{ person_id | default("", true) }}"}'
|
||||
hx-target="#container" hx-push-url="true">{{ t.tag_name }}</span>
|
||||
|
||||
{# Delete Button #}
|
||||
<span
|
||||
class="float-right w-4 cursor-pointer pl-[8px] text-[16px] text-[#afafaf] opacity-[.53] transition-all duration-200 ease-in-out hover:text-[#8b8b8b] dark:text-neutral-400 dark:hover:text-neutral-100"
|
||||
hx-get="{{ url_for('delete_tag', tag_id=t['tag_id']) }}" {% if person_id %}
|
||||
hx-vals='{"filter": "{{ t["tag_filter"] }}", "person_id": "{{ person_id }}"}' {% else %}
|
||||
hx-vals='{"filter": "{{ t["tag_filter"] }}"}' {% endif%} hx-target="#container" hx-push-url="true" _="on htmx:confirm(issueRequest)
|
||||
halt the event
|
||||
call Swal.fire({title: 'Confirm', text:'Are you sure you want to delete {{ t['tag_name'] }} tag?'})
|
||||
if result.isConfirmed issueRequest()">
|
||||
class="ml-1 cursor-pointer text-gray-400 hover:text-gray-600 dark:text-neutral-400 dark:hover:text-neutral-100"
|
||||
hx-delete="{{ url_for('tags.delete_tag', tag_id=t.tag_id) }}" hx-target="#tags-container" {# Target the
|
||||
container to refresh the list #} hx-swap="outerHTML" {# Replace the whole container #}
|
||||
hx-confirm="Are you sure you want to delete the '{{ t.tag_name }}' tag?"
|
||||
hx-vals='{"person_id": "{{ person_id | default("", true) }}", "current_filter": "{{ request.query_string | default("", true) }}"}'>
|
||||
{# Pass context if needed by backend #}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
stroke="currentColor" class="h-3 w-3">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
@@ -26,52 +27,44 @@
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="flex justify-center space-x-2">
|
||||
<div>
|
||||
<button type="button" data-te-ripple-init data-te-ripple-color="light"
|
||||
class="inline-block rounded-full bg-primary p-2 uppercase leading-normal text-white shadow-md transition duration-150 ease-in-out hover:bg-primary-600 hover:shadow-[0_8px_9px_-4px_rgba(59,113,202,0.3),0_4px_18px_0_rgba(59,113,202,0.2)] focus:bg-primary-600 focus:shadow-[0_8px_9px_-4px_rgba(59,113,202,0.3),0_4px_18px_0_rgba(59,113,202,0.2)] focus:outline-none focus:ring-0 active:bg-primary-700 active:shadow-[0_8px_9px_-4px_rgba(59,113,202,0.3),0_4px_18px_0_rgba(59,113,202,0.2)]"
|
||||
id="add-tag">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m3.75 9v6m3-3H9m1.5-12H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
|
||||
</svg>
|
||||
{# Add Tag Section - Initially Hidden Button, reveals Form #}
|
||||
<div id="add-tag-section" class="my-1">
|
||||
{# Show Add Button #}
|
||||
<button id="show-add-tag-form-btn"
|
||||
class="inline-block rounded-full bg-blue-500 p-2 text-xs font-medium uppercase leading-normal text-white shadow-md transition duration-150 ease-in-out hover:bg-blue-600 hover:shadow-lg focus:bg-blue-600 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-700 active:shadow-lg"
|
||||
_="on click toggle .hidden on #add-tag-form then toggle .hidden on me">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
stroke="currentColor" class="w-4 h-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{# Add Tag Form (Initially Hidden) #}
|
||||
<form id="add-tag-form" class="hidden flex items-center space-x-1" hx-post="{{ url_for('tags.add_tag') }}"
|
||||
hx-target="#tags-container" {# Target the container to refresh the list #} hx-swap="outerHTML" {# Replace
|
||||
the whole container #}
|
||||
_="on htmx:afterRequest toggle .hidden on #show-add-tag-form-btn then toggle .hidden on me then set me.tag_name.value to ''">
|
||||
{# Hide form, show button, clear input after submit #}
|
||||
|
||||
<input type="hidden" name="person_id" value="{{ person_id | default('', true) }}">
|
||||
<input type="hidden" name="current_filter" value="{{ request.query_string.decode() | default('', true) }}">
|
||||
{# Pass
|
||||
context
|
||||
if needed #}
|
||||
|
||||
<input type="text" name="tag_name" required
|
||||
class="h-8 rounded border border-gray-300 px-2 text-sm focus:border-blue-500 focus:ring-blue-500"
|
||||
placeholder="New tag...">
|
||||
<button type="submit"
|
||||
class="inline-block rounded bg-green-500 px-3 py-1.5 text-xs font-medium uppercase leading-normal text-white shadow-md transition duration-150 ease-in-out hover:bg-green-600 hover:shadow-lg focus:bg-green-600 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-green-700 active:shadow-lg">
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
<button type="button"
|
||||
class="inline-block rounded bg-gray-400 px-3 py-1.5 text-xs font-medium uppercase leading-normal text-white shadow-md transition duration-150 ease-in-out hover:bg-gray-500 hover:shadow-lg focus:bg-gray-500 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-gray-600 active:shadow-lg"
|
||||
_="on click toggle .hidden on #show-add-tag-form-btn then toggle .hidden on the closest <form/>">
|
||||
Cancel
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
document.querySelector('#add-tag').addEventListener('click', function () {
|
||||
Swal.fire({
|
||||
title: 'Create a tag',
|
||||
input: 'text',
|
||||
inputAttributes: {
|
||||
autocapitalize: 'off'
|
||||
},
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'Add',
|
||||
showLoaderOnConfirm: true,
|
||||
preConfirm: (tag) => {
|
||||
return fetch(`{{ url_for('add_tag') }}?tag=${encodeURIComponent(tag)}&filter=${encodeURIComponent(window.location.search)}{% if person_id %}{{ "&person_id={person_id}".format(person_id=person_id) | safe }} {% endif%}`)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(response.statusText)
|
||||
}
|
||||
return response.text()
|
||||
})
|
||||
.catch(error => {
|
||||
Swal.showValidationMessage(
|
||||
`Request failed: ${error}`
|
||||
)
|
||||
})
|
||||
},
|
||||
allowOutsideClick: () => !Swal.isLoading()
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
htmx.ajax('GET', `{{ (url_for('person_overview', person_id=person_id) if person_id else url_for('dashboard')) + '?' + request.query_string.decode() }}`, '#container')
|
||||
}
|
||||
})
|
||||
|
||||
})
|
||||
</script>
|
||||
</div>
|
||||
@@ -92,7 +92,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ render_partial('partials/tags.html',person_id=person_id, tags=tags) }}
|
||||
<div id="tags-container">
|
||||
{{ render_partial('partials/tags.html', person_id=person_id, tags=tags) }}
|
||||
</div>
|
||||
|
||||
<div class="hidden" hx-get="{{ url_for('get_people_graphs') }}"
|
||||
hx-include="[name='exercise_id'],[name='min_date'],[name='max_date']"
|
||||
|
||||
Reference in New Issue
Block a user