lots of things

This commit is contained in:
2025-06-06 22:02:44 -05:00
parent 9daee50a3a
commit 96c634897b
30 changed files with 1120 additions and 182 deletions

View File

@ -1,14 +0,0 @@
from flask_wtf import FlaskForm
from wtforms import StringField, SelectField, SubmitField, IntegerField
from flask_wtf.file import FileField, FileAllowed, FileRequired
from wtforms.validators import DataRequired
class MediaUploadForm(FlaskForm):
image = FileField('Image', validators=[
FileRequired(),
FileAllowed(['jpg', 'jpeg', 'png', 'gif'], 'Images only!')
])
caption = StringField('Caption')
plant_id = IntegerField('Plant ID')
growlog_id = IntegerField('GrowLog ID')
submit = SubmitField('Upload')

View File

@ -1,40 +1,42 @@
# plugins/media/models.py
from app import db
from datetime import datetime
from plugins.plant.models import Plant
class Media(db.Model):
__tablename__ = 'media'
__table_args__ = {'extend_existing': True}
__tablename__ = "media"
__table_args__ = {"extend_existing": True}
id = db.Column(db.Integer, primary_key=True)
file_url = db.Column(db.String(256), nullable=False)
uploaded_at = db.Column(db.DateTime, default=datetime.utcnow)
plant_id = db.Column(db.Integer, db.ForeignKey('plant.id'), nullable=True)
growlog_id = db.Column(db.Integer, db.ForeignKey('grow_logs.id'), nullable=True)
update_id = db.Column(db.Integer, db.ForeignKey('plant_updates.id'), nullable=True)
uploader_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
caption = db.Column(db.String(255), nullable=True)
# Relationship to PlantUpdate
update = db.relationship('PlantUpdate', back_populates='media_items', lazy=True)
plant_id = db.Column(db.Integer, db.ForeignKey("plant.id"), nullable=True)
growlog_id = db.Column(db.Integer, db.ForeignKey("grow_logs.id"), nullable=True)
update_id = db.Column(db.Integer, db.ForeignKey("plant_updates.id"), nullable=True)
update = db.relationship("PlantUpdate", back_populates="media_items")
class ImageHeart(db.Model):
__tablename__ = 'image_hearts'
__table_args__ = {'extend_existing': True}
__tablename__ = "image_hearts"
__table_args__ = {"extend_existing": True}
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
submission_image_id = db.Column(db.Integer, db.ForeignKey('submission_images.id'), nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
media_id = db.Column(db.Integer, db.ForeignKey("media.id"), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
media = db.relationship("Media", backref="hearts")
class FeaturedImage(db.Model):
__tablename__ = 'featured_images'
__table_args__ = {'extend_existing': True}
__tablename__ = "featured_images"
__table_args__ = {"extend_existing": True}
id = db.Column(db.Integer, primary_key=True)
submission_image_id = db.Column(db.Integer, db.ForeignKey('submission_images.id'), nullable=False)
media_id = db.Column(db.Integer, db.ForeignKey("media.id"), nullable=False)
override_text = db.Column(db.String(255), nullable=True)
is_featured = db.Column(db.Boolean, default=True)
media = db.relationship("Media", backref="featured_entries")

View File

@ -1,77 +1,84 @@
from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify, send_from_directory, current_app
# plugins/media/routes.py
from flask import (
Blueprint,
redirect,
url_for,
request,
flash,
send_from_directory,
current_app,
jsonify
)
from flask_login import current_user, login_required
from werkzeug.utils import secure_filename
import os
from app import db
from .models import Media, ImageHeart, FeaturedImage
from plugins.plant.models import Plant
bp = Blueprint("media", __name__, template_folder="templates")
UPLOAD_FOLDER = "static/uploads"
# We store only "YYYY/MM/DD/<uuid>.ext" in Media.file_url.
# All files live under "/app/static/uploads/YYYY/MM/DD/<uuid>.ext" in the container.
BASE_UPLOAD_FOLDER = "static/uploads"
ALLOWED_EXTENSIONS = {"png", "jpg", "jpeg", "gif"}
def allowed_file(filename):
return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS
return (
"." in filename
and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS
)
@bp.route("/media/upload", methods=["GET", "POST"])
@login_required
def upload_media():
if request.method == "POST":
file = request.files.get("image")
caption = request.form.get("caption")
plant_id = request.form.get("plant_id")
@bp.route("/media/", methods=["GET"])
def media_index():
"""
/media/ is not used standalone—redirect back to homepage.
"""
return redirect(url_for("core_ui.home"))
if file and allowed_file(file.filename):
filename = secure_filename(file.filename)
save_path = os.path.join(current_app.root_path, UPLOAD_FOLDER)
os.makedirs(save_path, exist_ok=True)
file.save(os.path.join(save_path, filename))
media = Media(file_url=f"{UPLOAD_FOLDER}/{filename}", caption=caption, plant_id=plant_id)
db.session.add(media)
db.session.commit()
flash("Image uploaded successfully.", "success")
return redirect(url_for("media.upload_media"))
else:
flash("Invalid file or no file uploaded.", "danger")
return render_template("media/upload.html")
@bp.route("/media/files/<path:filename>")
@bp.route("/media/files/<path:filename>", methods=["GET"])
def media_file(filename):
return send_from_directory(os.path.join(current_app.root_path, "static/uploads"), filename)
"""
Serve files from "/app/static/uploads/<filename>".
Example: GET /media/files/2025/06/07/abcdef1234abcd.jpg
"""
# Use os.getcwd() to guarantee "/app/static/uploads" (not "/app/app/static/uploads")
full_dir = os.path.join(os.getcwd(), BASE_UPLOAD_FOLDER)
return send_from_directory(full_dir, filename)
@bp.route("/media/heart/<int:image_id>", methods=["POST"])
@bp.route("/media/heart/<int:media_id>", methods=["POST"])
@login_required
def toggle_heart(image_id):
existing = ImageHeart.query.filter_by(user_id=current_user.id, submission_image_id=image_id).first()
def toggle_heart(media_id):
"""
Add/remove a "heart" from an image.
"""
existing = ImageHeart.query.filter_by(
user_id=current_user.id, media_id=media_id
).first()
if existing:
db.session.delete(existing)
db.session.commit()
return jsonify({"status": "unhearted"})
else:
heart = ImageHeart(user_id=current_user.id, submission_image_id=image_id)
heart = ImageHeart(user_id=current_user.id, media_id=media_id)
db.session.add(heart)
db.session.commit()
return jsonify({"status": "hearted"})
@bp.route("/media/feature/<int:image_id>", methods=["POST"])
@bp.route("/media/feature/<int:media_id>", methods=["POST"])
@login_required
def set_featured_image(image_id):
image = Media.query.get_or_404(image_id)
plant = image.plant
if not plant:
flash("This image is not linked to a plant.", "danger")
return redirect(request.referrer or url_for("core_ui.home"))
if current_user.id != plant.owner_id and current_user.role != "admin":
def set_featured_image(media_id):
"""
Toggle featured status on a media item. Only the uploader or an admin may do so.
"""
media = Media.query.get_or_404(media_id)
if (current_user.id != media.uploader_id) and (current_user.role != "admin"):
flash("Not authorized to set featured image.", "danger")
return redirect(request.referrer or url_for("core_ui.home"))
FeaturedImage.query.filter_by(submission_image_id=image_id).delete()
featured = FeaturedImage(submission_image_id=image_id, is_featured=True)
# Remove any existing featured entries for this media
FeaturedImage.query.filter_by(media_id=media_id).delete()
featured = FeaturedImage(media_id=media_id, is_featured=True)
db.session.add(featured)
db.session.commit()
flash("Image set as featured.", "success")

View File

@ -1,12 +0,0 @@
{% extends 'core_ui/base.html' %}
{% block content %}
<h2>Upload Media</h2>
<form method="POST" enctype="multipart/form-data">
{{ form.hidden_tag() }}
<p>{{ form.image.label }}<br>{{ form.image() }}</p>
<p>{{ form.caption.label }}<br>{{ form.caption(size=40) }}</p>
<p>{{ form.plant_id.label }}<br>{{ form.plant_id() }}</p>
<p>{{ form.growlog_id.label }}<br>{{ form.growlog_id() }}</p>
<p>{{ form.submit() }}</p>
</form>
{% endblock %}

25
plugins/media/utils.py Normal file
View File

@ -0,0 +1,25 @@
# plugins/media/utils.py
import os
import uuid
from PIL import Image
def generate_random_filename(original_filename):
"""
Returns a random filename preserving the original extension.
e.g. “abcd1234efgh.jpg” for “myphoto.jpg”.
"""
ext = os.path.splitext(original_filename)[1].lower() # includes dot, e.g. ".jpg"
random_name = uuid.uuid4().hex # 32char hex string
return f"{random_name}{ext}"
def strip_metadata_and_save(source_file, destination_path):
"""
Opens an image with Pillow, strips EXIF (metadata), and saves it cleanly.
Supports common formats (JPEG, PNG).
"""
with Image.open(source_file) as img:
data = list(img.getdata())
clean_image = Image.new(img.mode, img.size)
clean_image.putdata(data)
clean_image.save(destination_path)