messing stuff up
This commit is contained in:
@ -79,14 +79,14 @@ def create_app():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[⚠️] Failed to load CLI for plugin '{plugin}': {e}")
|
print(f"[⚠️] Failed to load CLI for plugin '{plugin}': {e}")
|
||||||
|
|
||||||
# 3. Auto-load plugin models for migrations
|
## 3. Auto-load plugin models for migrations
|
||||||
if os.path.isfile(model_file):
|
#if os.path.isfile(model_file):
|
||||||
try:
|
# try:
|
||||||
spec = importlib.util.spec_from_file_location(f"plugins.{plugin}.models", model_file)
|
# spec = importlib.util.spec_from_file_location(f"plugins.{plugin}.models", model_file)
|
||||||
mod = importlib.util.module_from_spec(spec)
|
# mod = importlib.util.module_from_spec(spec)
|
||||||
spec.loader.exec_module(mod)
|
# spec.loader.exec_module(mod)
|
||||||
except Exception as e:
|
# except Exception as e:
|
||||||
print(f"[⚠️] Failed to load models from plugin '{plugin}': {e}")
|
# print(f"[⚠️] Failed to load models from plugin '{plugin}': {e}")
|
||||||
|
|
||||||
@app.context_processor
|
@app.context_processor
|
||||||
def inject_current_year():
|
def inject_current_year():
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>400 – Bad Request</h1>
|
<h1>400 – Bad Request</h1>
|
||||||
<p>{{ e.description or "Sorry, we couldn’t understand that request." }}</p>
|
<p>{{ error.description or "Sorry, we couldn’t understand that request." }}</p>
|
||||||
<a href="{{ url_for('main.index') }}">Return home</a>
|
<a href="{{ url_for('main.index') }}">Return home</a>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -34,7 +34,7 @@ services:
|
|||||||
MYSQL_USER: ${MYSQL_USER}
|
MYSQL_USER: ${MYSQL_USER}
|
||||||
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
|
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
|
||||||
ports:
|
ports:
|
||||||
- "3306:3306"
|
- "42000:3306"
|
||||||
volumes:
|
volumes:
|
||||||
- plant_price_tracker_mysql_data:/var/lib/mysql
|
- plant_price_tracker_mysql_data:/var/lib/mysql
|
||||||
healthcheck:
|
healthcheck:
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
# Wait for the database service to be ready
|
# 1) Wait for the database service to be ready
|
||||||
DB_HOST=${DB_HOST:-db}
|
DB_HOST=${DB_HOST:-db}
|
||||||
DB_PORT=${DB_PORT:-3306}
|
DB_PORT=${DB_PORT:-3306}
|
||||||
echo "[⏳] Waiting for database at $DB_HOST:$DB_PORT..."
|
echo "[⏳] Waiting for database at $DB_HOST:$DB_PORT..."
|
||||||
@ -10,22 +10,30 @@ until nc -z $DB_HOST $DB_PORT; do
|
|||||||
done
|
done
|
||||||
echo "[✔] Database is up and reachable"
|
echo "[✔] Database is up and reachable"
|
||||||
|
|
||||||
# Initialize migrations folder if needed
|
# If there’s no migrations folder yet, initialize Alembic here:
|
||||||
if [ ! -d migrations ]; then
|
if [ ! -d "./migrations" ]; then
|
||||||
echo "[✔] Initializing migrations directory"
|
echo "[🆕] No migrations directory found; initializing Alembic"
|
||||||
flask db init
|
flask db init
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "[✔] Running migrations"
|
# 2) Always apply any already-created migration scripts first
|
||||||
flask db migrate -m "auto"
|
echo "[▶️] Applying existing migrations (upgrade)"
|
||||||
flask db upgrade
|
flask db upgrade
|
||||||
|
|
||||||
# Seed database if enabled (accept “1” or “true”)
|
# 3) Now attempt to autogenerate a new migration if the models changed
|
||||||
|
echo "[✨] Autogenerating new migration (if needed)"
|
||||||
|
flask db migrate -m "auto"
|
||||||
|
|
||||||
|
# 4) Apply that new migration (if one was generated)
|
||||||
|
echo "[▶️] Applying any newly autogenerated migration"
|
||||||
|
flask db upgrade
|
||||||
|
|
||||||
|
# 5) Optionally seed data
|
||||||
if [ "$ENABLE_DB_SEEDING" = "true" ] || [ "$ENABLE_DB_SEEDING" = "1" ]; then
|
if [ "$ENABLE_DB_SEEDING" = "true" ] || [ "$ENABLE_DB_SEEDING" = "1" ]; then
|
||||||
echo "[🌱] Seeding Data"
|
echo "[🌱] Seeding Data"
|
||||||
flask preload-data
|
flask preload-data
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# 6) Finally, run the Flask application
|
||||||
# Start the main process
|
echo "[🚀] Starting Flask"
|
||||||
exec "$@"
|
exec "$@"
|
||||||
|
BIN
main-app.zip
BIN
main-app.zip
Binary file not shown.
11
plugins/admin/routes.py
Normal file
11
plugins/admin/routes.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
from flask import Blueprint, render_template
|
||||||
|
from flask_login import login_required, current_user
|
||||||
|
|
||||||
|
bp = Blueprint('admin', __name__, template_folder='templates')
|
||||||
|
|
||||||
|
@bp.route('/admin')
|
||||||
|
@login_required
|
||||||
|
def admin_dashboard():
|
||||||
|
if current_user.role != 'admin':
|
||||||
|
return "Access denied", 403
|
||||||
|
return render_template('admin/admin_dashboard.html')
|
@ -38,14 +38,14 @@ def preload_data(auto=False):
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
# COMMON & SCIENTIFIC NAMES
|
# COMMON & SCIENTIFIC NAMES
|
||||||
monstera_common = PlantCommonName(name='Monstera')
|
#monstera_common = PlantCommonName(name='Monstera')
|
||||||
deliciosa_sci = PlantScientificName(name='Monstera deliciosa')
|
#deliciosa_sci = PlantScientificName(name='Monstera deliciosa')
|
||||||
aurea_sci = PlantScientificName(name='Monstera aurea')
|
#aurea_sci = PlantScientificName(name='Monstera aurea')
|
||||||
db.session.add_all([monstera_common, deliciosa_sci, aurea_sci])
|
#db.session.add_all([monstera_common, deliciosa_sci, aurea_sci])
|
||||||
db.session.commit()
|
#db.session.commit()
|
||||||
|
|
||||||
# PLANTS
|
# PLANTS
|
||||||
parent_plant = Plant(
|
""" parent_plant = Plant(
|
||||||
common_name_id=monstera_common.id,
|
common_name_id=monstera_common.id,
|
||||||
scientific_name_id=deliciosa_sci.id,
|
scientific_name_id=deliciosa_sci.id,
|
||||||
created_by_user_id=admin.id
|
created_by_user_id=admin.id
|
||||||
@ -115,7 +115,7 @@ def preload_data(auto=False):
|
|||||||
ImageHeart(user_id=admin.id, submission_image_id=image.id),
|
ImageHeart(user_id=admin.id, submission_image_id=image.id),
|
||||||
FeaturedImage(submission_image_id=image.id, override_text='Gorgeous coloration', is_featured=True)
|
FeaturedImage(submission_image_id=image.id, override_text='Gorgeous coloration', is_featured=True)
|
||||||
])
|
])
|
||||||
db.session.commit()
|
db.session.commit() """
|
||||||
|
|
||||||
if not auto:
|
if not auto:
|
||||||
click.echo("🎉 Demo data seeded successfully.")
|
click.echo("🎉 Demo data seeded successfully.")
|
||||||
|
@ -1 +1,6 @@
|
|||||||
{ "name": "core_ui", "version": "1.1", "description": "Media rendering macros and styling helpers" }
|
{
|
||||||
|
"name": "core_ui",
|
||||||
|
"version": "1.1.0",
|
||||||
|
"description": "Media rendering macros and styling helpers",
|
||||||
|
"entry_point": null
|
||||||
|
}
|
@ -7,13 +7,6 @@ bp = Blueprint('core_ui', __name__, template_folder='templates')
|
|||||||
def home():
|
def home():
|
||||||
return render_template('core_ui/home.html')
|
return render_template('core_ui/home.html')
|
||||||
|
|
||||||
@bp.route('/admin')
|
|
||||||
@login_required
|
|
||||||
def admin_dashboard():
|
|
||||||
if current_user.role != 'admin':
|
|
||||||
return "Access denied", 403
|
|
||||||
return render_template('core_ui/admin_dashboard.html')
|
|
||||||
|
|
||||||
@bp.route('/health')
|
@bp.route('/health')
|
||||||
def health():
|
def health():
|
||||||
return 'OK', 200
|
return 'OK', 200
|
@ -31,8 +31,9 @@
|
|||||||
<ul class="navbar-nav me-auto">
|
<ul class="navbar-nav me-auto">
|
||||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('core_ui.home') }}">Home</a></li>
|
<li class="nav-item"><a class="nav-link" href="{{ url_for('core_ui.home') }}">Home</a></li>
|
||||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('plant.index') }}">Plants</a></li>
|
<li class="nav-item"><a class="nav-link" href="{{ url_for('plant.index') }}">Plants</a></li>
|
||||||
{% if current_user.is_authenticated and current_user.role == 'admin' %}
|
<li class="nav-item"><a class="nav-link" href="{{ url_for('importer.upload_csv') }}">Import</a></li>
|
||||||
<li class="nav-item"><a class="nav-link text-danger" href="{{ url_for('core_ui.admin_dashboard') }}">Admin Dashboard</a></li>
|
{% if current_user.is_authenticated and current_user.role == 'admin' %}
|
||||||
|
<li class="nav-item"><a class="nav-link text-danger" href="{{ url_for('admin.admin_dashboard') }}">Admin Dashboard</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% block plugin_links %}{% endblock %}
|
{% block plugin_links %}{% endblock %}
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -1 +0,0 @@
|
|||||||
# growlog plugin init
|
|
@ -1 +1,6 @@
|
|||||||
{ "name": "growlog", "version": "1.0", "description": "Tracks time-based plant care logs" }
|
{
|
||||||
|
"name": "growlog",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Tracks time-based plant care logs",
|
||||||
|
"entry_point": null
|
||||||
|
}
|
0
plugins/importer/__init__.py
Normal file
0
plugins/importer/__init__.py
Normal file
6
plugins/importer/plugin.json
Normal file
6
plugins/importer/plugin.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "importer",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Plant import from stand alone application",
|
||||||
|
"entry_point": null
|
||||||
|
}
|
174
plugins/importer/routes.py
Normal file
174
plugins/importer/routes.py
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
# plugins/importer/routes.py
|
||||||
|
|
||||||
|
import csv
|
||||||
|
import io
|
||||||
|
import re
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from flask import (
|
||||||
|
Blueprint, flash, redirect, render_template, request, url_for
|
||||||
|
)
|
||||||
|
from flask_login import login_required, current_user
|
||||||
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
|
|
||||||
|
from app import db
|
||||||
|
from plugins.plant.models import (
|
||||||
|
Plant, PlantCommonName, PlantScientificName, PlantLineage
|
||||||
|
)
|
||||||
|
|
||||||
|
# Blueprint setup
|
||||||
|
bp = Blueprint(
|
||||||
|
'importer',
|
||||||
|
__name__,
|
||||||
|
template_folder='templates',
|
||||||
|
url_prefix='/import'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Expected CSV headers (exact, in this order)
|
||||||
|
EXPECTED_HEADERS = ['uuid', 'plant_type', 'name', 'scientific_name', 'mother_uuid']
|
||||||
|
|
||||||
|
# Strict regex for UUIDv4
|
||||||
|
UUID_REGEX = re.compile(
|
||||||
|
r'^[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}$'
|
||||||
|
)
|
||||||
|
|
||||||
|
def is_valid_uuid(u: str) -> bool:
|
||||||
|
"""Return True if 'u' matches UUIDv4 format exactly."""
|
||||||
|
return bool(UUID_REGEX.match(u))
|
||||||
|
|
||||||
|
def sanitize_field(field_value: str) -> str:
|
||||||
|
"""
|
||||||
|
Prevent CSV-injection by prefixing any cell that starts with
|
||||||
|
'=', '+', '-', or '@' with a space.
|
||||||
|
"""
|
||||||
|
if isinstance(field_value, str) and field_value and field_value[0] in ['=', '+', '-', '@']:
|
||||||
|
return ' ' + field_value
|
||||||
|
return field_value
|
||||||
|
|
||||||
|
def validate_row(row: dict, line_number: int):
|
||||||
|
"""
|
||||||
|
Validate a single CSV row. Returns (cleaned_row_dict, errors_list).
|
||||||
|
If errors_list is not empty, the row is invalid.
|
||||||
|
"""
|
||||||
|
errors = []
|
||||||
|
cleaned = {}
|
||||||
|
|
||||||
|
# 1) uuid: if provided, must be valid; otherwise generate a new one
|
||||||
|
raw_uuid = row.get('uuid', '').strip()
|
||||||
|
if raw_uuid:
|
||||||
|
if not is_valid_uuid(raw_uuid):
|
||||||
|
errors.append(f"Line {line_number}: Invalid UUID format '{raw_uuid}'.")
|
||||||
|
else:
|
||||||
|
cleaned['uuid'] = raw_uuid
|
||||||
|
else:
|
||||||
|
# auto-generate
|
||||||
|
cleaned['uuid'] = str(uuid.uuid4())
|
||||||
|
|
||||||
|
# 2) plant_type: required, <= 50 chars
|
||||||
|
plant_type = row.get('plant_type', '').strip()
|
||||||
|
if not plant_type:
|
||||||
|
errors.append(f"Line {line_number}: 'plant_type' is required.")
|
||||||
|
elif len(plant_type) > 50:
|
||||||
|
errors.append(f"Line {line_number}: 'plant_type' exceeds 50 characters.")
|
||||||
|
else:
|
||||||
|
cleaned['plant_type'] = sanitize_field(plant_type)
|
||||||
|
|
||||||
|
# 3) name: required, <= 100 chars
|
||||||
|
name = row.get('name', '').strip()
|
||||||
|
if not name:
|
||||||
|
errors.append(f"Line {line_number}: 'name' (common name) is required.")
|
||||||
|
elif len(name) > 100:
|
||||||
|
errors.append(f"Line {line_number}: 'name' exceeds 100 characters.")
|
||||||
|
else:
|
||||||
|
cleaned['name'] = sanitize_field(name)
|
||||||
|
|
||||||
|
# 4) scientific_name: required, <= 100 chars
|
||||||
|
sci = row.get('scientific_name', '').strip()
|
||||||
|
if not sci:
|
||||||
|
errors.append(f"Line {line_number}: 'scientific_name' is required.")
|
||||||
|
elif len(sci) > 100:
|
||||||
|
errors.append(f"Line {line_number}: 'scientific_name' exceeds 100 characters.")
|
||||||
|
else:
|
||||||
|
cleaned['scientific_name'] = sanitize_field(sci)
|
||||||
|
|
||||||
|
# 5) mother_uuid: optional. If present (and not 'N/A'), must be valid
|
||||||
|
raw_mother = row.get('mother_uuid', '').strip()
|
||||||
|
if raw_mother and raw_mother.upper() != 'N/A':
|
||||||
|
if not is_valid_uuid(raw_mother):
|
||||||
|
errors.append(f"Line {line_number}: 'mother_uuid' has invalid UUID '{raw_mother}'.")
|
||||||
|
else:
|
||||||
|
cleaned['mother_uuid'] = raw_mother
|
||||||
|
else:
|
||||||
|
cleaned['mother_uuid'] = None
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
return None, errors
|
||||||
|
|
||||||
|
return cleaned, None
|
||||||
|
|
||||||
|
@bp.route("/", methods=["GET", "POST"])
|
||||||
|
@login_required
|
||||||
|
def upload_csv():
|
||||||
|
headers = [
|
||||||
|
"name", "scientific_name", "type", "status", "description"
|
||||||
|
]
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
file = request.files.get("file")
|
||||||
|
if not file or not file.filename.endswith(".csv"):
|
||||||
|
flash("Please upload a valid CSV file.", "danger")
|
||||||
|
return redirect(request.url)
|
||||||
|
|
||||||
|
try:
|
||||||
|
stream = io.StringIO(file.stream.read().decode("UTF-8"))
|
||||||
|
csv_reader = csv.reader(stream)
|
||||||
|
data = list(csv_reader)
|
||||||
|
|
||||||
|
if not data or data[0] != headers:
|
||||||
|
flash("CSV headers must exactly match: " + ", ".join(headers), "danger")
|
||||||
|
return redirect(request.url)
|
||||||
|
|
||||||
|
# Skip header row
|
||||||
|
rows = data[1:]
|
||||||
|
|
||||||
|
with db.session.begin_nested(): # rollback safe transaction
|
||||||
|
for row in rows:
|
||||||
|
name, sci_name, plant_type, plant_status, desc = row
|
||||||
|
|
||||||
|
# Get or create scientific name entry
|
||||||
|
sci = PlantScientificName.query.filter_by(name=sci_name.strip()).first()
|
||||||
|
if not sci:
|
||||||
|
sci = PlantScientificName(name=sci_name.strip())
|
||||||
|
db.session.add(sci)
|
||||||
|
db.session.flush() # ensure sci.id is available
|
||||||
|
|
||||||
|
# Create plant entry
|
||||||
|
new_plant = Plant(
|
||||||
|
uuid=str(uuid4()),
|
||||||
|
name=name.strip(),
|
||||||
|
scientific_name_id=sci.id,
|
||||||
|
type=plant_type.strip(),
|
||||||
|
status=plant_status.strip(),
|
||||||
|
description=desc.strip()
|
||||||
|
)
|
||||||
|
db.session.add(new_plant)
|
||||||
|
db.session.flush() # get new_plant.id for ownership log
|
||||||
|
|
||||||
|
# Log ownership
|
||||||
|
log = PlantOwnershipLog(
|
||||||
|
plant_id=new_plant.id,
|
||||||
|
user_id=current_user.id,
|
||||||
|
date_acquired=datetime.utcnow()
|
||||||
|
)
|
||||||
|
db.session.add(log)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
flash(f"Successfully imported {len(rows)} plants.", "success")
|
||||||
|
return redirect(url_for("importer.upload_csv"))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
flash(f"Error importing CSV: {str(e)}", "danger")
|
||||||
|
return redirect(request.url)
|
||||||
|
|
||||||
|
return render_template("importer/upload.html", headers=", ".join(headers))
|
32
plugins/importer/templates/importer/upload.html
Normal file
32
plugins/importer/templates/importer/upload.html
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{% extends 'core_ui/base.html' %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mt-4">
|
||||||
|
<h2 class="mb-3">Import Plants from CSV</h2>
|
||||||
|
|
||||||
|
<div class="alert alert-info" role="alert">
|
||||||
|
<strong>Expected CSV headers (in this exact order):</strong>
|
||||||
|
<code>{{ headers }}</code>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="POST" enctype="multipart/form-data" class="needs-validation" novalidate>
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="file" class="form-label">Select CSV file to upload</label>
|
||||||
|
<input type="file" class="form-control" id="file" name="file" accept=".csv" required>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Upload & Import</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
<div class="mt-4">
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<div class="alert alert-{{ 'danger' if category == 'error' else category }}" role="alert">
|
||||||
|
{{ message }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -1 +0,0 @@
|
|||||||
# media plugin init
|
|
@ -1 +1,6 @@
|
|||||||
{ "name": "media", "version": "1.0", "description": "Upload and attach media to plants and grow logs" }
|
{
|
||||||
|
"name": "media",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Upload and attach media to plants and grow logs",
|
||||||
|
"entry_point": null
|
||||||
|
}
|
@ -37,6 +37,17 @@ class Plant(db.Model):
|
|||||||
date_added = db.Column(db.DateTime, default=datetime.utcnow)
|
date_added = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
created_by_user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
|
created_by_user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
|
||||||
|
|
||||||
|
class Plant(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
uuid = db.Column(db.String(36), unique=True, nullable=False, default=lambda: str(uuid4()))
|
||||||
|
mother_uuid = db.Column(db.String(36), nullable=True) # Optional parent reference
|
||||||
|
name_id = db.Column(db.Integer, db.ForeignKey("plant_common_name.id"), nullable=True)
|
||||||
|
scientific_name_id = db.Column(db.Integer, db.ForeignKey("plant_scientific_name.id"), nullable=True)
|
||||||
|
plant_type = db.Column(db.String(50), nullable=False)
|
||||||
|
status = db.Column(db.String(50), nullable=False)
|
||||||
|
notes = db.Column(db.Text)
|
||||||
|
|
||||||
|
|
||||||
# Relationships
|
# Relationships
|
||||||
updates = db.relationship('PlantUpdate', backref='growlog', lazy=True)
|
updates = db.relationship('PlantUpdate', backref='growlog', lazy=True)
|
||||||
lineage = db.relationship('PlantLineage', backref='child', lazy=True, foreign_keys='PlantLineage.child_plant_id')
|
lineage = db.relationship('PlantLineage', backref='child', lazy=True, foreign_keys='PlantLineage.child_plant_id')
|
||||||
|
@ -1 +0,0 @@
|
|||||||
# search plugin initialization
|
|
@ -1 +1,6 @@
|
|||||||
{ "name": "search", "version": "1.1", "description": "Updated search plugin with live Plant model integration" }
|
{
|
||||||
|
"name": "search",
|
||||||
|
"version": "1.1",
|
||||||
|
"description": "Updated search plugin with live Plant model integration",
|
||||||
|
"entry_point": null
|
||||||
|
}
|
@ -1,5 +0,0 @@
|
|||||||
def register(app):
|
|
||||||
"""Register submission routes & blueprint."""
|
|
||||||
from .routes import bp as submissions_bp
|
|
||||||
app.register_blueprint(submissions_bp)
|
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "submission",
|
"name": "submission",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "Plugin to handle user-submitted plant data and images."
|
"description": "Plugin to handle user-submitted plant data and images.",
|
||||||
|
"entry_point": null
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user