Repository: learningequality/kolibri

Issue:
Remote browsing - Filtering by categories is not working while I am viewing a specific channel
## Observed behavior
Filtering by categories is not working while I am viewing a specific channel. The only option available is to filter by 'Uncategorized. The filter by Categories is also not working correctly for the channels already downloaded to my library.

## Expected behavior
Filtering by categories should be working correctly.

## Steps to reproduce the issue
1. Install the build asset from the following [PR](https://github.com/learningequality/kolibri/pull/10974)
2. Navigate to Library > Other libraries > Explore > Channel or Library > My library > Channel
3. Attempt to filter by Category

## Video

https://github.com/learningequality/kolibri/assets/79847249/b53eca1a-6004-43b1-a0f2-c9f79a3698fa

## Usage Details
Kolibri 0.16alfa19
Windows 10, Ubuntu - Chrome


Discussion:
@radinamatic 

Resolving patch:
diff --git a/kolibri/core/content/models.py b/kolibri/core/content/models.py
--- a/kolibri/core/content/models.py
+++ b/kolibri/core/content/models.py
@@ -108,7 +108,15 @@ def has_all_labels(self, field_name, labels):
         annotations = {}
         for bitmask_fieldname, bits in bits.items():
             annotation_fieldname = "{}_{}".format(bitmask_fieldname, "masked")
-            filters[annotation_fieldname + "__gt"] = 0
+            # To get the correct result, i.e. an AND that all the labels are present,
+            # we need to check that the aggregated value is euqal to the bits.
+            # If we wanted an OR (which would check for any being present),
+            # we would have to use GREATER THAN 0 here.
+            filters[annotation_fieldname] = bits
+            # This ensures that the annotated value is the result of the AND operation
+            # so if all the values are present, the result will be the same as the bits
+            # but if any are missing, it will not be equal to the bits, but will only be
+            # 0 if none of the bits are present.
             annotations[annotation_fieldname] = F(bitmask_fieldname).bitand(bits)
 
         return self.annotate(**annotations).filter(**filters)


Test patch:
diff --git a/kolibri/core/content/test/test_search.py b/kolibri/core/content/test/test_search.py
--- a/kolibri/core/content/test/test_search.py
+++ b/kolibri/core/content/test/test_search.py
@@ -67,6 +67,34 @@ def test_bitmasks(self, field, label):
             "{} {}".format(field, label),
         )
 
+    def test_bitmasks_and_not_or(self):
+        for field in metadata_lookup.keys():
+            node = ContentNode.objects.create(
+                content_id=uuid4().hex,
+                channel_id=uuid4().hex,
+                description="Blah blah blah",
+                id=uuid4().hex,
+                license_name="GNU",
+                license_owner="",
+                license_description=None,
+                lang_id=None,
+                author="",
+                title="Test{}".format(field),
+                parent_id=None,
+                kind=content_kinds.VIDEO,
+                coach_content=False,
+                available=False,
+                **{field: ",".join(metadata_lookup[field])}
+            )
+            annotate_label_bitmasks(ContentNode.objects.filter(id=node.id))
+            self.assertEqual(
+                1,
+                ContentNode.objects.has_all_labels(
+                    field, metadata_lookup[field]
+                ).count(),
+                "{}".format(field),
+            )
+
 
 class RandomMetadataLabelsTestCase(TestCase):
     @classmethod
diff --git a/kolibri/plugins/learn/assets/src/composables/__tests__/useSearch.spec.js b/kolibri/plugins/learn/assets/src/composables/__tests__/useSearch.spec.js
--- a/kolibri/plugins/learn/assets/src/composables/__tests__/useSearch.spec.js
+++ b/kolibri/plugins/learn/assets/src/composables/__tests__/useSearch.spec.js
@@ -471,22 +471,4 @@ describe(`useSearch`, () => {
       });
     });
   });
-  describe('setCategory method', () => {
-    it('should remove all other category filters and add the new one', () => {
-      const { setCategory, router } = prep({
-        categories: 'test1,test2',
-        channels: 'channel1',
-        keywords: 'this',
-      });
-      setCategory('newcategory');
-      expect(router.push).toHaveBeenCalledWith({
-        name,
-        query: {
-          categories: 'newcategory',
-          channels: 'channel1',
-          keywords: 'this',
-        },
-      });
-    });
-  });
 });
diff --git a/kolibri/plugins/learn/assets/test/views/library-page.spec.js b/kolibri/plugins/learn/assets/test/views/library-page.spec.js
--- a/kolibri/plugins/learn/assets/test/views/library-page.spec.js
+++ b/kolibri/plugins/learn/assets/test/views/library-page.spec.js
@@ -33,6 +33,7 @@ const mockStore = (rootNodes = ['length'], getters = {}) => {
       isCoach: jest.fn(),
       isAppContext: jest.fn(),
       getUserKind: jest.fn(),
+      getRootNodesLoading: jest.fn(),
       ...getters,
     },
   });
@@ -115,10 +116,10 @@ describe('LibraryPage', () => {
         options: { stubs: ['SidePanelModal', 'LearnTopNav'] },
       });
       // not displayed by default
-      expect(wrapper.findComponent({ name: 'SidePanel' }).vm.$children.length).toEqual(0);
+      expect(wrapper.findComponent({ name: 'SearchFiltersPanel' }).element).toBeUndefined();
       wrapper.find('[data-test="filter-button"]').trigger('click');
       await wrapper.vm.$nextTick();
-      expect(wrapper.findComponent({ name: 'SidePanel' }).vm.$children.length).not.toEqual(0);
+      expect(wrapper.findComponent({ name: 'SearchFiltersPanel' }).element).toBeTruthy();
     });
   });
 
@@ -305,6 +306,9 @@ describe('LibraryPage', () => {
 
   describe('SidePanel', () => {
     it('display side panel if local libraries are available', () => {
+      useKResponsiveWindow.mockImplementation(() => ({
+        windowIsLarge: true,
+      }));
       const wrapper = makeWrapper();
       expect(wrapper.find('[data-test="side-panel"').element).toBeTruthy();
     });
diff --git a/kolibri/plugins/learn/assets/test/views/topics-page.spec.js b/kolibri/plugins/learn/assets/test/views/topics-page.spec.js
--- a/kolibri/plugins/learn/assets/test/views/topics-page.spec.js
+++ b/kolibri/plugins/learn/assets/test/views/topics-page.spec.js
@@ -3,13 +3,13 @@ import VueRouter from 'vue-router';
 import { createLocalVue, shallowMount, mount } from '@vue/test-utils';
 import { ContentNodeKinds } from 'kolibri.coreVue.vuex.constants';
 import { useDevicesWithFacility } from 'kolibri.coreVue.componentSets.sync';
+import plugin_data from 'plugin_data';
 import makeStore from '../makeStore';
 import CustomContentRenderer from '../../src/views/ChannelRenderer/CustomContentRenderer';
 import { PageNames } from '../../src/constants';
 import TopicsPage from '../../src/views/TopicsPage';
 // eslint-disable-next-line import/named
 import useSearch, { useSearchMock } from '../../src/composables/useSearch';
-import plugin_data from 'plugin_data';
 
 jest.mock('kolibri.coreVue.componentSets.sync');
 jest.mock('plugin_data', () => {