Files
clovening/splitchat/core/views.py
MonkeyStrongTogether 87ec426fed Inital commit
2026-05-22 17:48:03 +02:00

349 lines
13 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import json
from decimal import Decimal
from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth import login, logout, authenticate
from django.contrib.auth.decorators import login_required
from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth.models import User
from django.contrib import messages
from django.http import JsonResponse
from django.views.decorators.http import require_POST, require_GET
from django.db.models import Sum, Q
from django.utils import timezone
from .models import ChatRoom, Membership, Message, Event, Expense, ExpenseSplit, UserProfile
from .forms import SignUpForm, ChatRoomForm, EventForm, ExpenseForm
AVATAR_COLORS = [
'#6366f1', '#8b5cf6', '#ec4899', '#f43f5e',
'#f97316', '#eab308', '#22c55e', '#14b8a6', '#3b82f6'
]
def get_or_create_profile(user):
profile, created = UserProfile.objects.get_or_create(user=user)
if created:
color_idx = user.id % len(AVATAR_COLORS)
profile.avatar_color = AVATAR_COLORS[color_idx]
profile.save()
return profile
# ─── Auth ───────────────────────────────────────────────────────────────────
def signup_view(request):
if request.user.is_authenticated:
return redirect('dashboard')
form = SignUpForm(request.POST or None)
if request.method == 'POST' and form.is_valid():
user = form.save()
profile = get_or_create_profile(user)
login(request, user)
messages.success(request, f'Welcome, {user.username}!')
return redirect('dashboard')
return render(request, 'core/auth.html', {'form': form, 'mode': 'signup'})
def login_view(request):
if request.user.is_authenticated:
return redirect('dashboard')
form = AuthenticationForm(request, request.POST or None)
if request.method == 'POST' and form.is_valid():
user = form.get_user()
get_or_create_profile(user)
login(request, user)
return redirect(request.GET.get('next', 'dashboard'))
return render(request, 'core/auth.html', {'form': form, 'mode': 'login'})
@login_required
def logout_view(request):
logout(request)
return redirect('login')
# ─── Dashboard ───────────────────────────────────────────────────────────────
@login_required
def dashboard(request):
profile = get_or_create_profile(request.user)
rooms = ChatRoom.objects.filter(
memberships__user=request.user, memberships__is_active=True
).prefetch_related('memberships')
upcoming_events = Event.objects.filter(
room__memberships__user=request.user,
room__memberships__is_active=True,
is_settled=False,
).select_related('room').order_by('event_date', '-created_at')[:10]
# Compute overall balance
total_owed = Decimal('0') # others owe me
total_owing = Decimal('0') # I owe others
events_with_balance = Event.objects.filter(
room__memberships__user=request.user,
room__memberships__is_active=True,
is_settled=False,
)
for event in events_with_balance:
balances = event.compute_balances()
bal = balances.get(request.user.id, Decimal('0'))
if bal > 0:
total_owed += bal
else:
total_owing += abs(bal)
return render(request, 'core/dashboard.html', {
'rooms': rooms,
'upcoming_events': upcoming_events,
'total_owed': total_owed,
'total_owing': total_owing,
'net_balance': total_owed - total_owing,
'profile': profile,
})
# ─── Rooms ───────────────────────────────────────────────────────────────────
@login_required
def room_create(request):
form = ChatRoomForm(request.POST or None)
if request.method == 'POST' and form.is_valid():
room = form.save(commit=False)
room.created_by = request.user
room.save()
Membership.objects.create(user=request.user, room=room, is_admin=True)
messages.success(request, f'Room "{room.name}" created!')
return redirect('room_detail', room_id=str(room.id))
return render(request, 'core/room_form.html', {'form': form, 'action': 'Create'})
@login_required
def room_join(request):
if request.method == 'POST':
code = request.POST.get('invite_code', '').strip().upper()
try:
room = ChatRoom.objects.get(invite_code=code)
membership, created = Membership.objects.get_or_create(
user=request.user, room=room,
defaults={'is_active': True}
)
if not created and not membership.is_active:
membership.is_active = True
membership.save()
messages.success(request, f'Joined "{room.name}"!')
return redirect('room_detail', room_id=str(room.id))
except ChatRoom.DoesNotExist:
messages.error(request, 'Invalid invite code.')
return render(request, 'core/room_join.html')
@login_required
def room_join_link(request, invite_code):
room = get_object_or_404(ChatRoom, invite_code=invite_code)
membership, created = Membership.objects.get_or_create(
user=request.user, room=room, defaults={'is_active': True}
)
if not created and not membership.is_active:
membership.is_active = True
membership.save()
return redirect('room_detail', room_id=str(room.id))
@login_required
def room_detail(request, room_id):
room = get_object_or_404(ChatRoom, id=room_id)
membership = get_object_or_404(Membership, user=request.user, room=room, is_active=True)
profile = get_or_create_profile(request.user)
messages_qs = Message.objects.filter(room=room).select_related('sender', 'sender__profile').order_by('created_at')
members = User.objects.filter(memberships__room=room, memberships__is_active=True).select_related('profile')
events = room.events.select_related('created_by').all()
# Enrich members with profiles
for m in members:
get_or_create_profile(m)
return render(request, 'core/room_detail.html', {
'room': room,
'membership': membership,
'messages': messages_qs,
'members': members,
'events': events,
'profile': profile,
'user': request.user,
})
# ─── Events ──────────────────────────────────────────────────────────────────
@login_required
def event_create(request, room_id):
room = get_object_or_404(ChatRoom, id=room_id)
get_object_or_404(Membership, user=request.user, room=room, is_active=True)
form = EventForm(request.POST or None)
if request.method == 'POST' and form.is_valid():
event = form.save(commit=False)
event.room = room
event.created_by = request.user
event.save()
# Send system message to room
msg = Message.objects.create(
room=room, sender=request.user,
content=f'📅 Created event: **{event.title}**',
msg_type='event'
)
_broadcast_room_update(room_id, 'new_event', {
'event_id': str(event.id),
'event_title': event.title,
'created_by': request.user.username,
})
messages.success(request, f'Event "{event.title}" created!')
return redirect('event_detail', room_id=str(room.id), event_id=str(event.id))
return render(request, 'core/event_form.html', {'form': form, 'room': room})
@login_required
def event_detail(request, room_id, event_id):
room = get_object_or_404(ChatRoom, id=room_id)
get_object_or_404(Membership, user=request.user, room=room, is_active=True)
event = get_object_or_404(Event, id=event_id, room=room)
expenses = event.expenses.select_related('paid_by').prefetch_related('splits__user')
members = User.objects.filter(memberships__room=room, memberships__is_active=True)
for m in members:
get_or_create_profile(m)
balances = event.compute_balances()
settlements = event.compute_settlements()
expense_form = ExpenseForm(room=room)
# Enrich balances with user objects
balance_display = []
for uid, amount in balances.items():
try:
u = User.objects.select_related('profile').get(id=uid)
get_or_create_profile(u)
balance_display.append({'user': u, 'amount': amount})
except User.DoesNotExist:
pass
return render(request, 'core/event_detail.html', {
'room': room,
'event': event,
'expenses': expenses,
'members': members,
'balance_display': sorted(balance_display, key=lambda x: x['amount'], reverse=True),
'settlements': settlements,
'expense_form': expense_form,
'profile': get_or_create_profile(request.user),
})
@login_required
@require_POST
def expense_create(request, room_id, event_id):
room = get_object_or_404(ChatRoom, id=room_id)
get_object_or_404(Membership, user=request.user, room=room, is_active=True)
event = get_object_or_404(Event, id=event_id, room=room)
form = ExpenseForm(request.POST, room=room)
if form.is_valid():
amount = form.cleaned_data['amount']
paid_by = form.cleaned_data['paid_by']
participants = form.cleaned_data['participants']
split_type = form.cleaned_data['split_type']
description = form.cleaned_data['description']
expense = Expense.objects.create(
event=event,
description=description,
amount=amount,
paid_by=paid_by,
split_type=split_type,
)
if split_type == Expense.SPLIT_EQUAL:
per_person = (amount / len(participants)).quantize(Decimal('0.01'))
for participant in participants:
ExpenseSplit.objects.create(expense=expense, user=participant, amount=per_person)
else:
# Custom: equal for now, can be extended
per_person = (amount / len(participants)).quantize(Decimal('0.01'))
for participant in participants:
custom_key = f'custom_{participant.id}'
custom_amount = request.POST.get(custom_key)
if custom_amount:
try:
amt = Decimal(custom_amount)
except Exception:
amt = per_person
else:
amt = per_person
ExpenseSplit.objects.create(expense=expense, user=participant, amount=amt)
_broadcast_room_update(room_id, 'new_expense', {
'event_id': str(event.id),
'event_title': event.title,
'expense_description': description,
'amount': str(amount),
'paid_by': paid_by.username,
})
messages.success(request, f'Expense "${amount} {description}" added!')
else:
messages.error(request, 'Invalid expense data.')
return redirect('event_detail', room_id=str(room.id), event_id=str(event.id))
@login_required
@require_POST
def event_settle(request, room_id, event_id):
room = get_object_or_404(ChatRoom, id=room_id)
get_object_or_404(Membership, user=request.user, room=room, is_active=True)
event = get_object_or_404(Event, id=event_id, room=room)
event.is_settled = True
event.save()
messages.success(request, f'Event "{event.title}" marked as settled!')
return redirect('event_detail', room_id=str(room.id), event_id=str(event.id))
# ─── API endpoints ───────────────────────────────────────────────────────────
@login_required
def api_room_messages(request, room_id):
room = get_object_or_404(ChatRoom, id=room_id)
get_object_or_404(Membership, user=request.user, room=room, is_active=True)
msgs = Message.objects.filter(room=room).select_related('sender', 'sender__profile').order_by('-created_at')[:50]
data = [{
'id': str(m.id),
'content': m.content,
'sender': m.sender.username if m.sender else 'deleted',
'display_name': (m.sender.profile.name if hasattr(m.sender, 'profile') else m.sender.username) if m.sender else 'deleted',
'timestamp': m.created_at.strftime('%H:%M'),
'type': m.msg_type,
} for m in reversed(list(msgs))]
return JsonResponse({'messages': data})
# ─── Helpers ─────────────────────────────────────────────────────────────────
def _broadcast_room_update(room_id, update_type, payload):
channel_layer = get_channel_layer()
if channel_layer:
try:
async_to_sync(channel_layer.group_send)(
f'chat_{room_id}',
{'type': 'room_update', 'update_type': update_type, **payload}
)
except Exception:
pass