feat: Implement task and reward tracking feature
All checks were successful
Gitea Actions Demo / build-and-push (push) Successful in 24s
All checks were successful
Gitea Actions Demo / build-and-push (push) Successful in 24s
- Added tracking events for tasks, penalties, and rewards with timestamps. - Created new TinyDB table for tracking records to maintain audit history. - Developed backend API for querying tracking events with filters and pagination. - Implemented logging for tracking events with per-user rotating log files. - Added unit tests for tracking event creation, querying, and anonymization. - Deferred frontend changes for future implementation. - Established acceptance criteria and documentation for the tracking feature. feat: Introduce account deletion scheduler - Implemented a scheduler to delete accounts marked for deletion after a configurable threshold. - Added new fields to the User model to manage deletion status and attempts. - Created admin API endpoints for managing deletion thresholds and viewing the deletion queue. - Integrated error handling and logging for the deletion process. - Developed unit tests for the deletion scheduler and related API endpoints. - Documented the deletion process and acceptance criteria.
This commit is contained in:
@@ -73,6 +73,7 @@ reward_path = os.path.join(base_dir, 'rewards.json')
|
||||
image_path = os.path.join(base_dir, 'images.json')
|
||||
pending_reward_path = os.path.join(base_dir, 'pending_rewards.json')
|
||||
users_path = os.path.join(base_dir, 'users.json')
|
||||
tracking_events_path = os.path.join(base_dir, 'tracking_events.json')
|
||||
|
||||
# Use separate TinyDB instances/files for each collection
|
||||
_child_db = TinyDB(child_path, indent=2)
|
||||
@@ -81,6 +82,7 @@ _reward_db = TinyDB(reward_path, indent=2)
|
||||
_image_db = TinyDB(image_path, indent=2)
|
||||
_pending_rewards_db = TinyDB(pending_reward_path, indent=2)
|
||||
_users_db = TinyDB(users_path, indent=2)
|
||||
_tracking_events_db = TinyDB(tracking_events_path, indent=2)
|
||||
|
||||
# Expose table objects wrapped with locking
|
||||
child_db = LockedTable(_child_db)
|
||||
@@ -89,6 +91,7 @@ reward_db = LockedTable(_reward_db)
|
||||
image_db = LockedTable(_image_db)
|
||||
pending_reward_db = LockedTable(_pending_rewards_db)
|
||||
users_db = LockedTable(_users_db)
|
||||
tracking_events_db = LockedTable(_tracking_events_db)
|
||||
|
||||
if os.environ.get('DB_ENV', 'prod') == 'test':
|
||||
child_db.truncate()
|
||||
@@ -97,4 +100,5 @@ if os.environ.get('DB_ENV', 'prod') == 'test':
|
||||
image_db.truncate()
|
||||
pending_reward_db.truncate()
|
||||
users_db.truncate()
|
||||
tracking_events_db.truncate()
|
||||
|
||||
|
||||
125
backend/db/tracking.py
Normal file
125
backend/db/tracking.py
Normal file
@@ -0,0 +1,125 @@
|
||||
"""Helper functions for tracking events database operations."""
|
||||
import logging
|
||||
from typing import Optional, List
|
||||
from tinydb import Query
|
||||
from db.db import tracking_events_db
|
||||
from models.tracking_event import TrackingEvent, EntityType, ActionType
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def insert_tracking_event(event: TrackingEvent) -> str:
|
||||
"""
|
||||
Insert a tracking event into the database.
|
||||
|
||||
Args:
|
||||
event: TrackingEvent instance to insert
|
||||
|
||||
Returns:
|
||||
The event ID
|
||||
"""
|
||||
try:
|
||||
tracking_events_db.insert(event.to_dict())
|
||||
logger.info(f"Tracking event created: {event.action} {event.entity_type} {event.entity_id} for child {event.child_id}")
|
||||
return event.id
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to insert tracking event: {e}")
|
||||
raise
|
||||
|
||||
|
||||
def get_tracking_events_by_child(
|
||||
child_id: str,
|
||||
limit: int = 50,
|
||||
offset: int = 0,
|
||||
entity_type: Optional[EntityType] = None,
|
||||
action: Optional[ActionType] = None
|
||||
) -> tuple[List[TrackingEvent], int]:
|
||||
"""
|
||||
Query tracking events for a specific child with optional filters.
|
||||
|
||||
Args:
|
||||
child_id: Child ID to filter by
|
||||
limit: Maximum number of results (default 50, max 500)
|
||||
offset: Number of results to skip
|
||||
entity_type: Optional filter by entity type
|
||||
action: Optional filter by action type
|
||||
|
||||
Returns:
|
||||
Tuple of (list of TrackingEvent instances, total count)
|
||||
"""
|
||||
limit = min(limit, 500)
|
||||
|
||||
TrackingQuery = Query()
|
||||
query_condition = TrackingQuery.child_id == child_id
|
||||
|
||||
if entity_type:
|
||||
query_condition &= TrackingQuery.entity_type == entity_type
|
||||
if action:
|
||||
query_condition &= TrackingQuery.action == action
|
||||
|
||||
all_results = tracking_events_db.search(query_condition)
|
||||
total = len(all_results)
|
||||
|
||||
# Sort by occurred_at desc, then created_at desc
|
||||
all_results.sort(key=lambda x: (x.get('occurred_at', ''), x.get('created_at', 0)), reverse=True)
|
||||
|
||||
paginated = all_results[offset:offset + limit]
|
||||
events = [TrackingEvent.from_dict(r) for r in paginated]
|
||||
|
||||
return events, total
|
||||
|
||||
|
||||
def get_tracking_events_by_user(
|
||||
user_id: str,
|
||||
limit: int = 50,
|
||||
offset: int = 0,
|
||||
entity_type: Optional[EntityType] = None
|
||||
) -> tuple[List[TrackingEvent], int]:
|
||||
"""
|
||||
Query tracking events for a specific user.
|
||||
|
||||
Args:
|
||||
user_id: User ID to filter by
|
||||
limit: Maximum number of results
|
||||
offset: Number of results to skip
|
||||
entity_type: Optional filter by entity type
|
||||
|
||||
Returns:
|
||||
Tuple of (list of TrackingEvent instances, total count)
|
||||
"""
|
||||
limit = min(limit, 500)
|
||||
|
||||
TrackingQuery = Query()
|
||||
query_condition = TrackingQuery.user_id == user_id
|
||||
|
||||
if entity_type:
|
||||
query_condition &= TrackingQuery.entity_type == entity_type
|
||||
|
||||
all_results = tracking_events_db.search(query_condition)
|
||||
total = len(all_results)
|
||||
|
||||
all_results.sort(key=lambda x: (x.get('occurred_at', ''), x.get('created_at', 0)), reverse=True)
|
||||
|
||||
paginated = all_results[offset:offset + limit]
|
||||
events = [TrackingEvent.from_dict(r) for r in paginated]
|
||||
|
||||
return events, total
|
||||
|
||||
|
||||
def anonymize_tracking_events_for_user(user_id: str) -> int:
|
||||
"""
|
||||
Anonymize tracking events by setting user_id to None.
|
||||
Called when a user is deleted.
|
||||
|
||||
Args:
|
||||
user_id: User ID to anonymize
|
||||
|
||||
Returns:
|
||||
Number of records anonymized
|
||||
"""
|
||||
TrackingQuery = Query()
|
||||
result = tracking_events_db.update({'user_id': None}, TrackingQuery.user_id == user_id)
|
||||
count = len(result) if result else 0
|
||||
logger.info(f"Anonymized {count} tracking events for user {user_id}")
|
||||
return count
|
||||
Reference in New Issue
Block a user