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&apos;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;