600 lines
25 KiB
Python
600 lines
25 KiB
Python
from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify, send_file
|
|
from flask_login import login_required, current_user
|
|
from models import db, Conversation, User, Message, MessageAttachment, DocuPulseSettings
|
|
from forms import ConversationForm
|
|
from routes.auth import require_password_change
|
|
from utils import log_event, create_notification, get_unread_count
|
|
import os
|
|
from werkzeug.utils import secure_filename
|
|
from datetime import datetime
|
|
|
|
conversations_bp = Blueprint('conversations', __name__, url_prefix='/conversations')
|
|
|
|
# Configure upload settings
|
|
UPLOAD_FOLDER = os.environ.get('UPLOAD_FOLDER', 'uploads')
|
|
ALLOWED_EXTENSIONS = {
|
|
# Documents
|
|
'pdf', 'docx', 'doc', 'txt', 'rtf', 'odt', 'md', 'csv',
|
|
# Spreadsheets
|
|
'xlsx', 'xls', 'ods', 'xlsm',
|
|
# Presentations
|
|
'pptx', 'ppt', 'odp',
|
|
# Images
|
|
'jpg', 'jpeg', 'png', 'gif', 'bmp', 'svg', 'webp', 'tiff',
|
|
# Archives
|
|
'zip', 'rar', '7z', 'tar', 'gz',
|
|
# Code/Text
|
|
'py', 'js', 'html', 'css', 'json', 'xml', 'sql', 'sh', 'bat',
|
|
# Audio
|
|
'mp3', 'wav', 'ogg', 'm4a', 'flac',
|
|
# Video
|
|
'mp4', 'avi', 'mov', 'wmv', 'flv', 'mkv', 'webm',
|
|
# CAD/Design
|
|
'dwg', 'dxf', 'ai', 'psd', 'eps', 'indd',
|
|
# Other
|
|
'eml', 'msg', 'vcf', 'ics'
|
|
}
|
|
MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB
|
|
|
|
def allowed_file(filename):
|
|
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
|
|
|
|
def get_file_extension(filename):
|
|
return filename.rsplit('.', 1)[1].lower() if '.' in filename else ''
|
|
|
|
@conversations_bp.route('/')
|
|
@login_required
|
|
@require_password_change
|
|
def conversations():
|
|
search = request.args.get('search', '').strip()
|
|
if current_user.is_admin:
|
|
query = Conversation.query
|
|
else:
|
|
query = Conversation.query.filter(Conversation.members.any(id=current_user.id))
|
|
if search:
|
|
query = query.filter(Conversation.name.ilike(f'%{search}%'))
|
|
conversations = query.order_by(Conversation.created_at.desc()).all()
|
|
unread_count = get_unread_count(current_user.id)
|
|
usage_stats = DocuPulseSettings.get_usage_stats()
|
|
return render_template('conversations/conversations.html', conversations=conversations, search=search, unread_notifications=unread_count, usage_stats=usage_stats)
|
|
|
|
@conversations_bp.route('/create', methods=['GET', 'POST'])
|
|
@login_required
|
|
@require_password_change
|
|
def create_conversation():
|
|
if not (current_user.is_admin or current_user.is_manager):
|
|
flash('Only administrators and managers can create conversations.', 'error')
|
|
return redirect(url_for('conversations.conversations'))
|
|
|
|
form = ConversationForm()
|
|
if form.validate_on_submit():
|
|
conversation = Conversation(
|
|
name=form.name.data,
|
|
description=form.description.data,
|
|
created_by=current_user.id
|
|
)
|
|
|
|
# Add creator as a member
|
|
conversation.members.append(current_user)
|
|
creator_id = current_user.id
|
|
# Add selected members, skipping the creator if present
|
|
for user_id in form.members.data:
|
|
if int(user_id) != creator_id:
|
|
user = User.query.get(user_id)
|
|
if user and user not in conversation.members:
|
|
conversation.members.append(user)
|
|
|
|
db.session.add(conversation)
|
|
db.session.commit()
|
|
|
|
# Create notifications for all members except the creator
|
|
for member in conversation.members:
|
|
if member.id != current_user.id:
|
|
create_notification(
|
|
notif_type='conversation_invite',
|
|
user_id=member.id,
|
|
sender_id=current_user.id,
|
|
details={
|
|
'message': f'You have been added to conversation "{conversation.name}"',
|
|
'conversation_id': conversation.id,
|
|
'conversation_name': conversation.name,
|
|
'invited_by': f"{current_user.username} {current_user.last_name}",
|
|
'timestamp': datetime.utcnow().isoformat()
|
|
}
|
|
)
|
|
|
|
# Log conversation creation
|
|
log_event(
|
|
event_type='conversation_create',
|
|
details={
|
|
'conversation_id': conversation.id,
|
|
'created_by': current_user.id,
|
|
'created_by_name': f"{current_user.username} {current_user.last_name}",
|
|
'name': conversation.name,
|
|
'description': conversation.description,
|
|
'member_count': len(conversation.members),
|
|
'member_ids': [member.id for member in conversation.members]
|
|
}
|
|
)
|
|
db.session.commit()
|
|
|
|
flash('Conversation created successfully!', 'success')
|
|
return redirect(url_for('conversations.conversations'))
|
|
unread_count = get_unread_count(current_user.id)
|
|
return render_template('conversations/create_conversation.html', form=form, unread_notifications=unread_count)
|
|
|
|
@conversations_bp.route('/<int:conversation_id>')
|
|
@login_required
|
|
@require_password_change
|
|
def conversation(conversation_id):
|
|
conversation = Conversation.query.get_or_404(conversation_id)
|
|
# Check if user is a member
|
|
if not current_user.is_admin and current_user not in conversation.members:
|
|
flash('You do not have access to this conversation.', 'error')
|
|
return redirect(url_for('conversations.conversations'))
|
|
|
|
# Log conversation open
|
|
log_event(
|
|
event_type='conversation_open',
|
|
details={
|
|
'conversation_id': conversation.id,
|
|
'conversation_name': conversation.name,
|
|
'user_id': current_user.id,
|
|
'user_name': f"{current_user.username} {current_user.last_name}",
|
|
'message_count': Message.query.filter_by(conversation_id=conversation_id).count()
|
|
}
|
|
)
|
|
db.session.commit()
|
|
|
|
# Query messages directly using the Message model
|
|
messages = Message.query.filter_by(conversation_id=conversation_id).order_by(Message.created_at.asc()).all()
|
|
|
|
# Get all users for member selection (needed for admin and manager)
|
|
all_users = User.query.all() if (current_user.is_admin or current_user.is_manager) else None
|
|
|
|
unread_count = get_unread_count(current_user.id)
|
|
return render_template('conversations/conversation.html',
|
|
conversation=conversation,
|
|
messages=messages,
|
|
all_users=all_users,
|
|
unread_notifications=unread_count)
|
|
|
|
@conversations_bp.route('/<int:conversation_id>/members')
|
|
@login_required
|
|
@require_password_change
|
|
def conversation_members(conversation_id):
|
|
conversation = Conversation.query.get_or_404(conversation_id)
|
|
if not current_user.is_admin and current_user not in conversation.members:
|
|
flash('You do not have access to this conversation.', 'error')
|
|
return redirect(url_for('conversations.conversations'))
|
|
|
|
if not (current_user.is_admin or current_user.is_manager):
|
|
flash('Only administrators and managers can manage conversation members.', 'error')
|
|
return redirect(url_for('conversations.conversation', conversation_id=conversation_id))
|
|
|
|
available_users = User.query.filter(~User.id.in_([m.id for m in conversation.members])).all()
|
|
unread_count = get_unread_count(current_user.id)
|
|
return render_template('conversations/conversation_members.html',
|
|
conversation=conversation,
|
|
available_users=available_users,
|
|
unread_notifications=unread_count)
|
|
|
|
@conversations_bp.route('/<int:conversation_id>/members/add', methods=['POST'])
|
|
@login_required
|
|
@require_password_change
|
|
def add_member(conversation_id):
|
|
conversation = Conversation.query.get_or_404(conversation_id)
|
|
if not current_user.is_admin:
|
|
flash('Only administrators can manage conversation members.', 'error')
|
|
return redirect(url_for('conversations.conversation', conversation_id=conversation_id))
|
|
|
|
user_id = request.form.get('user_id')
|
|
if not user_id:
|
|
flash('Please select a user to add.', 'error')
|
|
return redirect(url_for('conversations.conversation_members', conversation_id=conversation_id))
|
|
|
|
user = User.query.get_or_404(user_id)
|
|
if user in conversation.members:
|
|
flash('User is already a member of this conversation.', 'error')
|
|
else:
|
|
conversation.members.append(user)
|
|
db.session.commit()
|
|
|
|
# Create notification for the invited user
|
|
create_notification(
|
|
notif_type='conversation_invite',
|
|
user_id=user.id,
|
|
sender_id=current_user.id,
|
|
details={
|
|
'message': f'You have been invited to join conversation "{conversation.name}"',
|
|
'conversation_id': conversation.id,
|
|
'conversation_name': conversation.name,
|
|
'invited_by': f"{current_user.username} {current_user.last_name}",
|
|
'timestamp': datetime.utcnow().isoformat()
|
|
}
|
|
)
|
|
|
|
# Log member addition
|
|
log_event(
|
|
event_type='conversation_member_add',
|
|
details={
|
|
'conversation_id': conversation.id,
|
|
'conversation_name': conversation.name,
|
|
'added_by': current_user.id,
|
|
'added_by_name': f"{current_user.username} {current_user.last_name}",
|
|
'added_user_id': user.id,
|
|
'added_user_name': f"{user.username} {user.last_name}",
|
|
'added_user_email': user.email
|
|
}
|
|
)
|
|
db.session.commit()
|
|
|
|
flash(f'{user.username} has been added to the conversation.', 'success')
|
|
|
|
return redirect(url_for('conversations.conversation_members', conversation_id=conversation_id))
|
|
|
|
@conversations_bp.route('/<int:conversation_id>/members/<int:user_id>/remove', methods=['POST'])
|
|
@login_required
|
|
@require_password_change
|
|
def remove_member(conversation_id, user_id):
|
|
conversation = Conversation.query.get_or_404(conversation_id)
|
|
if not current_user.is_admin:
|
|
flash('Only administrators can manage conversation members.', 'error')
|
|
return redirect(url_for('conversations.conversation', conversation_id=conversation_id))
|
|
|
|
if user_id == conversation.created_by:
|
|
flash('Cannot remove the conversation creator.', 'error')
|
|
else:
|
|
user = User.query.get_or_404(user_id)
|
|
if user not in conversation.members:
|
|
flash('User is not a member of this conversation.', 'error')
|
|
else:
|
|
conversation.members.remove(user)
|
|
db.session.commit()
|
|
|
|
# Create notification for the removed user
|
|
create_notification(
|
|
notif_type='conversation_invite_removed',
|
|
user_id=user.id,
|
|
sender_id=current_user.id,
|
|
details={
|
|
'message': f'You have been removed from conversation "{conversation.name}"',
|
|
'conversation_id': conversation.id,
|
|
'conversation_name': conversation.name,
|
|
'removed_by': f"{current_user.username} {current_user.last_name}",
|
|
'timestamp': datetime.utcnow().isoformat()
|
|
}
|
|
)
|
|
|
|
# Log member removal
|
|
log_event(
|
|
event_type='conversation_member_remove',
|
|
details={
|
|
'conversation_id': conversation.id,
|
|
'conversation_name': conversation.name,
|
|
'removed_by': current_user.id,
|
|
'removed_by_name': f"{current_user.username} {current_user.last_name}",
|
|
'removed_user_id': user.id,
|
|
'removed_user_name': f"{user.username} {user.last_name}",
|
|
'removed_user_email': user.email
|
|
}
|
|
)
|
|
db.session.commit()
|
|
|
|
flash('User has been removed from the conversation.', 'success')
|
|
|
|
return redirect(url_for('conversations.conversation_members', conversation_id=conversation_id))
|
|
|
|
@conversations_bp.route('/<int:conversation_id>/edit', methods=['GET', 'POST'])
|
|
@login_required
|
|
@require_password_change
|
|
def edit_conversation(conversation_id):
|
|
if not current_user.is_admin:
|
|
flash('Only administrators can edit conversations.', 'error')
|
|
return redirect(url_for('conversations.conversations'))
|
|
conversation = Conversation.query.get_or_404(conversation_id)
|
|
form = ConversationForm(obj=conversation)
|
|
|
|
if request.method == 'POST':
|
|
# Store old values for comparison
|
|
old_values = {
|
|
'name': conversation.name,
|
|
'description': conversation.description,
|
|
'member_ids': [member.id for member in conversation.members]
|
|
}
|
|
|
|
# Update name and description from form data
|
|
conversation.name = form.name.data
|
|
conversation.description = form.description.data
|
|
|
|
# Get members from the form data and convert to integers
|
|
member_ids = [int(id) for id in request.form.getlist('members')]
|
|
|
|
# Update members
|
|
current_member_ids = {user.id for user in conversation.members}
|
|
new_member_ids = set(member_ids)
|
|
|
|
# Remove members that are no longer in the list
|
|
members_to_remove = current_member_ids - new_member_ids
|
|
for member_id in members_to_remove:
|
|
if member_id != conversation.created_by: # Don't remove the creator
|
|
user = User.query.get(member_id)
|
|
if user:
|
|
conversation.members.remove(user)
|
|
# Create notification for removed user
|
|
create_notification(
|
|
notif_type='conversation_invite_removed',
|
|
user_id=user.id,
|
|
sender_id=current_user.id,
|
|
details={
|
|
'message': f'You have been removed from conversation "{conversation.name}"',
|
|
'conversation_id': conversation.id,
|
|
'conversation_name': conversation.name,
|
|
'removed_by': f"{current_user.username} {current_user.last_name}",
|
|
'timestamp': datetime.utcnow().isoformat()
|
|
}
|
|
)
|
|
|
|
# Add new members
|
|
members_to_add = new_member_ids - current_member_ids
|
|
for member_id in members_to_add:
|
|
user = User.query.get(member_id)
|
|
if user and user not in conversation.members:
|
|
conversation.members.append(user)
|
|
# Create notification for the invited user
|
|
create_notification(
|
|
notif_type='conversation_invite',
|
|
user_id=user.id,
|
|
sender_id=current_user.id,
|
|
details={
|
|
'message': f'You have been invited to join conversation "{conversation.name}"',
|
|
'conversation_id': conversation.id,
|
|
'conversation_name': conversation.name,
|
|
'invited_by': f"{current_user.username} {current_user.last_name}",
|
|
'timestamp': datetime.utcnow().isoformat()
|
|
}
|
|
)
|
|
|
|
db.session.commit()
|
|
|
|
# Log conversation update
|
|
log_event(
|
|
event_type='conversation_update',
|
|
details={
|
|
'conversation_id': conversation.id,
|
|
'updated_by': current_user.id,
|
|
'updated_by_name': f"{current_user.username} {current_user.last_name}",
|
|
'old_values': old_values,
|
|
'new_values': {
|
|
'name': conversation.name,
|
|
'description': conversation.description,
|
|
'member_ids': [member.id for member in conversation.members],
|
|
'member_names': [f"{member.username} {member.last_name}" for member in conversation.members]
|
|
}
|
|
}
|
|
)
|
|
db.session.commit()
|
|
|
|
flash('Conversation updated successfully!', 'success')
|
|
|
|
# Check if redirect parameter is provided
|
|
redirect_url = request.args.get('redirect')
|
|
if redirect_url:
|
|
return redirect(redirect_url)
|
|
return redirect(url_for('conversations.conversations'))
|
|
|
|
# Prepopulate form members with current members
|
|
form.members.data = [str(user.id) for user in conversation.members]
|
|
unread_count = get_unread_count(current_user.id)
|
|
return render_template('conversations/create_conversation.html', form=form, edit_mode=True, conversation=conversation, unread_notifications=unread_count)
|
|
|
|
@conversations_bp.route('/<int:conversation_id>/delete', methods=['POST'])
|
|
@login_required
|
|
@require_password_change
|
|
def delete_conversation(conversation_id):
|
|
if not current_user.is_admin:
|
|
flash('Only administrators can delete conversations.', 'error')
|
|
return redirect(url_for('conversations.conversations'))
|
|
|
|
conversation = Conversation.query.get_or_404(conversation_id)
|
|
|
|
# Log conversation deletion
|
|
log_event(
|
|
event_type='conversation_delete',
|
|
details={
|
|
'conversation_id': conversation.id,
|
|
'conversation_name': conversation.name,
|
|
'deleted_by': current_user.id,
|
|
'deleted_by_name': f"{current_user.username} {current_user.last_name}",
|
|
'member_count': len(conversation.members),
|
|
'message_count': Message.query.filter_by(conversation_id=conversation_id).count()
|
|
}
|
|
)
|
|
db.session.commit()
|
|
|
|
# Get all messages in the conversation
|
|
messages = Message.query.filter_by(conversation_id=conversation_id).all()
|
|
|
|
# Delete attachments and their files first
|
|
for message in messages:
|
|
for attachment in message.attachments:
|
|
# Delete the physical file
|
|
if os.path.exists(attachment.path):
|
|
os.remove(attachment.path)
|
|
# Delete the attachment record
|
|
db.session.delete(attachment)
|
|
|
|
# Delete all messages in the conversation
|
|
for message in messages:
|
|
db.session.delete(message)
|
|
|
|
# Delete the conversation
|
|
db.session.delete(conversation)
|
|
db.session.commit()
|
|
|
|
flash('Conversation has been deleted successfully.', 'success')
|
|
return redirect(url_for('conversations.conversations'))
|
|
|
|
@conversations_bp.route('/<int:conversation_id>/messages', methods=['GET'])
|
|
@login_required
|
|
@require_password_change
|
|
def get_messages(conversation_id):
|
|
conversation = Conversation.query.get_or_404(conversation_id)
|
|
|
|
# Check if user is a member
|
|
if not current_user.is_admin and current_user not in conversation.members:
|
|
return jsonify({'error': 'Unauthorized'}), 403
|
|
|
|
# Get the last message ID from the request
|
|
last_message_id = request.args.get('last_message_id', type=int)
|
|
|
|
# Query for new messages
|
|
query = Message.query.filter_by(conversation_id=conversation_id)
|
|
if last_message_id:
|
|
query = query.filter(Message.id > last_message_id)
|
|
|
|
messages = query.order_by(Message.created_at.asc()).all()
|
|
|
|
# Format messages for response
|
|
message_data = [{
|
|
'id': message.id,
|
|
'content': message.content,
|
|
'created_at': message.created_at.strftime('%b %d, %Y %H:%M'),
|
|
'sender_id': str(message.user_id),
|
|
'sender_name': f"{message.user.username} {message.user.last_name}",
|
|
'attachments': [{
|
|
'name': attachment.name,
|
|
'size': attachment.size,
|
|
'url': url_for('conversations.download_attachment', message_id=message.id, attachment_index=index)
|
|
} for index, attachment in enumerate(message.attachments)]
|
|
} for message in messages]
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'messages': message_data
|
|
})
|
|
|
|
@conversations_bp.route('/<int:conversation_id>/send_message', methods=['POST'])
|
|
@login_required
|
|
@require_password_change
|
|
def send_message(conversation_id):
|
|
conversation = Conversation.query.get_or_404(conversation_id)
|
|
|
|
# Check if user is a member
|
|
if not current_user.is_admin and current_user not in conversation.members:
|
|
return jsonify({'error': 'Unauthorized'}), 403
|
|
|
|
message_content = request.form.get('message', '').strip()
|
|
file_count = int(request.form.get('file_count', 0))
|
|
|
|
if not message_content and file_count == 0:
|
|
return jsonify({'error': 'Message cannot be empty'}), 400
|
|
|
|
# Create the message
|
|
message = Message(
|
|
content=message_content,
|
|
user_id=current_user.id,
|
|
conversation_id=conversation_id
|
|
)
|
|
db.session.add(message)
|
|
db.session.flush() # Get the message ID
|
|
|
|
# Handle file attachments
|
|
attachments = []
|
|
for i in range(file_count):
|
|
file_key = f'file_{i}'
|
|
if file_key in request.files:
|
|
file = request.files[file_key]
|
|
if file and allowed_file(file.filename):
|
|
filename = secure_filename(file.filename)
|
|
file_path = os.path.join(UPLOAD_FOLDER, filename)
|
|
file.save(file_path)
|
|
|
|
attachment = MessageAttachment(
|
|
message_id=message.id,
|
|
name=filename,
|
|
path=file_path,
|
|
size=os.path.getsize(file_path)
|
|
)
|
|
db.session.add(attachment)
|
|
attachments.append(attachment)
|
|
|
|
db.session.commit()
|
|
|
|
# Create notifications for all conversation members except the sender
|
|
for member in conversation.members:
|
|
if member.id != current_user.id:
|
|
create_notification(
|
|
notif_type='conversation_message',
|
|
user_id=member.id,
|
|
sender_id=current_user.id,
|
|
details={
|
|
'message': f'New message in conversation "{conversation.name}"',
|
|
'conversation_id': conversation.id,
|
|
'conversation_name': conversation.name,
|
|
'sender': f"{current_user.username} {current_user.last_name}",
|
|
'message_preview': message_content[:100] + ('...' if len(message_content) > 100 else ''),
|
|
'has_attachments': len(attachments) > 0,
|
|
'attachment_count': len(attachments),
|
|
'timestamp': datetime.utcnow().isoformat()
|
|
}
|
|
)
|
|
|
|
# Log message creation
|
|
log_event(
|
|
event_type='message_create',
|
|
details={
|
|
'message_id': message.id,
|
|
'conversation_id': conversation_id,
|
|
'conversation_name': conversation.name,
|
|
'sender_id': current_user.id,
|
|
'sender_name': f"{current_user.username} {current_user.last_name}",
|
|
'has_attachments': len(attachments) > 0,
|
|
'attachment_count': len(attachments),
|
|
'attachment_types': [get_file_extension(att.name) for att in attachments] if attachments else []
|
|
}
|
|
)
|
|
db.session.commit()
|
|
|
|
# Prepare message data for response
|
|
message_data = {
|
|
'id': message.id,
|
|
'content': message.content,
|
|
'created_at': message.created_at.strftime('%b %d, %Y %H:%M'),
|
|
'sender_id': str(current_user.id),
|
|
'sender_name': f"{current_user.username} {current_user.last_name}",
|
|
'sender_avatar': url_for('profile_pic', filename=current_user.profile_picture) if current_user.profile_picture else url_for('static', filename='default-avatar.png'),
|
|
'attachments': [{
|
|
'name': attachment.name,
|
|
'size': attachment.size,
|
|
'url': url_for('conversations.download_attachment', message_id=message.id, attachment_index=index)
|
|
} for index, attachment in enumerate(attachments)]
|
|
}
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'message': message_data
|
|
})
|
|
|
|
@conversations_bp.route('/messages/<int:message_id>/attachment/<int:attachment_index>')
|
|
@login_required
|
|
@require_password_change
|
|
def download_attachment(message_id, attachment_index):
|
|
message = Message.query.get_or_404(message_id)
|
|
conversation = message.conversation
|
|
|
|
# Check if user is a member
|
|
if not current_user.is_admin and current_user not in conversation.members:
|
|
flash('You do not have access to this file.', 'error')
|
|
return redirect(url_for('conversations.conversation', conversation_id=conversation.id))
|
|
|
|
try:
|
|
attachment = message.attachments[attachment_index]
|
|
return send_file(
|
|
attachment.path,
|
|
as_attachment=True,
|
|
download_name=attachment.name
|
|
)
|
|
except (IndexError, Exception) as e:
|
|
flash('File not found.', 'error')
|
|
return redirect(url_for('conversations.conversation', conversation_id=conversation.id)) |