Static queryset
Static queryset¶
tasks is a bare Django queryset assigned once in on_connect to a
many=True rx.model field – that one assignment is the whole
declaration. The client keeps the list correct on its own: rows are
filtered by the queryset’s conditions and sorted by its ordering,
re-evaluated live as updates arrive. Toggling a task’s status flips it out
of (or back into) the list; raising its priority re-sorts it; deleting it
removes it. One deliberate limitation: a task created after the snapshot
appears only when the field is rebound – for lists where new rows arrive
live, see Task board.
Backend¶
from rxdjango import ContextChannel, action, rx
from .models import Task
from .serializers import TaskSerializer
class StaticQuerysetChannel(ContextChannel):
"""A bare queryset assigned to a `many=True` field. `on_connect` binds
`Task.objects.filter(status='open').order_by('-priority', 'id')` -- no
other declaration.
The client keeps the list correct from there: `toggle_status` flips a
task out of (or back into) the list, `bump_priority` re-sorts it, and
`delete_task` removes it. `add_task` deliberately does *not* appear
until `rebind` runs again -- new rows arrive live only on a routed
list (see `task_board`).
"""
tasks = rx.model(TaskSerializer(many=True))
async def on_connect(self):
self._bind()
def _bind(self):
self.tasks = Task.objects.filter(status='open').order_by('-priority', 'id')
@action
async def rebind(self):
self._bind()
@action
async def toggle_status(self, task_id: int):
task = await Task.objects.aget(id=task_id)
task.status = 'closed' if task.status == 'open' else 'open'
await task.asave()
@action
async def bump_priority(self, task_id: int, delta: int):
task = await Task.objects.aget(id=task_id)
task.priority += delta
await task.asave()
@action
async def delete_task(self, task_id: int):
task = await Task.objects.aget(id=task_id)
await task.adelete()
@action
async def add_task(self, name: str, priority: int):
await Task.objects.acreate(name=name, status='open', priority=priority)
Models¶
from django.db import models
from rxdjango.models import ReactiveModel
class Task(ReactiveModel):
"""A task on a shared board: the channel's queryset filters on
`status` and orders by `priority`."""
name = models.CharField(max_length=64)
status = models.CharField(max_length=16, default='open')
priority = models.IntegerField(default=0)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['id']
def __str__(self):
return self.name
Serializers¶
from rest_framework import serializers
from .models import Task
class TaskSerializer(serializers.ModelSerializer):
class Meta:
model = Task
fields = ['id', 'name', 'status', 'priority', 'created_at']
Frontend¶
import React, { useState } from 'react';
import { useChannel } from '@rxdjango/react';
import { StaticQuerysetChannel } from '../../rx/static_queryset/static_queryset.channels';
import {
Sections,
Button,
TextInput,
Row,
Note,
} from '../../components/demo';
export function StaticQuerysetDemo() {
const channel = useChannel(StaticQuerysetChannel);
const [name, setName] = useState('');
const [priority, setPriority] = useState('0');
return (
<Sections>
<div>
<Note>
`tasks` is a bare queryset bound once in `on_connect` -- open
tasks, ordered by descending priority. Toggling a task's
status flips it out of (or back into) the list; bumping priority
re-sorts it; deleting a task removes it. A newly added task only
appears when you press Rebind -- updates reach rows already in
the list, never new rows.
</Note>
</div>
<div>
{channel.tasks === null ? (
<p>
Connecting...
</p>
) : channel.tasks.length === 0 ? (
<p data-testid="empty-state">
No open tasks.
</p>
) : (
<ul className="space-y-3">
{channel.tasks.map((task) => (
<li
key={task.id}
data-testid={`task-${task.id}`}
className="flex flex-col gap-2 rounded-md border border-ink/20 p-3 sm:flex-row sm:items-center sm:justify-between"
>
<div>
<span className="font-medium text-ink">
{task.name}
</span>
<span className="ml-2 text-sm text-primary-700">
priority {task.priority}
</span>
</div>
<Row>
<Button
variant="secondary"
onClick={() => channel.bump_priority(task.id, 1)}
>
+1 priority
</Button>
<Button
variant="secondary"
onClick={() => channel.toggle_status(task.id)}
>
Close
</Button>
<Button
variant="secondary"
onClick={() => channel.delete_task(task.id)}
>
Delete
</Button>
</Row>
</li>
))}
</ul>
)}
</div>
<div>
<Row>
<TextInput
id="static-queryset-new-task-name"
label="New task name"
value={name}
onChange={setName}
/>
<TextInput
id="static-queryset-new-task-priority"
label="Priority"
value={priority}
onChange={setPriority}
/>
<Button
variant="secondary"
onClick={() => channel.add_task(name, parseInt(priority, 10) || 0)}
>
Add task
</Button>
<Button
variant="primary"
onClick={() => channel.rebind()}
>
Rebind
</Button>
</Row>
</div>
</Sections>
);
}
export default StaticQuerysetDemo;