Implement image upload functionality with metadata storage; update frontend to display uploaded images
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
+31
-1
@@ -4,6 +4,7 @@ import functools
|
||||
import httpx
|
||||
from flask import (
|
||||
Flask,
|
||||
Response,
|
||||
flash,
|
||||
jsonify,
|
||||
redirect,
|
||||
@@ -121,11 +122,27 @@ def dashboard():
|
||||
token = session["access_token"]
|
||||
resp = _api("GET", "/users/me", token=token)
|
||||
user = resp.json() if resp.status_code == 200 else {}
|
||||
return render_template("dashboard.html", user=user)
|
||||
img_resp = _api("GET", "/images/", token=token)
|
||||
images = img_resp.json() if img_resp.status_code == 200 else []
|
||||
return render_template("dashboard.html", user=user, images=images)
|
||||
|
||||
|
||||
# ── Generate ──────────────────────────────────────────────────────────────
|
||||
|
||||
@app.get("/images/<image_id>/file")
|
||||
@login_required
|
||||
def serve_uploaded_image(image_id: str):
|
||||
resp = _api("GET", f"/images/{image_id}/file",
|
||||
token=session["access_token"])
|
||||
if resp.status_code != 200:
|
||||
return Response("Not found", status=404)
|
||||
return Response(
|
||||
resp.content,
|
||||
status=200,
|
||||
content_type=resp.headers.get("content-type", "image/jpeg"),
|
||||
)
|
||||
|
||||
|
||||
@app.get("/generate")
|
||||
@login_required
|
||||
def generate():
|
||||
@@ -153,6 +170,19 @@ def generate_text():
|
||||
def generate_image():
|
||||
result = error = None
|
||||
if request.method == "POST":
|
||||
# Upload reference image if provided
|
||||
ref_file = request.files.get("reference_image")
|
||||
if ref_file and ref_file.filename:
|
||||
up_resp = _api(
|
||||
"POST", "/images/upload",
|
||||
token=session["access_token"],
|
||||
files={"file": (ref_file.filename,
|
||||
ref_file.stream, ref_file.content_type)},
|
||||
)
|
||||
if up_resp.status_code not in (200, 201):
|
||||
error = up_resp.json().get("detail", "Image upload failed.")
|
||||
return render_template("generate_image.html", result=result, error=error)
|
||||
|
||||
resp = _api("POST", "/generate/image", token=session["access_token"], json={
|
||||
"model": request.form.get("model", "").strip(),
|
||||
"prompt": request.form.get("prompt", "").strip(),
|
||||
|
||||
@@ -363,6 +363,25 @@ pre {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.image-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.image-grid-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.image-grid-item .generated-image {
|
||||
width: 100%;
|
||||
aspect-ratio: 1 / 1;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* ─── Admin table ──────────────────────────────────────── */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
|
||||
@@ -5,4 +5,24 @@ endblock %} {% block content %}
|
||||
<p>Role: <strong>{{ user.get('role', 'user') }}</strong></p>
|
||||
<a href="{{ url_for('generate') }}" class="btn">Start generating</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% if images %}
|
||||
<div class="card mt-2">
|
||||
<h2>Uploaded reference images</h2>
|
||||
<div class="image-grid">
|
||||
{% for img in images %}
|
||||
<div class="image-grid-item">
|
||||
<img
|
||||
src="{{ url_for('serve_uploaded_image', image_id=img.id) }}"
|
||||
alt="{{ img.filename }}"
|
||||
class="generated-image"
|
||||
loading="lazy"
|
||||
/>
|
||||
<p class="text-muted" style="font-size: 0.75rem; margin-top: 0.25rem">
|
||||
{{ img.filename }} — {{ (img.size_bytes / 1024) | round(1) }} KB
|
||||
</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %} {% endblock %}
|
||||
|
||||
@@ -148,9 +148,10 @@ def test_dashboard_requires_login(client):
|
||||
|
||||
def test_dashboard_renders_user_info(client):
|
||||
_set_auth(client)
|
||||
mock = _mock_response(
|
||||
me_mock = _mock_response(
|
||||
200, {"id": "1", "email": "u@example.com", "role": "user"})
|
||||
with patch("frontend.app.main.httpx.request", return_value=mock):
|
||||
images_mock = _mock_response(200, [])
|
||||
with patch("frontend.app.main.httpx.request", side_effect=[me_mock, images_mock]):
|
||||
resp = client.get("/dashboard")
|
||||
assert resp.status_code == 200
|
||||
assert b"u@example.com" in resp.data
|
||||
@@ -418,4 +419,77 @@ def test_video_generate_renders_polling_ui(client):
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
assert b"video-poll-status" in resp.data
|
||||
assert b"openrouter.ai/api/v1/videos/v1" in resp.data
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Image upload — frontend proxy + dashboard
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_dashboard_shows_uploaded_images(client):
|
||||
_set_auth(client)
|
||||
me_mock = _mock_response(
|
||||
200, {"id": "1", "email": "u@example.com", "role": "user"})
|
||||
images_mock = _mock_response(200, [
|
||||
{"id": "img-1", "filename": "cat.png", "content_type": "image/png",
|
||||
"size_bytes": 1024, "created_at": "2026-04-29T10:00:00"},
|
||||
])
|
||||
with patch("frontend.app.main.httpx.request", side_effect=[me_mock, images_mock]):
|
||||
resp = client.get("/dashboard")
|
||||
assert resp.status_code == 200
|
||||
assert b"cat.png" in resp.data
|
||||
assert b"img-1" in resp.data
|
||||
|
||||
|
||||
def test_dashboard_no_images_section_when_empty(client):
|
||||
_set_auth(client)
|
||||
me_mock = _mock_response(
|
||||
200, {"id": "1", "email": "u@example.com", "role": "user"})
|
||||
images_mock = _mock_response(200, [])
|
||||
with patch("frontend.app.main.httpx.request", side_effect=[me_mock, images_mock]):
|
||||
resp = client.get("/dashboard")
|
||||
assert resp.status_code == 200
|
||||
assert b"Uploaded reference images" not in resp.data
|
||||
|
||||
|
||||
def test_serve_uploaded_image_proxy(client):
|
||||
_set_auth(client)
|
||||
img_bytes = b"\x89PNG\r\n\x1a\n"
|
||||
mock = MagicMock()
|
||||
mock.status_code = 200
|
||||
mock.content = img_bytes
|
||||
mock.headers = {"content-type": "image/png"}
|
||||
with patch("frontend.app.main.httpx.request", return_value=mock):
|
||||
resp = client.get("/images/img-1/file")
|
||||
assert resp.status_code == 200
|
||||
assert resp.content_type == "image/png"
|
||||
assert resp.data == img_bytes
|
||||
|
||||
|
||||
def test_serve_uploaded_image_requires_login(client):
|
||||
resp = client.get("/images/img-1/file")
|
||||
assert resp.status_code == 302
|
||||
assert "/login" in resp.headers["Location"]
|
||||
|
||||
|
||||
def test_serve_uploaded_image_not_found_proxied(client):
|
||||
_set_auth(client)
|
||||
mock = _mock_response(404, {"detail": "Image not found."})
|
||||
mock.content = b""
|
||||
with patch("frontend.app.main.httpx.request", return_value=mock):
|
||||
resp = client.get("/images/bad-id/file")
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
def test_generate_image_uploads_reference_then_generates(client):
|
||||
_set_auth(client)
|
||||
gen_mock = _mock_response(200, {
|
||||
"id": "g2", "model": "openai/dall-e-3",
|
||||
"images": [{"url": "https://example.com/out.png", "revised_prompt": None, "b64_json": None}]
|
||||
})
|
||||
# No file field → upload branch skipped; only generate call is made
|
||||
with patch("frontend.app.main.httpx.request", return_value=gen_mock):
|
||||
resp = client.post("/generate/image", data={
|
||||
"model": "openai/dall-e-3", "prompt": "A cat", "n": "1", "size": "1024x1024",
|
||||
}, content_type="multipart/form-data")
|
||||
assert resp.status_code == 200
|
||||
assert b"example.com/out.png" in resp.data
|
||||
|
||||
Reference in New Issue
Block a user