improvement
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
**/*.pyc
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,4 +1,4 @@
|
|||||||
from flask import Blueprint, render_template, request
|
from flask import Blueprint, render_template, request, jsonify
|
||||||
from app import db
|
from app import db
|
||||||
from app.models import Folder, User
|
from app.models import Folder, User
|
||||||
import uuid
|
import uuid
|
||||||
@@ -87,4 +87,68 @@ def delete_folder(folder_id):
|
|||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
# Return the folders list unchanged
|
# Return the folders list unchanged
|
||||||
folders = Folder.query.filter_by(user_id=MOCK_USER_ID).all()
|
folders = Folder.query.filter_by(user_id=MOCK_USER_ID).all()
|
||||||
return render_template('partials/folders_list.html', folders=folders)
|
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()
|
||||||
|
|
||||||
|
# Return the updated folders list HTML
|
||||||
|
return render_template('partials/folders_list.html', folders=folders)
|
||||||
|
|
||||||
|
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
|
||||||
@@ -1,62 +1,32 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en" data-theme="cupcake">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Email Organizer - Prototype</title>
|
<title>Email Organizer - Prototype</title>
|
||||||
<script src="https://unpkg.com/htmx.org@1.9.12"></script>
|
<script src="https://unpkg.com/htmx.org@1.9.12"></script>
|
||||||
<link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css" />
|
<link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css" />
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/daisyui@5/themes.css" rel="stylesheet" type="text/css" />
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
<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 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>
|
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||||
<script>
|
|
||||||
tailwind.config = {
|
|
||||||
theme: {
|
|
||||||
extend: {
|
|
||||||
colors: {
|
|
||||||
primary: '#8B5CF6', /* Purple 500 */
|
|
||||||
secondary: '#A78BFA', /* Purple 400 */
|
|
||||||
accent: '#C4B5FD', /* Purple 300 */
|
|
||||||
background: '#0F172A', /* Slate 900 */
|
|
||||||
surface: '#1E293B', /* Slate 800 */
|
|
||||||
text: '#F1F5F9', /* Slate 100 */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
background-color: #0F172A; /* background */
|
|
||||||
color: #F1F5F9; /* text */
|
|
||||||
font-family: 'Inter', sans-serif;
|
|
||||||
}
|
|
||||||
.sidebar {
|
|
||||||
background-color: #1E293B; /* surface */
|
|
||||||
border-right: 1px solid #334155; /* slate 700 */
|
|
||||||
}
|
|
||||||
.card {
|
|
||||||
background-color: #1E293B; /* surface */
|
|
||||||
border: 1px solid #334155; /* slate 700 */
|
|
||||||
}
|
|
||||||
/* Using DaisyUI classes instead of custom CSS */
|
|
||||||
.user-dropdown {
|
|
||||||
background-color: #1E293B; /* surface */
|
|
||||||
border: 1px solid #334155; /* slate 700 */
|
|
||||||
}
|
|
||||||
.modal-box {
|
|
||||||
background-color: #1E293B; /* surface */
|
|
||||||
border: 1px solid #334155; /* slate 700 */
|
|
||||||
}
|
|
||||||
.notification-badge {
|
|
||||||
background-color: #F87171; /* red 400 */
|
|
||||||
border-radius: 50%;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
padding: 0.125rem 0.375rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body class="min-h-screen flex" x-data @open-add-folder-modal.window="document.getElementById('add-folder-modal').showModal()">
|
<body class="min-h-screen flex"
|
||||||
|
x-data="{
|
||||||
|
editFolderData: { id: null, name: '', rule_text: '', priority: 0 },
|
||||||
|
openEditFolderModal(folderData) {
|
||||||
|
console.log(folderData)
|
||||||
|
this.editFolderData = {
|
||||||
|
id: folderData.detail.id,
|
||||||
|
name: folderData.detail.name,
|
||||||
|
rule_text: folderData.detail.rule_text,
|
||||||
|
priority: folderData.detail.priority
|
||||||
|
};
|
||||||
|
document.getElementById('edit-folder-modal').showModal();
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
@open-add-folder-modal.window="document.getElementById('add-folder-modal').showModal()"
|
||||||
|
@open-edit-folder-modal.window="openEditFolderModal($event.detail)">
|
||||||
<!-- Sidebar -->
|
<!-- Sidebar -->
|
||||||
<div class="sidebar w-64 min-h-screen p-4 flex flex-col">
|
<div class="sidebar w-64 min-h-screen p-4 flex flex-col">
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
@@ -64,7 +34,7 @@
|
|||||||
<i class="fas fa-envelope mr-2"></i>
|
<i class="fas fa-envelope mr-2"></i>
|
||||||
Email Organizer
|
Email Organizer
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-secondary text-sm mt-1">AI-powered email organization</p>
|
<p class="text-sm mt-1">AI-powered email organization</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav class="flex-grow menu bg-transparent">
|
<nav class="flex-grow menu bg-transparent">
|
||||||
@@ -116,7 +86,7 @@
|
|||||||
<header class="border-b border-slate-700 p-4 flex justify-between items-center">
|
<header class="border-b border-slate-700 p-4 flex justify-between items-center">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-xl font-semibold">Folders</h2>
|
<h2 class="text-xl font-semibold">Folders</h2>
|
||||||
<p class="text-secondary text-sm">Manage your email organization rules</p>
|
<p class="text-sm">Manage your email organization rules</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center space-x-4">
|
<div class="flex items-center space-x-4">
|
||||||
@@ -149,7 +119,7 @@
|
|||||||
<div class="flex justify-between items-center mb-6">
|
<div class="flex justify-between items-center mb-6">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-2xl font-semibold">Email Folders</h2>
|
<h2 class="text-2xl font-semibold">Email Folders</h2>
|
||||||
<p class="text-secondary">Create and manage your email organization rules</p>
|
<p class="">Create and manage your email organization rules</p>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-primary" onclick="document.getElementById('add-folder-modal').showModal()">
|
<button class="btn btn-primary" onclick="document.getElementById('add-folder-modal').showModal()">
|
||||||
<i class="fas fa-plus mr-2"></i>
|
<i class="fas fa-plus mr-2"></i>
|
||||||
@@ -192,5 +162,40 @@
|
|||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
|
<!-- Edit Folder Modal -->
|
||||||
|
<dialog id="edit-folder-modal" class="modal">
|
||||||
|
<div class="modal-box">
|
||||||
|
<h3 class="font-bold text-lg mb-4">Edit Folder</h3>
|
||||||
|
<p x-text="`/api/folders/${editFolderData.id}`" />
|
||||||
|
<form id="edit-folder-form"
|
||||||
|
:hx-put="`/api/folders/${editFolderData.id}`"
|
||||||
|
x-effect="editFolderData.id; htmx.process($el)"
|
||||||
|
hx-target="#folders-list"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-on:htmx:after-request="document.getElementById('edit-folder-modal').close()">
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="edit-folder-name" class="block text-sm font-medium mb-1">Name</label>
|
||||||
|
<input type="text" id="edit-folder-name" name="name" class="input input-bordered w-full" placeholder="e.g., Work, Personal, Newsletters" required x-model="editFolderData.name">
|
||||||
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="edit-folder-rule" class="block text-sm font-medium mb-1">Rule (Natural Language)</label>
|
||||||
|
<textarea id="edit-folder-rule" name="rule_text" class="textarea textarea-bordered w-full h-24" placeholder="e.g., Move emails from 'newsletter@company.com' to this folder" required x-model="editFolderData.rule_text"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="edit-folder-priority" class="block text-sm font-medium mb-1">Priority</label>
|
||||||
|
<select id="edit-folder-priority" name="priority" class="select select-bordered w-full" x-model="editFolderData.priority">
|
||||||
|
<option value="1" :selected="editFolderData.priority == 1">High</option>
|
||||||
|
<option value="0" :selected="editFolderData.priority == 0">Normal</option>
|
||||||
|
<option value="-1" :selected="editFolderData.priority == -1">Low</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="modal-action">
|
||||||
|
<button type="button" class="btn btn-outline" onclick="document.getElementById('edit-folder-modal').close()">Cancel</button>
|
||||||
|
<button type="submit" class="btn btn-primary">Update Folder</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,13 +1,23 @@
|
|||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
{% for folder in folders %}
|
{% for folder in folders %}
|
||||||
<div class="card rounded-lg p-6 shadow-lg">
|
<div class="card bg-base-100 shadow-sm">
|
||||||
<div class="flex justify-between items-start mb-4">
|
<div class="card-body">
|
||||||
<h3 class="text-xl font-bold">{{ folder.name }}</h3>
|
<div class="flex justify-between items-start mb-2">
|
||||||
<div class="flex space-x-2">
|
<h3 class="text-xl font-bold">{{ folder.name }}</h3>
|
||||||
<button class="p-2 rounded-full hover:bg-slate-700">
|
<div class="flex space-x-2">
|
||||||
|
<button class="btn"
|
||||||
|
@click="$dispatch('open-edit-folder-modal', {
|
||||||
|
detail: {
|
||||||
|
id: '{{ folder.id }}',
|
||||||
|
name: '{{ folder.name }}',
|
||||||
|
rule_text: '{{ folder.rule_text }}',
|
||||||
|
priority: '{{ folder.priority or 0 }}'
|
||||||
|
}
|
||||||
|
})">
|
||||||
|
Edit
|
||||||
<i class="fas fa-edit"></i>
|
<i class="fas fa-edit"></i>
|
||||||
</button>
|
</button>
|
||||||
<button class="p-2 rounded-full hover:bg-slate-700 text-red-400"
|
<button class="text-red-400 btn"
|
||||||
hx-delete="/api/folders/{{ folder.id }}"
|
hx-delete="/api/folders/{{ folder.id }}"
|
||||||
hx-target="#folders-list"
|
hx-target="#folders-list"
|
||||||
hx-swap="innerHTML"
|
hx-swap="innerHTML"
|
||||||
@@ -15,20 +25,24 @@
|
|||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-secondary mb-4">{{ folder.rule_text }}</p>
|
|
||||||
|
<ul class="list bg-base-200 rounded">
|
||||||
|
<li class="list-row">{{ folder.rule_text }}</li>
|
||||||
|
</ul>
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<span class="text-xs px-2 py-1 rounded-full bg-slate-700">Priority: {{ folder.priority or 'Normal' }}</span>
|
<span class="badge badge-primary">Priority: {{ folder.priority or 'Normal' }}</span>
|
||||||
<span class="text-xs text-secondary">0 emails</span>
|
<span class="text-xs badge badge-secondary">0 emails</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="col-span-full text-center py-12">
|
<div class="col-span-full text-center py-12">
|
||||||
<div class="text-5xl mb-4 text-secondary">
|
<div class="text-5xl mb-4">
|
||||||
<i class="fas fa-folder-open"></i>
|
<i class="fas fa-folder-open"></i>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-xl font-semibold mb-2">No folders yet</h3>
|
<h3 class="text-xl font-semibold mb-2">No folders yet</h3>
|
||||||
<p class="text-secondary mb-4">Add your first folder to get started organizing your emails.</p>
|
<p class="mb-4">Add your first folder to get started organizing your emails.</p>
|
||||||
<button class="btn btn-primary" @click="$dispatch('open-add-folder-modal')">
|
<button class="btn btn-primary" @click="$dispatch('open-add-folder-modal')">
|
||||||
<i class="fas fa-plus mr-2"></i>
|
<i class="fas fa-plus mr-2"></i>
|
||||||
Create Folder
|
Create Folder
|
||||||
|
|||||||
75
test_edit_functionality.py
Normal file
75
test_edit_functionality.py
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import unittest
|
||||||
|
from app import create_app, db
|
||||||
|
from app.models import Folder, User
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
class EditFolderTestCase(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.app = create_app()
|
||||||
|
self.app.config['TESTING'] = True
|
||||||
|
self.app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:'
|
||||||
|
self.client = self.app.test_client()
|
||||||
|
|
||||||
|
with self.app.app_context():
|
||||||
|
db.create_all()
|
||||||
|
|
||||||
|
# Create a test user
|
||||||
|
self.user_id = '123e4567-e89b-12d3-a456-426614174000'
|
||||||
|
user = User(id=self.user_id, email='test@example.com')
|
||||||
|
db.session.add(user)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# Create a test folder
|
||||||
|
self.folder_id = str(uuid.uuid4())
|
||||||
|
folder = Folder(
|
||||||
|
id=self.folder_id,
|
||||||
|
user_id=self.user_id,
|
||||||
|
name='Test Folder',
|
||||||
|
rule_text='Move all emails to this folder',
|
||||||
|
priority=0
|
||||||
|
)
|
||||||
|
db.session.add(folder)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
with self.app.app_context():
|
||||||
|
db.session.remove()
|
||||||
|
db.drop_all()
|
||||||
|
|
||||||
|
def test_get_folder(self):
|
||||||
|
with self.app.app_context():
|
||||||
|
response = self.client.get(f'/api/folders/{self.folder_id}')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
data = response.get_json()
|
||||||
|
self.assertEqual(data['name'], 'Test Folder')
|
||||||
|
self.assertEqual(data['rule_text'], 'Move all emails to this folder')
|
||||||
|
self.assertEqual(data['priority'], 0)
|
||||||
|
|
||||||
|
def test_update_folder(self):
|
||||||
|
with self.app.app_context():
|
||||||
|
# Update the folder
|
||||||
|
update_data = {
|
||||||
|
'name': 'Updated Folder',
|
||||||
|
'rule_text': 'Move important emails here',
|
||||||
|
'priority': '1'
|
||||||
|
}
|
||||||
|
|
||||||
|
response = self.client.put(
|
||||||
|
f'/api/folders/{self.folder_id}',
|
||||||
|
data=update_data,
|
||||||
|
content_type='application/x-www-form-urlencoded'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check that the response is successful
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
# Verify the folder was updated in the database
|
||||||
|
folder = Folder.query.filter_by(id=self.folder_id).first()
|
||||||
|
self.assertIsNotNone(folder)
|
||||||
|
self.assertEqual(folder.name, 'Updated Folder')
|
||||||
|
self.assertEqual(folder.rule_text, 'Move important emails here')
|
||||||
|
self.assertEqual(folder.priority, 1)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user