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
7 changes: 3 additions & 4 deletions mytral/blueprints/exercise_types_crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from mytral import forms
from mytral import muscle_groups as mg
from mytral import settings as user_settings
from mytral import utils
from mytral.blobstore import EntityPhotoService
from mytral.blobstore import exceptions as blob_exc
from mytral.blobstore import validation as blob_validation
Expand Down Expand Up @@ -321,8 +322,7 @@ def settings_exercises_create():
elif flask.request.method == "POST":
form = UpdateExerciseTypeForm()
if form.validate_on_submit():
tags_str = form.tags.data or ""
tags_list = [tag.strip() for tag in tags_str.split(",") if tag.strip()]
tags_list = utils.parse_tags_csv(form.tags.data)
muscle_groups_csv = flask.request.form.get("muscle_groups", "")
muscle_groups_list = mg.parse_muscle_groups_csv(muscle_groups_csv)
muscle_groups_secondary_csv = flask.request.form.get(
Expand Down Expand Up @@ -507,8 +507,7 @@ def settings_exercises_update(key: str):
elif flask.request.method == "POST":
form = UpdateExerciseTypeForm()
if form.validate_on_submit():
tags_str = form.tags.data or ""
tags_list = [tag.strip() for tag in tags_str.split(",") if tag.strip()]
tags_list = utils.parse_tags_csv(form.tags.data)
muscle_groups_csv = flask.request.form.get("muscle_groups", "")
muscle_groups_list = mg.parse_muscle_groups_csv(muscle_groups_csv)
muscle_groups_secondary_csv = flask.request.form.get(
Expand Down
12 changes: 12 additions & 0 deletions mytral/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from wtforms import validators

from mytral import commons
from mytral import utils
from mytral.backends import entities


Expand Down Expand Up @@ -658,6 +659,16 @@ class CreateActivityForm(flask_wtf.FlaskForm):
validators=[],
default="",
)
tags = wtforms.StringField(
render_kw={"placeholder": "trail, morning, with friends"},
label="Tags (comma-separated)",
description=(
"Labels to make the activity easy to find, separated by commas "
"(e.g. trail, morning, with friends)"
),
validators=[validators.Optional()],
default="",
)

###

Expand Down Expand Up @@ -1354,6 +1365,7 @@ def from_activity_form(
entity.gears = list(form.gears.data) if form.gears.data else []
entity.outfit = form.outfit.data or ""
entity.formula = form.formula.data or ""
entity.tags = utils.parse_tags_csv(form.tags.data)
if form.exercises.entries:
for exercise_form in form.exercises.entries:
entity.exercises.append(from_activity_exercise_form(exercise_form))
Expand Down
13 changes: 11 additions & 2 deletions mytral/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2732,6 +2732,7 @@ def _update_activity(key: str, template: str):
)
form.outfit.data = db_entity.outfit if hasattr(db_entity, "outfit") else ""
form.formula.data = db_entity.formula
form.tags.data = ", ".join(db_entity.tags) if db_entity.tags else ""

# exercises
if db_entity.exercises:
Expand Down Expand Up @@ -3818,6 +3819,7 @@ def list_activities_year(year):
filter_intensity = flask.request.args.get("intensity", "")
filter_type = flask.request.args.get("type", "")
filter_source = flask.request.args.get("source", "")
filter_tag = flask.request.args.get("tag", "")
aspect = flask.request.args.get("aspect", "feed")

# apply filters
Expand Down Expand Up @@ -3847,6 +3849,8 @@ def list_activities_year(year):
activities = [a for a in activities if a.ranked]
if filter_source:
activities = [a for a in activities if a.src == filter_source]
if filter_tag:
activities = [a for a in activities if filter_tag in (a.tags or [])]

# get sort parameters
sort_by = flask.request.args.get("sort", "when")
Expand Down Expand Up @@ -3916,6 +3920,7 @@ def list_activities_year(year):
)
unique_intensities = sorted(set(a.intensity for a in all_activities if a.intensity))
unique_sources = sorted(set(a.src for a in all_activities if a.src))
unique_tags = sorted({tag for a in all_activities for tag in (a.tags or [])})

# calculate year statistics for the cards
year_total_distance = sum(a.distance for a in activities if a.distance)
Expand Down Expand Up @@ -3997,11 +4002,13 @@ def list_activities_year(year):
unique_activity_types=unique_activity_types,
unique_intensities=unique_intensities,
unique_sources=unique_sources,
unique_tags=unique_tags,
filter_activity_type=filter_activity_type,
filter_gear=filter_gear,
filter_intensity=filter_intensity,
filter_type=filter_type,
filter_source=filter_source,
filter_tag=filter_tag,
sort_by=sort_by,
sort_order=sort_order,
aspect=aspect,
Expand Down Expand Up @@ -4037,12 +4044,14 @@ def search_activities():
sort_by_when=True,
)

# filter by case-insensitive substring match on name and description
# filter by case-insensitive substring match on name, description and tags
q_lower = q.lower()
activities = [
a
for a in activities
if q_lower in (a.name or "").lower() or q_lower in (a.description or "").lower()
if q_lower in (a.name or "").lower()
or q_lower in (a.description or "").lower()
or any(q_lower in tag.lower() for tag in (a.tags or []))
]

activities_weekdays = {
Expand Down
1 change: 1 addition & 0 deletions mytral/templates/activity-create.html
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ <h3 class="card-title">Activity Details</h3>
<div class="card-body">
{{ field_macros.add_field(form.name) }}
{{ field_macros.add_field(form.description) }}
{{ field_macros.add_field(form.tags) }}
</div>
</div>
</div>
Expand Down
11 changes: 11 additions & 0 deletions mytral/templates/activity-get.html
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,17 @@ <h2 class="mb-3">
{{ activity_entity.description|markdown }}
</div>

{% if activity_entity.tags %}
<div class="mb-4">
{% for tag in activity_entity.tags %}
{% set tag_color = tag|tag_to_color %}
<a href="{{ url_for('list_activities_year', year=activity_entity.when_year, tag=tag) }}" class="text-decoration-none">
<span class="badge bg-{{ tag_color }} text-{{ tag_color }}-fg">{{ tag }}</span>
</a>
Comment thread
Copilot marked this conversation as resolved.
{% endfor %}
</div>
{% endif %}

{% if activity_entity.distance %}
<div class="row g-3 mb-4">
<div class="col-sm-6 col-lg-3">
Expand Down
26 changes: 24 additions & 2 deletions mytral/templates/activity-list-year.html
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ <h2 class="page-title">
<div class="btn-list">
{% if activities|length > 0 %}
<div class="btn-group">
<a href="{{ url_for('list_activities_year', year=year, aspect='list', activity_type=filter_activity_type, gear=filter_gear, intensity=filter_intensity, type=filter_type, source=filter_source, sort=sort_by, order=sort_order) }}" class="btn btn-outline-primary">
<a href="{{ url_for('list_activities_year', year=year, aspect='list', activity_type=filter_activity_type, gear=filter_gear, intensity=filter_intensity, type=filter_type, source=filter_source, tag=filter_tag, sort=sort_by, order=sort_order) }}" class="btn btn-outline-primary">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-table" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M3 5a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v14a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-14z"></path>
Expand All @@ -68,7 +68,7 @@ <h2 class="page-title">
</svg>
List
</a>
<a href="{{ url_for('list_activities_year', year=year, aspect='feed', activity_type=filter_activity_type, gear=filter_gear, intensity=filter_intensity, type=filter_type, source=filter_source, sort=sort_by, order=sort_order) }}" class="btn btn-outline-primary">
<a href="{{ url_for('list_activities_year', year=year, aspect='feed', activity_type=filter_activity_type, gear=filter_gear, intensity=filter_intensity, type=filter_type, source=filter_source, tag=filter_tag, sort=sort_by, order=sort_order) }}" class="btn btn-outline-primary">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-layout-cards" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M4 4m0 2a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-2a2 2 0 0 1 -2 -2z"></path>
Expand Down Expand Up @@ -209,6 +209,18 @@ <h2 class="page-title">
{% endfor %}
</select>
</div>

<div class="col-md-2">
<label class="form-label">Tag</label>
<select class="form-select" name="tag" onchange="this.form.submit()">
<option value="">All</option>
{% for tag in unique_tags %}
<option value="{{ tag }}" {% if tag == filter_tag %}selected{% endif %}>
{{ tag }}
</option>
{% endfor %}
</select>
</div>
</div>

<div class="row g-3 mt-2">
Expand Down Expand Up @@ -438,6 +450,16 @@ <h3 class="mb-1">
<span class="mx-2">·</span>
<a href="{{ url_for('settings_activity_types_get', key=activity.activity_type_key) }}" class="text-reset">{{ activity_types.name(activity.activity_type_key) }}</a>
</div>
{% if activity.tags %}
<div class="mt-1">
{% for tag in activity.tags %}
{% set tag_color = tag|tag_to_color %}
<a href="{{ url_for('list_activities_year', year=year, aspect=aspect, tag=tag) }}" class="text-decoration-none">
<span class="badge bg-{{ tag_color }} text-{{ tag_color }}-fg">{{ tag }}</span>
</a>
Comment thread
Copilot marked this conversation as resolved.
{% endfor %}
</div>
{% endif %}
</div>
<div class="text-end">
{% if activity.commute %}
Expand Down
1 change: 1 addition & 0 deletions mytral/templates/activity-update-all.html
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ <h3 class="card-title">Activity Details</h3>
<div class="card-body">
{{ field_macros.add_field(form.name) }}
{{ field_macros.add_field(form.description) }}
{{ field_macros.add_field(form.tags) }}
</div>
</div>
</div>
Expand Down
1 change: 1 addition & 0 deletions mytral/templates/activity-update.html
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ <h3 class="card-title">Details</h3>
<div class="card-body">
{{ field_macros.add_field(form.name) }}
{{ field_macros.add_field(form.description) }}
{{ field_macros.add_field(form.tags) }}
</div>
</div>
</div>
Expand Down
13 changes: 13 additions & 0 deletions mytral/templates/macros/activity_list_table.html
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,19 @@
{{ gear.to_dict().get(gear_key, {}).name }}
{% endif %}{% if not loop.last %}, {% endif %}
{% endfor %}
{% if activity.tags %}
<div class="mt-1">
{% for tag in activity.tags %}
{% set tag_color = tag|tag_to_color %}
{% set badge %}<span class="badge bg-{{ tag_color }} text-{{ tag_color }}-fg">{{ tag }}</span>{% endset %}
{% if year %}
<a href="{{ url_for('list_activities_year', year=year, aspect='list', tag=tag) }}" class="text-decoration-none">{{ badge }}</a>
{% else %}
{{ badge }}
{% endif %}
{% endfor %}
</div>
{% endif %}
</td>
<td>
{{ activity_types.emoji(activity.activity_type_key) }}
Expand Down
22 changes: 22 additions & 0 deletions mytral/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,25 @@ def tag_to_color(tag: str) -> str:
color_index = tag_hash % len(colors)

return colors[color_index]


def parse_tags_csv(tags_csv: str | None) -> list[str]:
"""Parse a comma-separated tags string into a normalized list.

Parameters
----------
tags_csv : str | None
raw comma-separated tags input (may be None or empty)

Returns
-------
list[str]
trimmed, non-empty tags in input order with duplicates removed

"""
tags: list[str] = []
for tag in (tags_csv or "").split(","):
tag = tag.strip()
if tag and tag not in tags:
tags.append(tag)
return tags
Comment thread
dvorka marked this conversation as resolved.
Loading
Loading