Is the __date filter broken?

I’ve been banging my head against the wall for hours on this.

I have a Calendar model that has a one to many relationship with CalendarEvent model. Each User model is given a one to one relationship with a calendar instance. In relevant part, the CalendarEvent model has a datetime object to represent the start time of the event.

In my user model, I can pull all my events in the default_calendar with self.default_calendar.events.all(), and they all show up. I can list them out and print their datetime.date(). Events are being created, stored, and can be recalled. Cool.

The problem is that len(self.default_calendar.events.filter(datetime__date=date)) always returns zero. It is never not zero. Doesn’t throw an error or exception. It just returns no results, even when date is set with a datetime pulled from the events listed by all().

I’ve checked if it’s a tz issue, and all elements are returning as timezone aware, including the date being used in the filter.

I’m at a loss here.

EDIT:

Here is additional information:

python 3.10.12
Django 5.0.4

Big picture, this part of the project is to track scheduling for a law office and update user’s google calendars. This was working, but I recently moved my development environment from Windows to a Linux VM in order to incorporate Celery.

I’ve traced the issue from data entry to the exact spot in the code where it goes off rails. For brevity, I’m removing code that isn’t relevant to this particular string of operations. Also I’m removing confidential information from the outputs.

UpdateDataFromEfiling.py pulls in scheduling data for cases:

def _get_calendar_events(driver, user, county, case_number, default_official):       
        events = []
        events_trs = driver.find_elements(By.XPATH, "//*[@id='activities']/div/div/div/table/tbody/tr")
        for tr in events_trs:
            # Extracting the date and time text
            date_str = tr.find_element(By.XPATH, "./td[1]").text.strip()  # Adjust XPath index
            time_str = tr.find_element(By.XPATH, "./td[2]").text.strip()  # Adjust XPath index
            start_datetime = _combine_date_and_time(date_str, time_str)
            if start_datetime < datetime.now(start_datetime.tzinfo):  continue
            duration = _get_event_length(user, start_datetime, case_number)
            if not duration: duration = timedelta(minutes=30)
            location_text = _format_location_text(tr.find_element(By.XPATH, "./td[3]").text.strip())      
            place_id = None  # Initialize the place_id variable
            # Open the CSV file and read it
            with open('government_contacts.csv', mode='r', encoding='utf-8') as csvfile:
                reader = csv.DictReader(csvfile)
                
                # Iterate through each row
                for row in reader:
                    # Check if the location_text matches the court_name
                    if row['name'].strip().lower() == location_text.strip().lower():                        
                        place_id = row['place_id']
                        break  # Exit the loop as we found the match
            
            if not place_id:
                with open('government_contacts.csv', mode='r', encoding='utf-8') as csvfile:
                    reader = csv.DictReader(csvfile)
                    location_text = f"{county} County Courthouse"
                    for row in reader:                                    
                        if row['name'].strip().lower() == location_text.strip().lower():
                            place_id = row['place_id']
                            break  # Exit the loop as we found the match

            # Create the event dictionary
            event = {
                'datetime': start_datetime,
                'duration': duration,
                'location': place_id,  # Adjust XPath index
                'title': tr.find_element(By.XPATH, "./td[4]").text,  # Adjust XPath index
                'official': tr.find_element(By.XPATH, "./td[6]").text,
                'default_official': default_official,
                'event_type' : 'court',
            }
            events.append(event)
            print(f"Event {event['title']} added to event list from efiling")
        return events

Output:

Event Telephone scheduling conference added to event list from efiling

The event dictionary is attached to a larger dictionary of case data and passed into my Django CircuitCourt model’s create_or_update_case method:

class AssignedCase(CalendaredModel):
    judge = models.ForeignKey(Contact, on_delete=models.SET_NULL, null=True, blank=True, related_name='%(class)s_cases_presiding')
    communication_logs = models.ManyToManyField('logs.CommunicationLog', related_name='%(class)s_cases_in', blank=True)
    viewed = GenericRelation('oauth_app.User')
    court_calendar = models.OneToOneField('calendars.Calendar', null=True, blank=True, on_delete=models.PROTECT, related_name='assigned_case_court_calendar')
    default_event_location = models.ForeignKey(Address, null=True, blank=True, on_delete=models.PROTECT)

class CircuitCase(AssignedCase):
    case_type = models.CharField(max_length=50, null=True, blank=True)
    county = models.CharField(max_length=15)
    branch = models.SmallIntegerField(default=1, null=True, blank=True)    
    case_num = models.CharField(max_length=15)
    caption = models.CharField(max_length=100)
    force_update = models.BooleanField(default=False)
    filing_date = models.DateField(default='1900-01-01')
    last_updated = models.DateTimeField(default=timezone.now)
    ccap_notes_updated = models.DateField(default='1980-01-01')  

    @classmethod
    def create_or_update_case(cls, case_data, user=None):
        with SPD_Base.current_user(user):
            # Remove None values
            case_data = {key: value for key, value in case_data.items() if value is not None}

            # Extract required and optional fields from case_data
            # Non-nullable fields
            try:
                county = case_data['county']
                caption = case_data['caption']
                case_number = CircuitCase._get_proper_casenumber(case_data['case_num'])
                filing_date = case_data['filing_date']
                parties = case_data['parties']
            except KeyError as e:
                raise ValueError(f"Missing required case_data field: {e}")

            # Nullable fields with default values
            judge = case_data.get('judge', None)
            case_type = case_data.get('case_type', None)
            branch = case_data.get('branch', 1) # Default branch 1
            witnesses = case_data.get('witnesses', [])
            charges = case_data.get('charges', [])
            ccap_notes = case_data.get('ccap_notes', [])
            events = case_data.get('events', [])

            # Additional logic for identifying and updating/creating the CircuitCase
            lookup_kwargs = {
                'county': county,
                'case_num': case_number
            }
            
            # Creating or updating the CircuitCase
            circuit_case, created = cls.objects.update_or_create(
                **lookup_kwargs,
                defaults={
                    'judge': judge,
                    'case_type': case_type,
                    'branch': branch,
                    'caption': caption,
                    'filing_date': filing_date,
                    'force_update': False,
                    'last_updated': timezone.now,
                    # Add other fields as needed
                }
            )

        circuit_case.update_or_create_parties(parties, user=user)
        circuit_case.update_or_create_witnesses(witnesses, user=user)
        circuit_case.update_or_create_charges(charges, user=user)
        circuit_case.update_or_create_ccap_notes(ccap_notes, user=user)
        circuit_case.court_calendar.update_or_create_events(events, user=user)

        return circuit_case, created

The event dictionary is extracted and passed into the update_or_create_events method of the court_calendar instance…

class Calendar(SPD_Base):
    #* Model Fields
    title = models.CharField(max_length=100, default="Automated Calendar")
    description = models.TextField(null=True, blank=True)
    locked = models.BooleanField(default=False)
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, null=True)
    object_id = models.PositiveIntegerField(null=True)
    parent_object = GenericForeignKey('content_type', 'object_id')
    google_calendar_id = models.CharField(max_length=255, blank=True, null=True)

    #* Constructor

    #* Class Methods and Static methods

    #* Properties
    @property
    def assigned_case(self):
        return self.assignedcase_calendared_models.first()
    
    @property
    def case_caption(self):
        from apps.spd_files.models import AssignedCase
        if isinstance(self.parent_object, AssignedCase):
            assigned_case = self.parent_object
            return assigned_case.calendar_caption
        else:
            raise ValueError("The parent_object is not an instance of AssignedCase")
    
    #* Public Methods    
    def update_or_create_events(self, events, user=None):
        """
        Updates or creates events for the calendar based on a provided list of event details.

        This method first identifies and deletes any future events that are not present in the
        provided list, assuming such events have been canceled. Then, it filters out events that
        already exist in the calendar to prevent duplicates. Finally, it creates new events for
        any remaining entries in the provided list.

        Parameters:
            events (list of dict): A list of dictionaries, each containing details of an event to be updated or created.
            user (User, optional): The user responsible for the update or creation of events. Used for logging purposes.

        """

        future_events = self.events.filter(datetime__gte=timezone.now())
        # Convert incoming events to a comparable form (e.g., a set of tuples)
        incoming_event_signatures = {
            (event['title'], event['datetime'], event['duration'], event['official'])
            for event in events
        }

        # Identify future events to delete
        events_to_delete = [
            event for event in future_events
            if (event.title, event.datetime, event.duration, event.official.name) not in incoming_event_signatures
        ]

        # Delete identified events
        for event in events_to_delete:
            self.delete_event(event, user)

        # Filter out events that already exist
        events_to_create = [
            event for event in events
            if (event['title'], event['datetime'], event['duration'], event['official']) not in {
                (existing_event.title, existing_event.datetime, existing_event.duration, existing_event.official.name)
                for existing_event in future_events
            }
        ]
        print(f"Creating {len(events_to_create)} events in {self}")
        for event in events_to_create:
            self.add_event(event, user)

    def add_event(self, event_data, appearance_mode=None, user=None):
        """
        Adds a new event to the calendar based on provided event details.

        This method creates a new event in the calendar using the specified event details.
        It handles the creation of related objects, such as event attendees, and performs
        any necessary additional setup or notifications related to the new event.

        Parameters:
            event_data (dict): A dictionary containing the details of the event to be created.
                            Expected keys include 'title', 'datetime', 'duration', 'location', and 'official'.
            user (User, optional): The user responsible for creating the event, used for logging and auditing.
        """

        # Adding event to the calendar
        from apps.spd_files.models import AssignedCase
        is_court_event = isinstance(self.parent_object, AssignedCase) 
        event_data['calendar'] = self
        event = CalendarEvent.create_event(event_data, user=user)
        
        for contact in self.contact_calendared_models.all():            
            case_party = None
            if is_court_event:
                associated_case = self.parent_object
                for party in associated_case.parties.all():
                    if contact == party.party: case_party = party
                    for attorney in party.attorneys.all():
                        if contact == attorney.attorney: case_party = party                
            # Create EventAttendees for each contact associated with this calendar    
            print(f"Adding {contact} as attendee for {event}")            
            EventAttendees.objects.create(
                event=event,
                contact=contact,
                case_party=case_party,
                appearance_mode=appearance_mode
            )

            # Check for conflicts if the contact has a user account
            if hasattr(contact, 'user_account') and contact.user_account:
                contact.user_account.check_scheduling_conflicts(event)            
            if is_court_event:
                print(f"Adding {event} to {contact}'s Court Calendar") 
                contact.add_court_event(event)
            else: print("Not adding to court calendar")
            contact.refresh_schedule_calendar(event.datetime)
            contact.refresh_availability_calendar(event.datetime)
            contact.create_travel_events(event.datetime)
            contact.update_google_calendars(event.datetime)

Output:

Creating 1 events in Automated Court Calendar for XXXXXXX XXXXXXXXXX
Adding Client Doe, John as attendee for Telephone scheduling conference
Adding Telephone scheduling conference to Client Doe, John’s Court Calendar

Then it refreshes each attendee’s scheduling calendar. Each contact has multiple calendars that are tracked from different sources and then consolidated into a front facing calendar that is then uploaded to the user’s google calendar. This is where it breaks down.

class Contact(CalendaredModel):
    #* Model Fields
    first_name = models.CharField(max_length=50, null=True, blank=True, default=None)
    middle_name = models.CharField(max_length=50, null=True, blank=True, default=None)
    last_name_business_name = models.CharField(max_length=200)
    birth_year = models.IntegerField(null=True, blank=True, default=None)
    birth_month = models.IntegerField(null=True, blank=True, default=None)
    birth_day = models.IntegerField(null=True, blank=True, default=None)
    death_date = models.DateField(null=True, blank=True)
    doc_id = models.CharField(max_length=15, null=True, blank=True, default=None)
    bar_number = models.CharField(max_length=15,null=True, blank=True, default=None, unique=True)
    ssn = models.CharField(max_length=9, null=True, blank=True)
    suffix = models.CharField(max_length=5, null=True, blank=True, default=None)
    photo = models.ImageField(null=True, default="default-avatar.png")
    viewed = GenericRelation('oauth_app.User')
    court_calendar = models.OneToOneField('calendars.Calendar', null=True, blank=True, on_delete=models.PROTECT, related_name='court_calendar_contact')
    schedule_calendar = models.OneToOneField('calendars.Calendar', null=True, blank=True, on_delete=models.PROTECT, related_name='schedule_calendar_contact')
    availability_calendar = models.OneToOneField('calendars.Calendar', null=True, blank=True, on_delete=models.PROTECT, related_name='avilability_contact')

    def refresh_schedule_calendar(self, date):
        """
        Refreshes the schedule calendar for a given date by consolidating events from the court and default calendars.

        This method first clears existing events from the schedule calendar for the specified date. It then iterates
        through events in the court and default calendars, creating corresponding events in the schedule calendar.
        Events where the contact is marked as 'not attending' are skipped. The title and notes of events are adjusted
        based on the event's appearance mode and type. After populating the schedule calendar, similar events within
        the same day are merged to simplify the calendar view.

        Parameters:
        - date (datetime.date): The date for which the schedule calendar is being refreshed.

        Notes:
        - The method assumes 'appearance_mode' and 'event_type' are consistently managed within the event data.
        - Events are merged based on a predefined logic that considers their titles, types, and timing.
        """
        from apps.calendars.models import CalendarEvent
        test_court_events = self.court_calendar.events.all()
        print(f"There are {len(test_court_events)} events in the Court Calendar.  The date looking for is: {date}")
        for e in test_court_events:
            print(f"Date for {e} is {e.datetime.date()}, Aware: {is_aware(e.datetime)}")

        print(f"Date filter is aware?  {is_aware(date)}")
        self.schedule_calendar.events.filter(datetime__date=date).delete()
        calendars = [self.court_calendar, self.default_calendar]

        for calendar in calendars:
            events = calendar.events.filter(datetime__date=date)
            print(f"Adding {len(events)} events from {calendar} to Schedule Calendar")
            . . . .

Output:

Added Telephone scheduling conference to contact’s court calendar at 2024-04-18 10:00:00-05:00
There are 1 events in the Court Calendar. The date looking for is: 2024-04-18 10:00:00-05:00
Date for Telephone scheduling conference is 2024-04-18, Aware: True
Date filter is aware? True
Adding 0 events from Court Calendar (Automated) to Schedule Calendar

No, the date filter is not broken.

I think we’re going to need to see a lot more details about this issue.

Please post the models involved, the actual code that you’re running, (Are you doing this in the Django shell? Or is this from a view? If you’re looking at output from a view, please post the complete view.), and a sample of the data in the database showing the fields being checked.

Side note: When posting code here, enclose the code between lines of three backtick - ` characters. This means you’ll have a line of ```, then your code, then another line of ```. This forces the forum software to keep your code properly formatted. (I’ve taken the liberty of fixing your original post for you.)

Also please identify what versions of Django and Python you are using, along with identifying which database engine is being used and what version of the database.

I’ve edited the original post with more information.

Can you try calling calendar.refresh_from_db() here? (Not sure exactly when [self.court_calendar, self.default_calendar] are set, but if you then go and create the events, they maybe/likely have a stale cache.)

Maybe, and I’ll test that when I get home, but if it’s a matter of a stale cache, I’m curious what’s going on under the hood that all() would return expected results, but filter() would not.

One potentially problematic issue is this:

There is nothing here preventing some mistaken code from assigning the same calendar to more than one of these fields - perhaps a case where you’re copying a reference rather than data causing these to become confused.

So I think the first thing I’d check is to verify that all three of these calendar fields are actually referencing different calendars.

Why might this be a cause of these symptoms?

You have this code:

If, for some reason, self.schedule_calendar is a reference to the same Calendar as self.court_calendar, then you’ve just deleted the events.

So, first verify that these three OneToOneField fields are distinct.

Then, try testing the filter in the Django shell - away from all the other processing that is going on that is potentially affecting the results. Verify to your own satisfaction that just the queries are going to work the way you’re expecting them to work.

I’ll check on that and post the results. The calendars are created and assigned on creation of the contact instance, and I haven’t made any changes to that particular part of the code, so I wouldn’t think that would be the issue, but it’s worthwhile to rule it out. If the one to one relationships are being reassigned somewhere, that would cause significant problems. Thank you.

I added the refresh command before calling for the filtered list of events and it didn’t change anything.

I moved this line to after the line that deletes the schedule calendar:

        print(f"There are {len(test_court_events)} events in the Court Calendar.  The date looking for is: {date}")
        for e in test_court_events:
            print(f"Date for {e} is {e.datetime.date()}, Aware: {is_aware(e.datetime)}")

No change. The events aren’t being deleted. They are there. Just not filtering properly.

Creating 1 events in Automated Court Calendar for [COUNTY CASE NUMBER]
Adding [CLIENT] as attendee for Plea/sentencing hearing
Adding Plea/sentencing hearing to [CLIENT]'s Court Calendar
Added Plea/sentencing hearing to contact’s court calendar at 2024-05-10 13:45:00-05:00
Date filter is aware? True
There are 1 events in the Court Calendar. The date looking for is: 2024-05-10 13:45:00-05:00
Date for Plea/sentencing hearing is 2024-05-10, Aware: True
Adding 0 events from Court Calendar (Automated) to Schedule Calendar

Can you post the Event model?

Just as a datapoint, this is from my own website:

>>> from nwp_content.models import Post
>>> p = Post.objects.first()
>>> p.logged
datetime.datetime(2020, 1, 6, 12, 21, 20, tzinfo=datetime.timezone.utc)
>>> Post.objects.filter(logged__date=p.logged).count()
1

It’s not the __date filter. It’s either that the QuerySet has no values, or the value passing to the filter isn’t what you think.

It’s kind of really hard to say from the pasted code. Can you try and reduce it until you find the issue?

Sure. Here is the entire model. Conceptually, when a new court event is created in the automated court calendar for a case, it is also added to the automated court calendar for all contacts associated with that case. It then refreshes the contacts’ schedule calendars to take into account that new scheduling information. The filter criteria that is being passed in is the datetime of the newly created event, so, at minimum, it should find itself (and it does, indeed, show up in all() with the correct datetime information, it is making it into the contact’s court calendar).

class CalendarEvent(SPD_Base):
    #* Model Fields
    title = models.CharField(max_length=100)
    event_type = models.CharField(max_length=15, choices=EVENT_TYPES, default='court')
    calendar = models.ForeignKey(Calendar, on_delete=models.CASCADE, related_name='events')
    datetime = models.DateTimeField(null=True, default=timezone.now)
    duration = models.DurationField(default=timedelta(minutes=30))
    location = models.ForeignKey('contacts.Address', null=True, blank=True, on_delete=models.PROTECT)
    notes = models.TextField(default='')
    default_appearance_mode = models.CharField(max_length=15, choices=APPEARANCE_MODE_CHOICES, default='in_person')
    travel_event = models.BooleanField(default=False) 
    lunch_event = models.BooleanField(default=False)    
    additional_data = models.JSONField(default=dict)
    official = models.ForeignKey('contacts.Contact', null=True, blank=True, on_delete=models.PROTECT)
    google_event_id = models.CharField(max_length=255, blank=True, null=True)

    #* Constructor

    #* Class Variables

    #* Class Methods and Static methods
    @classmethod
    def _get_official(cls, name, county):
        """
        Resolves the name of an official to a Contact instance, excluding names with certain keywords.

        This class method attempts to resolve the provided name to a Contact instance, unless the name contains
        specific keywords indicating it should not correspond to a real contact. Keywords include generic terms like
        'unassigned', types of events ('conference', 'civil', 'criminal'), or modes of appearance ('telephone').
        Additionally, names containing the county followed by "County" are also excluded, assuming such names
        refer to generic or placeholder officials rather than specific individuals.

        Parameters:
            name (str): The name of the official to resolve.
            county (str): The county name to consider for additional keyword filtering.

        Returns:
            Contact or None: A Contact instance if the name is resolved successfully, None otherwise.

        Example:
            resolved_contact = CalendarEvent._get_official('John Doe', 'Dane')
        """
        if name is None: return None
        from apps.contacts.models import Contact
        no_contact_words = ["unassigned", "conference", "conferences", "status", "criminal", "civil", "telephone"]
        if county: no_contact_words.append(f"{county} County".lower())
        cleaned_name = re.sub(r'[^a-zA-Z\s]', '', name)  # Keep only letters and spaces
        parts_in_name = cleaned_name.lower().split()
        if any(word in no_contact_words for word in parts_in_name):
            return None
        else:
            contact, created = Contact.get_or_create_contact({'name': name})
            return contact
    
    @classmethod
    def create_event(cls, event_data, user=None):
        """
        Creates a new event based on the provided event details.

        This class method creates an event, handling the resolution of the event's location and official.
        It automatically logs the event creation, attributing the action to either a specified user or a default "ParalegalBot".

        Parameters:
            event_data (dict): Event details, including title, datetime, duration, location, and official. The 'calendar'
                            key is expected to be included but is popped for direct use in event creation.
            user (User, optional): The user responsible for the event's creation. Used for logging.

        Returns:
            The newly created CalendarEvent instance.

        Raises:
            ValueError: If necessary location or official resolution fails. (Consider implementing in your logic)

        Example:
            CalendarEvent.create_event({
                'title': 'Court Hearing',
                'datetime': datetime(2023, 5, 17, 14, 30),
                'duration': timedelta(hours=2),
                'location': 'ChIJW4cUIGQ4VU0RlRqksXwIAJs',
                'official': 'Judge Doe',
                'calendar': some_calendar_instance
            }, user=current_user)
        """

        from apps.contacts.models import Address, parse_address
        user_text = "ParalegalBot" if user is None else user.a_element        

        # Unpack event_data to separate the 'calendar' key if present
        calendar = event_data.pop('calendar', None)
        default_official = event_data.pop('default_official', None)
        location_txt = event_data.pop('location', calendar.assigned_case.default_event_location)        

        if isinstance(location_txt, Address):
            event_data['location'] = location_txt
        elif len(location_txt.split(",")) > 1:
            address_parts = parse_address(location_txt)
            event_data['location'], created = Address.objects.get_or_create(**address_parts)
        else: event_data['location'] = Address.objects.get(place_id=location_txt)
        county = None
        if event_data['location']: county = event_data['location'].county
        event_data['official'] = cls._get_official(event_data['official'], county)
        if not event_data['official']: event_data['official'] = default_official
        event = cls.objects.create(**event_data, calendar=calendar)

        loggable_object = calendar.parent_object
        loggable_object.logs.create(title=f"{user_text}: {event} created", user=user)
        
        return event    

    @staticmethod
    def should_merge(event1, event2, time_threshold_minutes):
        """
        Determines if two events should be merged based on their title, county, and the proximity of their start times.

        Parameters:
        - event1, event2: Instances of CalendarEvent to be compared.
        - time_threshold_minutes: The maximum time difference (in minutes) between the events for them to be considered for merging.

        Returns:
        - bool: True if the events should be merged, False otherwise.
        """        
        # Ensure event1 starts before event2
        if event1.datetime > event2.datetime:
            event1, event2 = event2, event1  # Swap to maintain chronological order

        # Check if the titles and counties are the same
        titles_match = event1.title == event2.title
        counties_match = event1.county == event2.county  # Assuming both events have a 'county' property that returns the county name

        # Calculate the gap between the end of the first event and the start of the second event
        end_of_first_event = event1.datetime + event1.duration
        start_of_second_event = event2.datetime
        if start_of_second_event <= end_of_first_event: time_gap = timedelta(minutes=1)
        else: time_gap = start_of_second_event - end_of_first_event

        within_time_threshold = time_gap <= timedelta(minutes=time_threshold_minutes)
        
        # Determine if the events are within the time threshold and have matching criteria        
        return titles_match and counties_match and within_time_threshold
    
    @staticmethod
    def merge_two_events(event1, event2):
        """
        Merges two calendar events into a single event.

        Args:
            event1 (CalendarEvent): The first event, assumed to start before event2.
            event2 (CalendarEvent): The second event.

        Returns:
            CalendarEvent: A new CalendarEvent instance that spans the combined duration of event1 and event2.
        """     
        # Ensure event1 starts before event2
        if event1.datetime > event2.datetime:
            event1, event2 = event2, event1  # Swap to maintain chronological order

        # Calculate the new duration to cover both events
        start_datetime = event1.datetime
        end_datetime = max(event1.datetime + event1.duration, event2.datetime + event2.duration)
        total_duration = end_datetime - start_datetime

        # Combine notes from both events
        note1 = event1.notes or ''
        note2 = event2.notes or ''
        combined_notes = f"{note1}{note2}"

        appearance = event1.additional_data.get('appearance', 'in_person')
        appearance_display = event1.additional_data.get('appearance_readable', 'In Person')
        merged_additional_data = {
            'appearance' : appearance,
            'appearance_readable' : appearance_display,
        } 

        # Create a new event with the combined details
        new_event = CalendarEvent.objects.create(
            title=event1.title,  # Assuming the title remains the same
            calendar=event1.calendar,  # Assuming both events are from the same calendar
            datetime=start_datetime,
            duration=total_duration,
            location=event1.location,  # Using location from event1
            notes=combined_notes,
            default_appearance_mode=event1.default_appearance_mode,  # Assuming appearance mode remains consistent
            travel_event=False,  
            additional_data=merged_additional_data,
            official=event1.official,  # Assuming the official remains the same
            # Assuming google_event_id handling or other fields not detailed here
        )

        event1.delete()
        event2.delete()

        return new_event

    #* Properties
    @property
    def end_time(self):
        return self.datetime + self.duration
    
    @property
    def county(self):
        if self.location: return self.location.county
        else: return None
    
    #* Public Methods 
    def delete_event(self, user=None):
        """
        Deletes the event and logs the action.

        This method removes the event instance from the database, ensuring a log entry is created to record
        the event's cancellation. The log entry attributes the action either to a specified user or to a
        default "ParalegalBot".

        Parameters:
            user (User, optional): The user responsible for deleting the event, used for logging purposes.

        Example:
            some_event_instance.delete_event(user=current_user)
        """

        user_text = "ParalegalBot" if user is None else user.a_element
        loggable_object = self.calendar.parent_object
        loggable_object.logs.create(title=f"{user_text}: {self} cancelled.", user=user)
        self.delete()

    def get_event_dict(self, contact):
        case_caption = self.calendar.case_caption
        appearance = self.default_appearance_mode
        appearance_readable = self.get_default_appearance_mode_display()
        attendees = {}
        client = ''
        current_attendee = None
        for attendee in self.attendees.all():
            attendees[attendee] = attendee.calendar_entry
            if attendee.contact == contact:
                current_attendee = attendee
                appearance = attendee.appearance_mode
                appearance_readable = attendee.get_appearance_mode_display()

        attendees_copy = attendees.copy()
        for attendee, note in attendees_copy.items():
            delete_me = False
            if not note or contact == attendee.contact: delete_me = True
            if attendee.is_client(current_attendee.case_party):
                if not note: client = f"Client: {attendee.contact} excused from appearing\n"
                else: client = f"Client: {note}\n"
                delete_me = True
            if delete_me: del attendees[attendee]

        calendar_note = f"{case_caption}, {self.title} ({appearance_readable}) \n{client}"

        for attendee, note in attendees.items():
            calendar_note += note

        if self.notes:
            calendar_note += f"Notes: {self.notes}\n\n"
        else:
            calendar_note += "\n"

        calendar_note += "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n"

        return {
            'calendar_note': calendar_note,
            'appearance': appearance,
            'appearance_readable': appearance_readable,
        }
    
    #* Protected Methods
        
    #* Private Methods
    
    #* Special Methods
    def __str__(self):
        return self.title
    
    #* Inner Meta Class for model metadata

As a thought that may or may not be relevant…

When I moved from Windows to a Linux VM, the database server also changed from MySQL to MariaDB. My understanding is that they were essentially interchangeable, but, if there’s some quirk in mariadb that would need to be taken into account that I’m missing, I thought it would be worth mentioning.

Solved it. Filter wasn’t broken. MariaDB was. When I moved everything to a VM, I forgot to install the timezone tables, so the database didn’t know what to do with the timezone information and just kept kicking it back with no results. Now I have other issues, but the filter is returning results now. Thanks all!

1 Like