Skip to content

Commit 8930f50

Browse files
authored
feat(activity): ✨ adding activity tags (UI, routes, persistence) #45 (#136)
1 parent de5eea1 commit 8930f50

11 files changed

Lines changed: 349 additions & 8 deletions

mytral/blueprints/exercise_types_crud.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from mytral import forms
2626
from mytral import muscle_groups as mg
2727
from mytral import settings as user_settings
28+
from mytral import utils
2829
from mytral.blobstore import EntityPhotoService
2930
from mytral.blobstore import exceptions as blob_exc
3031
from mytral.blobstore import validation as blob_validation
@@ -321,8 +322,7 @@ def settings_exercises_create():
321322
elif flask.request.method == "POST":
322323
form = UpdateExerciseTypeForm()
323324
if form.validate_on_submit():
324-
tags_str = form.tags.data or ""
325-
tags_list = [tag.strip() for tag in tags_str.split(",") if tag.strip()]
325+
tags_list = utils.parse_tags_csv(form.tags.data)
326326
muscle_groups_csv = flask.request.form.get("muscle_groups", "")
327327
muscle_groups_list = mg.parse_muscle_groups_csv(muscle_groups_csv)
328328
muscle_groups_secondary_csv = flask.request.form.get(
@@ -507,8 +507,7 @@ def settings_exercises_update(key: str):
507507
elif flask.request.method == "POST":
508508
form = UpdateExerciseTypeForm()
509509
if form.validate_on_submit():
510-
tags_str = form.tags.data or ""
511-
tags_list = [tag.strip() for tag in tags_str.split(",") if tag.strip()]
510+
tags_list = utils.parse_tags_csv(form.tags.data)
512511
muscle_groups_csv = flask.request.form.get("muscle_groups", "")
513512
muscle_groups_list = mg.parse_muscle_groups_csv(muscle_groups_csv)
514513
muscle_groups_secondary_csv = flask.request.form.get(

mytral/forms.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from wtforms import validators
2424

2525
from mytral import commons
26+
from mytral import utils
2627
from mytral.backends import entities
2728

2829

@@ -658,6 +659,16 @@ class CreateActivityForm(flask_wtf.FlaskForm):
658659
validators=[],
659660
default="",
660661
)
662+
tags = wtforms.StringField(
663+
render_kw={"placeholder": "trail, morning, with friends"},
664+
label="Tags (comma-separated)",
665+
description=(
666+
"Labels to make the activity easy to find, separated by commas "
667+
"(e.g. trail, morning, with friends)"
668+
),
669+
validators=[validators.Optional()],
670+
default="",
671+
)
661672

662673
###
663674

@@ -1354,6 +1365,7 @@ def from_activity_form(
13541365
entity.gears = list(form.gears.data) if form.gears.data else []
13551366
entity.outfit = form.outfit.data or ""
13561367
entity.formula = form.formula.data or ""
1368+
entity.tags = utils.parse_tags_csv(form.tags.data)
13571369
if form.exercises.entries:
13581370
for exercise_form in form.exercises.entries:
13591371
entity.exercises.append(from_activity_exercise_form(exercise_form))

mytral/routes.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2732,6 +2732,7 @@ def _update_activity(key: str, template: str):
27322732
)
27332733
form.outfit.data = db_entity.outfit if hasattr(db_entity, "outfit") else ""
27342734
form.formula.data = db_entity.formula
2735+
form.tags.data = ", ".join(db_entity.tags) if db_entity.tags else ""
27352736

27362737
# exercises
27372738
if db_entity.exercises:
@@ -3818,6 +3819,7 @@ def list_activities_year(year):
38183819
filter_intensity = flask.request.args.get("intensity", "")
38193820
filter_type = flask.request.args.get("type", "")
38203821
filter_source = flask.request.args.get("source", "")
3822+
filter_tag = flask.request.args.get("tag", "")
38213823
aspect = flask.request.args.get("aspect", "feed")
38223824

38233825
# apply filters
@@ -3847,6 +3849,8 @@ def list_activities_year(year):
38473849
activities = [a for a in activities if a.ranked]
38483850
if filter_source:
38493851
activities = [a for a in activities if a.src == filter_source]
3852+
if filter_tag:
3853+
activities = [a for a in activities if filter_tag in (a.tags or [])]
38503854

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

39203925
# calculate year statistics for the cards
39213926
year_total_distance = sum(a.distance for a in activities if a.distance)
@@ -3997,11 +4002,13 @@ def list_activities_year(year):
39974002
unique_activity_types=unique_activity_types,
39984003
unique_intensities=unique_intensities,
39994004
unique_sources=unique_sources,
4005+
unique_tags=unique_tags,
40004006
filter_activity_type=filter_activity_type,
40014007
filter_gear=filter_gear,
40024008
filter_intensity=filter_intensity,
40034009
filter_type=filter_type,
40044010
filter_source=filter_source,
4011+
filter_tag=filter_tag,
40054012
sort_by=sort_by,
40064013
sort_order=sort_order,
40074014
aspect=aspect,
@@ -4037,12 +4044,14 @@ def search_activities():
40374044
sort_by_when=True,
40384045
)
40394046

4040-
# filter by case-insensitive substring match on name and description
4047+
# filter by case-insensitive substring match on name, description and tags
40414048
q_lower = q.lower()
40424049
activities = [
40434050
a
40444051
for a in activities
4045-
if q_lower in (a.name or "").lower() or q_lower in (a.description or "").lower()
4052+
if q_lower in (a.name or "").lower()
4053+
or q_lower in (a.description or "").lower()
4054+
or any(q_lower in tag.lower() for tag in (a.tags or []))
40464055
]
40474056

40484057
activities_weekdays = {

mytral/templates/activity-create.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ <h3 class="card-title">Activity Details</h3>
6464
<div class="card-body">
6565
{{ field_macros.add_field(form.name) }}
6666
{{ field_macros.add_field(form.description) }}
67+
{{ field_macros.add_field(form.tags) }}
6768
</div>
6869
</div>
6970
</div>

mytral/templates/activity-get.html

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,17 @@ <h2 class="mb-3">
196196
{{ activity_entity.description|markdown }}
197197
</div>
198198

199+
{% if activity_entity.tags %}
200+
<div class="mb-4">
201+
{% for tag in activity_entity.tags %}
202+
{% set tag_color = tag|tag_to_color %}
203+
<a href="{{ url_for('list_activities_year', year=activity_entity.when_year, tag=tag) }}" class="text-decoration-none">
204+
<span class="badge bg-{{ tag_color }} text-{{ tag_color }}-fg">{{ tag }}</span>
205+
</a>
206+
{% endfor %}
207+
</div>
208+
{% endif %}
209+
199210
{% if activity_entity.distance %}
200211
<div class="row g-3 mb-4">
201212
<div class="col-sm-6 col-lg-3">

mytral/templates/activity-list-year.html

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ <h2 class="page-title">
5959
<div class="btn-list">
6060
{% if activities|length > 0 %}
6161
<div class="btn-group">
62-
<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">
62+
<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">
6363
<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">
6464
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
6565
<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>
@@ -68,7 +68,7 @@ <h2 class="page-title">
6868
</svg>
6969
List
7070
</a>
71-
<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">
71+
<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">
7272
<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">
7373
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
7474
<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>
@@ -209,6 +209,18 @@ <h2 class="page-title">
209209
{% endfor %}
210210
</select>
211211
</div>
212+
213+
<div class="col-md-2">
214+
<label class="form-label">Tag</label>
215+
<select class="form-select" name="tag" onchange="this.form.submit()">
216+
<option value="">All</option>
217+
{% for tag in unique_tags %}
218+
<option value="{{ tag }}" {% if tag == filter_tag %}selected{% endif %}>
219+
{{ tag }}
220+
</option>
221+
{% endfor %}
222+
</select>
223+
</div>
212224
</div>
213225

214226
<div class="row g-3 mt-2">
@@ -438,6 +450,16 @@ <h3 class="mb-1">
438450
<span class="mx-2">·</span>
439451
<a href="{{ url_for('settings_activity_types_get', key=activity.activity_type_key) }}" class="text-reset">{{ activity_types.name(activity.activity_type_key) }}</a>
440452
</div>
453+
{% if activity.tags %}
454+
<div class="mt-1">
455+
{% for tag in activity.tags %}
456+
{% set tag_color = tag|tag_to_color %}
457+
<a href="{{ url_for('list_activities_year', year=year, aspect=aspect, tag=tag) }}" class="text-decoration-none">
458+
<span class="badge bg-{{ tag_color }} text-{{ tag_color }}-fg">{{ tag }}</span>
459+
</a>
460+
{% endfor %}
461+
</div>
462+
{% endif %}
441463
</div>
442464
<div class="text-end">
443465
{% if activity.commute %}

mytral/templates/activity-update-all.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ <h3 class="card-title">Activity Details</h3>
6464
<div class="card-body">
6565
{{ field_macros.add_field(form.name) }}
6666
{{ field_macros.add_field(form.description) }}
67+
{{ field_macros.add_field(form.tags) }}
6768
</div>
6869
</div>
6970
</div>

mytral/templates/activity-update.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ <h3 class="card-title">Details</h3>
246246
<div class="card-body">
247247
{{ field_macros.add_field(form.name) }}
248248
{{ field_macros.add_field(form.description) }}
249+
{{ field_macros.add_field(form.tags) }}
249250
</div>
250251
</div>
251252
</div>

mytral/templates/macros/activity_list_table.html

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,19 @@
7070
{{ gear.to_dict().get(gear_key, {}).name }}
7171
{% endif %}{% if not loop.last %}, {% endif %}
7272
{% endfor %}
73+
{% if activity.tags %}
74+
<div class="mt-1">
75+
{% for tag in activity.tags %}
76+
{% set tag_color = tag|tag_to_color %}
77+
{% set badge %}<span class="badge bg-{{ tag_color }} text-{{ tag_color }}-fg">{{ tag }}</span>{% endset %}
78+
{% if year %}
79+
<a href="{{ url_for('list_activities_year', year=year, aspect='list', tag=tag) }}" class="text-decoration-none">{{ badge }}</a>
80+
{% else %}
81+
{{ badge }}
82+
{% endif %}
83+
{% endfor %}
84+
</div>
85+
{% endif %}
7386
</td>
7487
<td>
7588
{{ activity_types.emoji(activity.activity_type_key) }}

mytral/utils.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,3 +118,25 @@ def tag_to_color(tag: str) -> str:
118118
color_index = tag_hash % len(colors)
119119

120120
return colors[color_index]
121+
122+
123+
def parse_tags_csv(tags_csv: str | None) -> list[str]:
124+
"""Parse a comma-separated tags string into a normalized list.
125+
126+
Parameters
127+
----------
128+
tags_csv : str | None
129+
raw comma-separated tags input (may be None or empty)
130+
131+
Returns
132+
-------
133+
list[str]
134+
trimmed, non-empty tags in input order with duplicates removed
135+
136+
"""
137+
tags: list[str] = []
138+
for tag in (tags_csv or "").split(","):
139+
tag = tag.strip()
140+
if tag and tag not in tags:
141+
tags.append(tag)
142+
return tags

0 commit comments

Comments
 (0)