feat: add chore, kindness, and penalty management components
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 2m34s
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 2m34s
- Implemented ChoreAssignView for assigning chores to children. - Created ChoreConfirmDialog for confirming chore completion. - Developed KindnessAssignView for assigning kindness acts. - Added PenaltyAssignView for assigning penalties. - Introduced ChoreEditView and ChoreView for editing and viewing chores. - Created KindnessEditView and KindnessView for managing kindness acts. - Developed PenaltyEditView and PenaltyView for managing penalties. - Added TaskSubNav for navigation between chores, kindness acts, and penalties.
This commit is contained in:
196
backend/scripts/migrate_tasks_to_types.py
Normal file
196
backend/scripts/migrate_tasks_to_types.py
Normal file
@@ -0,0 +1,196 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Migration script: Convert legacy is_good field to type field across all data.
|
||||
|
||||
Steps:
|
||||
1. tasks.json: is_good=True → type='chore', is_good=False → type='penalty'. Remove is_good field.
|
||||
2. pending_rewards.json → pending_confirmations.json: Convert PendingReward records to
|
||||
PendingConfirmation format with entity_type='reward'.
|
||||
3. tracking_events.json: Update entity_type='task' → 'chore' or 'penalty' based on the
|
||||
referenced task's old is_good value.
|
||||
4. child_overrides.json: Update entity_type='task' → 'chore' or 'penalty' based on the
|
||||
referenced task's old is_good value.
|
||||
|
||||
Usage:
|
||||
cd backend
|
||||
python -m scripts.migrate_tasks_to_types [--dry-run]
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
from datetime import datetime
|
||||
|
||||
DRY_RUN = '--dry-run' in sys.argv
|
||||
|
||||
DATA_DIR = os.path.join(os.path.dirname(__file__), '..', 'data', 'db')
|
||||
|
||||
|
||||
def load_json(filename: str) -> dict:
|
||||
path = os.path.join(DATA_DIR, filename)
|
||||
if not os.path.exists(path):
|
||||
return {"_default": {}}
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def save_json(filename: str, data: dict) -> None:
|
||||
path = os.path.join(DATA_DIR, filename)
|
||||
if DRY_RUN:
|
||||
print(f" [DRY RUN] Would write {path}")
|
||||
return
|
||||
# Backup original
|
||||
backup_path = path + f'.bak.{datetime.now().strftime("%Y%m%d_%H%M%S")}'
|
||||
if os.path.exists(path):
|
||||
shutil.copy2(path, backup_path)
|
||||
print(f" Backed up {path} → {backup_path}")
|
||||
with open(path, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||
print(f" Wrote {path}")
|
||||
|
||||
|
||||
def migrate_tasks() -> dict[str, str]:
|
||||
"""Migrate tasks.json: is_good → type. Returns task_id → type mapping."""
|
||||
print("\n=== Step 1: Migrate tasks.json ===")
|
||||
data = load_json('tasks.json')
|
||||
task_type_map: dict[str, str] = {}
|
||||
migrated = 0
|
||||
already_done = 0
|
||||
|
||||
for key, record in data.get("_default", {}).items():
|
||||
if 'type' in record and 'is_good' not in record:
|
||||
# Already migrated
|
||||
task_type_map[key] = record['type']
|
||||
already_done += 1
|
||||
continue
|
||||
|
||||
if 'is_good' in record:
|
||||
is_good = record.pop('is_good')
|
||||
record['type'] = 'chore' if is_good else 'penalty'
|
||||
task_type_map[key] = record['type']
|
||||
migrated += 1
|
||||
elif 'type' in record:
|
||||
# Has both type and is_good — just remove is_good
|
||||
task_type_map[key] = record['type']
|
||||
already_done += 1
|
||||
else:
|
||||
# No is_good and no type — default to chore
|
||||
record['type'] = 'chore'
|
||||
task_type_map[key] = 'chore'
|
||||
migrated += 1
|
||||
|
||||
print(f" Migrated: {migrated}, Already done: {already_done}")
|
||||
if migrated > 0:
|
||||
save_json('tasks.json', data)
|
||||
else:
|
||||
print(" No changes needed.")
|
||||
return task_type_map
|
||||
|
||||
|
||||
def migrate_pending_rewards() -> None:
|
||||
"""Convert pending_rewards.json → pending_confirmations.json."""
|
||||
print("\n=== Step 2: Migrate pending_rewards.json → pending_confirmations.json ===")
|
||||
pr_data = load_json('pending_rewards.json')
|
||||
pc_path = os.path.join(DATA_DIR, 'pending_confirmations.json')
|
||||
|
||||
if not os.path.exists(os.path.join(DATA_DIR, 'pending_rewards.json')):
|
||||
print(" pending_rewards.json not found — skipping.")
|
||||
return
|
||||
|
||||
records = pr_data.get("_default", {})
|
||||
if not records:
|
||||
print(" No pending reward records to migrate.")
|
||||
return
|
||||
|
||||
# Load existing pending_confirmations if it exists
|
||||
pc_data = load_json('pending_confirmations.json')
|
||||
pc_records = pc_data.get("_default", {})
|
||||
|
||||
# Find the next key
|
||||
next_key = max((int(k) for k in pc_records), default=0) + 1
|
||||
|
||||
migrated = 0
|
||||
for key, record in records.items():
|
||||
# Convert PendingReward → PendingConfirmation
|
||||
new_record = {
|
||||
'child_id': record.get('child_id', ''),
|
||||
'entity_id': record.get('reward_id', ''),
|
||||
'entity_type': 'reward',
|
||||
'user_id': record.get('user_id', ''),
|
||||
'status': record.get('status', 'pending'),
|
||||
'approved_at': None,
|
||||
'created_at': record.get('created_at', 0),
|
||||
'updated_at': record.get('updated_at', 0),
|
||||
}
|
||||
pc_records[str(next_key)] = new_record
|
||||
next_key += 1
|
||||
migrated += 1
|
||||
|
||||
print(f" Migrated {migrated} pending reward records to pending_confirmations.")
|
||||
pc_data["_default"] = pc_records
|
||||
save_json('pending_confirmations.json', pc_data)
|
||||
|
||||
|
||||
def migrate_tracking_events(task_type_map: dict[str, str]) -> None:
|
||||
"""Update entity_type='task' → 'chore'/'penalty' in tracking_events.json."""
|
||||
print("\n=== Step 3: Migrate tracking_events.json ===")
|
||||
data = load_json('tracking_events.json')
|
||||
records = data.get("_default", {})
|
||||
migrated = 0
|
||||
|
||||
for key, record in records.items():
|
||||
if record.get('entity_type') == 'task':
|
||||
entity_id = record.get('entity_id', '')
|
||||
# Look up the task's type
|
||||
new_type = task_type_map.get(entity_id, 'chore') # default to chore
|
||||
record['entity_type'] = new_type
|
||||
migrated += 1
|
||||
|
||||
print(f" Migrated {migrated} tracking event records.")
|
||||
if migrated > 0:
|
||||
save_json('tracking_events.json', data)
|
||||
else:
|
||||
print(" No changes needed.")
|
||||
|
||||
|
||||
def migrate_child_overrides(task_type_map: dict[str, str]) -> None:
|
||||
"""Update entity_type='task' → 'chore'/'penalty' in child_overrides.json."""
|
||||
print("\n=== Step 4: Migrate child_overrides.json ===")
|
||||
data = load_json('child_overrides.json')
|
||||
records = data.get("_default", {})
|
||||
migrated = 0
|
||||
|
||||
for key, record in records.items():
|
||||
if record.get('entity_type') == 'task':
|
||||
entity_id = record.get('entity_id', '')
|
||||
new_type = task_type_map.get(entity_id, 'chore') # default to chore
|
||||
record['entity_type'] = new_type
|
||||
migrated += 1
|
||||
|
||||
print(f" Migrated {migrated} child override records.")
|
||||
if migrated > 0:
|
||||
save_json('child_overrides.json', data)
|
||||
else:
|
||||
print(" No changes needed.")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
print("=" * 60)
|
||||
print("Task Type Migration Script")
|
||||
if DRY_RUN:
|
||||
print("*** DRY RUN MODE — no files will be modified ***")
|
||||
print("=" * 60)
|
||||
|
||||
task_type_map = migrate_tasks()
|
||||
migrate_pending_rewards()
|
||||
migrate_tracking_events(task_type_map)
|
||||
migrate_child_overrides(task_type_map)
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("Migration complete!" + (" (DRY RUN)" if DRY_RUN else ""))
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user