Skip to content

Commit

Permalink
Add distance in km to activity records.
Browse files Browse the repository at this point in the history
  • Loading branch information
caarmen committed Jul 26, 2024
1 parent eff5777 commit 9dcbaab
Show file tree
Hide file tree
Showing 12 changed files with 115 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""add_fitbit_activities_distance_km
Revision ID: 22857e6099f4
Revises: 77dca2f35afa
Create Date: 2024-07-26 22:14:38.959111
"""

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = "22857e6099f4"
down_revision = "77dca2f35afa"
branch_labels = None
depends_on = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("fitbit_activities", schema=None) as batch_op:
batch_op.add_column(sa.Column("distance_km", sa.Float(), nullable=True))

# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("fitbit_activities", schema=None) as batch_op:
batch_op.drop_column("distance_km")

# ### end Alembic commands ###
1 change: 1 addition & 0 deletions slackhealthbot/data/database/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ class FitbitActivity(TimestampMixin, Base):
type_id: Mapped[int] = mapped_column()
total_minutes: Mapped[int] = mapped_column()
calories: Mapped[int] = mapped_column()
distance_km: Mapped[Optional[float]] = mapped_column()
fat_burn_minutes: Mapped[Optional[int]] = mapped_column()
cardio_minutes: Mapped[Optional[int]] = mapped_column()
peak_minutes: Mapped[Optional[int]] = mapped_column()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ async def create_activity_for_user(
type_id=activity.type_id,
total_minutes=activity.total_minutes,
calories=activity.calories,
distance_km=activity.distance_km,
**{f"{x.zone}_minutes": x.minutes for x in activity.zone_minutes},
fitbit_user_id=user.id,
)
Expand Down Expand Up @@ -276,6 +277,7 @@ async def get_top_activity_stats_by_user_and_activity_type(

columns = [
models.FitbitActivity.calories,
models.FitbitActivity.distance_km,
models.FitbitActivity.total_minutes,
models.FitbitActivity.fat_burn_minutes,
models.FitbitActivity.cardio_minutes,
Expand All @@ -301,6 +303,7 @@ async def get_top_activity_stats_by_user_and_activity_type(

return TopActivityStats(
top_calories=row["top_calories"],
top_distance_km=row["top_distance_km"],
top_total_minutes=row["top_total_minutes"],
top_zone_minutes=[
ActivityZoneMinutes(
Expand All @@ -320,6 +323,7 @@ def _db_activity_to_domain_activity(
log_id=db_activity.log_id,
type_id=db_activity.type_id,
calories=db_activity.calories,
distance_km=db_activity.distance_km,
total_minutes=db_activity.total_minutes,
zone_minutes=[
ActivityZoneMinutes(
Expand Down
2 changes: 2 additions & 0 deletions slackhealthbot/domain/models/activity.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@ class ActivityData:
type_id: int
total_minutes: int
calories: int
distance_km: float | None
zone_minutes: list[ActivityZoneMinutes]


@dataclasses.dataclass
class TopActivityStats:
top_calories: int | None
top_distance_km: float | None
top_total_minutes: int | None
top_zone_minutes: list[ActivityZoneMinutes]

Expand Down
44 changes: 43 additions & 1 deletion slackhealthbot/domain/usecases/slack/usecase_post_activity.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,19 @@ def create_message(
calories_icon = get_activity_calories_change_icon(
activity.calories - activity_history.latest_activity_data.calories,
)
distance_km_icon = (
get_activity_distance_km_change_icon(
(
activity.distance_km
- activity_history.latest_activity_data.distance_km
)
* 100
/ activity.distance_km,
)
if activity.distance_km
else None
)

for zone_minutes in activity.zone_minutes:
last_zone_minutes = next(
(
Expand All @@ -48,7 +61,7 @@ def create_message(
)

else:
duration_icon = calories_icon = ""
duration_icon = calories_icon = distance_km_icon = ""
duration_record_text = get_ranking_text(
activity.total_minutes,
activity_history.all_time_top_activity_data.top_total_minutes,
Expand All @@ -61,6 +74,17 @@ def create_message(
activity_history.recent_top_activity_data.top_calories,
record_history_days=record_history_days,
)
distance_km_record_text = (
get_ranking_text(
activity.distance_km,
activity_history.all_time_top_activity_data.top_distance_km,
activity_history.recent_top_activity_data.top_distance_km,
record_history_days=record_history_days,
)
if activity.distance_km
else None
)

for zone_minutes in activity.zone_minutes:
all_time_top_value = next(
(
Expand Down Expand Up @@ -89,6 +113,8 @@ def create_message(
• Duration: {activity.total_minutes} minutes {duration_icon} {duration_record_text}
• Calories: {activity.calories} {calories_icon} {calories_record_text}
"""
if activity.distance_km:
message += f" • Distance: {activity.distance_km}km {distance_km_icon} {distance_km_record_text}"
message += "\n".join(
[
f" • {format_activity_zone(zone_minutes.zone)}"
Expand Down Expand Up @@ -137,6 +163,22 @@ def get_activity_calories_change_icon(calories_change: int) -> str:
return "➡️"


DISTANCE_CHANGE_PCT_SMALL = 15
DISTANCE_CHANGE_PCT_LARGE = 25


def get_activity_distance_km_change_icon(distance_km_change_pct: int) -> str:
if distance_km_change_pct > DISTANCE_CHANGE_PCT_LARGE:
return "⬆️"
if distance_km_change_pct > DISTANCE_CHANGE_PCT_SMALL:
return "↗️"
if distance_km_change_pct < -DISTANCE_CHANGE_PCT_LARGE:
return "⬇️"
if distance_km_change_pct < -DISTANCE_CHANGE_PCT_LARGE:
return "↘️"
return "➡️"


def get_ranking_text(
value: int,
all_time_top_value: int,
Expand Down
2 changes: 2 additions & 0 deletions slackhealthbot/remoteservices/api/fitbit/activityapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ class FitbitActivity(BaseModel):
activityTypeId: int
calories: int
duration: int
distance: float | None = None
distanceUnit: str | None = None


class FitbitActivities(BaseModel):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@ def remote_service_activity_to_domain_activity(
log_id=fitbit_activity.logId,
type_id=fitbit_activity.activityTypeId,
calories=fitbit_activity.calories,
distance_km=(
fitbit_activity.distance
if fitbit_activity.distanceUnit == "Kilometer"
else None
),
total_minutes=fitbit_activity.duration // 60000,
zone_minutes=[
ActivityZoneMinutes(zone=ActivityZone[x.type.upper()], minutes=x.minutes)
Expand Down
11 changes: 11 additions & 0 deletions tests/data/repositories/test_fitbitrepository.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ async def test_top_activities(
fitbit_user_id=user.fitbit.id,
type_id=activity_type,
calories=600,
distance_km=None, # TODO test this
total_minutes=18,
fat_burn_minutes=17,
cardio_minutes=16,
Expand All @@ -51,6 +52,7 @@ async def test_top_activities(
fitbit_user_id=user.fitbit.id,
type_id=activity_type,
calories=599,
distance_km=None, # TODO test this
total_minutes=18,
fat_burn_minutes=17,
cardio_minutes=16,
Expand All @@ -65,6 +67,7 @@ async def test_top_activities(
fitbit_user_id=user.fitbit.id,
type_id=activity_type,
calories=333,
distance_km=None, # TODO test this
total_minutes=30,
fat_burn_minutes=29,
cardio_minutes=28,
Expand All @@ -78,6 +81,7 @@ async def test_top_activities(
fitbit_user_id=user.fitbit.id,
type_id=activity_type,
calories=333,
distance_km=None, # TODO test this
total_minutes=29,
fat_burn_minutes=28,
cardio_minutes=27,
Expand All @@ -90,6 +94,7 @@ async def test_top_activities(
fitbit_user_id=user.fitbit.id,
type_id=activity_type,
calories=400,
distance_km=None, # TODO test this
total_minutes=20,
fat_burn_minutes=19,
cardio_minutes=18,
Expand All @@ -102,6 +107,7 @@ async def test_top_activities(
fitbit_user_id=other_user.fitbit.id,
type_id=activity_type,
calories=800,
distance_km=None, # TODO test this
total_minutes=69,
fat_burn_minutes=68,
cardio_minutes=67,
Expand All @@ -114,6 +120,7 @@ async def test_top_activities(
fitbit_user_id=user.fitbit.id,
type_id=999,
calories=900,
distance_km=None, # TODO test this
total_minutes=98,
fat_burn_minutes=97,
cardio_minutes=96,
Expand All @@ -129,6 +136,7 @@ async def test_top_activities(
)
assert all_time_top_activity_stats == TopActivityStats(
top_calories=all_time_top_calories_activity.calories,
top_distance_km=None, # TODO test this
top_total_minutes=all_time_top_minutes_activity.total_minutes,
top_zone_minutes=[
ActivityZoneMinutes(
Expand All @@ -155,6 +163,7 @@ async def test_top_activities(
)
assert recent_top_activity_stats == TopActivityStats(
top_calories=recent_top_calories_activity.calories,
top_distance_km=None, # TODO test this
top_total_minutes=recent_top_minutes_activity.total_minutes,
top_zone_minutes=[
ActivityZoneMinutes(
Expand Down Expand Up @@ -191,6 +200,7 @@ async def test_top_activities_no_history(
)
assert all_time_top_activity_stats == TopActivityStats(
top_calories=None,
top_distance_km=None,
top_total_minutes=None,
top_zone_minutes=[],
)
Expand All @@ -204,6 +214,7 @@ async def test_top_activities_no_history(
)
assert recent_top_activity_stats == TopActivityStats(
top_calories=None,
top_distance_km=None,
top_total_minutes=None,
top_zone_minutes=[],
)
11 changes: 11 additions & 0 deletions tests/domain/usecases/slack/test_usecase_post_activity.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ def test_get_activity_calories_change_icon(
assert actual_output == expected_output


def test_get_activity_distance_km_change_icon():
# TODO
pass


@pytest.mark.parametrize(
[
"input_value",
Expand Down Expand Up @@ -102,6 +107,7 @@ class CreateMessageScenario:
type_id=123,
total_minutes=120,
calories=315,
distance_km=None, # TODO test this
zone_minutes=[
ActivityZoneMinutes(
zone=ActivityZone.CARDIO,
Expand All @@ -123,6 +129,7 @@ class CreateMessageScenario:
type_id=123,
total_minutes=90,
calories=175,
distance_km=None, # TODO test this
zone_minutes=[
ActivityZoneMinutes(
zone=ActivityZone.CARDIO,
Expand All @@ -144,6 +151,7 @@ class CreateMessageScenario:
type_id=123,
total_minutes=1,
calories=1,
distance_km=None, # TODO test this
zone_minutes=[
ActivityZoneMinutes(
zone=ActivityZone.CARDIO,
Expand Down Expand Up @@ -173,6 +181,7 @@ def test_create_message(scenario: CreateMessageScenario):
type_id=123,
total_minutes=15,
calories=150,
distance_km=None, # TODO test this
zone_minutes=[
ActivityZoneMinutes(
zone=ActivityZone.CARDIO,
Expand All @@ -187,6 +196,7 @@ def test_create_message(scenario: CreateMessageScenario):
all_time_top_activity_data=TopActivityStats(
top_total_minutes=100,
top_calories=215,
top_distance_km=None, # TODO test this
top_zone_minutes=[
ActivityZoneMinutes(
zone=ActivityZone.CARDIO,
Expand All @@ -201,6 +211,7 @@ def test_create_message(scenario: CreateMessageScenario):
recent_top_activity_data=TopActivityStats(
top_total_minutes=90,
top_calories=175,
top_distance_km=None, # TODO test this
top_zone_minutes=[
ActivityZoneMinutes(
zone=ActivityZone.CARDIO,
Expand Down
1 change: 1 addition & 0 deletions tests/test_factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ async def test_fitbit_activity_factory(
assert repo_activity.type_id == fitbit_activity.type_id
assert repo_activity.total_minutes == fitbit_activity.total_minutes
assert repo_activity.calories == fitbit_activity.calories
assert repo_activity.distance_km == fitbit_activity.distance_km
assert repo_activity.zone_minutes == [
ActivityZoneMinutes(
zone=ActivityZone.PEAK,
Expand Down
1 change: 1 addition & 0 deletions tests/testsupport/factories/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class Meta:
type_id = Faker("pyint")
total_minutes = Faker("pyint")
calories = Faker("pyint")
distance_km = Faker("pyfloat")
cardio_minutes = Faker("pyint")
fat_burn_minutes = Faker("pyint")
peak_minutes = Faker("pyint")
Expand Down
1 change: 1 addition & 0 deletions tests/testsupport/fixtures/fitbit_scenarios.py
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,7 @@ def is_new_log_expected(self) -> bool:
+ "Fat burn.*12.*⬆️ New all-time record.*Cardio.*9.*⬇️ New record.*Out of range.*10.*↗️.*Peak.*11.*⬆️ New "
"all-time record",
),
# TODO activity with kilometers
"New unrecognized activity": FitbitActivityScenario(
input_initial_activity_data={
"log_id": 1234,
Expand Down

0 comments on commit 9dcbaab

Please sign in to comment.