csv upload
This commit is contained in:
156
app.py
156
app.py
@@ -1,4 +1,4 @@
|
|||||||
from flask import Flask, render_template, request, redirect, url_for, flash, session, jsonify
|
from flask import Flask, render_template, request, redirect, url_for, flash, session, jsonify, Response
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
from flask_migrate import Migrate
|
from flask_migrate import Migrate
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@@ -7,6 +7,8 @@ from werkzeug.utils import secure_filename
|
|||||||
import os
|
import os
|
||||||
from config import Config
|
from config import Config
|
||||||
import random
|
import random
|
||||||
|
import csv
|
||||||
|
import io
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.config.from_object(Config)
|
app.config.from_object(Config)
|
||||||
@@ -1453,6 +1455,158 @@ def recognize_plant():
|
|||||||
random_plant = random.choice(plants)
|
random_plant = random.choice(plants)
|
||||||
return jsonify({'redirect': url_for('plant', plant_id=random_plant.id)})
|
return jsonify({'redirect': url_for('plant', plant_id=random_plant.id)})
|
||||||
|
|
||||||
|
@app.route('/admin/plants/upload-csv', methods=['POST'])
|
||||||
|
def upload_plants_csv():
|
||||||
|
if not is_logged_in():
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
if 'csv_file' not in request.files:
|
||||||
|
flash('No file uploaded', 'error')
|
||||||
|
return redirect(url_for('manage_plants'))
|
||||||
|
|
||||||
|
file = request.files['csv_file']
|
||||||
|
if file.filename == '':
|
||||||
|
flash('No file selected', 'error')
|
||||||
|
return redirect(url_for('manage_plants'))
|
||||||
|
|
||||||
|
if not file.filename.endswith('.csv'):
|
||||||
|
flash('Please upload a CSV file', 'error')
|
||||||
|
return redirect(url_for('manage_plants'))
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Read the CSV file
|
||||||
|
stream = io.StringIO(file.stream.read().decode("UTF8"), newline=None)
|
||||||
|
csv_reader = csv.DictReader(stream)
|
||||||
|
|
||||||
|
# Get all the lookup dictionaries
|
||||||
|
climates = {c.name.lower(): c.id for c in Climate.query.all()}
|
||||||
|
environments = {e.name.lower(): e.id for e in Environment.query.all()}
|
||||||
|
lights = {l.name.lower(): l.id for l in Light.query.all()}
|
||||||
|
toxicities = {t.name.lower(): t.id for t in Toxicity.query.all()}
|
||||||
|
sizes = {s.name.lower(): s.id for s in Size.query.all()}
|
||||||
|
care_difficulties = {d.name.lower(): d.id for d in CareDifficulty.query.all()}
|
||||||
|
growth_rates = {r.name.lower(): r.id for r in GrowthRate.query.all()}
|
||||||
|
products = {p.name.lower(): p.id for p in Product.query.all()}
|
||||||
|
|
||||||
|
success_count = 0
|
||||||
|
error_count = 0
|
||||||
|
duplicate_count = 0
|
||||||
|
|
||||||
|
for row in csv_reader:
|
||||||
|
try:
|
||||||
|
# Check if plant with this name already exists
|
||||||
|
existing_plant = Plant.query.filter_by(name=row['name']).first()
|
||||||
|
if existing_plant:
|
||||||
|
duplicate_count += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Create new plant
|
||||||
|
plant = Plant(
|
||||||
|
name=row['name'],
|
||||||
|
description=row.get('description', ''),
|
||||||
|
care_guide=row.get('care_guide', ''),
|
||||||
|
date_added=datetime.utcnow()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set relationships using the lookup dictionaries
|
||||||
|
if row.get('climate'):
|
||||||
|
plant.climate_id = climates.get(row['climate'].lower())
|
||||||
|
if row.get('environment'):
|
||||||
|
plant.environment_id = environments.get(row['environment'].lower())
|
||||||
|
if row.get('light'):
|
||||||
|
plant.light_id = lights.get(row['light'].lower())
|
||||||
|
if row.get('toxicity'):
|
||||||
|
plant.toxicity_id = toxicities.get(row['toxicity'].lower())
|
||||||
|
if row.get('size'):
|
||||||
|
plant.size_id = sizes.get(row['size'].lower())
|
||||||
|
if row.get('care_difficulty'):
|
||||||
|
plant.care_difficulty_id = care_difficulties.get(row['care_difficulty'].lower())
|
||||||
|
if row.get('growth_rate'):
|
||||||
|
plant.growth_rate_id = growth_rates.get(row['growth_rate'].lower())
|
||||||
|
|
||||||
|
# Handle products (comma-separated list)
|
||||||
|
if row.get('products'):
|
||||||
|
product_ids = []
|
||||||
|
for product_name in row['products'].split(','):
|
||||||
|
product_name = product_name.strip().lower()
|
||||||
|
if product_name in products:
|
||||||
|
product_ids.append(str(products[product_name]))
|
||||||
|
plant.products = ','.join(product_ids)
|
||||||
|
|
||||||
|
db.session.add(plant)
|
||||||
|
success_count += 1
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_count += 1
|
||||||
|
print(f"Error processing row: {row}, Error: {str(e)}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
message = f'Successfully imported {success_count} plants.'
|
||||||
|
if duplicate_count > 0:
|
||||||
|
message += f' {duplicate_count} duplicate plants were skipped.'
|
||||||
|
if error_count > 0:
|
||||||
|
message += f' {error_count} errors occurred.'
|
||||||
|
flash(message, 'success')
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
flash(f'Error processing CSV file: {str(e)}', 'error')
|
||||||
|
db.session.rollback()
|
||||||
|
|
||||||
|
return redirect(url_for('manage_plants'))
|
||||||
|
|
||||||
|
@app.route('/admin/plants/download-template')
|
||||||
|
def download_plants_template():
|
||||||
|
if not is_logged_in():
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
# Create a CSV template with headers and example data
|
||||||
|
output = io.StringIO()
|
||||||
|
writer = csv.writer(output)
|
||||||
|
|
||||||
|
# Write headers
|
||||||
|
headers = [
|
||||||
|
'name',
|
||||||
|
'description',
|
||||||
|
'care_guide',
|
||||||
|
'climate',
|
||||||
|
'environment',
|
||||||
|
'light',
|
||||||
|
'toxicity',
|
||||||
|
'size',
|
||||||
|
'care_difficulty',
|
||||||
|
'growth_rate',
|
||||||
|
'products'
|
||||||
|
]
|
||||||
|
writer.writerow(headers)
|
||||||
|
|
||||||
|
# Write example row with available options
|
||||||
|
example_row = [
|
||||||
|
'Monstera Deliciosa', # name
|
||||||
|
'Beautiful tropical plant with distinctive leaf holes', # description
|
||||||
|
'Water weekly, mist leaves occasionally', # care_guide
|
||||||
|
', '.join([c.name for c in Climate.query.all()]), # climate options
|
||||||
|
', '.join([e.name for e in Environment.query.all()]), # environment options
|
||||||
|
', '.join([l.name for l in Light.query.all()]), # light options
|
||||||
|
', '.join([t.name for t in Toxicity.query.all()]), # toxicity options
|
||||||
|
', '.join([s.name for s in Size.query.all()]), # size options
|
||||||
|
', '.join([d.name for d in CareDifficulty.query.all()]), # care_difficulty options
|
||||||
|
', '.join([r.name for r in GrowthRate.query.all()]), # growth_rate options
|
||||||
|
', '.join([p.name for p in Product.query.all()]) # product options
|
||||||
|
]
|
||||||
|
writer.writerow(example_row)
|
||||||
|
|
||||||
|
# Create the response
|
||||||
|
output.seek(0)
|
||||||
|
return Response(
|
||||||
|
output,
|
||||||
|
mimetype='text/csv',
|
||||||
|
headers={
|
||||||
|
'Content-Disposition': 'attachment; filename=plants_template.csv',
|
||||||
|
'Content-Type': 'text/csv',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
db.create_all() # Create all tables
|
db.create_all() # Create all tables
|
||||||
|
|||||||
@@ -6,8 +6,19 @@
|
|||||||
<div class="bg-white rounded-lg shadow p-6">
|
<div class="bg-white rounded-lg shadow p-6">
|
||||||
<div class="flex justify-between items-center mb-6">
|
<div class="flex justify-between items-center mb-6">
|
||||||
<h2 class="text-2xl font-bold">All Plants</h2>
|
<h2 class="text-2xl font-bold">All Plants</h2>
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<a href="{{ url_for('download_plants_template') }}" class="btn-secondary px-6 py-2 font-semibold flex items-center gap-2">
|
||||||
|
<i class="fas fa-file-download"></i> Download Template
|
||||||
|
</a>
|
||||||
|
<form id="csv-upload-form" method="POST" action="{{ url_for('upload_plants_csv') }}" enctype="multipart/form-data" class="inline">
|
||||||
|
<input type="file" name="csv_file" id="csv_file" accept=".csv" class="hidden" onchange="this.form.submit()">
|
||||||
|
<button type="button" onclick="document.getElementById('csv_file').click()" class="btn-secondary px-6 py-2 font-semibold flex items-center gap-2">
|
||||||
|
<i class="fas fa-file-import"></i> Import CSV
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
<button id="show-add-plant" class="btn-main px-6 py-2 font-semibold">Add Plant</button>
|
<button id="show-add-plant" class="btn-main px-6 py-2 font-semibold">Add Plant</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div id="add-plant-form-card" class="hidden mb-8">
|
<div id="add-plant-form-card" class="hidden mb-8">
|
||||||
<div class="bg-gray-50 rounded-lg shadow p-6 max-w-2xl mx-auto" style="overflow:visible;">
|
<div class="bg-gray-50 rounded-lg shadow p-6 max-w-2xl mx-auto" style="overflow:visible;">
|
||||||
<div class="flex justify-between items-center mb-4">
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
|||||||
Reference in New Issue
Block a user