Inquiry closing
This commit is contained in:
parent
b8b8d9f2ca
commit
2657849748
11 changed files with 351 additions and 25 deletions
54
README.md
54
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:
|
||||
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)
|
||||
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)
|
||||
|
@ -83,6 +83,49 @@ This project uses Poetry for dependency management.
|
|||
- Run tests: `poetry run pytest`
|
||||
- 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
|
||||
|
||||
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.
|
||||
|
@ -147,14 +190,7 @@ The following security enhancements are planned for future releases:
|
|||
- Implement proper PKCE flow for added security
|
||||
- Support for custom OAuth providers for enterprise deployments
|
||||
- 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
|
||||
- [ ] Implement read-only sharing links for inquiries
|
||||
- Generate unique, cryptographically secure sharing links
|
||||
|
|
|
@ -6,6 +6,11 @@ echo "Running database initialization..."
|
|||
poetry run flask --app src/anonchat init-db
|
||||
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)
|
||||
echo "Executing command: $@"
|
||||
exec "$@"
|
|
@ -19,7 +19,8 @@ dependencies = [
|
|||
"psycopg2-binary (>=2.9.9,<3.0.0)",
|
||||
"redis (>=5.0.0,<6.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]
|
||||
|
|
|
@ -10,6 +10,7 @@ from flask_session import Session
|
|||
import redis
|
||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||
from argon2 import PasswordHasher
|
||||
from flask_apscheduler import APScheduler
|
||||
|
||||
# Load environment variables from .env file
|
||||
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_KEY_PREFIX'] = 'anonchat_session:'
|
||||
|
||||
# Scheduler configuration
|
||||
app.config['SCHEDULER_API_ENABLED'] = False
|
||||
app.config['SCHEDULER_TIMEZONE'] = 'UTC'
|
||||
|
||||
if app.config['BEHIND_PROXY']:
|
||||
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=2)
|
||||
|
||||
|
@ -69,6 +74,10 @@ limiter = Limiter(get_remote_address, app=app)
|
|||
# Initialize database
|
||||
db = SQLAlchemy(app)
|
||||
|
||||
# Initialize scheduler
|
||||
scheduler = APScheduler()
|
||||
scheduler.init_app(app)
|
||||
|
||||
# Import models
|
||||
from . import models
|
||||
# Explicitly import all models to ensure they are registered with SQLAlchemy
|
||||
|
@ -104,6 +113,37 @@ def ratelimit_handler(e):
|
|||
# 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
|
||||
@app.cli.command("init-db")
|
||||
def init_db_command():
|
||||
|
@ -113,6 +153,13 @@ def init_db_command():
|
|||
with app.app_context():
|
||||
print("Creating database tables...")
|
||||
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...")
|
||||
# Initialize settings if they don't exist
|
||||
# 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
|
||||
from argon2 import PasswordHasher
|
||||
from . import password_hasher
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
class Message(db.Model):
|
||||
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,
|
||||
cascade='all, delete-orphan',
|
||||
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):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
|
|
@ -8,7 +8,7 @@ import urllib.error
|
|||
import hmac
|
||||
import hashlib
|
||||
import json
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
def is_admin():
|
||||
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']
|
||||
|
||||
if request.method == 'POST':
|
||||
if is_admin:
|
||||
message_content = request.form.get('admin_response')
|
||||
is_admin_message = True
|
||||
else:
|
||||
message_content = request.form.get('message')
|
||||
is_admin_message = False
|
||||
# Handle closed inquiry differently for admin vs non-admin
|
||||
if inquiry.is_closed:
|
||||
# Auto-reopen the inquiry when admin responds
|
||||
inquiry.reopen()
|
||||
db.session.commit()
|
||||
|
||||
# Send webhook for reopened inquiry
|
||||
send_webhook('inquiry_reopened', {
|
||||
'inquiry_id': inquiry_id
|
||||
})
|
||||
|
||||
message_content = request.form.get('message')
|
||||
|
||||
if message_content and message_content.strip():
|
||||
message = Message(
|
||||
content=message_content,
|
||||
inquiry_id=inquiry_id,
|
||||
is_admin=is_admin_message
|
||||
is_admin=is_admin
|
||||
)
|
||||
db.session.add(message)
|
||||
db.session.commit()
|
||||
|
||||
if not is_admin_message:
|
||||
if not is_admin:
|
||||
send_webhook('new_message', {
|
||||
'inquiry_id': inquiry_id,
|
||||
'message': message_content,
|
||||
'is_admin': is_admin_message
|
||||
'is_admin': is_admin
|
||||
})
|
||||
|
||||
return redirect(url_for('inquiry', inquiry_id=inquiry_id))
|
||||
|
@ -323,4 +329,50 @@ def get_inquiry_messages(inquiry_id):
|
|||
return jsonify({
|
||||
'inquiry_id': inquiry_id,
|
||||
'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>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Status</th>
|
||||
<th>Messages</th>
|
||||
<th>Latest Message</th>
|
||||
<th>Time</th>
|
||||
|
@ -24,8 +25,20 @@
|
|||
</thead>
|
||||
<tbody>
|
||||
{% 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>
|
||||
{% 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>
|
||||
{% if item.latest_message %}
|
||||
|
@ -43,6 +56,19 @@
|
|||
</td>
|
||||
<td>
|
||||
<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;">
|
||||
<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>
|
||||
|
|
|
@ -6,7 +6,11 @@
|
|||
|
||||
{% 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 }}">
|
||||
<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 %}
|
||||
{{ 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>
|
||||
{% 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;">
|
||||
{% 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;">
|
||||
<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>
|
||||
|
@ -50,9 +72,15 @@
|
|||
<form method="POST" action="{{ url_for('inquiry', inquiry_id=inquiry.id) }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<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>
|
||||
<button type="submit">Send Message</button>
|
||||
<button type="submit">
|
||||
{%if inquiry.is_closed %}
|
||||
Send Message and Reopen Inquiry
|
||||
{% else %}
|
||||
Send Message
|
||||
{% endif %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue