more
This commit is contained in:
196
README.md
196
README.md
@ -1,47 +1,185 @@
|
|||||||
# Nature In Pots – Modular Flask Plant Management App
|
# 🌿 Nature In Pots — Ultra Plant Tracker Platform
|
||||||
|
|
||||||
This project is a modular, plugin-driven Flask application for tracking plants, their health, lineage, pricing, media, and more.
|
> Modular, collaborative, end-to-end plant tracking and pricing platform with QR+barcode IDs, propagation logs, resale tools, and role-based management.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📦 Core Features
|
## 🧩 Overview
|
||||||
|
|
||||||
- 🔐 User Authentication (`auth`)
|
This is a full-feature, plugin-driven Flask 2+ web application for managing and tracking plant ownership, propagation, pricing, and growth. It supports dynamic plant attributes, collaboration groups, trade logs, QR/barcode labeling, resale workflows, offline sync, moderator tools, and future AI/ML modules.
|
||||||
- 🌱 Plant Identity Profiles (`plant`)
|
|
||||||
- 📊 Grow Logs with Events (`growlog`)
|
Built for hobbyists, businesses, breeders, and community gardens.
|
||||||
- 🖼 Media Upload & Attachments (`media`)
|
|
||||||
- 🔍 Tag-based and name-based Search (`search`)
|
|
||||||
- 🛠 CLI Tools for Preloading and Seeding (`cli`)
|
|
||||||
- 🎨 UI Macros and Shared Layout (`core_ui`)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🧱 Plugin System
|
## 🚀 Core Features
|
||||||
|
|
||||||
Each feature is implemented as a plugin under the `plugins/` directory. Each plugin is self-contained and includes:
|
### 🌱 Plant Profiles
|
||||||
|
- Add, edit, and log plants with full propagation and ownership history
|
||||||
|
- Each plant is assigned a permanent, scannable **QR code** and **barcode**
|
||||||
|
- Plants can be marked as `Public`, `Unlisted`, or `Folder-only`
|
||||||
|
- All pricing, logs, and lineage are tied to a plant ID and user
|
||||||
|
|
||||||
- `models.py`
|
### 🧾 Grow Logs
|
||||||
- `routes.py`
|
- Add logs with images, notes, and growth metrics
|
||||||
- `forms.py` *(if applicable)*
|
- Track success/failure events, mutation events, pest/disease sightings
|
||||||
- `templates/`
|
- Link logs to substrate recipes and fertilizers
|
||||||
- `plugin.json`
|
|
||||||
|
|
||||||
Plugins are auto-loaded via `app/__init__.py`.
|
### 🔗 Verified Lineage Tracking
|
||||||
|
- Lineage links are created by the new owner
|
||||||
|
- Parent plant's owner must **approve** the linkage
|
||||||
|
- Verified lineage is marked with a badge and shown in plant lineage tree
|
||||||
|
- Pending links are visible only to the creator
|
||||||
|
|
||||||
|
### 💰 Pricing Logic
|
||||||
|
- Only the current owner and admins can see pricing
|
||||||
|
- On transfer, original price is retained but hidden from the buyer
|
||||||
|
- Buyer must submit their own price for tracking resale data
|
||||||
|
- Admins see full price history; mods and others do not
|
||||||
|
|
||||||
|
### 🧪 Substrate + Fertilizer Tracking
|
||||||
|
- Track custom mixes by ingredient (e.g., “Fine Pumice”, “Large Bark”)
|
||||||
|
- Store cost per ingredient and auto-calculate total mix cost
|
||||||
|
- Recipes can be reused across plants
|
||||||
|
- Fertilizer schedules can be attached to logs and outcomes tracked
|
||||||
|
|
||||||
|
### 📦 Shipping Tracker
|
||||||
|
- When a plant is sold, sellers can add:
|
||||||
|
- Carrier, tracking number, est. delivery date
|
||||||
|
- Ownership updates post buyer confirmation
|
||||||
|
- Shipping logs are attached to the plant transfer log
|
||||||
|
|
||||||
|
### 📁 Plant Folders
|
||||||
|
- Organize plants into folders (e.g., “For Sale”, “2025 Spring Batch”)
|
||||||
|
- Each folder gets its own QR code:
|
||||||
|
`https://domain.com/{username}/folder/{id}|{slug}`
|
||||||
|
- Folders can be public, private, or unlisted
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🏗 Installation
|
## 🧍♂️ Users, Roles, and Groups
|
||||||
|
|
||||||
### Prerequisites
|
### 👤 User Roles
|
||||||
|
- **User** – default
|
||||||
|
- **Moderator** – can manage flags, notes
|
||||||
|
- **Admin** – full backend, plugin, pricing, banning control
|
||||||
|
- Roles are extensible via admin UI
|
||||||
|
|
||||||
- Python 3.11+
|
### 🛡 Moderator Panel
|
||||||
- MySQL server
|
- View and resolve reports
|
||||||
- Virtualenv or Docker
|
- Add private notes to users or plants (e.g., warnings, suspicion)
|
||||||
|
- Ban users
|
||||||
|
- Banned users' plants become read-only
|
||||||
|
- Cannot add new plants
|
||||||
|
- Buyers can request transfer via email approval from banned seller
|
||||||
|
|
||||||
### Setup
|
### 👥 Collaboration Groups
|
||||||
|
- Users can form groups to share:
|
||||||
|
- Logs
|
||||||
|
- Images
|
||||||
|
- Pricing (opt-in)
|
||||||
|
- Groups have role-based permissions (manager, editor, viewer)
|
||||||
|
- Useful for stores, teams, or shared collections
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏷️ Labeling System: QR + Barcode
|
||||||
|
|
||||||
|
### QR Code
|
||||||
|
- Generated on server after initial sync
|
||||||
|
- Unique to plant, never changes
|
||||||
|
- SVG and PNG available
|
||||||
|
|
||||||
|
### Barcode Fallback
|
||||||
|
- CODE-128 format
|
||||||
|
- If data too long, split into stacked barcodes
|
||||||
|
- Same encoded info: plant ID, owner ID, visibility, timestamp
|
||||||
|
- Printable as label from dashboard
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 Permissions Matrix
|
||||||
|
|
||||||
|
| Feature | Owner | Group Member | Moderator | Admin |
|
||||||
|
|--------------------------|-------|---------------|-----------|--------|
|
||||||
|
| View Logs | ✅ | ✅ (if shared) | ✅ | ✅ |
|
||||||
|
| Edit Logs | ✅ | 🔁* | 🚫 | ✅ |
|
||||||
|
| View Pricing | ✅ | ✅ (opt-in) | 🚫 | ✅ |
|
||||||
|
| Ban User / Flag Review | 🚫 | 🚫 | ✅ | ✅ |
|
||||||
|
| Approve Lineage | 🚫 | 🚫 | 🚫 | ✅ |
|
||||||
|
| Confirm Lineage | ✅ | 🚫 | 🚫 | ✅ |
|
||||||
|
|
||||||
|
🔁* if granted by group manager
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌍 Offline Sync (PWA)
|
||||||
|
- Full add/log/edit possible offline
|
||||||
|
- Sync queue uploads on connection
|
||||||
|
- QR/barcodes generated **after** server confirms sync
|
||||||
|
- Client-side validation before queue
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧠 Smart Tools
|
||||||
|
|
||||||
|
### ⭐️ User Reputation System
|
||||||
|
- Users rated after trades (accuracy, responsiveness, helpfulness)
|
||||||
|
- “Trusted Grower” tag auto-assigned above threshold
|
||||||
|
- Can be revoked via vote or admin action
|
||||||
|
|
||||||
|
### 🌿 Inter-Plant Comparison
|
||||||
|
- Timeline comparison for growth, size, or log outcomes
|
||||||
|
- Side-by-side charts and event overlays
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Admin & Dev Tools
|
||||||
|
|
||||||
|
### 📤 Seed Data Generator
|
||||||
|
- Seeds with common aroids, herbs, and test users
|
||||||
|
- Covers full range of roles, plant types, and edge cases
|
||||||
|
|
||||||
|
### 🗂 Plugin System
|
||||||
|
- CLI & plugin discovery system
|
||||||
|
- Admin can toggle plugins
|
||||||
|
- Plugin types: CLI, UI Panel, API extension, webhook, scheduler
|
||||||
|
|
||||||
|
### 🗃 Data Export / Disaster Recovery
|
||||||
|
- Export: SQL dump, file archive, JSON profile
|
||||||
|
- Restore: Admin-initiated rollback or full upload restore
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛣 API System
|
||||||
|
|
||||||
|
### REST & GraphQL APIs
|
||||||
|
- REST: OpenAPI-documented endpoints
|
||||||
|
- GraphQL: Advanced multi-entity queries
|
||||||
|
- JWT-secured
|
||||||
|
- Follows role-based access rules
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌐 Internationalization
|
||||||
|
- Flask-Babel integration
|
||||||
|
- Language switcher in UI
|
||||||
|
- Community-managed translation interface (admin toggled)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📅 Future Enhancements
|
||||||
|
|
||||||
|
- 🧠 AI Journal Assistant (log suggestions, summarization)
|
||||||
|
- 📆 Calendar View for logs/reminders
|
||||||
|
- 🧰 Visual ERD Generator Tool
|
||||||
|
- 🛒 Live Auctions Plugin (for curated resale events)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧾 License & Contribution
|
||||||
|
|
||||||
|
This project is part of **Nature In Pots** under **High Thyme Ventures**.
|
||||||
|
|
||||||
|
Please contact the repository owner for collaboration, plugin submission, or access requests.
|
||||||
|
|
||||||
```bash
|
|
||||||
make install # Install dependencies
|
|
||||||
make dev # Start Flask development server
|
|
||||||
make db # Initialize database
|
|
||||||
make seed # Seed database with test data
|
|
||||||
|
@ -6,6 +6,10 @@ bp = Blueprint('errors', __name__)
|
|||||||
def bad_request(error):
|
def bad_request(error):
|
||||||
return render_template('400.html'), 400
|
return render_template('400.html'), 400
|
||||||
|
|
||||||
|
@bp.app_errorhandler(404)
|
||||||
|
def bad_request(error):
|
||||||
|
return render_template('404.html'), 404
|
||||||
|
|
||||||
@bp.app_errorhandler(500)
|
@bp.app_errorhandler(500)
|
||||||
def internal_error(error):
|
def internal_error(error):
|
||||||
return render_template('500.html'), 500
|
return render_template('500.html'), 500
|
||||||
|
12
app/templates/400.html
Normal file
12
app/templates/400.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>400 Bad Request</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>400 – Bad Request</h1>
|
||||||
|
<p>{{ e.description or "Sorry, we couldn’t understand that request." }}</p>
|
||||||
|
<a href="{{ url_for('main.index') }}">Return home</a>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -24,39 +24,6 @@ services:
|
|||||||
retries: 3
|
retries: 3
|
||||||
start_period: 30s
|
start_period: 30s
|
||||||
|
|
||||||
command: >
|
|
||||||
bash -c "
|
|
||||||
set -e
|
|
||||||
|
|
||||||
echo '[✔] Ensuring .env...'
|
|
||||||
if [ ! -f '.env' ]; then cp .env.example .env; fi
|
|
||||||
|
|
||||||
echo '[✔] Waiting for MySQL to be ready...'
|
|
||||||
until nc -z ${MYSQL_HOST} ${MYSQL_PORT}; do sleep 1; done
|
|
||||||
|
|
||||||
echo '[✔] Ensuring migration structure...'
|
|
||||||
if [ ! -d 'migrations' ]; then
|
|
||||||
echo '[ℹ] Running flask db init...'
|
|
||||||
flask db init
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo '[ℹ] Autogenerating migration...'
|
|
||||||
flask db migrate -m 'auto' || echo '[ℹ] No changes detected.'
|
|
||||||
|
|
||||||
echo '[✔] Running DB migrations...'
|
|
||||||
flask db upgrade
|
|
||||||
|
|
||||||
if [ \"$ENABLE_DB_SEEDING\" = \"1\" ]; then
|
|
||||||
echo '[🌱] Seeding Data...'
|
|
||||||
flask preload-data
|
|
||||||
else
|
|
||||||
echo '[⚠️] DB seeding skipped by config.'
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo '[🚀] Starting Flask server...'
|
|
||||||
flask run --host=0.0.0.0
|
|
||||||
"
|
|
||||||
|
|
||||||
|
|
||||||
db:
|
db:
|
||||||
image: mysql:8
|
image: mysql:8
|
||||||
|
@ -20,11 +20,12 @@ echo "[✔] Running migrations"
|
|||||||
flask db migrate -m "auto"
|
flask db migrate -m "auto"
|
||||||
flask db upgrade
|
flask db upgrade
|
||||||
|
|
||||||
# Seed database if enabled
|
# Seed database if enabled (accept “1” or “true”)
|
||||||
if [ "$ENABLE_DB_SEEDING" = "true" ]; 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
|
||||||
|
|
||||||
|
|
||||||
# Start the main process
|
# Start the main process
|
||||||
exec "$@"
|
exec "$@"
|
||||||
|
Binary file not shown.
@ -1,7 +1,8 @@
|
|||||||
{% extends 'core_ui/base.html' %}
|
{% extends 'core_ui/base.html' %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h2>Login</h2>
|
<h2>Login</h2>
|
||||||
<form method="POST" action="/login">
|
<form method="POST" action="{{ url_for('auth.login') }}">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="email" class="form-label">Email</label>
|
<label for="email" class="form-label">Email</label>
|
||||||
<input type="email" class="form-control" id="email" name="email" required>
|
<input type="email" class="form-control" id="email" name="email" required>
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<h2>Register</h2>
|
<h2>Register</h2>
|
||||||
<form method="POST">
|
<form method="POST">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label>Email</label>
|
<label>Email</label>
|
||||||
<input name="email" class="form-control" type="email" required>
|
<input name="email" class="form-control" type="email" required>
|
||||||
|
@ -13,3 +13,7 @@ def admin_dashboard():
|
|||||||
if current_user.role != 'admin':
|
if current_user.role != 'admin':
|
||||||
return "Access denied", 403
|
return "Access denied", 403
|
||||||
return render_template('core_ui/admin_dashboard.html')
|
return render_template('core_ui/admin_dashboard.html')
|
||||||
|
|
||||||
|
@bp.route('/health')
|
||||||
|
def health():
|
||||||
|
return 'OK', 200
|
@ -42,3 +42,14 @@ class Plant(db.Model):
|
|||||||
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')
|
||||||
tags = db.relationship('Tag', secondary=plant_tags, backref='plants')
|
tags = db.relationship('Tag', secondary=plant_tags, backref='plants')
|
||||||
|
|
||||||
|
# → relationships so we can pull in the actual names:
|
||||||
|
common_name = db.relationship(
|
||||||
|
'PlantCommonName',
|
||||||
|
backref=db.backref('plants', lazy='dynamic'),
|
||||||
|
lazy=True
|
||||||
|
)
|
||||||
|
scientific_name = db.relationship(
|
||||||
|
'PlantScientificName',
|
||||||
|
backref=db.backref('plants', lazy='dynamic'),
|
||||||
|
lazy=True
|
||||||
|
)
|
@ -1,9 +1,37 @@
|
|||||||
{% extends 'core_ui/base.html' %}
|
{% extends 'core_ui/base.html' %}
|
||||||
|
{% block title %}{{ plant.common_name.name }} – Nature In Pots{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>{{ plant.name }}</h1>
|
<div class="container my-4">
|
||||||
<p>Type: {{ plant.type }}</p>
|
<div class="row">
|
||||||
<p>{{ plant.notes }}</p>
|
<div class="col-md-4">
|
||||||
<p>Status: {% if plant.is_active %}Active{% else %}Inactive{% endif %}</p>
|
<img src="https://placehold.co/300x300"
|
||||||
<a href="{{ url_for('plant.edit', plant_id=plant.id) }}">Edit</a>
|
class="img-fluid rounded mb-3"
|
||||||
<a href="{{ url_for('plant.index') }}">Back to list</a>
|
alt="{{ plant.common_name.name }}">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-8">
|
||||||
|
<h1>{{ plant.common_name.name }}</h1>
|
||||||
|
{% if plant.scientific_name %}
|
||||||
|
<p class="text-muted"><em>{{ plant.scientific_name.name }}</em></p>
|
||||||
|
{% endif %}
|
||||||
|
<dl class="row">
|
||||||
|
<dt class="col-sm-3">Date Added</dt>
|
||||||
|
<dd class="col-sm-9">{{ plant.date_added.strftime('%Y-%m-%d') }}</dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-3">Status</dt>
|
||||||
|
<dd class="col-sm-9">
|
||||||
|
{{ 'Dead' if plant.is_dead else 'Active' }}
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<a href="{{ url_for('plant.index') }}" class="btn btn-secondary">
|
||||||
|
← Back to list
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('plant.edit', plant_id=plant.id) }}"
|
||||||
|
class="btn btn-primary">
|
||||||
|
Edit
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -1,10 +1,46 @@
|
|||||||
{% extends 'core_ui/base.html' %}
|
{% extends 'core_ui/base.html' %}
|
||||||
|
{% block title %}Plant List – Nature In Pots{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>Plant List</h1>
|
<div class="container my-4">
|
||||||
<ul>
|
<h1 class="mb-4">Plant List</h1>
|
||||||
|
|
||||||
|
{% if plants %}
|
||||||
|
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 g-4">
|
||||||
{% for plant in plants %}
|
{% for plant in plants %}
|
||||||
<li><a href="{{ url_for('plant.detail', plant_id=plant.id) }}">{{ plant.name }}</a></li>
|
<div class="col">
|
||||||
|
<div class="card h-100">
|
||||||
|
<!-- placeholder until you wire up real media -->
|
||||||
|
<img src="https://placehold.co/150x150"
|
||||||
|
class="card-img-top"
|
||||||
|
alt="{{ plant.common_name.name if plant.common_name else 'Plant' }}">
|
||||||
|
<div class="card-body d-flex flex-column">
|
||||||
|
<h5 class="card-title">
|
||||||
|
{{ plant.common_name.name if plant.common_name else 'Unnamed' }}
|
||||||
|
</h5>
|
||||||
|
{% if plant.scientific_name %}
|
||||||
|
<p class="card-text text-muted">
|
||||||
|
<em>{{ plant.scientific_name.name }}</em>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
<a href="{{ url_for('plant.detail', plant_id=plant.id) }}"
|
||||||
|
class="mt-auto btn btn-primary">
|
||||||
|
View Details
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</div>
|
||||||
<a href="{{ url_for('plant.create') }}">Add New Plant</a>
|
{% else %}
|
||||||
|
<p>No plants found yet. <a href="{{ url_for('plant.create') }}">Add one now.</a></p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<a href="{{ url_for('plant.create') }}"
|
||||||
|
class="btn btn-success">
|
||||||
|
Add New Plant
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
Reference in New Issue
Block a user