@@ -215,6 +215,27 @@ def only_calendars_available_in_ranges(
215215 """
216216 Returns calendars that have available time windows in all specified ranges.
217217 """
218+ return self ._only_calendars_available_in_ranges (ranges , with_bulk_modifications = False )
219+
220+ def only_calendars_available_in_ranges_with_bulk_modifications (
221+ self , ranges : Iterable [tuple [datetime .datetime , datetime .datetime ]]
222+ ):
223+ """
224+ Same as `only_calendars_available_in_ranges`, but recurring events and
225+ blocked times are expanded via their bulk-modification continuation
226+ series (`annotate_recurring_occurrences_with_bulk_modifications_on_date_range`).
227+ Use this when the caller has split a recurring series with a bulk
228+ modification and needs the continuation occurrences to count against
229+ availability.
230+ """
231+ return self ._only_calendars_available_in_ranges (ranges , with_bulk_modifications = True )
232+
233+ def _only_calendars_available_in_ranges (
234+ self ,
235+ ranges : Iterable [tuple [datetime .datetime , datetime .datetime ]],
236+ * ,
237+ with_bulk_modifications : bool ,
238+ ):
218239 from calendar_integration .models import AvailableTime , BlockedTime , CalendarEvent
219240
220241 if not ranges :
@@ -236,20 +257,28 @@ def only_calendars_available_in_ranges(
236257 ),
237258 )
238259
260+ if with_bulk_modifications :
261+ events_qs = CalendarEvent .objects .annotate_recurring_occurrences_with_bulk_modifications_on_date_range (
262+ start_datetime , end_datetime
263+ )
264+ recurring_occurrences_field = "recurring_occurrences"
265+ else :
266+ events_qs = CalendarEvent .objects .annotate_recurring_occurrences_on_date_range (
267+ start_datetime , end_datetime
268+ )
269+ recurring_occurrences_field = "recurring_occurrences"
270+
239271 # For unmanaged calendars: must NOT have conflicting events or blocked times
240272 unmanaged_query = Q (
241273 manage_available_windows = False ,
242274 ) & ~ Q (
243275 Q (
244276 id__in = Subquery (
245- CalendarEvent .objects .annotate_recurring_occurrences_on_date_range (
246- start_datetime , end_datetime
247- )
248- .filter (
277+ events_qs .filter (
249278 Q (start_time__range = (start_datetime , end_datetime ))
250279 | Q (end_time__range = (start_datetime , end_datetime ))
251280 | Q (start_time__lte = start_datetime , end_time__gte = end_datetime )
252- | Q (recurring_occurrences__len__gt = 0 ),
281+ | Q (** { f" { recurring_occurrences_field } __len__gt" : 0 } ),
253282 calendar_fk_id = OuterRef ("id" ),
254283 )
255284 .values ("calendar_fk_id" )
@@ -381,20 +410,42 @@ def only_groups_bookable_in_ranges(
381410 `required_count` calendars from its pool available
382411 (per CalendarQuerySet.only_calendars_available_in_ranges).
383412 """
413+ return self ._only_groups_bookable_in_ranges (ranges , with_bulk_modifications = False )
414+
415+ def only_groups_bookable_in_ranges_with_bulk_modifications (
416+ self , ranges : Iterable [tuple [datetime .datetime , datetime .datetime ]]
417+ ):
418+ """
419+ Same as `only_groups_bookable_in_ranges` but expands recurring events
420+ through their bulk-modification continuation series so split-off
421+ occurrences count against availability.
422+ """
423+ return self ._only_groups_bookable_in_ranges (ranges , with_bulk_modifications = True )
424+
425+ def _only_groups_bookable_in_ranges (
426+ self ,
427+ ranges : Iterable [tuple [datetime .datetime , datetime .datetime ]],
428+ * ,
429+ with_bulk_modifications : bool ,
430+ ):
384431 from calendar_integration .models import Calendar , CalendarGroupSlot
385432
386433 ranges = list (ranges )
387434 if not ranges :
388435 return self .none ()
389436
437+ calendar_method = (
438+ "only_calendars_available_in_ranges_with_bulk_modifications"
439+ if with_bulk_modifications
440+ else "only_calendars_available_in_ranges"
441+ )
442+
390443 qs = self
391444 for start_datetime , end_datetime in ranges :
392- available_calendar_ids = (
393- Calendar .objects .get_queryset ()
394- .filter (organization_id = OuterRef ("organization_id" ))
395- .only_calendars_available_in_ranges ([(start_datetime , end_datetime )])
396- .values ("id" )
397- )
445+ available_calendar_ids = getattr (
446+ Calendar .objects .get_queryset ().filter (organization_id = OuterRef ("organization_id" )),
447+ calendar_method ,
448+ )([(start_datetime , end_datetime )]).values ("id" )
398449 unsatisfied_slot = (
399450 CalendarGroupSlot .objects .get_queryset ()
400451 .filter (group_fk_id = OuterRef ("id" ))
0 commit comments