def _reschedule_duration(self): for entry, successor in window(self._entries): duration = successor.start_dt - entry.start_dt - self.gap if duration <= timedelta(0): raise UserValueError(_('The chosen time gap would result in an entry with a duration of less than a ' 'minute. Please choose a smaller gap between entries.')) entry.object.duration = duration
def _get_category_score(user, categ, attended_events, debug=False): if debug: print(repr(categ)) # We care about events in the whole timespan where the user attended some events. # However, this might result in some missed events e.g. if the user was not working for # a year and then returned. So we throw away old blocks (or rather adjust the start time # to the start time of the newest block) first_event_date = attended_events[0].start_dt.replace(hour=0, minute=0) last_event_date = attended_events[-1].start_dt.replace( hour=0, minute=0) + timedelta(days=1) blocks = _get_blocks( _query_categ_events(categ, first_event_date, last_event_date), attended_events) for a, b in window(blocks): # More than 3 months between blocks? Ignore the old block! if b[0].start_dt - a[-1].start_dt > timedelta(weeks=12): first_event_date = b[0].start_dt.replace(hour=0, minute=0) # Favorite categories get a higher base score score = int(categ in user.favorite_categories) if debug: print(f'{score:+.3f} - initial') # if there is a favorite event in the category if any(e.category == categ for e in user.favorite_events): score += 0.1 if debug: print(f'{score:+.3f} - favorite events') # Attendance percentage goes to the score directly. If the attendance is high chances are good that the user # is either very interested in whatever goes on in the category or it's something he has to attend regularily. total = _query_categ_events(categ, first_event_date, last_event_date).count() if total: attended_block_event_count = sum(1 for e in attended_events if e.start_dt >= first_event_date) score += attended_block_event_count / total if debug: print(f'{score:+.3f} - attendance') # If there are lots/few unattended events after the last attended one we also update the score with that total_after = _query_categ_events(categ, last_event_date + timedelta(days=1), None).count() if total_after < total * 0.05: score += 0.25 elif total_after > total * 0.25: score -= 0.5 if debug: print(f'{score:+.3f} - unattended new events') # Lower the score based on how long ago the last attended event was if there are no future events # We start applying this modifier only if the event has been more than 40 days in the past to avoid # it from happening in case of monthly events that are not created early enough. days_since_last_event = (date.today() - last_event_date.date()).days if days_since_last_event > 40: score -= 0.025 * days_since_last_event if debug: print(f'{score:+.3f} - days since last event') # For events in the future however we raise the score now_local = utc_to_server(now_utc()) attending_future = (_query_categ_events( categ, now_local, last_event_date).filter(Event.id.in_(e.id for e in attended_events)).all()) if attending_future: score += 0.25 * len(attending_future) if debug: print(f'{score:+.3f} - future event count') days_to_future_event = (attending_future[0].start_dt.date() - date.today()).days score += max(0.1, -(max(0, days_to_future_event - 2) / 4)**(1 / 3) + 2.5) if debug: print(f'{score:+.3f} - days to next future event') return score