works
This commit is contained in:
@@ -1,14 +1,17 @@
|
||||
from flask import Flask
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from config import config
|
||||
from app.models import db, Base
|
||||
from flask_migrate import Migrate
|
||||
|
||||
|
||||
db = SQLAlchemy()
|
||||
|
||||
def create_app(config_name='default'):
|
||||
app = Flask(__name__)
|
||||
app.config.from_object(config[config_name])
|
||||
|
||||
db.init_app(app)
|
||||
migrate = Migrate(app, db)
|
||||
|
||||
from app.routes import main
|
||||
app.register_blueprint(main)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,19 +1,24 @@
|
||||
from app import db
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import declarative_base
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
|
||||
import uuid
|
||||
|
||||
class User(db.Model):
|
||||
Base = declarative_base()
|
||||
db = SQLAlchemy(model_class=Base)
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = 'users'
|
||||
id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
email = db.Column(db.String(255), unique=True, nullable=False)
|
||||
# Placeholders for Milestone 1
|
||||
password_hash = db.Column(db.LargeBinary)
|
||||
imap_config = db.Column(db.JSON) # Using db.JSON instead of db.JSONB for compatibility
|
||||
|
||||
class Folder(db.Model):
|
||||
class Folder(Base):
|
||||
__tablename__ = 'folders'
|
||||
id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
user_id = db.Column(UUID(as_uuid=True), db.ForeignKey('users.id'), nullable=False)
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||
name = db.Column(db.String(255), nullable=False)
|
||||
rule_text = db.Column(db.Text)
|
||||
priority = db.Column(db.Integer)
|
||||
|
||||
191
app/routes.py
191
app/routes.py
@@ -7,14 +7,7 @@ import logging
|
||||
main = Blueprint('main', __name__)
|
||||
|
||||
# For prototype, use a fixed user ID
|
||||
MOCK_USER_ID = '123e4567-e89b-12d3-a456-426614174000'
|
||||
|
||||
@main.route('/api/folders/new', methods=['GET'])
|
||||
def new_folder_modal():
|
||||
# Return the add folder modal
|
||||
response = make_response(render_template('partials/folder_modal.html'))
|
||||
response.headers['HX-Trigger'] = 'open-modal'
|
||||
return response
|
||||
MOCK_USER_ID = 1
|
||||
|
||||
@main.route('/')
|
||||
def index():
|
||||
@@ -28,6 +21,13 @@ def index():
|
||||
folders = Folder.query.filter_by(user_id=MOCK_USER_ID).all()
|
||||
return render_template('index.html', folders=folders)
|
||||
|
||||
@main.route('/api/folders/new', methods=['GET'])
|
||||
def new_folder_modal():
|
||||
# Return the add folder modal
|
||||
response = make_response(render_template('partials/folder_modal.html'))
|
||||
response.headers['HX-Trigger'] = 'open-modal'
|
||||
return response
|
||||
|
||||
@main.route('/api/folders', methods=['POST'])
|
||||
def add_folder():
|
||||
try:
|
||||
@@ -36,18 +36,35 @@ def add_folder():
|
||||
rule_text = request.form.get('rule_text')
|
||||
priority = request.form.get('priority')
|
||||
|
||||
if not name:
|
||||
# Return the folders list unchanged with an error message
|
||||
folders = Folder.query.filter_by(user_id=MOCK_USER_ID).all()
|
||||
# We'll add error handling in the template
|
||||
return render_template('partials/folders_list.html', folders=folders)
|
||||
# Server-side validation
|
||||
errors = {}
|
||||
if not name or not name.strip():
|
||||
errors['name'] = 'Folder name is required'
|
||||
elif len(name.strip()) < 3:
|
||||
errors['name'] = 'Folder name must be at least 3 characters'
|
||||
elif len(name.strip()) > 50:
|
||||
errors['name'] = 'Folder name must be less than 50 characters'
|
||||
|
||||
if not rule_text or not rule_text.strip():
|
||||
errors['rule_text'] = 'Rule text is required'
|
||||
elif len(rule_text.strip()) < 10:
|
||||
errors['rule_text'] = 'Rule text must be at least 10 characters'
|
||||
elif len(rule_text.strip()) > 200:
|
||||
errors['rule_text'] = 'Rule text must be less than 200 characters'
|
||||
|
||||
# If there are validation errors, return the modal with errors
|
||||
if errors:
|
||||
response = make_response(render_template('partials/folder_modal.html', errors=errors, name=name, rule_text=rule_text, priority=priority))
|
||||
response.headers['HX-Retarget'] = '#folder-modal'
|
||||
response.headers['HX-Reswap'] = 'outerHTML'
|
||||
return response
|
||||
|
||||
# Create new folder
|
||||
folder = Folder(
|
||||
user_id=MOCK_USER_ID,
|
||||
name=name,
|
||||
rule_text=rule_text,
|
||||
priority=int(priority) if priority else None
|
||||
name=name.strip(),
|
||||
rule_text=rule_text.strip(),
|
||||
priority=int(priority) if priority else 0
|
||||
)
|
||||
|
||||
db.session.add(folder)
|
||||
@@ -57,18 +74,21 @@ def add_folder():
|
||||
folders = Folder.query.filter_by(user_id=MOCK_USER_ID).all()
|
||||
|
||||
# Return the updated folders list HTML
|
||||
|
||||
response = make_response(render_template('partials/folders_list.html', folders=folders))
|
||||
response.headers['HX-Trigger'] = 'close-modal'
|
||||
response.status_code = 201
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
# Print unhandled exceptions to the console as required
|
||||
logging.exception("Error adding folder: %s", e)
|
||||
db.session.rollback()
|
||||
# Return the folders list unchanged
|
||||
folders = Folder.query.filter_by(user_id=MOCK_USER_ID).all()
|
||||
return render_template('partials/folders_list.html', folders=folders)
|
||||
# Return error in modal
|
||||
errors = {'general': 'An unexpected error occurred. Please try again.'}
|
||||
response = make_response(render_template('partials/folder_modal.html', errors=errors, name=name, rule_text=rule_text, priority=priority))
|
||||
response.headers['HX-Retarget'] = '#folder-modal'
|
||||
response.headers['HX-Reswap'] = 'outerHTML'
|
||||
return response
|
||||
|
||||
@main.route('/api/folders/<folder_id>', methods=['DELETE'])
|
||||
def delete_folder(folder_id):
|
||||
@@ -99,72 +119,6 @@ def delete_folder(folder_id):
|
||||
folders = Folder.query.filter_by(user_id=MOCK_USER_ID).all()
|
||||
return render_template('partials/folders_list.html', folders=folders)
|
||||
|
||||
@main.route('/api/folders/<folder_id>', methods=['PUT'])
|
||||
def update_folder(folder_id):
|
||||
try:
|
||||
# Find the folder by ID
|
||||
folder = Folder.query.filter_by(id=folder_id, user_id=MOCK_USER_ID).first()
|
||||
|
||||
if not folder:
|
||||
# Folder not found
|
||||
folders = Folder.query.filter_by(user_id=MOCK_USER_ID).all()
|
||||
return render_template('partials/folders_list.html', folders=folders)
|
||||
|
||||
# Get form data
|
||||
name = request.form.get('name')
|
||||
rule_text = request.form.get('rule_text')
|
||||
priority = request.form.get('priority')
|
||||
|
||||
if not name:
|
||||
# Return the folders list unchanged with an error message
|
||||
folders = Folder.query.filter_by(user_id=MOCK_USER_ID).all()
|
||||
return render_template('partials/folders_list.html', folders=folders)
|
||||
|
||||
# Update folder
|
||||
folder.name = name
|
||||
folder.rule_text = rule_text
|
||||
folder.priority = int(priority) if priority else None
|
||||
|
||||
db.session.commit()
|
||||
|
||||
# Get updated list of folders
|
||||
folders = Folder.query.filter_by(user_id=MOCK_USER_ID).all()
|
||||
|
||||
response = make_response(render_template('partials/folders_list.html', folders=folders))
|
||||
response.headers['HX-Trigger'] = 'close-modal'
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
# Print unhandled exceptions to the console as required
|
||||
logging.exception("Error updating folder: %s", e)
|
||||
db.session.rollback()
|
||||
# Return the folders list unchanged
|
||||
folders = Folder.query.filter_by(user_id=MOCK_USER_ID).all()
|
||||
return render_template('partials/folders_list.html', folders=folders)
|
||||
|
||||
@main.route('/api/folders/<folder_id>', methods=['GET'])
|
||||
def get_folder(folder_id):
|
||||
try:
|
||||
# Find the folder by ID
|
||||
folder = Folder.query.filter_by(id=folder_id, user_id=MOCK_USER_ID).first()
|
||||
|
||||
if not folder:
|
||||
return jsonify({'error': 'Folder not found'}), 404
|
||||
|
||||
# Return folder data as JSON
|
||||
return jsonify({
|
||||
'id': folder.id,
|
||||
'name': folder.name,
|
||||
'rule_text': folder.rule_text,
|
||||
'priority': folder.priority
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
# Print unhandled exceptions to the console as required
|
||||
logging.exception("Error getting folder: %s", e)
|
||||
return jsonify({'error': 'Error retrieving folder'}), 500
|
||||
|
||||
|
||||
@main.route('/api/folders/<folder_id>/edit', methods=['GET'])
|
||||
def edit_folder_modal(folder_id):
|
||||
try:
|
||||
@@ -183,3 +137,68 @@ def edit_folder_modal(folder_id):
|
||||
# Print unhandled exceptions to the console as required
|
||||
logging.exception("Error getting folder for edit: %s", e)
|
||||
return jsonify({'error': 'Error retrieving folder'}), 500
|
||||
|
||||
@main.route('/api/folders/<folder_id>', methods=['PUT'])
|
||||
def update_folder(folder_id):
|
||||
try:
|
||||
# Find the folder by ID
|
||||
folder = Folder.query.filter_by(id=folder_id, user_id=MOCK_USER_ID).first()
|
||||
|
||||
if not folder:
|
||||
# Folder not found
|
||||
folders = Folder.query.filter_by(user_id=MOCK_USER_ID).all()
|
||||
return render_template('partials/folders_list.html', folders=folders)
|
||||
|
||||
# Get form data
|
||||
name = request.form.get('name')
|
||||
rule_text = request.form.get('rule_text')
|
||||
priority = request.form.get('priority')
|
||||
|
||||
# Server-side validation
|
||||
errors = {}
|
||||
if not name or not name.strip():
|
||||
errors['name'] = 'Folder name is required'
|
||||
elif len(name.strip()) < 3:
|
||||
errors['name'] = 'Folder name must be at least 3 characters'
|
||||
elif len(name.strip()) > 50:
|
||||
errors['name'] = 'Folder name must be less than 50 characters'
|
||||
|
||||
if not rule_text or not rule_text.strip():
|
||||
errors['rule_text'] = 'Rule text is required'
|
||||
elif len(rule_text.strip()) < 10:
|
||||
errors['rule_text'] = 'Rule text must be at least 10 characters'
|
||||
elif len(rule_text.strip()) > 200:
|
||||
errors['rule_text'] = 'Rule text must be less than 200 characters'
|
||||
|
||||
# If there are validation errors, return the modal with errors
|
||||
if errors:
|
||||
response = make_response(render_template('partials/folder_modal.html', folder=folder, errors=errors, name=name, rule_text=rule_text, priority=priority))
|
||||
response.headers['HX-Retarget'] = '#folder-modal'
|
||||
response.headers['HX-Reswap'] = 'outerHTML'
|
||||
return response
|
||||
|
||||
# Update folder
|
||||
folder.name = name.strip()
|
||||
folder.rule_text = rule_text.strip()
|
||||
folder.priority = int(priority) if priority else 0
|
||||
|
||||
db.session.commit()
|
||||
|
||||
# Get updated list of folders
|
||||
folders = Folder.query.filter_by(user_id=MOCK_USER_ID).all()
|
||||
|
||||
response = make_response(render_template('partials/folders_list.html', folders=folders))
|
||||
response.headers['HX-Trigger'] = 'close-modal'
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
# Print unhandled exceptions to the console as required
|
||||
logging.exception("Error updating folder: %s", e)
|
||||
db.session.rollback()
|
||||
# Return error in modal
|
||||
errors = {'general': 'An unexpected error occurred. Please try again.'}
|
||||
response = make_response(render_template('partials/folder_modal.html', folder=folder, errors=errors, name=name, rule_text=rule_text, priority=priority))
|
||||
response.headers['HX-Retarget'] = '#folder-modal'
|
||||
response.headers['HX-Reswap'] = 'outerHTML'
|
||||
return response
|
||||
|
||||
|
||||
@@ -10,11 +10,21 @@
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
<style>
|
||||
@keyframes shake {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); }
|
||||
20%, 40%, 60%, 80% { transform: translateX(5px); }
|
||||
}
|
||||
.shake {
|
||||
animation: shake 0.5s cubic-bezier(.36,.07,.19,.97) both;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="min-h-screen flex bg-base-200"
|
||||
x-data "{}"
|
||||
x-on:open-modal.window="$refs.modal.showModal()"
|
||||
x-on:close-modal.window="$refs.modal.close()">
|
||||
x-data="{}"
|
||||
x-on:open-modal.window="$refs.modal.showModal()"
|
||||
x-on:close-modal.window="$refs.modal.close()">
|
||||
<!-- Sidebar -->
|
||||
<div class="sidebar w-64 min-h-screen p-4 flex flex-col bg-base-100 shadow-lg">
|
||||
<div class="mb-8">
|
||||
|
||||
@@ -1,39 +1,53 @@
|
||||
<div id="folder-modal" @click.away="$refs.modal.close()" class="modal-box">
|
||||
<div id="folder-modal" @click.away="$refs.modal.close()" class="modal-box" x-data="{ errors: {{ 'true' if errors else 'false' }} }" x-init="$nextTick(() => { if (errors) { document.querySelector('#submit-btn').classList.add('shake'); } })">
|
||||
<h3 class="font-bold text-lg mb-4" id="modal-title">
|
||||
{% if folder %}Edit Folder{% else %}Add New Folder{% endif %}
|
||||
</h3>
|
||||
<form id="folder-form" {% if folder %} hx-put="/api/folders/{{ folder.id }}" {% else %} hx-post="/api/folders" {%
|
||||
endif %} hx-target="#folders-list" hx-swap="innerHTML">
|
||||
|
||||
{% if errors and errors.general %}
|
||||
<div class="alert alert-error mb-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
||||
<span>{{ errors.general }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form id="folder-form" {% if folder %} hx-put="/api/folders/{{ folder.id }}" {% else %} hx-post="/api/folders" {% endif %} hx-target="#folders-list" hx-swap="innerHTML">
|
||||
{% if folder %}
|
||||
<input type="hidden" id="folder-id" name="id" value="{{ folder.id }}">
|
||||
{% endif %}
|
||||
<div class="mb-4">
|
||||
<label for="folder-name" class="block text-sm font-medium mb-1">Name</label>
|
||||
<input type="text" id="folder-name" name="name" class="input input-bordered w-full"
|
||||
<input type="text" id="folder-name" name="name"
|
||||
class="input input-bordered w-full {% if errors and errors.name %}input-error{% endif %}"
|
||||
placeholder="e.g., Work, Personal, Newsletters" required
|
||||
value="{% if folder %}{{ folder.name }}{% endif %}">
|
||||
value="{% if name is defined %}{{ name }}{% elif folder %}{{ folder.name }}{% endif %}">
|
||||
{% if errors and errors.name %}
|
||||
<div class="text-error text-sm mt-1">{{ errors.name }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="folder-rule" class="block text-sm font-medium mb-1">Rule (Natural Language)</label>
|
||||
<textarea id="folder-rule" name="rule_text" class="textarea textarea-bordered w-full h-24"
|
||||
<textarea id="folder-rule" name="rule_text"
|
||||
class="textarea textarea-bordered w-full h-24 {% if errors and errors.rule_text %}textarea-error{% endif %}"
|
||||
placeholder="e.g., Move emails from 'newsletter@company.com' to this folder"
|
||||
required>{% if folder %}{{ folder.rule_text }}{% endif %}</textarea>
|
||||
required>{% if rule_text is defined %}{{ rule_text }}{% elif folder %}{{ folder.rule_text }}{% endif %}</textarea>
|
||||
{% if errors and errors.rule_text %}
|
||||
<div class="text-error text-sm mt-1">{{ errors.rule_text }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="folder-priority" class="block text-sm font-medium mb-1">Priority</label>
|
||||
<select id="folder-priority" name="priority" class="select select-bordered w-full">
|
||||
<option value="1" {% if folder and folder.priority==1 %}selected{% endif %}>High</option>
|
||||
<option value="0" {% if not folder or (folder and folder.priority==0) %}selected{% endif %}>Normal
|
||||
</option>
|
||||
<option value="-1" {% if folder and folder.priority==-1 %}selected{% endif %}>Low</option>
|
||||
<option value="1" {% if (priority is defined and priority == '1') or (folder and folder.priority==1) %}selected{% endif %}>High</option>
|
||||
<option value="0" {% if (priority is defined and priority == '0') or (not folder) or (folder and folder.priority==0) %}selected{% endif %}>Normal</option>
|
||||
<option value="-1" {% if (priority is defined and priority == '-1') or (folder and folder.priority==-1) %}selected{% endif %}>Low</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="modal-action">
|
||||
<button type="button" class="btn btn-outline"
|
||||
@click="$dispatch('close-modal')">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary" id="submit-btn">
|
||||
<button type="submit" class="btn btn-primary" id="submit-btn" :class="{ 'shake': errors }">
|
||||
{% if folder %}Update Folder{% else %}Add Folder{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user