Пример #1
0
def get_events_with_abstract_persons(user, dt=None):
    """
    Return a dict of event ids and the abstract submission related
    roles the user has in that event.

    :param user: A `User`
    :param dt: Only include events taking place on/after that date
    """
    data = defaultdict(set)
    bad_states = {AbstractState.withdrawn, AbstractState.rejected}
    # submitter
    query = (Abstract.query.filter(~Event.is_deleted, ~Abstract.is_deleted,
                                   ~Abstract.state.in_(bad_states),
                                   Event.ends_after(dt),
                                   Abstract.submitter == user).join(
                                       Abstract.event).options(
                                           load_only('event_id')))
    for abstract in query:
        data[abstract.event_id].add('abstract_submitter')
    # person
    abstract_criterion = db.and_(~Abstract.state.in_(bad_states),
                                 ~Abstract.is_deleted)
    query = (user.event_persons.filter(
        ~Event.is_deleted, Event.ends_after(dt),
        EventPerson.abstract_links.any(
            AbstractPersonLink.abstract.has(abstract_criterion))).join(
                EventPerson.event).options(load_only('event_id')))
    for person in query:
        data[person.event_id].add('abstract_person')
    return data
Пример #2
0
def get_events_with_abstract_reviewer_convener(user, dt=None):
    """
    Return a dict of event ids and the abstract reviewing related
    roles the user has in that event.

    :param user: A `User`
    :param dt: Only include events taking place on/after that date
    """
    data = defaultdict(set)
    # global reviewer/convener
    mapping = {
        'global_abstract_reviewer_for_events': 'abstract_reviewer',
        'global_convener_for_events': 'track_convener'
    }
    for rel, role in mapping.iteritems():
        query = (Event.query.with_parent(user, rel).filter(
            Event.ends_after(dt), ~Event.is_deleted).options(load_only('id')))
        for event in query:
            data[event.id].add(role)
    # track reviewer/convener
    mapping = {
        'abstract_reviewer_for_tracks': 'abstract_reviewer',
        'convener_for_tracks': 'track_convener'
    }
    for rel, role in mapping.iteritems():
        query = (Track.query.with_parent(user, rel).join(Track.event).filter(
            Event.ends_after(dt),
            ~Event.is_deleted).options(load_only('event_id')))
        for track in query:
            data[track.event_id].add(role)
    return data
Пример #3
0
 def event(self, idlist):
     query = (Event.find(
         Event.id.in_(idlist), ~Event.is_deleted,
         Event.happens_between(self._fromDT, self._toDT)).options(
             *self._get_query_options(self._detail_level)))
     query = self._update_query(query)
     return self.serialize_events(
         x for x in query
         if self._filter_event(x) and x.can_access(self.user))
Пример #4
0
def create_event(category, event_type, data, add_creator_as_manager=True, features=None):
    """Create a new event.

    :param category: The category in which to create the event
    :param event_type: An `EventType` value
    :param data: A dict containing data used to populate the event
    :param add_creator_as_manager: Whether the creator (current user)
                                   should be added as a manager
    :param features: A list of features that will be enabled for the
                     event. If set, only those features will be used
                     and the default feature set for the event type
                     will be ignored.
    """
    event = Event(category=category, type_=event_type)
    data.setdefault('creator', session.user)
    theme = data.pop('theme', None)
    person_link_data = data.pop('person_link_data', {})
    event.populate_from_dict(data)
    db.session.flush()
    event.person_link_data = person_link_data
    if theme is not None:
        layout_settings.set(event, 'timetable_theme', theme)
    if add_creator_as_manager:
        with event.logging_disabled:
            event.update_principal(event.creator, full_access=True)
    if features is not None:
        features_event_settings.set(event, 'enabled', features)
    db.session.flush()
    signals.event.created.send(event)
    logger.info('Event %r created in %r by %r ', event, category, session.user)
    event.log(EventLogRealm.event, EventLogKind.positive, 'Event', 'Event created', session.user)
    db.session.flush()
    return event
Пример #5
0
 def deserialize(self):
     if not self.force and self.data['fossir_version'] != fossir.__version__:
         click.secho(
             'Version mismatch: trying to import event exported with {} to version {}'
             .format(self.data['fossir_version'], fossir.__version__),
             fg='red')
         return None
     self._load_users(self.data)
     for tablename, tabledata in self.data['objects']:
         self._deserialize_object(db.metadata.tables[tablename], tabledata)
     if self.deferred_idrefs:
         # Any reference to an ID that was exported need to be replaced
         # with an actual ID at some point - either immediately (if the
         # referenced row was already imported) or later (usually in case
         # of circular dependencies where one of the IDs is not available
         # when the row is inserted).
         click.secho('BUG: Not all deferred idrefs have been consumed',
                     fg='red')
         for uuid, values in self.deferred_idrefs.iteritems():
             click.secho('{}:'.format(uuid), fg='yellow', bold=True)
             for table, col, pk_value in values:
                 click.secho('  - {}.{} ({})'.format(
                     table.fullname, col, pk_value),
                             fg='yellow')
         raise Exception('Not all deferred idrefs have been consumed')
     event = Event.get(self.event_id)
     event.log(EventLogRealm.event, EventLogKind.other, 'Event',
               'Event imported from another fossir instance')
     self._associate_users_by_email(event)
     db.session.flush()
     return event
Пример #6
0
def serialize_category_atom(category, url, user, event_filter):
    """Export the events in a category to Atom

    :param category: The category to export
    :param url: The URL of the feed
    :param user: The user who needs to be able to access the events
    :param event_filter: A SQLalchemy criterion to restrict which
                         events will be returned.  Usually something
                         involving the start/end date of the event.
    """
    query = (Event.query
             .filter(Event.category_chain_overlaps(category.id),
                     ~Event.is_deleted,
                     event_filter)
             .options(load_only('id', 'category_id', 'start_dt', 'title', 'description', 'protection_mode',
                                'access_key'),
                      subqueryload('acl_entries'))
             .order_by(Event.start_dt))
    events = [e for e in query if e.can_access(user)]

    feed = AtomFeed(feed_url=url, title='fossir Feed [{}]'.format(category.title))
    for event in events:
        feed.add(title=event.title,
                 summary=unicode(event.description),  # get rid of RichMarkup
                 url=event.external_url,
                 updated=event.start_dt)
    return BytesIO(feed.to_string().encode('utf-8'))
Пример #7
0
 def _getParams(self):
     super(NoteExportHook, self)._getParams()
     event = self._obj = Event.get(self._pathParams['event_id'],
                                   is_deleted=False)
     if event is None:
         raise HTTPAPIError('No such event', 404)
     session_id = self._pathParams.get('session_id')
     if session_id:
         self._obj = Session.query.with_parent(event).filter_by(
             id=session_id).first()
         if self._obj is None:
             raise HTTPAPIError("No such session", 404)
     contribution_id = self._pathParams.get('contribution_id')
     if contribution_id:
         contribution = self._obj = (
             Contribution.query.with_parent(event).filter_by(
                 id=contribution_id, is_deleted=False).first())
         if contribution is None:
             raise HTTPAPIError("No such contribution", 404)
         subcontribution_id = self._pathParams.get('subcontribution_id')
         if subcontribution_id:
             self._obj = SubContribution.query.with_parent(
                 contribution).filter_by(id=subcontribution_id,
                                         is_deleted=False).first()
             if self._obj is None:
                 raise HTTPAPIError("No such subcontribution", 404)
     self._note = EventNote.get_for_linked_object(self._obj,
                                                  preload_event=False)
     if self._note is None or self._note.is_deleted:
         raise HTTPAPIError("No such note", 404)
Пример #8
0
        def _iterate_objs(query_string):
            query = (Event.query.filter(
                Event.title_matches(to_unicode(query_string)),
                ~Event.is_deleted).options(
                    undefer('effective_protection_mode')))
            sort_dir = db.desc if self._descending else db.asc
            if self._orderBy == 'start':
                query = query.order_by(sort_dir(Event.start_dt))
            elif self._orderBy == 'end':
                query = query.order_by(sort_dir(Event.end_dt))
            elif self._orderBy == 'id':
                query = query.order_by(sort_dir(Event.id))
            elif self._orderBy == 'title':
                query = query.order_by(sort_dir(db.func.lower(Event.title)))

            counter = 0
            # Query the DB in chunks of 1000 records per query until the limit is satisfied
            for event in query.yield_per(1000):
                if event.can_access(self._user):
                    counter += 1
                    # Start yielding only when the counter reaches the given offset
                    if (self._offset is None) or (counter > self._offset):
                        yield event
                        # Stop querying the DB when the limit is satisfied
                        if (self._limit is not None) and (
                                counter == self._offset + self._limit):
                            break
Пример #9
0
def event_or_shorturl(confId, shorturl_namespace=False, force_overview=False):
    func = None
    event_ = Event.get(int(confId)) if confId.isdigit() else None
    if event_ and event_.is_deleted:
        raise NotFound(_('This event has been deleted.'))
    elif event_:
        # For obvious reasons an event id always comes first.
        # If it's used within the short url namespace we redirect to the event namespace, otherwise
        # we call the RH to display the event
        if shorturl_namespace:
            func = lambda: redirect(event_.url)
        else:
            request.view_args['confId'] = int(request.view_args['confId'])
            func = lambda: RHDisplayEvent().process()
    else:
        shorturl_event = (Event.query.filter(
            db.func.lower(Event.url_shortcut) == confId.lower(),
            ~Event.is_deleted).one_or_none())
        if (shorturl_namespace or config.ROUTE_OLD_URLS) and shorturl_event:
            if shorturl_namespace:
                # Correct namespace => redirect to the event
                func = lambda: redirect(shorturl_event.url)
            else:
                # Old event namespace => 301-redirect to the new shorturl first to get Google etc. to update it
                func = lambda: redirect(shorturl_event.short_url, 301)
        elif is_legacy_id(confId):
            mapping = LegacyEventMapping.find_first(legacy_event_id=confId)
            if mapping is not None:
                url = url_for('events.display', confId=mapping.event_id)
                func = lambda: redirect(url, 301)

    if func is None:
        raise NotFound(_('An event with this ID/shortcut does not exist.'))
    return func()
Пример #10
0
def get_events_with_linked_sessions(user, dt=None):
    """Returns a dict with keys representing event_id and the values containing
    data about the user rights for sessions within the event

    :param user: A `User`
    :param dt: Only include events taking place on/after that date
    """
    query = (user.in_session_acls.options(
        load_only('session_id', 'roles', 'full_access',
                  'read_access')).options(noload('*')).options(
                      contains_eager(SessionPrincipal.session).load_only(
                          'event_id')).join(Session).join(
                              Event, Event.id == Session.event_id).filter(
                                  ~Session.is_deleted, ~Event.is_deleted,
                                  Event.ends_after(dt)))
    data = defaultdict(set)
    for principal in query:
        roles = data[principal.session.event_id]
        if 'coordinate' in principal.roles:
            roles.add('session_coordinator')
        if 'submit' in principal.roles:
            roles.add('session_submission')
        if principal.full_access:
            roles.add('session_manager')
        if principal.read_access:
            roles.add('session_access')
    return data
Пример #11
0
 def export_timetable(self, user):
     events = Event.find_all(Event.id.in_(map(int, self._idList)),
                             ~Event.is_deleted)
     return {
         event.id: TimetableSerializer(event, management=False,
                                       user=user).serialize_timetable()
         for event in events
     }
Пример #12
0
 def _getParams(self):
     super(AgreementExportHook, self)._getParams()
     type_ = self._pathParams['agreement_type']
     try:
         self._definition = get_agreement_definitions()[type_]
     except KeyError:
         raise HTTPAPIError('No such agreement type', 404)
     self.event = Event.get(self._pathParams['event_id'], is_deleted=False)
     if self.event is None:
         raise HTTPAPIError('No such event', 404)
Пример #13
0
 def category_extra(self, ids):
     if self._toDT is None:
         has_future_events = False
     else:
         query = Event.find(Event.category_id.in_(ids), ~Event.is_deleted,
                            Event.start_dt > self._toDT)
         has_future_events = query.has_rows()
     return {
         'eventCategories': self._build_category_path_data(ids),
         'moreFutureEvents': has_future_events
     }
Пример #14
0
def get_events_created_by(user, dt=None):
    """Gets the IDs of events created by the user

    :param user: A `User`
    :param dt: Only include events taking place on/after that date
    :return: A set of event ids
    """
    query = (user.created_events.filter(~Event.is_deleted,
                                        Event.ends_after(dt)).options(
                                            load_only('id')))
    return {event.id for event in query}
Пример #15
0
def restore(event_id):
    """Restores a deleted event."""
    event = Event.get(event_id)
    if event is None:
        click.secho('This event does not exist', fg='red')
        sys.exit(1)
    elif not event.is_deleted:
        click.secho('This event is not deleted', fg='yellow')
        sys.exit(1)
    event.is_deleted = False
    db.session.commit()
    click.secho('Event undeleted: "{}"'.format(event.title), fg='green')
Пример #16
0
 def validate_entries(self, field):
     if field.errors:
         return
     for entry in field.data:
         if entry['days'] < 0:
             raise ValidationError(_("'Days' must be a positive integer"))
         if entry['type'] not in {'category', 'event'}:
             raise ValidationError(_('Invalid type'))
         if entry['type'] == 'category' and not Category.get(entry['id'], is_deleted=False):
             raise ValidationError(_('Invalid category: {}').format(entry['id']))
         if entry['type'] == 'event' and not Event.get(entry['id'], is_deleted=False):
             raise ValidationError(_('Invalid event: {}').format(entry['id']))
Пример #17
0
def get_events_managed_by(user, dt=None):
    """Gets the IDs of events where the user has management privs.

    :param user: A `User`
    :param dt: Only include events taking place on/after that date
    :return: A set of event ids
    """
    query = (user.in_event_acls.join(Event).options(
        noload('user'), noload('local_group'), load_only('event_id')).filter(
            ~Event.is_deleted, Event.ends_after(dt)).filter(
                EventPrincipal.has_management_role('ANY')))
    return {principal.event_id for principal in query}
Пример #18
0
 def category(self, idlist, format):
     try:
         idlist = map(int, idlist)
     except ValueError:
         raise HTTPAPIError('Category IDs must be numeric', 400)
     if format == 'ics':
         buf = serialize_categories_ical(idlist,
                                         self.user,
                                         event_filter=Event.happens_between(
                                             self._fromDT, self._toDT),
                                         event_filter_fn=self._filter_event,
                                         update_query=self._update_query)
         return send_file('events.ics', buf, 'text/calendar')
     else:
         query = (Event.query.filter(
             ~Event.is_deleted, Event.category_chain_overlaps(idlist),
             Event.happens_between(self._fromDT, self._toDT)).options(
                 *self._get_query_options(self._detail_level)))
     query = self._update_query(query)
     return self.serialize_events(
         x for x in query
         if self._filter_event(x) and x.can_access(self.user))
Пример #19
0
 def _create_event(id_=None, **kwargs):
     # we specify `acl_entries` so SA doesn't load it when accessing it for
     # the first time, which would require no_autoflush blocks in some cases
     now = now_utc(exact=False)
     kwargs.setdefault('type_', EventType.meeting)
     kwargs.setdefault(
         'title', u'dummy#{}'.format(id_) if id_ is not None else u'dummy')
     kwargs.setdefault('start_dt', now)
     kwargs.setdefault('end_dt', now + timedelta(hours=1))
     kwargs.setdefault('timezone', 'UTC')
     kwargs.setdefault('category', dummy_category)
     event = Event(id=id_, creator=dummy_user, acl_entries=set(), **kwargs)
     db.session.flush()
     return event
Пример #20
0
 def session(self, idlist):
     event = Event.get(self._eventId, is_deleted=False)
     if not event:
         return []
     idlist = set(map(int, idlist))
     sessions = (Session.query.with_parent(event).filter(
         Session.id.in_(idlist), ~Session.is_deleted).all())
     # Fallback for friendly_id
     sessions += (Session.query.with_parent(event).filter(
         Session.friendly_id.in_(idlist - {s.id
                                           for s in sessions}),
         ~Session.is_deleted).all())
     self._detail_level = 'contributions'
     return self._build_sessions_api_data(sessions)
Пример #21
0
def is_feature_enabled(event, name):
    """Checks if a feature is enabled for an event.

    :param event: The event (or event ID) to check.
    :param name: The name of the feature.
    """
    feature = get_feature_definition(name)
    enabled_features = features_event_settings.get(event, 'enabled')
    if enabled_features is not None:
        return feature.name in enabled_features
    else:
        if isinstance(event, (basestring, int, long)):
            event = Event.get(event)
        return event and feature.is_default_for_event(event)
Пример #22
0
def export(event_id, target_file):
    """Exports all data associated with an event.

    This exports the whole event as an archive which can be imported
    on another other fossir instance.  Importing an event is only
    guaranteed to work if it was exported on the same fossir version.
    """
    event = Event.get(event_id)
    if event is None:
        click.secho('This event does not exist', fg='red')
        sys.exit(1)
    elif event.is_deleted:
        click.secho('This event has been deleted', fg='yellow')
        click.confirm('Export it anyway?', abort=True)
    export_event(event, target_file)
Пример #23
0
def get_events_with_submitted_surveys(user, dt=None):
    """Gets the IDs of events where the user submitted a survey.

    :param user: A `User`
    :param dt: Only include events taking place on/after that date
    :return: A set of event ids
    """
    from fossir.modules.events.surveys.models.surveys import Survey
    # Survey submissions are not stored in links anymore, so we need to get them directly
    query = (user.survey_submissions
             .options(load_only('survey_id'))
             .options(joinedload(SurveySubmission.survey).load_only('event_id'))
             .join(Survey)
             .join(Event)
             .filter(~Survey.is_deleted, ~Event.is_deleted, Event.ends_after(dt)))
    return {submission.survey.event_id for submission in query}
Пример #24
0
def get_events_with_paper_roles(user, dt=None):
    """
    Get the IDs and PR roles of events where the user has any kind
    of paper reviewing privileges.

    :param user: A `User`
    :param dt: Only include events taking place on/after that date
    :return: A dict mapping event IDs to a set of roles
    """
    paper_roles = {'paper_manager', 'paper_judge', 'paper_content_reviewer', 'paper_layout_reviewer'}
    role_criteria = [EventPrincipal.has_management_role(role, explicit=True) for role in paper_roles]
    query = (user.in_event_acls
             .join(Event)
             .options(noload('user'), noload('local_group'), load_only('event_id', 'roles'))
             .filter(~Event.is_deleted, Event.ends_after(dt))
             .filter(db.or_(*role_criteria)))
    return {principal.event_id: set(principal.roles) & paper_roles for principal in query}
Пример #25
0
def get_events_with_linked_event_persons(user, dt=None):
    """
    Returns a dict containing the event ids and role for all events
    where the user is a chairperson or (in case of a lecture) speaker.

    :param user: A `User`
    :param dt: Only include events taking place on/after that date
    """
    query = (user.event_persons.with_entities(
        EventPerson.event_id,
        Event._type).join(Event, Event.id == EventPerson.event_id).filter(
            EventPerson.event_links.any()).filter(~Event.is_deleted,
                                                  Event.ends_after(dt)))
    return {
        event_id: ('lecture_speaker'
                   if event_type == EventType.lecture else 'conference_chair')
        for event_id, event_type in query
    }
Пример #26
0
def test_deleted_relationships(db, dummy_event):
    event = dummy_event
    assert not event.contributions
    assert not event.sessions
    s = Session(event=event, title='s')
    sd = Session(event=event, title='sd', is_deleted=True)
    c = Contribution(event=event,
                     title='c',
                     session=sd,
                     duration=timedelta(minutes=30))
    cd = Contribution(event=event,
                      title='cd',
                      session=sd,
                      duration=timedelta(minutes=30),
                      is_deleted=True)
    sc = SubContribution(contribution=c,
                         title='sc',
                         duration=timedelta(minutes=10))
    scd = SubContribution(contribution=c,
                          title='scd',
                          duration=timedelta(minutes=10),
                          is_deleted=True)
    db.session.flush()
    db.session.expire_all()
    # reload all the objects from the db
    event = Event.get(event.id)
    s = Session.get(s.id)
    sd = Session.get(sd.id)
    c = Contribution.get(c.id)
    cd = Contribution.get(cd.id)
    sc = SubContribution.get(sc.id)
    scd = SubContribution.get(scd.id)
    # deleted items should not be in the lists
    assert event.sessions == [s]
    assert event.contributions == [c]
    assert sd.contributions == [c]
    assert c.subcontributions == [sc]
    # the other direction should work fine even in case of deletion
    assert s.event == event
    assert sd.event == event
    assert c.event == event
    assert cd.event == event
    assert sc.contribution == c
    assert scd.contribution == c
Пример #27
0
def get_object_from_args(args=None):
    """Retrieves an event object from request arguments.

    This utility is meant to be used in cases where the same controller
    can deal with objects attached to various parts of an event which
    use different URLs to indicate which object to use.

    :param args: The request arguments. If unspecified,
                 ``request.view_args`` is used.
    :return: An ``(object_type, event, object)`` tuple.  The event is
             always the :class:`Event` associated with the object.
             The object may be an `Event`, `Session`, `Contribution`
             or `SubContribution`.  If the object does not exist,
             ``(object_type, None, None)`` is returned.
    """
    if args is None:
        args = request.view_args
    object_type = args['object_type']
    event = Event.find_first(id=args['confId'], is_deleted=False)
    if event is None:
        obj = None
    elif object_type == 'event':
        obj = event
    elif object_type == 'session':
        obj = Session.query.with_parent(event).filter_by(
            id=args['session_id']).first()
    elif object_type == 'contribution':
        obj = Contribution.query.with_parent(event).filter_by(
            id=args['contrib_id']).first()
    elif object_type == 'subcontribution':
        obj = SubContribution.find(
            SubContribution.id == args['subcontrib_id'],
            ~SubContribution.is_deleted,
            SubContribution.contribution.has(event=event,
                                             id=args['contrib_id'],
                                             is_deleted=False)).first()
    else:
        raise ValueError('Unexpected object type: {}'.format(object_type))
    if obj is not None:
        return object_type, event, obj
    else:
        return object_type, None, None
Пример #28
0
 def _process(self):
     self.user.settings.set('suggest_categories', True)
     tz = session.tzinfo
     hours, minutes = timedelta_split(tz.utcoffset(datetime.now()))[:2]
     categories = get_related_categories(self.user)
     categories_events = []
     if categories:
         category_ids = {c['categ'].id for c in categories.itervalues()}
         today = now_utc(False).astimezone(tz).date()
         query = (Event.query.filter(
             ~Event.is_deleted, Event.category_chain_overlaps(category_ids),
             Event.start_dt.astimezone(session.tzinfo) >= today).options(
                 joinedload('category').load_only('id', 'title'),
                 joinedload('series'), subqueryload('acl_entries'),
                 load_only('id', 'category_id', 'start_dt', 'end_dt',
                           'title', 'access_key', 'protection_mode',
                           'series_id', 'series_pos',
                           'series_count')).order_by(
                               Event.start_dt, Event.id))
         categories_events = get_n_matching(
             query, 10, lambda x: x.can_access(self.user))
     from_dt = now_utc(False) - relativedelta(
         weeks=1, hour=0, minute=0, second=0)
     linked_events = [(event, {
         'management': bool(roles & self.management_roles),
         'reviewing': bool(roles & self.reviewer_roles),
         'attendance': bool(roles & self.attendance_roles)
     })
                      for event, roles in get_linked_events(
                          self.user, from_dt, 10).iteritems()]
     return WPUser.render_template(
         'dashboard.html',
         'dashboard',
         offset='{:+03d}:{:02d}'.format(hours, minutes),
         user=self.user,
         categories=categories,
         categories_events=categories_events,
         suggested_categories=get_suggested_categories(self.user),
         linked_events=linked_events)
Пример #29
0
 def _getParams(self):
     super(AttachmentsExportHook, self)._getParams()
     event = self._obj = Event.get(self._pathParams['event_id'],
                                   is_deleted=False)
     if event is None:
         raise HTTPAPIError('No such event', 404)
     session_id = self._pathParams.get('session_id')
     if session_id:
         self._obj = Session.query.with_parent(event).filter_by(
             id=session_id).first()
         if self._obj is None:
             raise HTTPAPIError("No such session", 404)
     contribution_id = self._pathParams.get('contribution_id')
     if contribution_id:
         contribution = self._obj = Contribution.query.with_parent(
             event).filter_by(id=contribution_id).first()
         if contribution is None:
             raise HTTPAPIError("No such contribution", 404)
         subcontribution_id = self._pathParams.get('subcontribution_id')
         if subcontribution_id:
             self._obj = SubContribution.query.with_parent(
                 contribution).filter_by(id=subcontribution_id).first()
             if self._obj is None:
                 raise HTTPAPIError("No such subcontribution", 404)
Пример #30
0
def _query_categ_events(categ, start_dt, end_dt):
    return (Event.query
            .with_parent(categ)
            .filter(Event.happens_between(start_dt, end_dt))
            .options(load_only('id', 'start_dt', 'end_dt')))