added user images partitioning
2
.gitignore
vendored
@@ -40,7 +40,7 @@ env/
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
db/*.json
|
||||
data/db/*.json
|
||||
|
||||
# Flask
|
||||
instance/
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import os
|
||||
import uuid
|
||||
from io import BytesIO
|
||||
|
||||
from PIL import Image as PILImage, UnidentifiedImageError
|
||||
from flask import Blueprint, request, jsonify, send_file
|
||||
from tinydb import Query
|
||||
from config.paths import get_user_image_dir
|
||||
|
||||
from db.db import image_db
|
||||
from models.image import Image
|
||||
from tinydb import Query
|
||||
from PIL import Image as PILImage, UnidentifiedImageError
|
||||
|
||||
image_api = Blueprint('image_api', __name__)
|
||||
UPLOAD_FOLDER = './resources/images'
|
||||
UPLOAD_FOLDER = get_user_image_dir("user123")
|
||||
ALLOWED_EXTENSIONS = {'jpg', 'jpeg', 'png'}
|
||||
IMAGE_TYPE_PROFILE = 1
|
||||
IMAGE_TYPE_ICON = 2
|
||||
@@ -61,7 +61,7 @@ def upload():
|
||||
format_extension_map = {'JPEG': '.jpg', 'PNG': '.png'}
|
||||
extension = format_extension_map.get(original_format, '.png')
|
||||
|
||||
image_record = Image(extension=extension, permanent=perm, type=image_type)
|
||||
image_record = Image(extension=extension, permanent=perm, type=image_type, user="user123")
|
||||
filename = image_record.id + extension
|
||||
filepath = os.path.abspath(os.path.join(UPLOAD_FOLDER, filename))
|
||||
|
||||
@@ -83,11 +83,11 @@ def upload():
|
||||
@image_api.route('/image/request/<id>', methods=['GET'])
|
||||
def request_image(id):
|
||||
ImageQuery = Query()
|
||||
image = image_db.get(ImageQuery.id == id)
|
||||
image: Image = Image.from_dict(image_db.get(ImageQuery.id == id))
|
||||
if not image:
|
||||
return jsonify({'error': 'Image not found'}), 404
|
||||
filename = f"{image['id']}{image['extension']}"
|
||||
filepath = os.path.abspath(os.path.join(UPLOAD_FOLDER, filename))
|
||||
filename = f"{image.id}{image.extension}"
|
||||
filepath = os.path.abspath(os.path.join(get_user_image_dir(image.user), filename))
|
||||
if not os.path.exists(filepath):
|
||||
return jsonify({'error': 'File not found'}), 404
|
||||
return send_file(filepath)
|
||||
|
||||
27
config/paths.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# python
|
||||
# file: config/paths.py
|
||||
import os
|
||||
|
||||
# Constant directory names
|
||||
DATA_DIR_NAME = 'data'
|
||||
TEST_DATA_DIR_NAME = 'test_data'
|
||||
|
||||
# Project root (two levels up from this file)
|
||||
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
def get_database_dir(db_env: str | None = None) -> str:
|
||||
"""
|
||||
Return the absolute base directory path for the given DB env.
|
||||
db_env: 'prod' uses `data/db`, anything else uses `test_data/db`.
|
||||
"""
|
||||
env = (db_env or os.environ.get('DB_ENV', 'prod')).lower()
|
||||
base_name = DATA_DIR_NAME if env == 'prod' else TEST_DATA_DIR_NAME
|
||||
return os.path.join(PROJECT_ROOT, base_name, 'db')
|
||||
|
||||
def get_user_image_dir(username: str | None) -> str:
|
||||
"""
|
||||
Return the absolute directory path for storing images for a specific user.
|
||||
"""
|
||||
if username:
|
||||
return os.path.join(PROJECT_ROOT, DATA_DIR_NAME, 'images', username)
|
||||
return os.path.join(PROJECT_ROOT, 'resources', 'images')
|
||||
14
db/db.py
@@ -1,18 +1,10 @@
|
||||
# python
|
||||
import os
|
||||
from config.paths import get_database_dir
|
||||
import threading
|
||||
from tinydb import TinyDB
|
||||
|
||||
DB_ENV = os.environ.get('DB_ENV', 'prod')
|
||||
|
||||
|
||||
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
if DB_ENV == 'prod':
|
||||
base_dir = os.path.join(project_root, 'data/db')
|
||||
else:
|
||||
base_dir = os.path.join(project_root, 'test_data/db')
|
||||
|
||||
base_dir = get_database_dir()
|
||||
os.makedirs(base_dir, exist_ok=True)
|
||||
|
||||
|
||||
@@ -95,7 +87,7 @@ reward_db = LockedTable(_reward_db)
|
||||
image_db = LockedTable(_image_db)
|
||||
pending_reward_db = LockedTable(_pending_rewards_db)
|
||||
|
||||
if DB_ENV == 'test':
|
||||
if os.environ.get('DB_ENV', 'prod') == 'test':
|
||||
child_db.truncate()
|
||||
task_db.truncate()
|
||||
reward_db.truncate()
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
# python
|
||||
# File: db/debug.py
|
||||
|
||||
import random
|
||||
from models.child import Child
|
||||
from models.task import Task
|
||||
from models.reward import Reward
|
||||
from models.image import Image
|
||||
from tinydb import Query
|
||||
from db.db import child_db, task_db, reward_db, image_db
|
||||
|
||||
from api.image_api import IMAGE_TYPE_ICON, IMAGE_TYPE_PROFILE
|
||||
from db.db import task_db, reward_db, image_db
|
||||
from models.image import Image
|
||||
from models.reward import Reward
|
||||
from models.task import Task
|
||||
|
||||
|
||||
def populate_default_data():
|
||||
# Create tasks
|
||||
@@ -86,19 +86,33 @@ def initializeImages():
|
||||
"""Initialize the image database with default images if empty."""
|
||||
if len(image_db.all()) == 0:
|
||||
image_defs = [
|
||||
('computer-game', IMAGE_TYPE_ICON, '.png', True),
|
||||
('ice-cream', IMAGE_TYPE_ICON, '.png', True),
|
||||
('meal', IMAGE_TYPE_ICON, '.png', True),
|
||||
('playground', IMAGE_TYPE_ICON, '.png', True),
|
||||
('tablet', IMAGE_TYPE_ICON, '.png', True),
|
||||
('boy01', IMAGE_TYPE_PROFILE, '.png', True),
|
||||
('girl01', IMAGE_TYPE_PROFILE, '.png', True),
|
||||
('girl02', IMAGE_TYPE_PROFILE, '.png', True),
|
||||
('boy02', IMAGE_TYPE_PROFILE, '.png', True),
|
||||
('boy03', IMAGE_TYPE_PROFILE, '.png', True),
|
||||
('girl03', IMAGE_TYPE_PROFILE, '.png', True),
|
||||
('boy04', IMAGE_TYPE_PROFILE, '.png', True),
|
||||
('broom', IMAGE_TYPE_ICON, '.png', True),
|
||||
('computer-game', IMAGE_TYPE_ICON, '.png', True),
|
||||
('fighting', IMAGE_TYPE_ICON, '.png', True),
|
||||
('games-with-dad', IMAGE_TYPE_ICON, '.png', True),
|
||||
('girl01', IMAGE_TYPE_PROFILE, '.png', True),
|
||||
('girl02', IMAGE_TYPE_PROFILE, '.png', True),
|
||||
('girl03', IMAGE_TYPE_PROFILE, '.png', True),
|
||||
('girl04', IMAGE_TYPE_PROFILE, '.png', True),
|
||||
('good', IMAGE_TYPE_ICON, '.png', True),
|
||||
('homework', IMAGE_TYPE_ICON, '.png', True),
|
||||
('ice-cream', IMAGE_TYPE_ICON, '.png', True),
|
||||
('ignore', IMAGE_TYPE_ICON, '.png', True),
|
||||
('lying', IMAGE_TYPE_ICON, '.png', True),
|
||||
('make-the-bed', IMAGE_TYPE_ICON, '.png', True),
|
||||
('meal', IMAGE_TYPE_ICON, '.png', True),
|
||||
('money', IMAGE_TYPE_ICON, '.png', True),
|
||||
('playground', IMAGE_TYPE_ICON, '.png', True),
|
||||
('tablet', IMAGE_TYPE_ICON, '.png', True),
|
||||
('toilet', IMAGE_TYPE_ICON, '.png', True),
|
||||
('trash-can', IMAGE_TYPE_ICON, '.png', True),
|
||||
('tv', IMAGE_TYPE_ICON, '.png', True),
|
||||
('vacuum', IMAGE_TYPE_ICON, '.png', True),
|
||||
('yelling', IMAGE_TYPE_ICON, '.png', True),
|
||||
]
|
||||
for _id, _type, ext, perm in image_defs:
|
||||
img = Image(type=_type, extension=ext, permanent=perm)
|
||||
|
||||
7
main.py
@@ -1,5 +1,5 @@
|
||||
import sys, logging
|
||||
|
||||
import sys, logging, os
|
||||
from config.paths import get_user_image_dir
|
||||
from flask import Flask, request
|
||||
from flask_cors import CORS
|
||||
|
||||
@@ -55,7 +55,8 @@ def start_background_threads():
|
||||
broadcaster.daemon = True
|
||||
broadcaster.start()
|
||||
|
||||
# Initialize background workers on server start
|
||||
# TODO: implement users
|
||||
os.makedirs(get_user_image_dir("user123"), exist_ok=True)
|
||||
initializeImages()
|
||||
start_background_threads()
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ class Image(BaseModel):
|
||||
type: int
|
||||
extension: str
|
||||
permanent: bool = False
|
||||
user: str | None = None
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d: dict):
|
||||
@@ -17,7 +18,8 @@ class Image(BaseModel):
|
||||
permanent=d.get('permanent', False),
|
||||
id=d.get('id'),
|
||||
created_at=d.get('created_at'),
|
||||
updated_at=d.get('updated_at')
|
||||
updated_at=d.get('updated_at'),
|
||||
user=d.get('user')
|
||||
)
|
||||
|
||||
def to_dict(self):
|
||||
@@ -25,6 +27,7 @@ class Image(BaseModel):
|
||||
base.update({
|
||||
'type': self.type,
|
||||
'permanent': self.permanent,
|
||||
'extension': self.extension
|
||||
'extension': self.extension,
|
||||
'user': self.user
|
||||
})
|
||||
return base
|
||||
|
||||
BIN
resources/images/broom.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
resources/images/fighting.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
resources/images/games-with-dad.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
resources/images/good.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
resources/images/homework.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
resources/images/ignore.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
resources/images/lying.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
resources/images/make-the-bed.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
resources/images/money.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
resources/images/toilet.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
resources/images/trash-can.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
resources/images/tv.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
resources/images/vacuum.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
resources/images/yelling.png
Normal file
|
After Width: | Height: | Size: 51 KiB |
@@ -159,7 +159,7 @@ const showBack = computed(
|
||||
/* top bar holds title and logout button */
|
||||
.topbar {
|
||||
display: grid;
|
||||
grid-template-columns: 76px 1fr 76px;
|
||||
grid-template-columns: 46px 1fr 46px;
|
||||
align-items: center;
|
||||
padding: 5px 5px;
|
||||
}
|
||||
@@ -235,7 +235,11 @@ const showBack = computed(
|
||||
@media (max-width: 480px) {
|
||||
.back-btn {
|
||||
padding: 0.45rem 0.75rem;
|
||||
font-size: 0.95rem;
|
||||
font-size: 0.6rem;
|
||||
height: 100%;
|
||||
}
|
||||
.login-btn {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -250,4 +254,12 @@ const showBack = computed(
|
||||
color: #667eea;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.login-btn button {
|
||||
padding: 0.45rem 0.75rem;
|
||||
font-size: 0.6rem;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||