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:
Minecon724 2025-04-02 16:24:39 +02:00
commit efcfb529c6
Signed by: Minecon724
GPG key ID: A02E6E67AB961189
10 changed files with 176 additions and 88 deletions

View file

@ -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)

View file

@ -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
})

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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() }}"/>

View file

@ -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>

View file

@ -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>

View file

@ -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() }}"/>

View 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 %}

View file

@ -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 %}