Refactor Message model to use composite primary key and ensure message numbering is handled automatically; update inquiry retrieval to order by message number. Enhance admin templates to include flash messages and remove deprecated header template. Add API endpoint for fetching inquiry messages with pagination support.
This commit is contained in:
parent
6c0f8ca955
commit
efcfb529c6
10 changed files with 176 additions and 88 deletions
|
@ -6,15 +6,29 @@ from argon2 import PasswordHasher
|
|||
from . import password_hasher
|
||||
|
||||
class Message(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
inquiry_id = db.Column(db.String(16), db.ForeignKey('inquiry.id'), nullable=False, primary_key=True)
|
||||
message_number = db.Column(db.Integer, primary_key=True)
|
||||
content = db.Column(db.Text, nullable=False)
|
||||
timestamp = db.Column(db.DateTime, default=db.func.current_timestamp())
|
||||
inquiry_id = db.Column(db.String(16), db.ForeignKey('inquiry.id'), nullable=False)
|
||||
is_admin = db.Column(db.Boolean, default=False)
|
||||
|
||||
__table_args__ = (db.UniqueConstraint('inquiry_id', 'message_number'),)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(Message, self).__init__(**kwargs)
|
||||
if self.message_number is None:
|
||||
# Find the highest message number for this inquiry and increment
|
||||
last_message = Message.query.filter_by(
|
||||
inquiry_id=self.inquiry_id
|
||||
).order_by(Message.message_number.desc()).first()
|
||||
|
||||
self.message_number = 1 if last_message is None else last_message.message_number + 1
|
||||
|
||||
class Inquiry(db.Model):
|
||||
id = db.Column(db.String(16), primary_key=True, default=lambda: secrets.token_hex(8))
|
||||
messages = db.relationship('Message', backref='inquiry', lazy=True, cascade='all, delete-orphan')
|
||||
id = db.Column(db.String(16), primary_key=True, unique=True, default=lambda: secrets.token_hex(8))
|
||||
messages = db.relationship('Message', backref='inquiry', lazy=True,
|
||||
cascade='all, delete-orphan',
|
||||
order_by='Message.message_number')
|
||||
|
||||
class Settings(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
|
|
@ -126,17 +126,14 @@ def inquiry(inquiry_id):
|
|||
'is_admin': is_admin_message
|
||||
})
|
||||
|
||||
if is_admin:
|
||||
flash('Response sent successfully')
|
||||
return redirect(url_for('inquiry', inquiry_id=inquiry_id))
|
||||
|
||||
messages = Message.query.filter_by(inquiry_id=inquiry_id).order_by(Message.timestamp).all()
|
||||
messages = Message.query.filter_by(inquiry_id=inquiry_id).order_by(Message.message_number).all()
|
||||
return render_template('inquiry.html', inquiry=inquiry, messages=messages, is_admin=is_admin)
|
||||
|
||||
@app.route('/admin', methods=['GET'])
|
||||
def admin_login():
|
||||
error = None
|
||||
return render_template('admin_login.html', error=error)
|
||||
return render_template('admin_login.html')
|
||||
|
||||
@app.route('/admin', methods=['POST'])
|
||||
@limiter.limit("1 per minute")
|
||||
|
@ -160,9 +157,9 @@ def admin_login_post():
|
|||
return redirect(next_page)
|
||||
return redirect(url_for('admin_dashboard'))
|
||||
else:
|
||||
error = 'Invalid credentials'
|
||||
flash('Invalid username or password', 'error')
|
||||
|
||||
return render_template('admin_login.html', error=error)
|
||||
return redirect(url_for('admin_login'))
|
||||
|
||||
|
||||
@app.route('/admin/logout')
|
||||
|
@ -180,7 +177,7 @@ def admin_dashboard():
|
|||
# For each inquiry, get the latest message timestamp
|
||||
inquiries_with_data = []
|
||||
for inquiry in inquiries:
|
||||
latest_message = Message.query.filter_by(inquiry_id=inquiry.id).order_by(Message.timestamp.desc()).first()
|
||||
latest_message = Message.query.filter_by(inquiry_id=inquiry.id).order_by(Message.message_number.desc()).first()
|
||||
message_count = Message.query.filter_by(inquiry_id=inquiry.id).count()
|
||||
inquiries_with_data.append({
|
||||
'inquiry': inquiry,
|
||||
|
@ -293,4 +290,36 @@ def handle_csrf_error(e):
|
|||
if request.is_json or request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||
return jsonify(error="Invalid request. CSRF token missing or incorrect."), 400
|
||||
else:
|
||||
return render_template('error.html', error="Invalid request. Please try again."), 400
|
||||
return render_template('error.html', error="Invalid request. Please try again."), 400
|
||||
|
||||
@app.route('/api/inquiry/<inquiry_id>/messages', methods=['GET'])
|
||||
@limiter.limit("10 per hour", deduct_when=lambda response: response.status_code != 200)
|
||||
@limiter.limit("30 per minute")
|
||||
def get_inquiry_messages(inquiry_id):
|
||||
inquiry = Inquiry.query.get_or_404(inquiry_id)
|
||||
|
||||
# Get after_message_number from query parameters if provided
|
||||
after_message_number = request.args.get('after_message_number', type=int)
|
||||
|
||||
# Base query
|
||||
query = Message.query.filter_by(inquiry_id=inquiry_id)
|
||||
|
||||
# Add after_message_number filter if provided
|
||||
if after_message_number is not None:
|
||||
query = query.filter(Message.message_number > after_message_number)
|
||||
|
||||
# Get messages ordered by message_number
|
||||
messages = query.order_by(Message.message_number).all()
|
||||
|
||||
# Convert messages to JSON format
|
||||
messages_json = [{
|
||||
'message_number': message.message_number,
|
||||
'content': message.content,
|
||||
'is_admin': message.is_admin,
|
||||
'timestamp': message.timestamp.isoformat()
|
||||
} for message in messages]
|
||||
|
||||
return jsonify({
|
||||
'inquiry_id': inquiry_id,
|
||||
'messages': messages_json
|
||||
})
|
|
@ -1,5 +1,5 @@
|
|||
{% extends 'base.html' %}
|
||||
{% from 'admin_header.html' import admin_header %}
|
||||
{% from 'headers.html' import admin_header, flash_messages %}
|
||||
|
||||
{% block title %}Admin Dashboard - {{ config.SITE_TITLE }}{% endblock %}
|
||||
|
||||
|
@ -7,7 +7,8 @@
|
|||
<h2>Admin Dashboard</h2>
|
||||
|
||||
{{ admin_header() }}
|
||||
|
||||
{{ flash_messages() }}
|
||||
|
||||
<h3>All Inquiries</h3>
|
||||
|
||||
{% if inquiries_with_data %}
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
{% macro admin_header() %}
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<a href="{{ url_for('admin_dashboard') }}" style="margin-right: 1rem;">Admin Dashboard</a>
|
||||
<a href="{{ url_for('admin_settings') }}" style="margin-right: 1rem;">Admin Settings</a>
|
||||
<a href="{{ url_for('admin_webhook') }}" style="margin-right: 1rem;">Webhook Settings</a>
|
||||
<a href="{{ url_for('admin_logout') }}" style="margin-right: 1rem;">Logout</a>
|
||||
</div>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div style="background-color: {% if category == 'error' %}#f8d7da{% else %}#d4edda{% endif %};
|
||||
color: {% if category == 'error' %}#721c24{% else %}#155724{% endif %};
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
border-radius: 0.25rem;">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endmacro %}
|
|
@ -1,13 +1,11 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% from 'headers.html' import flash_messages %}
|
||||
{% block title %}Admin Login - {{ config.SITE_TITLE }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Admin Login</h2>
|
||||
|
||||
{% if error %}
|
||||
<div style="color: red; margin-bottom: 1rem;">{{ error }}</div>
|
||||
{% endif %}
|
||||
|
||||
{{ flash_messages() }}
|
||||
|
||||
<form method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% from 'admin_header.html' import admin_header %}
|
||||
{% from 'headers.html' import admin_header, flash_messages %}
|
||||
|
||||
{% block title %}Admin Settings - {{ config.SITE_TITLE }}{% endblock %}
|
||||
|
||||
|
@ -8,6 +8,7 @@
|
|||
<h2>Admin Settings</h2>
|
||||
|
||||
{{ admin_header() }}
|
||||
{{ flash_messages() }}
|
||||
|
||||
<div class="settings-form">
|
||||
<h3>Change Admin Password</h3>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{% extends 'base.html' %}
|
||||
{% from 'admin_header.html' import admin_header %}
|
||||
{% from 'headers.html' import admin_header, flash_messages %}
|
||||
|
||||
{% block title %}Webhook Settings - {{ config.SITE_TITLE }}{% endblock %}
|
||||
|
||||
|
@ -7,6 +7,7 @@
|
|||
<h2>Webhook Settings</h2>
|
||||
|
||||
{{ admin_header() }}
|
||||
{{ flash_messages() }}
|
||||
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<p>Configure webhook settings to receive notifications for new inquiries and responses.</p>
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% from 'headers.html' import flash_messages %}
|
||||
|
||||
{% block title %}Create New Inquiry - {{ config.SITE_TITLE }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
@ -10,21 +12,7 @@
|
|||
<strong>Note:</strong> This is early software still under development. Please do not abuse this system.
|
||||
</div>
|
||||
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div style="background-color: #d4edda; color: #155724; padding: 0.75rem; margin-bottom: 1rem; border-radius: 0.25rem;">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{% if error %}
|
||||
<div class="error-message" style="color: red; margin-bottom: 15px; padding: 10px; background-color: #ffeeee; border-radius: 4px;">
|
||||
{{ error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{{ flash_messages() }}
|
||||
|
||||
<form method="POST" action="{{ url_for('index') }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
|
|
24
src/anonchat/templates/headers.html
Normal file
24
src/anonchat/templates/headers.html
Normal file
|
@ -0,0 +1,24 @@
|
|||
{% macro admin_header() %}
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<a href="{{ url_for('admin_dashboard') }}" style="margin-right: 1rem;">Admin Dashboard</a>
|
||||
<a href="{{ url_for('admin_settings') }}" style="margin-right: 1rem;">Admin Settings</a>
|
||||
<a href="{{ url_for('admin_webhook') }}" style="margin-right: 1rem;">Webhook Settings</a>
|
||||
<a href="{{ url_for('admin_logout') }}" style="margin-right: 1rem;">Logout</a>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro flash_messages() %}
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div style="background-color: {% if category == 'error' %}#f8d7da{% else %}#d4edda{% endif %};
|
||||
color: {% if category == 'error' %}#721c24{% else %}#155724{% endif %};
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
border-radius: 0.25rem;">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endmacro %}
|
|
@ -1,16 +1,18 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% from 'admin_header.html' import admin_header %}
|
||||
{% from 'headers.html' import admin_header, flash_messages %}
|
||||
|
||||
{% block title %}{% if is_admin %}Admin View - {% endif %}Inquiry #{{ inquiry.id[:6] }} - {{ config.SITE_TITLE }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="inquiry-details">
|
||||
<h2>Inquiry #{{ inquiry.id[:6] }}</h2>
|
||||
|
||||
{% if is_admin %}
|
||||
{{ admin_header() }}
|
||||
{% endif %}
|
||||
|
||||
<h2>Inquiry #{{ inquiry.id[:6] }}</h2>
|
||||
{{ flash_messages() }}
|
||||
|
||||
{% if not is_admin %}
|
||||
<p>This is your conversation link: <code style="user-select: all;">{{ request.url }}</code></p>
|
||||
|
@ -24,35 +26,23 @@
|
|||
</form>
|
||||
</div>
|
||||
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div style="background-color: #d4edda; color: #155724; padding: 0.75rem; margin-bottom: 1rem; border-radius: 0.25rem;">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<div class="messages">
|
||||
<div class="messages" id="messages-container">
|
||||
<h3>Messages</h3>
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="message {% if message.is_admin %}admin-message{% else %}user-message{% endif %}">
|
||||
<div class="content">
|
||||
{% if message.is_admin %}<span class="admin-badge">ADMIN:</span> {% endif %}
|
||||
{{ message.content }}
|
||||
<div id="messages-list">
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="message {% if message.is_admin %}admin-message{% else %}user-message{% endif %}" data-message-id="{{ message.id }}">
|
||||
<div class="content">
|
||||
{% if message.is_admin %}<span class="admin-badge">ADMIN:</span> {% endif %}
|
||||
{{ message.content }}
|
||||
</div>
|
||||
<div class="timestamp">{{ message.timestamp.strftime('%Y-%m-%d %H:%M:%S') }}</div>
|
||||
</div>
|
||||
<div class="timestamp">{{ message.timestamp.strftime('%Y-%m-%d %H:%M:%S') }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p>No messages yet. Start the conversation!</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="refresh-notice">
|
||||
<p><em>Please refresh the page to see new messages.</em></p>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p>No messages yet. Start the conversation!</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="reply-form">
|
||||
|
@ -66,4 +56,68 @@
|
|||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let lastMessageNumber = {{ messages[-1].message_number if messages else 0 }};
|
||||
const inquiryId = '{{ inquiry.id }}';
|
||||
const messagesContainer = document.getElementById('messages-list');
|
||||
let updateInterval = null;
|
||||
|
||||
function formatTimestamp(timestamp) {
|
||||
const date = new Date(timestamp);
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
}
|
||||
|
||||
function createMessageElement(message) {
|
||||
const div = document.createElement('div');
|
||||
div.className = `message ${message.is_admin ? 'admin-message' : 'user-message'}`;
|
||||
div.dataset.messageNumber = message.message_number;
|
||||
div.innerHTML = `
|
||||
<div class="content">
|
||||
${message.is_admin ? '<span class="admin-badge">ADMIN:</span> ' : ''}
|
||||
${message.content}
|
||||
</div>
|
||||
<div class="timestamp">${formatTimestamp(message.timestamp)}</div>
|
||||
`;
|
||||
return div;
|
||||
}
|
||||
|
||||
function updateMessages() {
|
||||
fetch(`/api/inquiry/${inquiryId}/messages?after_message_number=${lastMessageNumber}`)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.messages && data.messages.length > 0) {
|
||||
data.messages.forEach(message => {
|
||||
if (message.message_number > lastMessageNumber) {
|
||||
messagesContainer.appendChild(createMessageElement(message));
|
||||
lastMessageNumber = message.message_number;
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching messages:', error);
|
||||
// Stop the interval if something goes wrong
|
||||
if (updateInterval) {
|
||||
clearInterval(updateInterval);
|
||||
updateInterval = null;
|
||||
console.log('Message updates stopped due to an error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Update messages every 5 seconds
|
||||
updateInterval = setInterval(updateMessages, 5000);
|
||||
</script>
|
||||
{% endblock %}
|
Loading…
Add table
Add a link
Reference in a new issue