Inital commit

This commit is contained in:
MonkeyStrongTogether
2026-05-22 17:48:03 +02:00
parent b691418e68
commit 87ec426fed
36 changed files with 2286 additions and 0 deletions

348
splitchat/core/views.py Normal file
View File

@@ -0,0 +1,348 @@
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