Inquiry closing
This commit is contained in:
parent
b8b8d9f2ca
commit
2657849748
11 changed files with 351 additions and 25 deletions
52
README.md
52
README.md
|
|
@ -15,7 +15,7 @@ AnonChat was created using "vibe coding" - a programming approach where develope
|
||||||
|
|
||||||
Rest assured though, I know what I'm (or the AI is) doing. Here's what would happen if I didn't:
|
Rest assured though, I know what I'm (or the AI is) doing. Here's what would happen if I didn't:
|
||||||
1. [my saas was built with Cursor, zero hand written code \
|
1. [my saas was built with Cursor, zero hand written code \
|
||||||
AI is no longer just an assistant, it’s also the builder \
|
AI is no longer just an assistant, it's also the builder \
|
||||||
Now, you can continue to whine about it or start building.](https://xcancel.com/leojr94_/status/1900767509621674109)
|
Now, you can continue to whine about it or start building.](https://xcancel.com/leojr94_/status/1900767509621674109)
|
||||||
2. [random thing are happening, maxed out usage on api keys, people bypassing the subscription, creating random shit on db \
|
2. [random thing are happening, maxed out usage on api keys, people bypassing the subscription, creating random shit on db \
|
||||||
there are just some weird ppl out there](https://xcancel.com/leojr94_/status/1901560276488511759)
|
there are just some weird ppl out there](https://xcancel.com/leojr94_/status/1901560276488511759)
|
||||||
|
|
@ -83,6 +83,49 @@ This project uses Poetry for dependency management.
|
||||||
- Run tests: `poetry run pytest`
|
- Run tests: `poetry run pytest`
|
||||||
- Run the application: `poetry run start`
|
- Run the application: `poetry run start`
|
||||||
|
|
||||||
|
### Database Migrations
|
||||||
|
|
||||||
|
AnonChat includes a custom database migration system to handle schema changes. When you make changes to the database models, you should create a migration script to apply these changes to existing databases.
|
||||||
|
|
||||||
|
#### Running Migrations
|
||||||
|
|
||||||
|
- Run all pending migrations: `poetry run flask --app src/anonchat run-migrations`
|
||||||
|
- The migrations are also automatically run when using the `init-db` command or when starting the application with the entrypoint script.
|
||||||
|
|
||||||
|
#### Creating New Migrations
|
||||||
|
|
||||||
|
To create a new migration:
|
||||||
|
|
||||||
|
1. Create a new Python file in the `src/anonchat/migrations` directory with a descriptive name (e.g., `add_new_column.py`)
|
||||||
|
2. Implement a `run_migration(db)` function that performs the necessary schema changes
|
||||||
|
3. The migration script should be idempotent (safe to run multiple times)
|
||||||
|
|
||||||
|
Example migration script:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from sqlalchemy import inspect
|
||||||
|
from sqlalchemy.sql import text
|
||||||
|
from flask import current_app
|
||||||
|
|
||||||
|
def run_migration(db):
|
||||||
|
"""Add a new column to a table."""
|
||||||
|
# Check if the column already exists
|
||||||
|
inspector = inspect(db.engine)
|
||||||
|
columns = [col['name'] for col in inspector.get_columns('your_table')]
|
||||||
|
|
||||||
|
# Only apply changes if needed
|
||||||
|
if 'your_new_column' not in columns:
|
||||||
|
current_app.logger.info("Adding new column to table")
|
||||||
|
with db.engine.connect() as conn:
|
||||||
|
conn.execute(text("ALTER TABLE your_table ADD COLUMN your_new_column TEXT"))
|
||||||
|
conn.commit()
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False # Return True if changes were made, False otherwise
|
||||||
|
```
|
||||||
|
|
||||||
|
Migrations are run in alphabetical order, so you may want to prefix migration filenames with a timestamp or sequence number for more complex projects.
|
||||||
|
|
||||||
## Admin Authentication
|
## Admin Authentication
|
||||||
|
|
||||||
AnonChat includes a secure admin authentication system that protects administrative routes and functions. This ensures that only authorized users can access the admin dashboard, manage inquiries, and configure system settings.
|
AnonChat includes a secure admin authentication system that protects administrative routes and functions. This ensures that only authorized users can access the admin dashboard, manage inquiries, and configure system settings.
|
||||||
|
|
@ -148,13 +191,6 @@ The following security enhancements are planned for future releases:
|
||||||
- Support for custom OAuth providers for enterprise deployments
|
- Support for custom OAuth providers for enterprise deployments
|
||||||
- Add multi-factor authentication options
|
- Add multi-factor authentication options
|
||||||
|
|
||||||
### Inquiry Management
|
|
||||||
- [ ] Add "Close Inquiry" functionality
|
|
||||||
- Mark inquiries as closed without immediate deletion
|
|
||||||
- Automatically delete closed inquiries after 2 days
|
|
||||||
- Allow reopening inquiries before deletion occurs
|
|
||||||
- Provide visual indicators for closed inquiries in admin interface
|
|
||||||
|
|
||||||
### Read-Only Links
|
### Read-Only Links
|
||||||
- [ ] Implement read-only sharing links for inquiries
|
- [ ] Implement read-only sharing links for inquiries
|
||||||
- Generate unique, cryptographically secure sharing links
|
- Generate unique, cryptographically secure sharing links
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,11 @@ echo "Running database initialization..."
|
||||||
poetry run flask --app src/anonchat init-db
|
poetry run flask --app src/anonchat init-db
|
||||||
echo "Database initialization complete."
|
echo "Database initialization complete."
|
||||||
|
|
||||||
|
# Run database migrations to ensure schema is up to date
|
||||||
|
echo "Running database migrations..."
|
||||||
|
poetry run flask --app src/anonchat run-migrations
|
||||||
|
echo "Database migrations complete."
|
||||||
|
|
||||||
# Execute the command passed as arguments (CMD in Dockerfile)
|
# Execute the command passed as arguments (CMD in Dockerfile)
|
||||||
echo "Executing command: $@"
|
echo "Executing command: $@"
|
||||||
exec "$@"
|
exec "$@"
|
||||||
|
|
@ -19,7 +19,8 @@ dependencies = [
|
||||||
"psycopg2-binary (>=2.9.9,<3.0.0)",
|
"psycopg2-binary (>=2.9.9,<3.0.0)",
|
||||||
"redis (>=5.0.0,<6.0.0)",
|
"redis (>=5.0.0,<6.0.0)",
|
||||||
"flask-session (>=0.5.0,<1.0.0)",
|
"flask-session (>=0.5.0,<1.0.0)",
|
||||||
"argon2-cffi (>=23.0.0,<24.0.0)"
|
"argon2-cffi (>=23.0.0,<24.0.0)",
|
||||||
|
"flask-apscheduler (>=1.13.1,<2.0.0)"
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ from flask_session import Session
|
||||||
import redis
|
import redis
|
||||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||||
from argon2 import PasswordHasher
|
from argon2 import PasswordHasher
|
||||||
|
from flask_apscheduler import APScheduler
|
||||||
|
|
||||||
# Load environment variables from .env file
|
# Load environment variables from .env file
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
@ -46,6 +47,10 @@ elif app.config['SESSION_TYPE'] == 'filesystem':
|
||||||
app.config['SESSION_FILE_MODE'] = os.environ.get('SESSION_FILE_MODE', 384)
|
app.config['SESSION_FILE_MODE'] = os.environ.get('SESSION_FILE_MODE', 384)
|
||||||
app.config['SESSION_KEY_PREFIX'] = 'anonchat_session:'
|
app.config['SESSION_KEY_PREFIX'] = 'anonchat_session:'
|
||||||
|
|
||||||
|
# Scheduler configuration
|
||||||
|
app.config['SCHEDULER_API_ENABLED'] = False
|
||||||
|
app.config['SCHEDULER_TIMEZONE'] = 'UTC'
|
||||||
|
|
||||||
if app.config['BEHIND_PROXY']:
|
if app.config['BEHIND_PROXY']:
|
||||||
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=2)
|
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=2)
|
||||||
|
|
||||||
|
|
@ -69,6 +74,10 @@ limiter = Limiter(get_remote_address, app=app)
|
||||||
# Initialize database
|
# Initialize database
|
||||||
db = SQLAlchemy(app)
|
db = SQLAlchemy(app)
|
||||||
|
|
||||||
|
# Initialize scheduler
|
||||||
|
scheduler = APScheduler()
|
||||||
|
scheduler.init_app(app)
|
||||||
|
|
||||||
# Import models
|
# Import models
|
||||||
from . import models
|
from . import models
|
||||||
# Explicitly import all models to ensure they are registered with SQLAlchemy
|
# Explicitly import all models to ensure they are registered with SQLAlchemy
|
||||||
|
|
@ -104,6 +113,37 @@ def ratelimit_handler(e):
|
||||||
# Import routes
|
# Import routes
|
||||||
from . import routes
|
from . import routes
|
||||||
|
|
||||||
|
# Setup scheduler jobs
|
||||||
|
def setup_scheduler_jobs():
|
||||||
|
# Import the function from tasks.py
|
||||||
|
from .tasks import check_and_delete_expired_inquiries
|
||||||
|
|
||||||
|
# Schedule the job to run every 6 hours
|
||||||
|
scheduler.add_job(
|
||||||
|
id='check_and_delete_expired_inquiries',
|
||||||
|
func=check_and_delete_expired_inquiries,
|
||||||
|
trigger='interval',
|
||||||
|
hours=6
|
||||||
|
)
|
||||||
|
|
||||||
|
# Start the scheduler
|
||||||
|
scheduler.start()
|
||||||
|
|
||||||
|
# Start the scheduler when the app is initialized
|
||||||
|
setup_scheduler_jobs()
|
||||||
|
|
||||||
|
# Flask CLI command to run migrations
|
||||||
|
@app.cli.command("run-migrations")
|
||||||
|
def run_migrations_command():
|
||||||
|
"""Run database migrations to update schema."""
|
||||||
|
with app.app_context():
|
||||||
|
# Import migrations module here to avoid circular imports
|
||||||
|
from .migrations import run_migrations
|
||||||
|
|
||||||
|
print("Running database migrations...")
|
||||||
|
migrations_run = run_migrations(db)
|
||||||
|
print(f"Database migrations completed: {migrations_run} migrations applied.")
|
||||||
|
|
||||||
# Flask CLI command to initialize the database
|
# Flask CLI command to initialize the database
|
||||||
@app.cli.command("init-db")
|
@app.cli.command("init-db")
|
||||||
def init_db_command():
|
def init_db_command():
|
||||||
|
|
@ -113,6 +153,13 @@ def init_db_command():
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
print("Creating database tables...")
|
print("Creating database tables...")
|
||||||
db.create_all() # Should now create all tables including Inquiry
|
db.create_all() # Should now create all tables including Inquiry
|
||||||
|
|
||||||
|
# Run migrations after creating tables
|
||||||
|
# This ensures any new columns added to existing tables are properly added
|
||||||
|
print("Running database migrations...")
|
||||||
|
from .migrations import run_migrations
|
||||||
|
run_migrations(db)
|
||||||
|
|
||||||
print("Initializing default settings...")
|
print("Initializing default settings...")
|
||||||
# Initialize settings if they don't exist
|
# Initialize settings if they don't exist
|
||||||
# Note: Models already imported above
|
# Note: Models already imported above
|
||||||
|
|
|
||||||
31
src/anonchat/migrations/__init__.py
Normal file
31
src/anonchat/migrations/__init__.py
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
from flask import current_app
|
||||||
|
from importlib import import_module
|
||||||
|
import os
|
||||||
|
import pkgutil
|
||||||
|
|
||||||
|
def run_migrations(db):
|
||||||
|
"""Run all migrations in the migrations directory"""
|
||||||
|
current_app.logger.info("Starting database migrations")
|
||||||
|
migrations_run = 0
|
||||||
|
|
||||||
|
# Get the list of all migration modules in this package
|
||||||
|
# This will look for all Python modules in the migrations directory
|
||||||
|
migrations_pkg = __name__
|
||||||
|
for _, name, ispkg in pkgutil.iter_modules([os.path.dirname(__file__)]):
|
||||||
|
if name.startswith('_') or ispkg:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Import the migration module
|
||||||
|
current_app.logger.info(f"Importing migration: {name}")
|
||||||
|
migration_module = import_module(f"{migrations_pkg}.{name}")
|
||||||
|
|
||||||
|
# Run the migration if it has a run_migration function
|
||||||
|
if hasattr(migration_module, 'run_migration'):
|
||||||
|
current_app.logger.info(f"Running migration: {name}")
|
||||||
|
if migration_module.run_migration(db):
|
||||||
|
migrations_run += 1
|
||||||
|
else:
|
||||||
|
current_app.logger.warning(f"Migration {name} has no run_migration function")
|
||||||
|
|
||||||
|
current_app.logger.info(f"Database migrations completed. {migrations_run} migrations applied.")
|
||||||
|
return migrations_run
|
||||||
43
src/anonchat/migrations/add_closed_status.py
Normal file
43
src/anonchat/migrations/add_closed_status.py
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
from sqlalchemy import Column, Boolean, DateTime, inspect
|
||||||
|
from sqlalchemy.sql import text
|
||||||
|
from flask import current_app
|
||||||
|
|
||||||
|
def run_migration(db):
|
||||||
|
"""Add is_closed and closing_timestamp columns to the Inquiry table if they don't exist."""
|
||||||
|
current_app.logger.info("Running migration: add_closed_status")
|
||||||
|
|
||||||
|
# Check if the columns already exist
|
||||||
|
inspector = inspect(db.engine)
|
||||||
|
columns = [col['name'] for col in inspector.get_columns('inquiry')]
|
||||||
|
|
||||||
|
# Track if we need to commit changes
|
||||||
|
changes_made = False
|
||||||
|
|
||||||
|
# Add is_closed column if it doesn't exist
|
||||||
|
if 'is_closed' not in columns:
|
||||||
|
current_app.logger.info("Adding is_closed column to inquiry table")
|
||||||
|
with db.engine.connect() as conn:
|
||||||
|
conn.execute(text("ALTER TABLE inquiry ADD COLUMN is_closed BOOLEAN DEFAULT FALSE"))
|
||||||
|
# We need to commit within the connection context for some database types
|
||||||
|
conn.commit()
|
||||||
|
changes_made = True
|
||||||
|
else:
|
||||||
|
current_app.logger.info("is_closed column already exists in inquiry table")
|
||||||
|
|
||||||
|
# Add closing_timestamp column if it doesn't exist
|
||||||
|
if 'closing_timestamp' not in columns:
|
||||||
|
current_app.logger.info("Adding closing_timestamp column to inquiry table")
|
||||||
|
with db.engine.connect() as conn:
|
||||||
|
conn.execute(text("ALTER TABLE inquiry ADD COLUMN closing_timestamp TIMESTAMP"))
|
||||||
|
# We need to commit within the connection context for some database types
|
||||||
|
conn.commit()
|
||||||
|
changes_made = True
|
||||||
|
else:
|
||||||
|
current_app.logger.info("closing_timestamp column already exists in inquiry table")
|
||||||
|
|
||||||
|
if changes_made:
|
||||||
|
current_app.logger.info("Migration completed successfully")
|
||||||
|
else:
|
||||||
|
current_app.logger.info("No changes needed, all columns already exist")
|
||||||
|
|
||||||
|
return changes_made
|
||||||
|
|
@ -4,6 +4,7 @@ import secrets
|
||||||
import argon2
|
import argon2
|
||||||
from argon2 import PasswordHasher
|
from argon2 import PasswordHasher
|
||||||
from . import password_hasher
|
from . import password_hasher
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
class Message(db.Model):
|
class Message(db.Model):
|
||||||
inquiry_id = db.Column(db.String(16), db.ForeignKey('inquiry.id'), nullable=False, primary_key=True)
|
inquiry_id = db.Column(db.String(16), db.ForeignKey('inquiry.id'), nullable=False, primary_key=True)
|
||||||
|
|
@ -29,6 +30,33 @@ class Inquiry(db.Model):
|
||||||
messages = db.relationship('Message', backref='inquiry', lazy=True,
|
messages = db.relationship('Message', backref='inquiry', lazy=True,
|
||||||
cascade='all, delete-orphan',
|
cascade='all, delete-orphan',
|
||||||
order_by='Message.message_number')
|
order_by='Message.message_number')
|
||||||
|
is_closed = db.Column(db.Boolean, default=False)
|
||||||
|
closing_timestamp = db.Column(db.DateTime, nullable=True)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""Mark an inquiry as closed"""
|
||||||
|
self.is_closed = True
|
||||||
|
self.closing_timestamp = datetime.utcnow()
|
||||||
|
|
||||||
|
def reopen(self):
|
||||||
|
"""Reopen a closed inquiry"""
|
||||||
|
self.is_closed = False
|
||||||
|
self.closing_timestamp = None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_expired_inquiries(days=2):
|
||||||
|
"""Get inquiries that have been closed for more than the specified days"""
|
||||||
|
expiry_date = datetime.utcnow() - timedelta(days=days)
|
||||||
|
return Inquiry.query.filter(
|
||||||
|
Inquiry.is_closed == True,
|
||||||
|
Inquiry.closing_timestamp <= expiry_date
|
||||||
|
).all()
|
||||||
|
|
||||||
|
def get_deletion_date(self):
|
||||||
|
"""Get the deletion date for a closed inquiry"""
|
||||||
|
if self.is_closed:
|
||||||
|
return self.closing_timestamp + timedelta(days=2)
|
||||||
|
return None
|
||||||
|
|
||||||
class Settings(db.Model):
|
class Settings(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import urllib.error
|
||||||
import hmac
|
import hmac
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
def is_admin():
|
def is_admin():
|
||||||
return 'admin_authenticated' in session and session['admin_authenticated']
|
return 'admin_authenticated' in session and session['admin_authenticated']
|
||||||
|
|
@ -105,27 +105,33 @@ def inquiry(inquiry_id):
|
||||||
is_admin = 'admin_authenticated' in session and session['admin_authenticated']
|
is_admin = 'admin_authenticated' in session and session['admin_authenticated']
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
if is_admin:
|
# Handle closed inquiry differently for admin vs non-admin
|
||||||
message_content = request.form.get('admin_response')
|
if inquiry.is_closed:
|
||||||
is_admin_message = True
|
# Auto-reopen the inquiry when admin responds
|
||||||
else:
|
inquiry.reopen()
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# Send webhook for reopened inquiry
|
||||||
|
send_webhook('inquiry_reopened', {
|
||||||
|
'inquiry_id': inquiry_id
|
||||||
|
})
|
||||||
|
|
||||||
message_content = request.form.get('message')
|
message_content = request.form.get('message')
|
||||||
is_admin_message = False
|
|
||||||
|
|
||||||
if message_content and message_content.strip():
|
if message_content and message_content.strip():
|
||||||
message = Message(
|
message = Message(
|
||||||
content=message_content,
|
content=message_content,
|
||||||
inquiry_id=inquiry_id,
|
inquiry_id=inquiry_id,
|
||||||
is_admin=is_admin_message
|
is_admin=is_admin
|
||||||
)
|
)
|
||||||
db.session.add(message)
|
db.session.add(message)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
if not is_admin_message:
|
if not is_admin:
|
||||||
send_webhook('new_message', {
|
send_webhook('new_message', {
|
||||||
'inquiry_id': inquiry_id,
|
'inquiry_id': inquiry_id,
|
||||||
'message': message_content,
|
'message': message_content,
|
||||||
'is_admin': is_admin_message
|
'is_admin': is_admin
|
||||||
})
|
})
|
||||||
|
|
||||||
return redirect(url_for('inquiry', inquiry_id=inquiry_id))
|
return redirect(url_for('inquiry', inquiry_id=inquiry_id))
|
||||||
|
|
@ -324,3 +330,49 @@ def get_inquiry_messages(inquiry_id):
|
||||||
'inquiry_id': inquiry_id,
|
'inquiry_id': inquiry_id,
|
||||||
'messages': messages_json
|
'messages': messages_json
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@app.route('/inquiry/<inquiry_id>/close', methods=['POST'])
|
||||||
|
@admin_required
|
||||||
|
def close_inquiry(inquiry_id):
|
||||||
|
inquiry = Inquiry.query.get_or_404(inquiry_id)
|
||||||
|
|
||||||
|
# Only close if not already closed
|
||||||
|
if not inquiry.is_closed:
|
||||||
|
inquiry.close()
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# Send webhook for closed inquiry
|
||||||
|
send_webhook('inquiry_closed', {
|
||||||
|
'inquiry_id': inquiry_id
|
||||||
|
})
|
||||||
|
|
||||||
|
# Redirect back to the inquiry or admin dashboard based on referrer
|
||||||
|
referrer = request.referrer
|
||||||
|
if referrer and 'admin/dashboard' in referrer:
|
||||||
|
return redirect(url_for('admin_dashboard'))
|
||||||
|
else:
|
||||||
|
return redirect(url_for('inquiry', inquiry_id=inquiry_id))
|
||||||
|
|
||||||
|
@app.route('/inquiry/<inquiry_id>/reopen', methods=['POST'])
|
||||||
|
@admin_required
|
||||||
|
def reopen_inquiry(inquiry_id):
|
||||||
|
inquiry = Inquiry.query.get_or_404(inquiry_id)
|
||||||
|
|
||||||
|
# Only reopen if currently closed
|
||||||
|
if inquiry.is_closed:
|
||||||
|
inquiry.reopen()
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# Send webhook for reopened inquiry
|
||||||
|
send_webhook('inquiry_reopened', {
|
||||||
|
'inquiry_id': inquiry_id
|
||||||
|
})
|
||||||
|
|
||||||
|
flash('Inquiry reopened successfully.')
|
||||||
|
|
||||||
|
# Redirect back to the inquiry or admin dashboard based on referrer
|
||||||
|
referrer = request.referrer
|
||||||
|
if referrer and 'admin/dashboard' in referrer:
|
||||||
|
return redirect(url_for('admin_dashboard'))
|
||||||
|
else:
|
||||||
|
return redirect(url_for('inquiry', inquiry_id=inquiry_id))
|
||||||
29
src/anonchat/tasks.py
Normal file
29
src/anonchat/tasks.py
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
from flask import current_app
|
||||||
|
from . import db
|
||||||
|
from .models import Inquiry
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
def check_and_delete_expired_inquiries():
|
||||||
|
"""Check and delete inquiries that have been closed for more than 2 days"""
|
||||||
|
# Get app context from current_app
|
||||||
|
app = current_app._get_current_object()
|
||||||
|
|
||||||
|
with app.app_context():
|
||||||
|
app.logger.info("Running scheduled task: check_and_delete_expired_inquiries")
|
||||||
|
expired_inquiries = Inquiry.get_expired_inquiries(days=2)
|
||||||
|
deleted_count = 0
|
||||||
|
|
||||||
|
for inquiry in expired_inquiries:
|
||||||
|
# Delete the inquiry
|
||||||
|
db.session.delete(inquiry)
|
||||||
|
deleted_count += 1
|
||||||
|
|
||||||
|
# Send webhook for automatically deleted inquiry
|
||||||
|
from .routes import send_webhook
|
||||||
|
send_webhook('inquiry_auto_deleted', {
|
||||||
|
'inquiry_id': inquiry.id
|
||||||
|
})
|
||||||
|
|
||||||
|
if deleted_count > 0:
|
||||||
|
db.session.commit()
|
||||||
|
app.logger.info(f"Automatically deleted {deleted_count} expired closed inquiries")
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>ID</th>
|
<th>ID</th>
|
||||||
|
<th>Status</th>
|
||||||
<th>Messages</th>
|
<th>Messages</th>
|
||||||
<th>Latest Message</th>
|
<th>Latest Message</th>
|
||||||
<th>Time</th>
|
<th>Time</th>
|
||||||
|
|
@ -24,8 +25,20 @@
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for item in inquiries_with_data %}
|
{% for item in inquiries_with_data %}
|
||||||
<tr>
|
<tr {% if item.inquiry.is_closed %}style="background-color: #f8f9fa; opacity: 0.8;"{% endif %}>
|
||||||
<td>{{ item.inquiry.id }}</td>
|
<td>{{ item.inquiry.id }}</td>
|
||||||
|
<td>
|
||||||
|
{% if item.inquiry.is_closed %}
|
||||||
|
<span style="color: #6c757d; font-size: 0.8rem; padding: 0.2rem 0.4rem; background-color: #e9ecef; border-radius: 0.25rem;">CLOSED</span>
|
||||||
|
{% if item.inquiry.closing_timestamp %}
|
||||||
|
<span style="display: block; font-size: 0.7rem; margin-top: 0.2rem;">
|
||||||
|
Closed on {{ item.inquiry.closing_timestamp.strftime('%Y-%m-%d %H:%M') }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<span style="color: #28a745; font-size: 0.8rem; padding: 0.2rem 0.4rem; background-color: #e9ecef; border-radius: 0.25rem;">OPEN</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
<td>{{ item.message_count }}</td>
|
<td>{{ item.message_count }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if item.latest_message %}
|
{% if item.latest_message %}
|
||||||
|
|
@ -43,6 +56,19 @@
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="{{ url_for('inquiry', inquiry_id=item.inquiry.id) }}">View/Respond</a>
|
<a href="{{ url_for('inquiry', inquiry_id=item.inquiry.id) }}">View/Respond</a>
|
||||||
|
|
||||||
|
{% if item.inquiry.is_closed %}
|
||||||
|
<form method="POST" action="{{ url_for('reopen_inquiry', inquiry_id=item.inquiry.id) }}" style="display: inline; margin-left: 0.5rem;">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
|
<button type="submit" style="background-color: #28a745; color: white; border: none; padding: 0.25rem 0.5rem; border-radius: 0.25rem; cursor: pointer; font-size: 0.8rem;">Reopen</button>
|
||||||
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<form method="POST" action="{{ url_for('close_inquiry', inquiry_id=item.inquiry.id) }}" style="display: inline; margin-left: 0.5rem;">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
|
<button type="submit" style="background-color: #6c757d; color: white; border: none; padding: 0.25rem 0.5rem; border-radius: 0.25rem; cursor: pointer; font-size: 0.8rem;">Close</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<form method="POST" action="{{ url_for('delete_inquiry', inquiry_id=item.inquiry.id) }}" style="display: inline; margin-left: 0.5rem;">
|
<form method="POST" action="{{ url_for('delete_inquiry', inquiry_id=item.inquiry.id) }}" style="display: inline; margin-left: 0.5rem;">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
<button type="submit" onclick="return confirm('Are you sure you want to delete this inquiry? This action cannot be undone.')" style="background-color: #dc3545; color: white; border: none; padding: 0.25rem 0.5rem; border-radius: 0.25rem; cursor: pointer; font-size: 0.8rem;">Delete</button>
|
<button type="submit" onclick="return confirm('Are you sure you want to delete this inquiry? This action cannot be undone.')" style="background-color: #dc3545; color: white; border: none; padding: 0.25rem 0.5rem; border-radius: 0.25rem; cursor: pointer; font-size: 0.8rem;">Delete</button>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,11 @@
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="inquiry-details" id="inquiry-details" data-inquiry-id="{{ inquiry.id }}" data-last-message-number="{{ messages[-1].message_number if messages else 0 }}">
|
<div class="inquiry-details" id="inquiry-details" data-inquiry-id="{{ inquiry.id }}" data-last-message-number="{{ messages[-1].message_number if messages else 0 }}">
|
||||||
<h2>Inquiry #{{ inquiry.id[:6] }}</h2>
|
<h2>Inquiry #{{ inquiry.id[:6] }}
|
||||||
|
{% if inquiry.is_closed %}
|
||||||
|
<span style="color: #6c757d; font-size: 0.9rem; padding: 0.3rem 0.6rem; background-color: #e9ecef; border-radius: 0.25rem; margin-left: 0.5rem;">CLOSED</span>
|
||||||
|
{% endif %}
|
||||||
|
</h2>
|
||||||
|
|
||||||
{% if is_admin %}
|
{% if is_admin %}
|
||||||
{{ admin_header() }}
|
{{ admin_header() }}
|
||||||
|
|
@ -19,7 +23,25 @@
|
||||||
<p class="warning"><strong>Important:</strong> Do not share this link with anyone else, as anyone you share it with could access this chat.</p>
|
<p class="warning"><strong>Important:</strong> Do not share this link with anyone else, as anyone you share it with could access this chat.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if inquiry.is_closed %}
|
||||||
|
<div style="margin-bottom: 1rem; padding: 0.75rem; background-color: #3a3a3a; border-radius: 0.25rem; border-left: 4px solid #8c8c8c; color: #f0f0f0;">
|
||||||
|
<strong>This inquiry is closed.</strong>
|
||||||
|
{% if inquiry.closing_timestamp %}It will be deleted on {{ inquiry.get_deletion_date().strftime('%Y-%m-%d %H:%M') }} UTC.{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div style="margin-bottom: 1rem;">
|
<div style="margin-bottom: 1rem;">
|
||||||
|
{% if inquiry.is_closed %}
|
||||||
|
<form method="POST" action="{{ url_for('reopen_inquiry', inquiry_id=inquiry.id) }}" style="display: inline;">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
|
<button type="submit" style="background-color: #28a745; color: white; border: none; padding: 0.375rem 0.75rem; border-radius: 0.25rem; cursor: pointer; margin-right: 0.5rem;">Reopen Inquiry</button>
|
||||||
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<form method="POST" action="{{ url_for('close_inquiry', inquiry_id=inquiry.id) }}" style="display: inline;">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
|
<button type="submit" style="background-color: #6c757d; color: white; border: none; padding: 0.375rem 0.75rem; border-radius: 0.25rem; cursor: pointer; margin-right: 0.5rem;">Close Inquiry</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
<form method="POST" action="{{ url_for('delete_inquiry', inquiry_id=inquiry.id) }}" style="display: inline;">
|
<form method="POST" action="{{ url_for('delete_inquiry', inquiry_id=inquiry.id) }}" style="display: inline;">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
<button type="submit" onclick="return confirm('Are you sure you want to delete this inquiry? This action cannot be undone.')" style="background-color: #dc3545; color: white; border: none; padding: 0.375rem 0.75rem; border-radius: 0.25rem; cursor: pointer;">Delete Inquiry</button>
|
<button type="submit" onclick="return confirm('Are you sure you want to delete this inquiry? This action cannot be undone.')" style="background-color: #dc3545; color: white; border: none; padding: 0.375rem 0.75rem; border-radius: 0.25rem; cursor: pointer;">Delete Inquiry</button>
|
||||||
|
|
@ -50,9 +72,15 @@
|
||||||
<form method="POST" action="{{ url_for('inquiry', inquiry_id=inquiry.id) }}">
|
<form method="POST" action="{{ url_for('inquiry', inquiry_id=inquiry.id) }}">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<textarea name="{% if is_admin %}admin_response{% else %}message{% endif %}" rows="4" placeholder="Type your message..." required></textarea>
|
<textarea name="message" rows="4" placeholder="Type your message..." required></textarea>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit">Send Message</button>
|
<button type="submit">
|
||||||
|
{%if inquiry.is_closed %}
|
||||||
|
Send Message and Reopen Inquiry
|
||||||
|
{% else %}
|
||||||
|
Send Message
|
||||||
|
{% endif %}
|
||||||
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue