Inital commit
This commit is contained in:
177
splitchat/core/models.py
Normal file
177
splitchat/core/models.py
Normal file
@@ -0,0 +1,177 @@
|
||||
import uuid
|
||||
from decimal import Decimal
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
class ChatRoom(models.Model):
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
name = models.CharField(max_length=100)
|
||||
description = models.TextField(blank=True)
|
||||
invite_code = models.CharField(max_length=12, unique=True, blank=True)
|
||||
created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name='created_rooms')
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.invite_code:
|
||||
self.invite_code = uuid.uuid4().hex[:8].upper()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def member_count(self):
|
||||
return self.memberships.filter(is_active=True).count()
|
||||
|
||||
|
||||
class Membership(models.Model):
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='memberships')
|
||||
room = models.ForeignKey(ChatRoom, on_delete=models.CASCADE, related_name='memberships')
|
||||
joined_at = models.DateTimeField(auto_now_add=True)
|
||||
is_active = models.BooleanField(default=True)
|
||||
is_admin = models.BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
unique_together = ('user', 'room')
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user.username} in {self.room.name}"
|
||||
|
||||
|
||||
class Message(models.Model):
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
room = models.ForeignKey(ChatRoom, on_delete=models.CASCADE, related_name='messages')
|
||||
sender = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name='messages')
|
||||
content = models.TextField()
|
||||
msg_type = models.CharField(max_length=20, default='chat',
|
||||
choices=[('chat', 'Chat'), ('system', 'System'), ('event', 'Event'), ('expense', 'Expense')])
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['created_at']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.sender}: {self.content[:50]}"
|
||||
|
||||
|
||||
class Event(models.Model):
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
room = models.ForeignKey(ChatRoom, on_delete=models.CASCADE, related_name='events')
|
||||
created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name='created_events')
|
||||
title = models.CharField(max_length=200)
|
||||
description = models.TextField(blank=True)
|
||||
location = models.CharField(max_length=200, blank=True)
|
||||
event_date = models.DateTimeField(null=True, blank=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
is_settled = models.BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
ordering = ['-created_at']
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
@property
|
||||
def total_amount(self):
|
||||
return self.expenses.aggregate(total=models.Sum('amount'))['total'] or Decimal('0')
|
||||
|
||||
def compute_balances(self):
|
||||
"""Returns dict: {user_id: net_balance} positive=owed, negative=owes"""
|
||||
balances = {}
|
||||
for expense in self.expenses.prefetch_related('splits'):
|
||||
payer_id = expense.paid_by_id
|
||||
balances[payer_id] = balances.get(payer_id, Decimal('0')) + expense.amount
|
||||
for split in expense.splits.all():
|
||||
uid = split.user_id
|
||||
balances[uid] = balances.get(uid, Decimal('0')) - split.amount
|
||||
return balances
|
||||
|
||||
def compute_settlements(self):
|
||||
"""Simplify debts: returns list of (debtor_user, creditor_user, amount)"""
|
||||
balances = self.compute_balances()
|
||||
# Fetch users
|
||||
user_ids = list(balances.keys())
|
||||
users = {u.id: u for u in User.objects.filter(id__in=user_ids)}
|
||||
|
||||
debtors = sorted(
|
||||
[(uid, -bal) for uid, bal in balances.items() if bal < -Decimal('0.01')],
|
||||
key=lambda x: -x[1]
|
||||
)
|
||||
creditors = sorted(
|
||||
[(uid, bal) for uid, bal in balances.items() if bal > Decimal('0.01')],
|
||||
key=lambda x: -x[1]
|
||||
)
|
||||
|
||||
settlements = []
|
||||
i, j = 0, 0
|
||||
debtors = [[uid, amt] for uid, amt in debtors]
|
||||
creditors = [[uid, amt] for uid, amt in creditors]
|
||||
|
||||
while i < len(debtors) and j < len(creditors):
|
||||
debtor_id, debt = debtors[i]
|
||||
creditor_id, credit = creditors[j]
|
||||
amount = min(debt, credit)
|
||||
if amount > Decimal('0.01'):
|
||||
settlements.append((users.get(debtor_id), users.get(creditor_id), round(amount, 2)))
|
||||
debtors[i][1] -= amount
|
||||
creditors[j][1] -= amount
|
||||
if debtors[i][1] < Decimal('0.01'):
|
||||
i += 1
|
||||
if creditors[j][1] < Decimal('0.01'):
|
||||
j += 1
|
||||
|
||||
return settlements
|
||||
|
||||
|
||||
class Expense(models.Model):
|
||||
SPLIT_EQUAL = 'equal'
|
||||
SPLIT_CUSTOM = 'custom'
|
||||
SPLIT_CHOICES = [(SPLIT_EQUAL, 'Equal'), (SPLIT_CUSTOM, 'Custom')]
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name='expenses')
|
||||
description = models.CharField(max_length=200)
|
||||
amount = models.DecimalField(max_digits=10, decimal_places=2)
|
||||
paid_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name='paid_expenses')
|
||||
split_type = models.CharField(max_length=10, choices=SPLIT_CHOICES, default=SPLIT_EQUAL)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['-created_at']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.description}: ${self.amount}"
|
||||
|
||||
|
||||
class ExpenseSplit(models.Model):
|
||||
expense = models.ForeignKey(Expense, on_delete=models.CASCADE, related_name='splits')
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='expense_splits')
|
||||
amount = models.DecimalField(max_digits=10, decimal_places=2)
|
||||
|
||||
class Meta:
|
||||
unique_together = ('expense', 'user')
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user.username}: ${self.amount}"
|
||||
|
||||
|
||||
class UserProfile(models.Model):
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
|
||||
display_name = models.CharField(max_length=100, blank=True)
|
||||
avatar_color = models.CharField(max_length=7, default='#6366f1')
|
||||
|
||||
def __str__(self):
|
||||
return f"Profile({self.user.username})"
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.display_name or self.user.get_full_name() or self.user.username
|
||||
|
||||
def get_avatar_initials(self):
|
||||
name = self.name
|
||||
parts = name.split()
|
||||
if len(parts) >= 2:
|
||||
return (parts[0][0] + parts[-1][0]).upper()
|
||||
return name[:2].upper()
|
||||
Reference in New Issue
Block a user