2626from le_utils .constants .labels .subjects import SUBJECTSLIST
2727
2828
29- metadata_lookup = {
29+ contentnode_metadata_lookup = {
3030 "learning_activities" : LEARNINGACTIVITIESLIST ,
3131 "categories" : SUBJECTSLIST ,
3232 "grade_levels" : LEVELSLIST ,
3333 "accessibility_labels" : ACCESSIBILITYCATEGORIESLIST ,
3434 "learner_needs" : NEEDSLIST ,
3535}
36+ contentnode_metadata_bitmasks = {}
37+ contentnode_bitmask_fieldnames = {}
3638
37-
38- metadata_bitmasks = {}
39-
40- bitmask_fieldnames = {}
41-
42-
43- for key , labels in metadata_lookup .items ():
44- bitmask_lookup = {}
45- i = 0
46- while labels [i : i + 64 ]:
47- bitmask_field_name = "{}_bitmask_{}" .format (key , i )
48- bitmask_fieldnames [bitmask_field_name ] = []
49- for j , label in enumerate (labels ):
50- info = {
51- "bitmask_field_name" : bitmask_field_name ,
52- "field_name" : key ,
53- "bits" : 2 ** j ,
54- "label" : label ,
55- }
56- bitmask_lookup [label ] = info
57- bitmask_fieldnames [bitmask_field_name ].append (info )
58- i += 64
59- metadata_bitmasks [key ] = bitmask_lookup
39+ channelmetadata_metadata_lookup = {
40+ "categories" : SUBJECTSLIST ,
41+ }
42+ channelmetadata_metadata_bitmasks = {}
43+ channelmetadata_bitmask_fieldnames = {}
44+
45+
46+ def _populate_bitmask_data (metadata_lookup , metadata_bitmasks , bitmask_fieldnames ):
47+
48+ for key , labels in metadata_lookup .items ():
49+ bitmask_lookup = {}
50+ i = 0
51+ while (chunk := labels [i : i + 64 ]) :
52+ bitmask_field_name = "{}_bitmask_{}" .format (key , i )
53+ bitmask_fieldnames [bitmask_field_name ] = []
54+ for j , label in enumerate (chunk ):
55+ info = {
56+ "bitmask_field_name" : bitmask_field_name ,
57+ "field_name" : key ,
58+ "bits" : 2 ** j ,
59+ "label" : label ,
60+ }
61+ bitmask_lookup [label ] = info
62+ bitmask_fieldnames [bitmask_field_name ].append (info )
63+ i += 64
64+ metadata_bitmasks [key ] = bitmask_lookup
65+
66+
67+ _populate_bitmask_data (
68+ contentnode_metadata_lookup ,
69+ contentnode_metadata_bitmasks ,
70+ contentnode_bitmask_fieldnames ,
71+ )
72+ _populate_bitmask_data (
73+ channelmetadata_metadata_lookup ,
74+ channelmetadata_metadata_bitmasks ,
75+ channelmetadata_bitmask_fieldnames ,
76+ )
6077
6178
6279def _get_available_languages (base_queryset ):
@@ -87,7 +104,7 @@ def _get_available_channels(base_queryset):
87104# Remove the SQLite Bitwise OR definition as not needed.
88105
89106
90- def get_available_metadata_labels (base_queryset ):
107+ def get_contentnode_available_metadata_labels (base_queryset ):
91108 # Updated to use the kolibri_public ChannelMetadata model
92109 from kolibri_public .models import ChannelMetadata
93110
@@ -101,12 +118,12 @@ def get_available_metadata_labels(base_queryset):
101118 if cache_key not in cache :
102119 base_queryset = base_queryset .order_by ()
103120 aggregates = {}
104- for field in bitmask_fieldnames :
121+ for field in contentnode_bitmask_fieldnames :
105122 field_agg = field + "_agg"
106123 aggregates [field_agg ] = BitOr (field )
107124 output = {}
108125 agg = base_queryset .aggregate (** aggregates )
109- for field , values in bitmask_fieldnames .items ():
126+ for field , values in contentnode_bitmask_fieldnames .items ():
110127 bit_value = agg [field + "_agg" ]
111128 for value in values :
112129 if value ["field_name" ] not in output :
@@ -123,10 +140,12 @@ def get_all_contentnode_label_metadata():
123140 # Updated to use the kolibri_public ContentNode model
124141 from kolibri_public .models import ContentNode
125142
126- return get_available_metadata_labels (ContentNode .objects .filter (available = True ))
143+ return get_contentnode_available_metadata_labels (
144+ ContentNode .objects .filter (available = True )
145+ )
127146
128147
129- def annotate_label_bitmasks (queryset ):
148+ def annotate_label_bitmasks (queryset , bitmask_fieldnames ):
130149 update_statements = {}
131150 for bitmask_fieldname , label_info in bitmask_fieldnames .items ():
132151 update_statements [bitmask_fieldname ] = sum (
@@ -142,3 +161,39 @@ def annotate_label_bitmasks(queryset):
142161 for info in label_info
143162 )
144163 queryset .update (** update_statements )
164+
165+
166+ def annotate_contentnode_label_bitmasks (queryset ):
167+ return annotate_label_bitmasks (queryset , contentnode_bitmask_fieldnames )
168+
169+
170+ def annotate_channelmetadata_label_bitmasks (queryset ):
171+ return annotate_label_bitmasks (queryset , channelmetadata_bitmask_fieldnames )
172+
173+
174+ def has_all_labels (queryset , metadata_bitmasks , field_name , labels ):
175+ bitmasks = metadata_bitmasks [field_name ]
176+ bits = {}
177+ for label in labels :
178+ if label in bitmasks :
179+ bitmask_fieldname = bitmasks [label ]["bitmask_field_name" ]
180+ if bitmask_fieldname not in bits :
181+ bits [bitmask_fieldname ] = 0
182+ bits [bitmask_fieldname ] += bitmasks [label ]["bits" ]
183+
184+ filters = {}
185+ annotations = {}
186+ for bitmask_fieldname , bits in bits .items ():
187+ annotation_fieldname = "{}_{}" .format (bitmask_fieldname , "masked" )
188+ # To get the correct result, i.e. an AND that all the labels are present,
189+ # we need to check that the aggregated value is euqal to the bits.
190+ # If we wanted an OR (which would check for any being present),
191+ # we would have to use GREATER THAN 0 here.
192+ filters [annotation_fieldname ] = bits
193+ # This ensures that the annotated value is the result of the AND operation
194+ # so if all the values are present, the result will be the same as the bits
195+ # but if any are missing, it will not be equal to the bits, but will only be
196+ # 0 if none of the bits are present.
197+ annotations [annotation_fieldname ] = F (bitmask_fieldname ).bitand (bits )
198+
199+ return queryset .annotate (** annotations ).filter (** filters )
0 commit comments