datamade/just-spaces

AgencyRestrictQuerysetMixin causes object create to fail

Closed this issue · 0 comments

@beamalsky noticed in #222 that after creating a new CensusArea, the new CensusArea is not immediately visible in the ListView that the app redirects to you.

After digging into it a little bit more, I was able to determine that this is a problem with the AgencyRestrictQuerysetMixin introduced in #218, meaning that it affects all inheritors of that mixin -- including LocationList, StudyList, SurveyListEdit, SurveyListRun, and SurveySubmittedList.

In each of these cases, we have a queryset defined as an attribute of the class, as well as a get_queryset() method that uses AgencyRestrictQuerysetMixin.get_queryset_for_agency() to restrict the queryset based on the agency, e.g.:

class CensusAreaList(AgencyRestrictQuerysetMixin, ListView):
model = survey_models.CensusArea
template_name = "census_area_list.html"
context_object_name = 'census_areas'
queryset = survey_models.CensusArea.objects.all().exclude(is_active=False)
def get_queryset(self):
return self.get_queryset_for_agency()

get_queryset_for_agency() in turn filters self.queryset to return the final queryset:

def get_queryset_for_agency(self, agency_filter='agency'):
"""
Filter the queryset based on the user's agency. The 'agency_filter' string
will be used as the filter kwarg for the Agency lookup; e.g. if the Agency
is accessible from 'instance.study.agency', the agency_filter should be
'study__agency'.
"""
agency_kwargs = {agency_filter: self.request.user.agency}
agency_null_kwargs = {agency_filter + '__isnull': True}
if self.request.user.agency is not None:
return self.queryset.filter(Q(**agency_kwargs) | Q(**agency_null_kwargs))
else:
return self.queryset

The crux of the problem is that self.queryset is cached in some kind of way that causes it to fail to re-evaluate when a new object is created. If you drop a breakpoint in get_queryset_for_agency() and create a new object, you'll notice that self.queryset does not include the object whereas super(CensusAreaList, self).get_queryset() does (since it's evaluated dynamically).

In a weird twist, the test client does not exhibit this caching behavior, and testing this behavior explicitly just causes tests to pass as normal.

I don't fully grasp the underlying caching behavior here since the Django docs don't provide any details. However, I think the right move is to shift these methods away from relying on self.queryset and instead call get_queryset() dynamically every time we need to filter.