Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion pavilion/asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,23 @@
import os

from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'pavilion.settings')

application = get_asgi_application()
# Initialize Django ASGI application early to ensure the AppRegistry
# is populated before importing code that may import ORM models.
django_asgi_app = get_asgi_application()

from pos_server.routing import websocket_urlpatterns

application = ProtocolTypeRouter({
"http": django_asgi_app,
"websocket": AuthMiddlewareStack(
URLRouter(
websocket_urlpatterns
)
),
})

12 changes: 12 additions & 0 deletions pavilion/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,14 @@

# Installed applications
INSTALLED_APPS = [
'daphne', # ASGI server for Django Channels
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'channels', # WebSocket support
'pos_server.apps.PosServerConfig', # POS system module
'webrtc', # WebRTC communication
'app_switcher', # App switcher page module
Expand Down Expand Up @@ -296,4 +298,14 @@
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'LOCATION': 'unique-snowflake', # Any unique identifier; it differentiates caches if you have multiple ones
}
}

# Django Channels configuration
ASGI_APPLICATION = 'pavilion.asgi.application'

# Channel layers for WebSocket communication
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels.layers.InMemoryChannelLayer"
}
}
195 changes: 195 additions & 0 deletions pos_server/consumers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
"""
WebSocket consumers for real-time order updates.

This module provides WebSocket consumers that enable real-time communication
between the server and clients for order updates. It replaces the polling-based
approach with a push-based WebSocket approach.
"""

import json
from datetime import datetime
from channels.generic.websocket import AsyncWebsocketConsumer
from channels.db import database_sync_to_async
from django.utils.timezone import now, localdate
from django.db.models import Q


class DateTimeEncoder(json.JSONEncoder):
"""Custom JSON encoder that handles datetime objects."""
def default(self, obj):
if isinstance(obj, datetime):
return obj.isoformat()
return super().default(obj)


def json_dumps(data):
"""JSON dumps with datetime support."""
return json.dumps(data, cls=DateTimeEncoder)


class OrderConsumer(AsyncWebsocketConsumer):
"""
WebSocket consumer for real-time order updates.

Clients connect to this consumer to receive real-time notifications
when orders are created, updated, or deleted.
"""

async def connect(self):
"""Handle WebSocket connection."""
self.room_group_name = 'orders'

# Join the orders group
await self.channel_layer.group_add(
self.room_group_name,
self.channel_name
)

await self.accept()

# Send initial active orders on connection
orders = await self.get_active_orders()
await self.send(text_data=json_dumps({
'type': 'initial_orders',
'orders': orders
}))

async def disconnect(self, close_code):
"""Handle WebSocket disconnection."""
# Leave the orders group
await self.channel_layer.group_discard(
self.room_group_name,
self.channel_name
)

async def receive(self, text_data):
"""Handle incoming WebSocket messages."""
# Currently, clients don't send messages, but this can be extended
pass

async def order_update(self, event):
"""
Handle order update events from the channel layer.

This is called when an order is created, updated, or deleted.
"""
await self.send(text_data=json_dumps({
'type': event['update_type'],
'order': event['order']
}))

async def order_delete(self, event):
"""
Handle order delete events from the channel layer.
"""
await self.send(text_data=json_dumps({
'type': 'order_deleted',
'order_id': event['order_id']
}))

@database_sync_to_async
def get_active_orders(self):
"""
Get all active orders for today.

Returns a list of serialized order data.
"""
from .models import Order, OrderDish
from .views import collect_order

today = localdate()
active_orders = Order.objects.filter(
Q(start_time__lte=now()) &
(Q(kitchen_status=0) |
Q(kitchen_status=1) |
Q(bar_status=0) |
Q(bar_status=1) |
Q(gng_status=0) |
Q(gng_status=1) |
Q(picked_up=False))
).filter(timestamp__date=today)

return [collect_order(order) for order in active_orders]


class OrderProgressConsumer(AsyncWebsocketConsumer):
"""
WebSocket consumer for customer-facing order progress screen.

This consumer provides real-time updates for the order status display
that customers see, showing which orders are in progress and ready.
"""

async def connect(self):
"""Handle WebSocket connection."""
self.room_group_name = 'order_progress'

# Join the order progress group
await self.channel_layer.group_add(
self.room_group_name,
self.channel_name
)

await self.accept()

# Send initial orders on connection
orders = await self.get_progress_orders()
await self.send(text_data=json_dumps({
'type': 'initial_orders',
'orders': orders
}))

async def disconnect(self, close_code):
"""Handle WebSocket disconnection."""
await self.channel_layer.group_discard(
self.room_group_name,
self.channel_name
)

async def receive(self, text_data):
"""Handle incoming WebSocket messages."""
pass

async def order_update(self, event):
"""Handle order update events."""
await self.send(text_data=json_dumps({
'type': event['update_type'],
'order': event['order']
}))

async def order_delete(self, event):
"""Handle order delete events."""
await self.send(text_data=json_dumps({
'type': 'order_deleted',
'order_id': event['order_id']
}))

@database_sync_to_async
def get_progress_orders(self):
"""
Get orders for the progress display (in progress and ready).
"""
from .models import Order
from .views import collect_order

today = localdate()

# Get in progress orders (kitchen_status = 1)
in_progress = Order.objects.filter(
kitchen_status=1,
timestamp__date=today
)

# Get ready orders (kitchen_status = 2 and not picked up)
ready = Order.objects.filter(
Q(kitchen_status=2) & Q(picked_up=False),
timestamp__date=today
)

in_progress_data = [collect_order(order) for order in in_progress]
ready_data = [collect_order(order) for order in ready]

return {
'in_progress': in_progress_data,
'ready': ready_data
}
11 changes: 11 additions & 0 deletions pos_server/routing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""
WebSocket URL routing for pos_server app.
"""

from django.urls import re_path
from . import consumers

websocket_urlpatterns = [
re_path(r'ws/orders/$', consumers.OrderConsumer.as_asgi()),
re_path(r'ws/order-progress/$', consumers.OrderProgressConsumer.as_asgi()),
]
97 changes: 96 additions & 1 deletion pos_server/signals.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,102 @@
from django.db.models.signals import post_save, pre_save
from django.db.models.signals import post_save, post_delete, pre_save
from django.dispatch import receiver
from .models import *
from . import globals
from channels.layers import get_channel_layer
from asgiref.sync import async_to_sync


def broadcast_order_update(order, update_type='order_updated'):
"""
Broadcast order update to all connected WebSocket clients.

Args:
order: The Order instance that was updated
update_type: The type of update ('order_created', 'order_updated', 'order_deleted')
"""
from .views import collect_order

channel_layer = get_channel_layer()
if channel_layer is None:
return

order_data = collect_order(order) if order else None

# Broadcast to order marking screen
async_to_sync(channel_layer.group_send)(
'orders',
{
'type': 'order_update',
'update_type': update_type,
'order': order_data
}
)

# Broadcast to order progress screen
async_to_sync(channel_layer.group_send)(
'order_progress',
{
'type': 'order_update',
'update_type': update_type,
'order': order_data
}
)


def broadcast_order_delete(order_id):
"""
Broadcast order deletion to all connected WebSocket clients.

Args:
order_id: The ID of the deleted order
"""
channel_layer = get_channel_layer()
if channel_layer is None:
return

# Broadcast to order marking screen
async_to_sync(channel_layer.group_send)(
'orders',
{
'type': 'order_delete',
'order_id': order_id
}
)

# Broadcast to order progress screen
async_to_sync(channel_layer.group_send)(
'order_progress',
{
'type': 'order_delete',
'order_id': order_id
}
)


@receiver(post_save, sender=Order)
def order_saved(sender, instance, created, **kwargs):
"""
Signal handler for when an Order is saved.
Broadcasts the update to all connected WebSocket clients.
"""
update_type = 'order_created' if created else 'order_updated'
broadcast_order_update(instance, update_type)


@receiver(pre_save, sender=Order)
def store_order_id_before_delete(sender, instance, **kwargs):
"""Store the order ID before deletion for broadcasting."""
pass # pre_save is not needed for deletion


@receiver(post_delete, sender=Order)
def order_deleted(sender, instance, **kwargs):
"""
Signal handler for when an Order is deleted.
Broadcasts the deletion to all connected WebSocket clients.
"""
broadcast_order_delete(instance.id)


# This is a corpse, maybe will be needed in the future.

Expand Down
Loading
Loading