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.)
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.