“My Dance Lists”

I spent some time drawing up a programme for the upcoming end-of-April Rhein-Main-Mini-Social and assigned it to the FSCDC teachers' group so Marie could have a look at it before I posted it to the public. But the list didn't show up on her “My Dance Lists” page. What was going on?

It turns out that “My Dance Lists” only shows the dance lists you actually own, not the dance lists that you can see or even change because they belong to a group that you're a member of. The “group” lists are accessible through the general “Dance Lists” page. This is something I didn't seem to remember from when I originally implemented the feature, and intuitively I would expect the “group” lists to be there, too.

Technically, the lists to be displayed on the page were determined by the following code in the views/lists.py file:

class ListsMyAjaxView(ListsAjaxView):

    def get_base_queryset(self):
        if self.request.user.is_authenticated:
            qs = super().get_base_queryset()
            return qs.filter(owner=self.request.user)
        return DanceList.objects.none()

The ListsMyAjaxView class-based view is used to fill in the table on the “My Dance Lists” page. It gets a queryset of dance lists from the ListsAjaxView CBV (which includes all the lists that are visible to the current user – public lists, “group” lists for groups where the current user is a member, and private lists owned by the current user) and reduces that down to those lists the current user actually owns (public, group-visible, or private). The get_base_queryset() method now reads like

def get_base_queryset(self):
    if self.request.user.is_authenticated:
        return DanceList.objects.visible(self.request.user, public=False)
    return DanceList.objects.none()

That is, we're now not bothering with ListAjaxView at all – instead we're directly fetching the dance lists which are visible to the current user, minus those which are “public” but owned by others. That of course amounts to the lists which are owned by the current user (private or public) and the lists assigned to groups of which the current user is a member. Yay!

(Theoretically, the “My Dance Lists” view is only available to logged-in users, but we're checking that here just to be safe. If you're somehow getting into “My Dance Lists” but aren't logged in to the database, you'll just get an empty table.)

“Owner” menu on SCDDB “My Dance Lists” page

Just to make things a little more interesting and fun, we're adding a drop-down menu to the “My Dance Lists” page that has checkboxes for all the user's groups so the user can display only the dance lists associated with a subset of the groups. We're generating the menu entries in the ListMyListView CBV (which is responsible for the “My Dance Lists” page as a whole – the ListsMyAjaxView view fills in the table on that page) by overriding its get_context_data() method:

def get_context_data(self, **kwargs):
    context = super().get_context_data(**kwargs)
    if self.request.user.is_authenticated:
        context["groups"] = [
            (0, f"Myself ({self.request.user.get_full_name()})")
        ] + [
            (g.id, f"Group “{g.name}”") for g in self.request.user.groups.all()
        ]
    return context

The groups context variable now contains a list of “(group ID, menu item text)” pairs which we can process into an actual drop-down menu with Django template code such as (in db/list_list.html):

{% if groups %}
  <div class="col-12 ps-0">
    <div class="dropdown">
      <button type="button" class="btn btn-secondary dropdown-toggle"
              data-bs-toggle="dropdown" aria-expanded="false">
        Owner
      </button>
      <ul class="dropdown-menu">
        {% for id, s in groups %}
          <li class="dropdown-item">
            <input type="checkbox" id="o_{{ id }}"
                   name="groups" value="{{ id }}" checked />
            <label for="o_{{ id }}">{{ s }}</label>
          </li>
        {% endfor %}
      </ul>
    </div>
  </div>
{% endif %}

All that needs to actually work is a little bit of JavaScript code that watches for changes to the checkboxes and reloads the table as needed:

const gs = document.querySelectorAll("input[name='groups']");
for  (const g of gs) {
    g.addEventListener('change', function () { dtab.draw(); });
}

We also have a few lines of JavaScript that read out which boxes in the menu are currently checked, as part of preparing the data to be included in the AJAX request that calls the ListsMyAjaxView CBV to fill in the table:

var groups = [];
$("input[name='groups']").each(function () {
  if ($(this).is(":checked")) {
    groups.push($(this).val());
  }
});
data.groups = groups;

(Yes, this is jQuery code. Sue me. I'll eventually rewrite it into Hyperscript.)

To make this actually work we need a little support on the server, in the shape of the ListsMyAjaxView's base_queryset_filter() method:

def base_queryset_filter(self, qs):
    qs = super().base_queryset_filter(qs)
    if self.request.user.is_authenticated:
        if gg := self.request.GET.getlist('groups[]'):
            groups = set(int(g) for g in gg if g.isdigit())
            criteria = Q(visibility=LV_PRIVATE, owner=self.request.user) if 0 in groups else Q()
            groups.discard(0)
            if len(groups):
                criteria |= Q(visibility=LV_GROUP, group_in=groups)
            qs = qs.distinct().filter(criteria)
    return qs

This picks up the group IDs from the groups parameter on the GET request to ListsMyAjaxView, and retains only those dance lists from the base queryset (remember get_base_queryset() above?) which are group-visible (LV_GROUP) and whose associated group ID is in groups. If you looked closely at our menu definition above, you'll recall that we treat the lists owned by the user as if they are part of a group with the ID 0 (which can't occur as an actual group ID).

So all of this should make it easier for Marie to find her dance lists! Hope you'll enjoy it, too; it will be pushed to the site soon.