Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 33 additions & 6 deletions app/backend/src/couchers/servicers/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -787,6 +787,10 @@ def _user_to_search_user(user_id: int) -> search_pb2.SearchUser:
def EventSearch(
self, request: search_pb2.EventSearchReq, context: CouchersContext, session: Session
) -> search_pb2.EventSearchRes:
if request.attending and request.exclude_attending:
context.abort_with_error_code(
grpc.StatusCode.INVALID_ARGUMENT, "cannot_combine_attending_and_exclude_attending"
)
Comment on lines +790 to +793

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The other one had include_attending which was a bit different but I think it still doesn't make sense to allow both at the same time?

statement = (
select(EventOccurrence).join(Event, Event.id == EventOccurrence.event_id).where(~EventOccurrence.is_deleted)
)
Expand All @@ -809,7 +813,13 @@ def EventSearch(
elif request.only_offline:
statement = statement.where(EventOccurrence.geom != None)

if request.subscribed or request.attending or request.organizing or request.my_communities:
if (
request.subscribed
or request.attending
or request.organizing
or request.my_communities
or request.exclude_attending
):
where_ = []

if request.subscribed:
Expand All @@ -818,21 +828,34 @@ def EventSearch(
and_(EventSubscription.event_id == Event.id, EventSubscription.user_id == context.user_id),
)
where_.append(EventSubscription.user_id != None)
if request.organizing:
if request.organizing or request.attending:
if request.organizing:
statement = statement.outerjoin(
EventOrganizer,
and_(EventOrganizer.event_id == Event.id, EventOrganizer.user_id == context.user_id),
)
where_.append(EventOrganizer.user_id != None)
if request.attending:
statement = statement.outerjoin(
EventOccurrenceAttendee,
and_(
EventOccurrenceAttendee.occurrence_id == EventOccurrence.id,
EventOccurrenceAttendee.user_id == context.user_id,
),
)
where_.append(EventOccurrenceAttendee.user_id != None)
elif request.exclude_attending:
statement = statement.outerjoin(
EventOrganizer,
and_(EventOrganizer.event_id == Event.id, EventOrganizer.user_id == context.user_id),
)
where_.append(EventOrganizer.user_id != None)
if request.attending:
statement = statement.outerjoin(
EventOccurrenceAttendee,
and_(
EventOccurrenceAttendee.occurrence_id == EventOccurrence.id,
EventOccurrenceAttendee.user_id == context.user_id,
),
)
where_.append(EventOccurrenceAttendee.user_id != None)
if request.my_communities:
my_communities = (
session.execute(
Expand All @@ -849,7 +872,11 @@ def EventSearch(
)
where_.append(Event.parent_node_id.in_(my_communities))

statement = statement.where(or_(*where_))
if where_:
statement = statement.where(or_(*where_))

Comment on lines +875 to +877

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

an empty OR means "no inclusion filter" (return everything), not "return nothing"?

if request.exclude_attending:
statement = statement.where(EventOccurrenceAttendee.user_id == None, EventOrganizer.user_id == None)

if not request.include_cancelled:
statement = statement.where(~EventOccurrence.is_cancelled)
Expand Down
57 changes: 57 additions & 0 deletions app/backend/src/tests/test_search.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from datetime import timedelta
from typing import Any

import grpc
import pytest
from google.protobuf import empty_pb2, wrappers_pb2
from sqlalchemy import select
Expand Down Expand Up @@ -751,6 +752,62 @@ def test_event_search_filter_subscription_attendance_organizing_my_communities(
assert {event.title for event in res.events} == {"Subscribed event", "Attending event", "Organized event"}


def test_event_search_exclude_attending(sample_community, create_event, moderator: Moderator):
"""Test that exclude_attending removes events the user is attending or organizing."""
user, token = generate_user()
other_user, other_token = generate_user()

with communities_session(token) as api:
api.JoinCommunity(communities_pb2.JoinCommunityReq(community_id=sample_community))

with session_scope() as session:
create_community(session, 55, 60, "Other community", [other_user], [], None)

with events_session(other_token) as api:
e_attending = create_event(api, title="Attending event")
e_community_only = create_event(api, title="Community only event")
create_event(
api,
title="Other community event",
offline_information=events_pb2.OfflineEventInformation(lat=58, lng=1, address="Somewhere"),
)

with session_scope() as session:
occurrence_ids = session.execute(select(EventOccurrence.id)).scalars().all()
for oid in occurrence_ids:
moderator.approve_event_occurrence(oid)

with events_session(token) as api:
e_organized = create_event(api, title="Organized event")
api.SetEventAttendance(
events_pb2.SetEventAttendanceReq(
event_id=e_attending.event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING
)
)

with search_session(token) as api:
# baseline: my_communities returns all community events including attended/organized
res = api.EventSearch(search_pb2.EventSearchReq(my_communities=True))
assert {event.title for event in res.events} == {
"Attending event",
"Community only event",
"Organized event",
}

# my_communities + exclude_attending: drops attended and organized events
res = api.EventSearch(search_pb2.EventSearchReq(my_communities=True, exclude_attending=True))
assert {event.title for event in res.events} == {"Community only event"}

# exclude_attending alone (no other filter = all events): drops attended and organized
res = api.EventSearch(search_pb2.EventSearchReq(exclude_attending=True))
assert {event.title for event in res.events} == {"Community only event", "Other community event"}

# attending + exclude_attending is invalid
with pytest.raises(grpc.RpcError) as e:
api.EventSearch(search_pb2.EventSearchReq(attending=True, exclude_attending=True))
assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT


def test_regression_search_multiple_pages(db):
"""
There was a bug when there are multiple pages of results
Expand Down
2 changes: 2 additions & 0 deletions app/proto/search.proto
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,8 @@ message EventSearchReq {
string page_token = 11;
uint32 page_number = 18;
}
// exclude events the user is already attending or organizing (useful with my_communities)
bool exclude_attending = 19;
}

message EventSearchRes {
Expand Down
Loading