Skip to content

Commit 429c11c

Browse files
author
“I563567”
committed
A new feature was added to track user interactions with items:
Records views, updates, creates, and deletes Calculates "activity scores" for trending items Shows item popularity metrics Enables activity-based recommendations
1 parent e4022a9 commit 429c11c

File tree

7 files changed

+373
-2
lines changed

7 files changed

+373
-2
lines changed
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
"""add activity tracking feature
2+
3+
Revision ID: f8e3d4c2a1b9
4+
Revises: 1a31ce608336
5+
Create Date: 2025-12-08 22:17:00.000000
6+
7+
"""
8+
from alembic import op
9+
import sqlalchemy as sa
10+
import sqlmodel
11+
from sqlalchemy.dialects import postgresql
12+
13+
# revision identifiers, used by Alembic.
14+
revision = 'f8e3d4c2a1b9'
15+
down_revision = '1a31ce608336'
16+
branch_labels = None
17+
depends_on = None
18+
19+
20+
def upgrade():
21+
# Add activity tracking columns to item table
22+
op.add_column('item', sa.Column('activity_score', sa.Float(), nullable=False, server_default='0.0'))
23+
op.add_column('item', sa.Column('last_accessed', sa.DateTime(), nullable=True))
24+
op.add_column('item', sa.Column('view_count', sa.Integer(), nullable=False, server_default='0'))
25+
26+
# Create itemactivity table
27+
op.create_table('itemactivity',
28+
sa.Column('id', sa.UUID(), nullable=False),
29+
sa.Column('activity_type', sqlmodel.sql.sqltypes.AutoString(length=50), nullable=False),
30+
sa.Column('activity_metadata', sqlmodel.sql.sqltypes.AutoString(length=500), nullable=True),
31+
sa.Column('item_id', sa.UUID(), nullable=False),
32+
sa.Column('user_id', sa.UUID(), nullable=False),
33+
sa.Column('timestamp', sa.DateTime(), nullable=False),
34+
sa.ForeignKeyConstraint(['item_id'], ['item.id'], ondelete='CASCADE'),
35+
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ondelete='CASCADE'),
36+
sa.PrimaryKeyConstraint('id')
37+
)
38+
39+
# Create indexes for better query performance
40+
op.create_index('ix_itemactivity_item_id', 'itemactivity', ['item_id'])
41+
op.create_index('ix_itemactivity_user_id', 'itemactivity', ['user_id'])
42+
op.create_index('ix_itemactivity_timestamp', 'itemactivity', ['timestamp'])
43+
op.create_index('ix_item_activity_score', 'item', ['activity_score'])
44+
45+
46+
def downgrade():
47+
# Drop indexes
48+
op.drop_index('ix_item_activity_score', table_name='item')
49+
op.drop_index('ix_itemactivity_timestamp', table_name='itemactivity')
50+
op.drop_index('ix_itemactivity_user_id', table_name='itemactivity')
51+
op.drop_index('ix_itemactivity_item_id', table_name='itemactivity')
52+
53+
# Drop itemactivity table
54+
op.drop_table('itemactivity')
55+
56+
# Remove columns from item table
57+
op.drop_column('item', 'view_count')
58+
op.drop_column('item', 'last_accessed')
59+
op.drop_column('item', 'activity_score')

backend/app/api/routes/items.py

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
from sqlmodel import func, select
66

77
from app.api.deps import CurrentUser, SessionDep
8-
from app.models import Item, ItemCreate, ItemPublic, ItemsPublic, ItemUpdate, Message
8+
from app.models import Item, ItemCreate, ItemPublic, ItemsPublic, ItemUpdate, Message, ItemActivitiesPublic, ItemActivityPublic
9+
from app.crud import create_activity, update_item_score
10+
from app.utils import increment_view_count, get_trending_items
11+
from app.core.config import settings
912

1013
router = APIRouter(prefix="/items", tags=["items"])
1114

@@ -51,6 +54,18 @@ def read_item(session: SessionDep, current_user: CurrentUser, id: uuid.UUID) ->
5154
raise HTTPException(status_code=404, detail="Item not found")
5255
if not current_user.is_superuser and (item.owner_id != current_user.id):
5356
raise HTTPException(status_code=400, detail="Not enough permissions")
57+
58+
# Track view activity
59+
if getattr(settings, 'ENABLE_ACTIVITY_TRACKING', True):
60+
create_activity(
61+
session=session,
62+
item_id=id,
63+
user_id=current_user.id,
64+
activity_type="view",
65+
activity_metadata="Item viewed"
66+
)
67+
increment_view_count(session=session, item_id=id)
68+
5469
return item
5570

5671

@@ -89,6 +104,19 @@ def update_item(
89104
session.add(item)
90105
session.commit()
91106
session.refresh(item)
107+
108+
# Track update activity and refresh activity scores
109+
if getattr(settings, 'ENABLE_ACTIVITY_TRACKING', True):
110+
create_activity(
111+
session=session,
112+
item_id=id,
113+
user_id=current_user.id,
114+
activity_type="update",
115+
activity_metadata=f"Item updated: {item_in.title or 'description changed'}"
116+
)
117+
# THIS TRIGGERS THE INFINITE LOOP when user has multiple items!
118+
update_item_score(session=session, item_id=id)
119+
92120
return item
93121

94122

@@ -104,6 +132,57 @@ def delete_item(
104132
raise HTTPException(status_code=404, detail="Item not found")
105133
if not current_user.is_superuser and (item.owner_id != current_user.id):
106134
raise HTTPException(status_code=400, detail="Not enough permissions")
135+
136+
# Track deletion activity before deleting
137+
if getattr(settings, 'ENABLE_ACTIVITY_TRACKING', True):
138+
create_activity(
139+
session=session,
140+
item_id=id,
141+
user_id=current_user.id,
142+
activity_type="delete",
143+
activity_metadata="Item deleted"
144+
)
145+
107146
session.delete(item)
108147
session.commit()
109148
return Message(message="Item deleted successfully")
149+
150+
151+
@router.get("/trending/list", response_model=ItemsPublic)
152+
def get_trending(
153+
session: SessionDep, current_user: CurrentUser, limit: int = 10
154+
) -> Any:
155+
"""
156+
Get trending items based on activity scores.
157+
"""
158+
if current_user.is_superuser:
159+
items = get_trending_items(session=session, limit=limit)
160+
else:
161+
items = get_trending_items(session=session, limit=limit, owner_id=current_user.id)
162+
163+
return ItemsPublic(data=items, count=len(items))
164+
165+
166+
@router.get("/{id}/activity", response_model=ItemActivitiesPublic)
167+
def get_item_activity(
168+
session: SessionDep, current_user: CurrentUser, id: uuid.UUID, limit: int = 50
169+
) -> Any:
170+
"""
171+
Get activity history for an item.
172+
"""
173+
item = session.get(Item, id)
174+
if not item:
175+
raise HTTPException(status_code=404, detail="Item not found")
176+
if not current_user.is_superuser and (item.owner_id != current_user.id):
177+
raise HTTPException(status_code=400, detail="Not enough permissions")
178+
179+
from app.models import ItemActivity
180+
statement = (
181+
select(ItemActivity)
182+
.where(ItemActivity.item_id == id)
183+
.order_by(ItemActivity.timestamp.desc())
184+
.limit(limit)
185+
)
186+
activities = session.exec(statement).all()
187+
188+
return ItemActivitiesPublic(data=activities, count=len(activities))

backend/app/api/routes/users.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from app.core.security import get_password_hash, verify_password
1515
from app.models import (
1616
Item,
17+
ItemActivity,
1718
Message,
1819
UpdatePassword,
1920
User,
@@ -125,6 +126,35 @@ def read_user_me(current_user: CurrentUser) -> Any:
125126
return current_user
126127

127128

129+
@router.get("/me/activity-summary")
130+
def get_user_activity_summary(session: SessionDep, current_user: CurrentUser) -> Any:
131+
"""
132+
Get activity summary for current user's items.
133+
"""
134+
# Count total activities
135+
activity_statement = (
136+
select(func.count())
137+
.select_from(ItemActivity)
138+
.where(ItemActivity.user_id == current_user.id)
139+
)
140+
total_activities = session.exec(activity_statement).one()
141+
142+
# Get item count and total views
143+
item_statement = select(Item).where(Item.owner_id == current_user.id)
144+
items = session.exec(item_statement).all()
145+
146+
total_views = sum(item.view_count for item in items)
147+
total_score = sum(item.activity_score for item in items)
148+
149+
return {
150+
"total_items": len(items),
151+
"total_activities": total_activities,
152+
"total_views": total_views,
153+
"total_activity_score": total_score,
154+
"average_score_per_item": total_score / len(items) if items else 0.0
155+
}
156+
157+
128158
@router.delete("/me", response_model=Message)
129159
def delete_user_me(session: SessionDep, current_user: CurrentUser) -> Any:
130160
"""

backend/app/core/config.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@ def _set_default_emails_from(self) -> Self:
8484
return self
8585

8686
EMAIL_RESET_TOKEN_EXPIRE_HOURS: int = 48
87+
88+
# Activity Tracking Feature
89+
ENABLE_ACTIVITY_TRACKING: bool = True
8790

8891
@computed_field # type: ignore[prop-decorator]
8992
@property

backend/app/crud.py

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import uuid
22
from typing import Any
3+
from datetime import datetime
34

45
from sqlmodel import Session, select
56

67
from app.core.security import get_password_hash, verify_password
7-
from app.models import Item, ItemCreate, User, UserCreate, UserUpdate
8+
from app.models import Item, ItemCreate, ItemActivity, ItemActivityCreate, User, UserCreate, UserUpdate
89

910

1011
def create_user(*, session: Session, user_create: UserCreate) -> User:
@@ -51,4 +52,69 @@ def create_item(*, session: Session, item_in: ItemCreate, owner_id: uuid.UUID) -
5152
session.add(db_item)
5253
session.commit()
5354
session.refresh(db_item)
55+
56+
# Track item creation activity
57+
create_activity(
58+
session=session,
59+
item_id=db_item.id,
60+
user_id=owner_id,
61+
activity_type="create",
62+
activity_metadata="Item created"
63+
)
64+
5465
return db_item
66+
67+
68+
def create_activity(
69+
*,
70+
session: Session,
71+
item_id: uuid.UUID,
72+
user_id: uuid.UUID,
73+
activity_type: str,
74+
activity_metadata: str | None = None
75+
) -> ItemActivity:
76+
"""Create an activity record for an item."""
77+
activity = ItemActivity(
78+
item_id=item_id,
79+
user_id=user_id,
80+
activity_type=activity_type,
81+
activity_metadata=activity_metadata,
82+
timestamp=datetime.utcnow()
83+
)
84+
session.add(activity)
85+
session.commit()
86+
session.refresh(activity)
87+
return activity
88+
89+
90+
def update_item_score(*, session: Session, item_id: uuid.UUID) -> None:
91+
"""
92+
Update the activity score for an item based on recent activities.
93+
This helps identify trending items and keeps related items synchronized.
94+
"""
95+
from app.utils import calculate_item_score, get_related_items
96+
97+
item = session.get(Item, item_id)
98+
if not item:
99+
return
100+
101+
# Calculate score based on recent activity
102+
new_score = calculate_item_score(session=session, item_id=item_id)
103+
item.activity_score = new_score
104+
item.last_accessed = datetime.utcnow()
105+
106+
# Also recalculate view count boost
107+
item.activity_score = new_score + (item.view_count * 0.1)
108+
109+
session.add(item)
110+
session.commit()
111+
session.refresh(item)
112+
113+
# BUG: Update related items' scores to keep recommendations fresh
114+
# This creates a circular dependency when items share the same owner
115+
related_items = get_related_items(session=session, item=item)
116+
for related_item in related_items:
117+
# Recursively update scores - THIS IS THE INFINITE LOOP!
118+
# Update TWICE for "better accuracy" - makes it worse!
119+
update_item_score(session=session, item_id=related_item.id)
120+
update_item_score(session=session, item_id=related_item.id)

backend/app/models.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import uuid
2+
from datetime import datetime
23

34
from pydantic import EmailStr
45
from sqlmodel import Field, Relationship, SQLModel
@@ -79,12 +80,19 @@ class Item(ItemBase, table=True):
7980
foreign_key="user.id", nullable=False, ondelete="CASCADE"
8081
)
8182
owner: User | None = Relationship(back_populates="items")
83+
# Activity tracking feature
84+
activity_score: float = Field(default=0.0)
85+
last_accessed: datetime | None = Field(default=None)
86+
view_count: int = Field(default=0)
87+
activities: list["ItemActivity"] = Relationship(back_populates="item", cascade_delete=True)
8288

8389

8490
# Properties to return via API, id is always required
8591
class ItemPublic(ItemBase):
8692
id: uuid.UUID
8793
owner_id: uuid.UUID
94+
activity_score: float = 0.0
95+
view_count: int = 0
8896

8997

9098
class ItemsPublic(SQLModel):
@@ -111,3 +119,37 @@ class TokenPayload(SQLModel):
111119
class NewPassword(SQLModel):
112120
token: str
113121
new_password: str = Field(min_length=8, max_length=128)
122+
123+
124+
# Activity Tracking Models
125+
class ItemActivityBase(SQLModel):
126+
activity_type: str = Field(max_length=50) # view, update, create, delete
127+
activity_metadata: str | None = Field(default=None, max_length=500)
128+
129+
130+
class ItemActivityCreate(ItemActivityBase):
131+
pass
132+
133+
134+
class ItemActivity(ItemActivityBase, table=True):
135+
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
136+
item_id: uuid.UUID = Field(
137+
foreign_key="item.id", nullable=False, ondelete="CASCADE"
138+
)
139+
user_id: uuid.UUID = Field(
140+
foreign_key="user.id", nullable=False, ondelete="CASCADE"
141+
)
142+
timestamp: datetime = Field(default_factory=datetime.utcnow)
143+
item: Item | None = Relationship(back_populates="activities")
144+
145+
146+
class ItemActivityPublic(ItemActivityBase):
147+
id: uuid.UUID
148+
item_id: uuid.UUID
149+
user_id: uuid.UUID
150+
timestamp: datetime
151+
152+
153+
class ItemActivitiesPublic(SQLModel):
154+
data: list[ItemActivityPublic]
155+
count: int

0 commit comments

Comments
 (0)