diff --git a/app/config.py b/app/config.py index 59d7a6f..40f6e1c 100644 --- a/app/config.py +++ b/app/config.py @@ -40,3 +40,6 @@ class Config: STANDARD_IMG_SIZE = tuple( map(int, os.getenv('STANDARD_IMG_SIZE', '300x200').split('x')) ) + + PLANT_CARDS_BASE_URL = "https://plant.cards" + ALLOW_REGISTRATION = False diff --git a/nip.zip b/nip.zip index 922fc06..44a807a 100644 Binary files a/nip.zip and b/nip.zip differ diff --git a/plugins/auth/routes.py b/plugins/auth/routes.py index fbed437..7502d08 100644 --- a/plugins/auth/routes.py +++ b/plugins/auth/routes.py @@ -1,4 +1,4 @@ -from flask import Blueprint, render_template, request, redirect, url_for, flash +from flask import Blueprint, render_template, request, redirect, url_for, flash, current_app from flask_login import login_user, logout_user, login_required from werkzeug.security import check_password_hash from app import db @@ -29,6 +29,10 @@ def logout(): @bp.route('/register', methods=['GET', 'POST']) def register(): + if not current_app.config.get('ALLOW_REGISTRATION', True): + flash('Registration is currently closed.', 'warning') + return redirect(url_for('auth.login')) + if request.method == 'POST': email = request.form['email'] password = request.form['password'] diff --git a/plugins/core_ui/templates/core_ui/base.html b/plugins/core_ui/templates/core_ui/base.html index b71f2f8..f4b691f 100644 --- a/plugins/core_ui/templates/core_ui/base.html +++ b/plugins/core_ui/templates/core_ui/base.html @@ -2,6 +2,8 @@ + + {% block title %}Nature In Pots Community{% endblock %} +{% endblock %} diff --git a/plugins/plant/templates/plant/detail.html b/plugins/plant/templates/plant/detail.html index 63caacf..d70d8bd 100644 --- a/plugins/plant/templates/plant/detail.html +++ b/plugins/plant/templates/plant/detail.html @@ -1,71 +1,176 @@ {% extends 'core_ui/base.html' %} - {% block title %} {{ plant.common_name.name if plant.common_name else "Unnamed Plant" }} – Nature In Pots {% endblock %} {% block content %} -
-
-
- {% set featured = plant.featured_media or plant.media|first %} - Image of {{ plant.common_name.name if plant.common_name else 'Plant' }} -
+ +
+ {% if prev_uuid %} + ← Previous + {% else %} +
+ {% endif %} + {% if next_uuid %} + Next → + {% else %} +
+ {% endif %} +
-
-

+
+
+

{{ plant.common_name.name if plant.common_name else "Unnamed Plant" }} + ({{ plant.uuid }})

- {% if plant.scientific_name %} -
- {{ plant.scientific_name.name }} -
+ {% if current_user.id == plant.owner_id %} + Edit {% endif %} +
+
+
+
Type
+
{{ plant.plant_type }}
-

- {{ plant.notes or "No description provided." }} -

+
Scientific Name
+
{{ plant.scientific_name.name }}
- {% if plant.mother_uuid %} -

- Parent: - - {{ plant.mother_uuid }} - -

- {% endif %} +
Mother UUID
+
+ {% if plant.mother_uuid %} + + {{ plant.mother_uuid }} + + {% else %} + N/A + {% endif %} +
-
- Edit - Back to List +
Notes
+
{{ plant.notes or '—' }}
+
+ + + + {% if plant.media %} +
Images
+
+ {% for m in plant.media %} +
+ + {{ m.filename }} + +
+ {% endfor %} +
+ {% else %} +

No images uploaded yet.

+ {% endif %} + + {% if current_user.id == plant.owner_id %} +
+
Generate Cuttings
+
+ +
+ + +
+
+ +
+
+ + {% if children %} +
+
Child Plants
+ + {% if children|length > 6 %} + + {% else %} +
+ {% for c in children %} +
+
+
+ {% if c.media %} + {% set first_media = c.media[0] %} + + {{ first_media.filename }} + + {% else %} +
+ {% endif %} +
+ + +
+
+ {% endfor %} +
+ {% endif %} + {% endif %} + {% endif %}
- {% if plant.media|length > (1 if plant.featured_media else 0) %} -
-

Additional Images

-
- {% for img in plant.media if img != featured %} - Plant image - {% endfor %} -
- {% endif %} -
+ ← Back to list {% endblock %} diff --git a/plugins/plant/templates/plant/edit.html b/plugins/plant/templates/plant/edit.html index a5ab9d2..b10ec6a 100644 --- a/plugins/plant/templates/plant/edit.html +++ b/plugins/plant/templates/plant/edit.html @@ -72,10 +72,12 @@ {# ——— Existing Images & Featured Toggle ——— #}

Existing Images

-
+ {{ form.csrf_token }}
{% for media in plant.media_items %} @@ -84,32 +86,43 @@ Plant Image
- {# Featured radio driven off media.featured #} -
+ {# — featured toggle — #} + - -
+
+ + +
+ - {# Rotate button #} + {# — rotate button — #} - {# Delete checkbox #} + {# — delete checkbox — #}
- +
+
- {% else %} -

No images uploaded yet.

{% endfor %}
- -{% endblock %} -{% block scripts %} - {{ super() }} - + }); + {% endblock %} diff --git a/plugins/plant/templates/plant/index.html b/plugins/plant/templates/plant/index.html index 1694249..8ae6472 100644 --- a/plugins/plant/templates/plant/index.html +++ b/plugins/plant/templates/plant/index.html @@ -148,17 +148,13 @@ data-name="{{ plant.common_name.name|lower }}" data-type="{{ plant.plant_type|lower }}">
- {% set featured = plant.featured_media %} + {# Determine featured image: first any marked featured, else first media #} + {% set featured = plant.media|selectattr('featured')|first %} {% if not featured and plant.media %} {% set featured = plant.media[0] %} {% endif %} - {# pick featured → first media if no explicit featured #} - {% set featured = plant.featured_media %} - {% if not featured and plant.media %} - {% set featured = plant.media[0] %} - {% endif %} 10: try: font = ImageFont.truetype(font_path, font_size) except OSError: font = ImageFont.load_default() - if draw.textlength(text, font=font) <= label_px - 20: + if draw.textlength(name, font=font) <= label_px - 20: break font_size -= 1 - while draw.textlength(text, font=font) > label_px - 20 and len(text) > 1: - text = text[:-1] - if len(text) < len((name or '').strip()): - text += "…" - x = (label_px - draw.textlength(text, font=font)) // 2 - y = label_px + 20 - draw.text((x, y), text, font=font, fill="black") + + if draw.textlength(name, font=font) > label_px - 20: + while draw.textlength(name + "…", font=font) > label_px - 20 and len(name) > 1: + name = name[:-1] + name += "…" + + # Draw text centered + text_x = (label_px - draw.textlength(name, font=font)) // 2 + text_y = 370 + draw.text((text_x, text_y), name, font=font, fill="black") buf = io.BytesIO() label_img.save(buf, format='PNG', dpi=(dpi, dpi)) buf.seek(0) - return send_file(buf, mimetype='image/png', download_name=download_filename, as_attachment=True) + + return send_file( + buf, + mimetype='image/png', + as_attachment=True, + download_name=filename + ) -@bp.route('//download_qr', methods=['GET']) +@bp.route('/download_qr/', methods=['GET']) +@login_required def download_qr(uuid_val): - p = Plant.query.filter_by(uuid=uuid_val).first_or_404() - if not p.short_id: + # Private “Direct QR” → f/ on plant.cards + p = Plant.query.filter_by(uuid=uuid_val, owner_id=current_user.id).first_or_404() + if not getattr(p, 'short_id', None): p.short_id = Plant.generate_short_id() db.session.commit() - qr_url = f'https://plant.cards/{p.short_id}' + + base = current_app.config.get('PLANT_CARDS_BASE_URL', 'https://plant.cards') + qr_url = f"{base}/f/{p.short_id}" filename = f"{p.short_id}.png" - return generate_label_with_name( - qr_url, - p.common_name.name or p.scientific_name, - filename - ) + return generate_label_with_name(qr_url, p.common_name.name, filename) -@bp.route('//download_qr_card', methods=['GET']) +@bp.route('/download_qr_card/', methods=['GET']) def download_qr_card(uuid_val): + # Public “Card QR” → / on plant.cards p = Plant.query.filter_by(uuid=uuid_val).first_or_404() - if not p.short_id: + if not getattr(p, 'short_id', None): p.short_id = Plant.generate_short_id() db.session.commit() - qr_url = f'https://plant.cards/{p.short_id}' + + base = current_app.config.get('PLANT_CARDS_BASE_URL', 'https://plant.cards') + qr_url = f"{base}/{p.short_id}" filename = f"{p.short_id}_card.png" - return generate_label_with_name( - qr_url, - p.common_name.name or p.scientific_name, - filename - ) + return generate_label_with_name(qr_url, p.common_name.name, filename)