Major Feature Update: Modern Chat System & Admin Management
Features Added: 🔥 Modern Chat System: - Real-time messaging with modern Tailwind CSS design - Post-linked discussions for adventure sharing - Chat categories (general, technical-support, adventure-planning) - Mobile-responsive interface with gradient backgrounds - JavaScript polling for live message updates 🎯 Comprehensive Admin Panel: - Chat room management with merge capabilities - Password reset system with email templates - User management with admin controls - Chat statistics and analytics dashboard - Room binding to posts and categorization �� Mobile API Integration: - RESTful API endpoints at /api/v1/chat - Session-based authentication for mobile apps - Comprehensive endpoints for rooms, messages, users - Mobile app compatibility (React Native, Flutter) 🛠️ Technical Improvements: - Enhanced database models with ChatRoom categories - Password reset token system with email verification - Template synchronization fixes for Docker deployment - Migration scripts for database schema updates - Improved error handling and validation 🎨 UI/UX Enhancements: - Modern card-based layouts matching app design - Consistent styling across chat and admin interfaces - Mobile-optimized touch interactions - Professional gradient designs and glass morphism effects 📚 Documentation: - Updated README with comprehensive API documentation - Added deployment instructions for Docker (port 8100) - Configuration guide for production environments - Mobile integration examples and endpoints This update transforms the platform into a comprehensive motorcycle adventure community with modern chat capabilities and professional admin management tools.
This commit is contained in:
@@ -7,9 +7,10 @@ from functools import wraps
|
||||
from datetime import datetime, timedelta
|
||||
from sqlalchemy import func, desc
|
||||
import secrets
|
||||
import uuid
|
||||
from app.routes.mail import mail
|
||||
from app.extensions import db
|
||||
from app.models import User, Post, PostImage, GPXFile, Comment, Like, PageView, MailSettings, SentEmail
|
||||
from app.models import User, Post, PostImage, GPXFile, Comment, Like, PageView, MailSettings, SentEmail, PasswordResetRequest, PasswordResetToken, ChatRoom, ChatMessage
|
||||
|
||||
admin = Blueprint('admin', __name__, url_prefix='/admin')
|
||||
|
||||
@@ -151,6 +152,22 @@ def dashboard():
|
||||
.order_by(desc('view_count'))\
|
||||
.limit(10).all()
|
||||
|
||||
# Password reset statistics
|
||||
pending_password_requests = PasswordResetRequest.query.filter_by(status='pending').count()
|
||||
active_reset_tokens = PasswordResetToken.query.filter_by(is_used=False).filter(
|
||||
PasswordResetToken.expires_at > datetime.utcnow()
|
||||
).count()
|
||||
|
||||
# Chat statistics
|
||||
total_chat_rooms = ChatRoom.query.count()
|
||||
linked_chat_rooms = ChatRoom.query.filter(ChatRoom.related_post_id.isnot(None)).count()
|
||||
active_chat_rooms = db.session.query(ChatRoom.id).filter(
|
||||
ChatRoom.messages.any(ChatMessage.created_at >= thirty_days_ago)
|
||||
).distinct().count()
|
||||
recent_chat_messages = ChatMessage.query.filter(
|
||||
ChatMessage.created_at >= thirty_days_ago
|
||||
).count()
|
||||
|
||||
return render_template('admin/dashboard.html',
|
||||
total_users=total_users,
|
||||
total_posts=total_posts,
|
||||
@@ -165,7 +182,13 @@ def dashboard():
|
||||
views_yesterday=views_yesterday,
|
||||
views_this_week=views_this_week,
|
||||
most_viewed_posts=most_viewed_posts,
|
||||
most_viewed_pages=most_viewed_pages)
|
||||
most_viewed_pages=most_viewed_pages,
|
||||
pending_password_requests=pending_password_requests,
|
||||
active_reset_tokens=active_reset_tokens,
|
||||
total_chat_rooms=total_chat_rooms,
|
||||
linked_chat_rooms=linked_chat_rooms,
|
||||
active_chat_rooms=active_chat_rooms,
|
||||
recent_chat_messages=recent_chat_messages)
|
||||
|
||||
@admin.route('/posts')
|
||||
@login_required
|
||||
@@ -506,3 +529,367 @@ def api_quick_stats():
|
||||
'pending_posts': pending_count,
|
||||
'today_views': today_views
|
||||
})
|
||||
|
||||
|
||||
# Password Reset Management Routes
|
||||
|
||||
@admin.route('/password-reset-requests')
|
||||
@login_required
|
||||
@admin_required
|
||||
def password_reset_requests():
|
||||
"""View all password reset requests"""
|
||||
page = request.args.get('page', 1, type=int)
|
||||
status = request.args.get('status', 'all')
|
||||
|
||||
query = PasswordResetRequest.query
|
||||
|
||||
if status != 'all':
|
||||
query = query.filter_by(status=status)
|
||||
|
||||
requests = query.order_by(PasswordResetRequest.created_at.desc()).paginate(
|
||||
page=page, per_page=20, error_out=False
|
||||
)
|
||||
|
||||
return render_template('admin/password_reset_requests.html',
|
||||
requests=requests, status=status)
|
||||
|
||||
|
||||
@admin.route('/password-reset-requests/<int:request_id>')
|
||||
@login_required
|
||||
@admin_required
|
||||
def password_reset_request_detail(request_id):
|
||||
"""View individual password reset request details"""
|
||||
reset_request = PasswordResetRequest.query.get_or_404(request_id)
|
||||
|
||||
# Get associated tokens
|
||||
tokens = PasswordResetToken.query.filter_by(request_id=request_id).order_by(
|
||||
PasswordResetToken.created_at.desc()
|
||||
).all()
|
||||
|
||||
return render_template('admin/password_reset_request_detail.html',
|
||||
request=reset_request, tokens=tokens)
|
||||
|
||||
|
||||
@admin.route('/password-reset-requests/<int:request_id>/generate-token', methods=['POST'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def generate_password_reset_token(request_id):
|
||||
"""Generate a new password reset token for a request"""
|
||||
reset_request = PasswordResetRequest.query.get_or_404(request_id)
|
||||
|
||||
# Create token
|
||||
token = str(uuid.uuid4())
|
||||
expires_at = datetime.utcnow() + timedelta(hours=24) # 24 hour expiry
|
||||
|
||||
reset_token = PasswordResetToken(
|
||||
token=token,
|
||||
request_id=request_id,
|
||||
user_id=reset_request.user.id,
|
||||
created_by_admin_id=current_user.id,
|
||||
expires_at=expires_at
|
||||
)
|
||||
|
||||
db.session.add(reset_token)
|
||||
reset_request.status = 'token_generated'
|
||||
reset_request.updated_at = datetime.utcnow()
|
||||
db.session.commit()
|
||||
|
||||
flash('Password reset token generated successfully!', 'success')
|
||||
return redirect(url_for('admin.password_reset_token_template', token_id=reset_token.id))
|
||||
|
||||
|
||||
@admin.route('/password-reset-tokens')
|
||||
@login_required
|
||||
@admin_required
|
||||
def password_reset_tokens():
|
||||
"""View all password reset tokens"""
|
||||
page = request.args.get('page', 1, type=int)
|
||||
status = request.args.get('status', 'all')
|
||||
|
||||
query = PasswordResetToken.query.join(User).order_by(PasswordResetToken.created_at.desc())
|
||||
|
||||
if status == 'active':
|
||||
query = query.filter_by(is_used=False).filter(PasswordResetToken.expires_at > datetime.utcnow())
|
||||
elif status == 'used':
|
||||
query = query.filter_by(is_used=True)
|
||||
elif status == 'expired':
|
||||
query = query.filter(PasswordResetToken.expires_at <= datetime.utcnow(), PasswordResetToken.is_used == False)
|
||||
|
||||
tokens = query.paginate(page=page, per_page=20, error_out=False)
|
||||
|
||||
# Get counts for statistics
|
||||
active_count = PasswordResetToken.query.filter_by(is_used=False).filter(
|
||||
PasswordResetToken.expires_at > datetime.utcnow()
|
||||
).count()
|
||||
used_count = PasswordResetToken.query.filter_by(is_used=True).count()
|
||||
expired_count = PasswordResetToken.query.filter(
|
||||
PasswordResetToken.expires_at <= datetime.utcnow(),
|
||||
PasswordResetToken.is_used == False
|
||||
).count()
|
||||
|
||||
return render_template('admin/password_reset_tokens.html',
|
||||
tokens=tokens, status=status,
|
||||
active_count=active_count, used_count=used_count, expired_count=expired_count)
|
||||
|
||||
|
||||
@admin.route('/manage-chats')
|
||||
@login_required
|
||||
@admin_required
|
||||
def manage_chats():
|
||||
"""Admin chat room management"""
|
||||
page = request.args.get('page', 1, type=int)
|
||||
category = request.args.get('category', '')
|
||||
status = request.args.get('status', '')
|
||||
search = request.args.get('search', '')
|
||||
|
||||
# Base query with message count
|
||||
query = db.session.query(
|
||||
ChatRoom,
|
||||
func.count(ChatMessage.id).label('message_count'),
|
||||
func.max(ChatMessage.created_at).label('last_activity')
|
||||
).outerjoin(ChatMessage).group_by(ChatRoom.id)
|
||||
|
||||
# Apply filters
|
||||
if category:
|
||||
query = query.filter(ChatRoom.category == category)
|
||||
|
||||
if status == 'linked':
|
||||
query = query.filter(ChatRoom.related_post_id.isnot(None))
|
||||
elif status == 'unlinked':
|
||||
query = query.filter(ChatRoom.related_post_id.is_(None))
|
||||
elif status == 'active':
|
||||
thirty_days_ago = datetime.utcnow() - timedelta(days=30)
|
||||
query = query.having(func.max(ChatMessage.created_at) >= thirty_days_ago)
|
||||
elif status == 'inactive':
|
||||
thirty_days_ago = datetime.utcnow() - timedelta(days=30)
|
||||
query = query.having(
|
||||
db.or_(
|
||||
func.max(ChatMessage.created_at) < thirty_days_ago,
|
||||
func.max(ChatMessage.created_at).is_(None)
|
||||
)
|
||||
)
|
||||
|
||||
if search:
|
||||
query = query.filter(
|
||||
db.or_(
|
||||
ChatRoom.name.contains(search),
|
||||
ChatRoom.description.contains(search)
|
||||
)
|
||||
)
|
||||
|
||||
# Order by last activity
|
||||
query = query.order_by(func.max(ChatMessage.created_at).desc().nullslast())
|
||||
|
||||
# Paginate
|
||||
results = query.paginate(page=page, per_page=20, error_out=False)
|
||||
|
||||
# Process results to add message count and last activity to room objects
|
||||
chat_rooms = []
|
||||
for room, message_count, last_activity in results.items:
|
||||
room.message_count = message_count
|
||||
room.last_activity = last_activity
|
||||
chat_rooms.append(room)
|
||||
|
||||
# Get statistics
|
||||
total_rooms = ChatRoom.query.count()
|
||||
linked_rooms = ChatRoom.query.filter(ChatRoom.related_post_id.isnot(None)).count()
|
||||
|
||||
thirty_days_ago = datetime.utcnow() - timedelta(days=30)
|
||||
active_today = db.session.query(ChatRoom.id).filter(
|
||||
ChatRoom.messages.any(
|
||||
func.date(ChatMessage.created_at) == datetime.utcnow().date()
|
||||
)
|
||||
).distinct().count()
|
||||
|
||||
total_messages = ChatMessage.query.count()
|
||||
|
||||
# Get available posts for linking
|
||||
available_posts = Post.query.filter_by(published=True).order_by(Post.title).all()
|
||||
|
||||
# Create pagination object with processed rooms
|
||||
class PaginationWrapper:
|
||||
def __init__(self, original_pagination, items):
|
||||
self.page = original_pagination.page
|
||||
self.per_page = original_pagination.per_page
|
||||
self.total = original_pagination.total
|
||||
self.pages = original_pagination.pages
|
||||
self.has_prev = original_pagination.has_prev
|
||||
self.prev_num = original_pagination.prev_num
|
||||
self.has_next = original_pagination.has_next
|
||||
self.next_num = original_pagination.next_num
|
||||
self.items = items
|
||||
|
||||
def iter_pages(self):
|
||||
return range(1, self.pages + 1)
|
||||
|
||||
pagination = PaginationWrapper(results, chat_rooms)
|
||||
|
||||
return render_template('admin/manage_chats.html',
|
||||
chat_rooms=chat_rooms,
|
||||
pagination=pagination,
|
||||
total_rooms=total_rooms,
|
||||
linked_rooms=linked_rooms,
|
||||
active_today=active_today,
|
||||
total_messages=total_messages,
|
||||
available_posts=available_posts)
|
||||
|
||||
|
||||
@admin.route('/api/chat-rooms')
|
||||
@login_required
|
||||
@admin_required
|
||||
def api_chat_rooms():
|
||||
"""API endpoint for chat rooms (for AJAX calls)"""
|
||||
exclude_id = request.args.get('exclude', type=int)
|
||||
|
||||
query = ChatRoom.query
|
||||
if exclude_id:
|
||||
query = query.filter(ChatRoom.id != exclude_id)
|
||||
|
||||
rooms = query.all()
|
||||
|
||||
# Get message counts
|
||||
room_data = []
|
||||
for room in rooms:
|
||||
message_count = ChatMessage.query.filter_by(room_id=room.id).count()
|
||||
room_data.append({
|
||||
'id': room.id,
|
||||
'name': room.name,
|
||||
'description': room.description,
|
||||
'category': room.category,
|
||||
'message_count': message_count,
|
||||
'created_at': room.created_at.isoformat() if room.created_at else None
|
||||
})
|
||||
|
||||
return jsonify({'success': True, 'rooms': room_data})
|
||||
|
||||
|
||||
@admin.route('/api/chat-rooms/<int:room_id>', methods=['PUT'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def api_update_chat_room(room_id):
|
||||
"""Update chat room details"""
|
||||
room = ChatRoom.query.get_or_404(room_id)
|
||||
|
||||
try:
|
||||
data = request.get_json()
|
||||
|
||||
room.name = data.get('name', room.name)
|
||||
room.description = data.get('description', room.description)
|
||||
room.category = data.get('category', room.category)
|
||||
|
||||
# Handle post linking
|
||||
related_post_id = data.get('related_post_id')
|
||||
if related_post_id:
|
||||
post = Post.query.get(related_post_id)
|
||||
if post:
|
||||
room.related_post_id = related_post_id
|
||||
else:
|
||||
return jsonify({'success': False, 'error': 'Post not found'})
|
||||
else:
|
||||
room.related_post_id = None
|
||||
|
||||
db.session.commit()
|
||||
return jsonify({'success': True, 'message': 'Room updated successfully'})
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({'success': False, 'error': str(e)})
|
||||
|
||||
|
||||
@admin.route('/api/chat-rooms/<int:room_id>/link-post', methods=['POST'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def api_link_chat_room_to_post(room_id):
|
||||
"""Link chat room to a post"""
|
||||
room = ChatRoom.query.get_or_404(room_id)
|
||||
|
||||
try:
|
||||
data = request.get_json()
|
||||
post_id = data.get('post_id')
|
||||
|
||||
if post_id:
|
||||
post = Post.query.get_or_404(post_id)
|
||||
room.related_post_id = post_id
|
||||
else:
|
||||
room.related_post_id = None
|
||||
|
||||
db.session.commit()
|
||||
return jsonify({'success': True, 'message': 'Room linked to post successfully'})
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({'success': False, 'error': str(e)})
|
||||
|
||||
|
||||
@admin.route('/api/chat-rooms/<int:source_room_id>/merge', methods=['POST'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def api_merge_chat_rooms(source_room_id):
|
||||
"""Merge source room into target room"""
|
||||
source_room = ChatRoom.query.get_or_404(source_room_id)
|
||||
|
||||
try:
|
||||
data = request.get_json()
|
||||
target_room_id = data.get('target_room_id')
|
||||
target_room = ChatRoom.query.get_or_404(target_room_id)
|
||||
|
||||
# Move all messages from source to target room
|
||||
messages = ChatMessage.query.filter_by(room_id=source_room_id).all()
|
||||
for message in messages:
|
||||
message.room_id = target_room_id
|
||||
|
||||
# Add system message about the merge
|
||||
merge_message = ChatMessage(
|
||||
room_id=target_room_id,
|
||||
sender_id=current_user.id,
|
||||
content=f"Room '{source_room.name}' has been merged into this room by admin {current_user.nickname}",
|
||||
message_type='system',
|
||||
is_system_message=True
|
||||
)
|
||||
db.session.add(merge_message)
|
||||
|
||||
# Delete the source room
|
||||
db.session.delete(source_room)
|
||||
|
||||
db.session.commit()
|
||||
return jsonify({'success': True, 'message': 'Rooms merged successfully'})
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({'success': False, 'error': str(e)})
|
||||
|
||||
|
||||
@admin.route('/api/chat-rooms/<int:room_id>', methods=['DELETE'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def api_delete_chat_room(room_id):
|
||||
"""Delete chat room and all its messages"""
|
||||
room = ChatRoom.query.get_or_404(room_id)
|
||||
|
||||
try:
|
||||
# Delete all messages first
|
||||
ChatMessage.query.filter_by(room_id=room_id).delete()
|
||||
|
||||
# Delete the room
|
||||
db.session.delete(room)
|
||||
|
||||
db.session.commit()
|
||||
return jsonify({'success': True, 'message': 'Room deleted successfully'})
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({'success': False, 'error': str(e)})
|
||||
|
||||
|
||||
@admin.route('/password-reset-tokens/<int:token_id>/template')
|
||||
@login_required
|
||||
@admin_required
|
||||
def password_reset_token_template(token_id):
|
||||
"""Display email template for password reset token"""
|
||||
token = PasswordResetToken.query.get_or_404(token_id)
|
||||
|
||||
# Generate the reset URL
|
||||
reset_url = url_for('auth.reset_password_with_token', token=token.token, _external=True)
|
||||
|
||||
return render_template('admin/password_reset_email_template.html',
|
||||
token=token, reset_url=reset_url)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash
|
||||
from flask_login import login_user, logout_user, login_required, current_user
|
||||
from werkzeug.security import check_password_hash
|
||||
from app.models import User, db
|
||||
from app.models import User, db, PasswordResetToken
|
||||
from app.routes.reset_password import RequestResetForm, ResetPasswordForm
|
||||
from flask_mail import Message
|
||||
from app.routes.mail import mail
|
||||
@@ -161,26 +161,6 @@ def forgot_password():
|
||||
flash('Your password reset request has been sent to administrators. They will contact you soon to help reset your password.', 'info')
|
||||
return redirect(url_for('auth.login'))
|
||||
return render_template('auth/forgot_password.html', form=form)
|
||||
# Password reset route
|
||||
@auth.route('/reset-password/<token>', methods=['GET', 'POST'])
|
||||
def reset_password(token):
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('main.index'))
|
||||
email = verify_reset_token(token)
|
||||
if not email:
|
||||
flash('Invalid or expired reset link.', 'danger')
|
||||
return redirect(url_for('auth.forgot_password'))
|
||||
user = User.query.filter_by(email=email).first()
|
||||
if not user:
|
||||
flash('Invalid or expired reset link.', 'danger')
|
||||
return redirect(url_for('auth.forgot_password'))
|
||||
form = ResetPasswordForm()
|
||||
if form.validate_on_submit():
|
||||
user.set_password(form.password.data)
|
||||
db.session.commit()
|
||||
flash('Your password has been reset. You can now log in.', 'success')
|
||||
return redirect(url_for('auth.login'))
|
||||
return render_template('auth/reset_password.html', form=form)
|
||||
|
||||
@auth.route('/change-password', methods=['POST'])
|
||||
@login_required
|
||||
@@ -235,3 +215,59 @@ def is_valid_password(password):
|
||||
if not re.search(r'\d', password):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class ResetPasswordWithTokenForm(FlaskForm):
|
||||
password = PasswordField('New Password', validators=[DataRequired(), Length(min=8)])
|
||||
password2 = PasswordField('Confirm New Password', validators=[DataRequired(), EqualTo('password')])
|
||||
submit = SubmitField('Reset Password')
|
||||
|
||||
|
||||
@auth.route('/reset-password/<token>', methods=['GET', 'POST'])
|
||||
def reset_password_with_token(token):
|
||||
"""Reset password using admin-generated token"""
|
||||
# Find the token in database
|
||||
reset_token = PasswordResetToken.query.filter_by(token=token).first()
|
||||
|
||||
if not reset_token:
|
||||
flash('Invalid or expired reset link.', 'error')
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
# Check if token is expired
|
||||
if reset_token.is_expired:
|
||||
flash('This reset link has expired. Please request a new one.', 'error')
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
# Check if token is already used
|
||||
if reset_token.is_used:
|
||||
flash('This reset link has already been used.', 'error')
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
form = ResetPasswordWithTokenForm()
|
||||
|
||||
if form.validate_on_submit():
|
||||
user = reset_token.user
|
||||
|
||||
# Validate password strength
|
||||
if not is_valid_password(form.password.data):
|
||||
flash('Password must be at least 8 characters long and contain both letters and numbers.', 'error')
|
||||
return render_template('auth/reset_password_with_token.html', form=form, token=token)
|
||||
|
||||
# Update password
|
||||
user.set_password(form.password.data)
|
||||
|
||||
# Mark token as used
|
||||
reset_token.used_at = datetime.utcnow()
|
||||
reset_token.user_ip = request.environ.get('REMOTE_ADDR')
|
||||
|
||||
# Update request status
|
||||
if reset_token.request:
|
||||
reset_token.request.status = 'completed'
|
||||
reset_token.request.updated_at = datetime.utcnow()
|
||||
|
||||
db.session.commit()
|
||||
|
||||
flash('Your password has been reset successfully! You can now log in with your new password.', 'success')
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
return render_template('auth/reset_password_with_token.html', form=form, token=token, user=reset_token.user)
|
||||
|
||||
@@ -5,6 +5,7 @@ Provides HTML templates and endpoints for web-based chat
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify
|
||||
from flask_login import login_required, current_user
|
||||
from sqlalchemy import desc, and_
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from app.extensions import db
|
||||
from app.models import ChatRoom, ChatMessage, ChatParticipant, Post
|
||||
@@ -90,7 +91,109 @@ def create_room_form():
|
||||
desc(Post.created_at)
|
||||
).limit(20).all()
|
||||
|
||||
return render_template('chat/create_room.html', posts=recent_posts)
|
||||
# Check if a specific post was requested
|
||||
pre_selected_post = request.args.get('post_id')
|
||||
if pre_selected_post:
|
||||
try:
|
||||
pre_selected_post = int(pre_selected_post)
|
||||
except ValueError:
|
||||
pre_selected_post = None
|
||||
|
||||
return render_template('chat/create_room.html', posts=recent_posts, pre_selected_post=pre_selected_post)
|
||||
|
||||
|
||||
@chat.route('/create', methods=['POST'])
|
||||
@login_required
|
||||
def create_room():
|
||||
"""Create a new chat room"""
|
||||
room_name = request.form.get('room_name')
|
||||
description = request.form.get('description', '')
|
||||
room_type = request.form.get('room_type', 'general')
|
||||
is_private = bool(request.form.get('is_private'))
|
||||
related_post_id = request.form.get('related_post_id')
|
||||
|
||||
if not room_name:
|
||||
flash('Room name is required.', 'error')
|
||||
return redirect(url_for('chat.create_room_form'))
|
||||
|
||||
# Convert to integer if post ID is provided
|
||||
if related_post_id:
|
||||
try:
|
||||
related_post_id = int(related_post_id)
|
||||
# Verify the post exists
|
||||
related_post = Post.query.get(related_post_id)
|
||||
if not related_post:
|
||||
flash('Selected post does not exist.', 'error')
|
||||
return redirect(url_for('chat.create_room_form'))
|
||||
# If post is selected, set room type to post_discussion
|
||||
room_type = 'post_discussion'
|
||||
except ValueError:
|
||||
related_post_id = None
|
||||
else:
|
||||
related_post_id = None
|
||||
# If no post selected, ensure it's general discussion
|
||||
if room_type == 'post_discussion':
|
||||
room_type = 'general'
|
||||
|
||||
# Check if room name already exists
|
||||
existing_room = ChatRoom.query.filter_by(name=room_name).first()
|
||||
if existing_room:
|
||||
flash('A room with that name already exists.', 'error')
|
||||
return redirect(url_for('chat.create_room_form'))
|
||||
|
||||
try:
|
||||
# Create the room
|
||||
room = ChatRoom(
|
||||
name=room_name,
|
||||
description=description,
|
||||
room_type=room_type,
|
||||
is_private=is_private,
|
||||
is_active=True,
|
||||
created_by_id=current_user.id,
|
||||
related_post_id=related_post_id
|
||||
)
|
||||
db.session.add(room)
|
||||
db.session.flush() # Get the room ID
|
||||
|
||||
# Add creator as participant with admin role
|
||||
participant = ChatParticipant(
|
||||
room_id=room.id,
|
||||
user_id=current_user.id,
|
||||
role='admin',
|
||||
joined_at=datetime.utcnow()
|
||||
)
|
||||
db.session.add(participant)
|
||||
|
||||
# Add welcome message
|
||||
if related_post_id:
|
||||
welcome_content = f"Welcome to the discussion for '{related_post.title}'! This room was created by {current_user.nickname} to discuss this post."
|
||||
else:
|
||||
welcome_content = f"Welcome to {room_name}! This room was created by {current_user.nickname}."
|
||||
|
||||
welcome_message = ChatMessage(
|
||||
content=welcome_content,
|
||||
room_id=room.id,
|
||||
sender_id=current_user.id,
|
||||
is_system_message=True
|
||||
)
|
||||
db.session.add(welcome_message)
|
||||
|
||||
# Update room activity
|
||||
room.last_activity = datetime.utcnow()
|
||||
room.message_count = 1
|
||||
|
||||
db.session.commit()
|
||||
|
||||
if related_post_id:
|
||||
flash(f'Chat room "{room_name}" created successfully and linked to the post!', 'success')
|
||||
else:
|
||||
flash(f'Chat room "{room_name}" created successfully!', 'success')
|
||||
return redirect(url_for('chat.room', room_id=room.id))
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
flash(f'Error creating room: {str(e)}', 'error')
|
||||
return redirect(url_for('chat.create_room_form'))
|
||||
|
||||
@chat.route('/support')
|
||||
@login_required
|
||||
@@ -123,3 +226,51 @@ def embed_post_chat(post_id):
|
||||
return render_template('chat/embed.html',
|
||||
post=post,
|
||||
discussion_room=discussion_room)
|
||||
|
||||
|
||||
@chat.route('/post-discussions')
|
||||
@login_required
|
||||
def post_discussions():
|
||||
"""View all chat rooms related to posts"""
|
||||
page = request.args.get('page', 1, type=int)
|
||||
|
||||
# Get all rooms that are linked to posts
|
||||
post_rooms = ChatRoom.query.filter(
|
||||
ChatRoom.related_post_id.isnot(None),
|
||||
ChatRoom.is_active == True
|
||||
).join(Post).order_by(ChatRoom.last_activity.desc()).paginate(
|
||||
page=page, per_page=20, error_out=False
|
||||
)
|
||||
|
||||
# Get statistics
|
||||
total_post_discussions = ChatRoom.query.filter(
|
||||
ChatRoom.related_post_id.isnot(None),
|
||||
ChatRoom.is_active == True
|
||||
).count()
|
||||
|
||||
active_discussions = ChatRoom.query.filter(
|
||||
ChatRoom.related_post_id.isnot(None),
|
||||
ChatRoom.is_active == True,
|
||||
ChatRoom.last_activity >= datetime.utcnow() - timedelta(days=7)
|
||||
).count()
|
||||
|
||||
return render_template('chat/post_discussions.html',
|
||||
rooms=post_rooms,
|
||||
total_discussions=total_post_discussions,
|
||||
active_discussions=active_discussions)
|
||||
|
||||
|
||||
@chat.route('/post/<int:post_id>/discussions')
|
||||
@login_required
|
||||
def post_specific_discussions(post_id):
|
||||
"""View all chat rooms for a specific post"""
|
||||
post = Post.query.get_or_404(post_id)
|
||||
|
||||
# Get all rooms for this specific post
|
||||
rooms = ChatRoom.query.filter(
|
||||
ChatRoom.related_post_id == post_id,
|
||||
ChatRoom.is_active == True
|
||||
).order_by(ChatRoom.last_activity.desc()).all()
|
||||
|
||||
return render_template('chat/post_specific_discussions.html',
|
||||
post=post, rooms=rooms)
|
||||
|
||||
Reference in New Issue
Block a user