Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to show accepted speakers/proposals #4354

Merged
merged 31 commits into from
Feb 17, 2025
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
25 changes: 25 additions & 0 deletions backend/api/cms/page/blocks/dynamic_content_display_section.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from typing import Self
import strawberry
from api.cms.page.registry import register_page_block
import enum


@strawberry.enum
class DynamicContentDisplaySectionSource(enum.Enum):
speakers = "speakers"
keynoters = "keynoters"
proposals = "proposals"


@register_page_block()
@strawberry.type
class DynamicContentDisplaySection:
id: strawberry.ID
source: DynamicContentDisplaySectionSource

@classmethod
def from_block(cls, block) -> Self:
return cls(
id=block.id,
source=DynamicContentDisplaySectionSource(block.value["source"]),
)
18 changes: 5 additions & 13 deletions backend/api/participants/queries.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import json
from typing import Optional
from django.conf import settings
import strawberry
from strawberry.tools import create_type
Expand All @@ -15,20 +14,13 @@


@strawberry.field
def participant(
info: Info, user_id: strawberry.ID, conference: str
) -> Optional[Participant]:
user = info.context.request.user
decoded_id = decode_hashid(user_id, salt=settings.USER_ID_HASH_SALT, min_length=6)
def participant(info: Info, id: strawberry.ID, conference: str) -> Participant | None:
decoded_id = decode_hashid(id, salt=settings.USER_ID_HASH_SALT, min_length=6)
participant = ParticipantModel.objects.filter(
conference__code=conference, user_id=decoded_id
conference__code=conference, id=decoded_id
).first()

if not participant or (
not participant.public_profile and (not user or participant.user_id != user.id)
):
# Profile doesn't exist, or
# Profile is not public, and the person requesting it is not the owner
if not participant:
return None

return Participant.from_model(participant)
Expand All @@ -37,7 +29,7 @@ def participant(
@strawberry.field
def ticket_id_to_user_hashid(
ticket_id: strawberry.ID, conference_code: str
) -> Optional[str]:
) -> str | None:
conference = Conference.objects.filter(code=conference_code).first()
decoded_ticket_id = decode_hashid(ticket_id)
order_position = pretix.get_order_position(conference, decoded_ticket_id)
Expand Down
Empty file.
103 changes: 103 additions & 0 deletions backend/api/participants/tests/test_queries.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
from conferences.tests.factories import ConferenceFactory
from participants.tests.factories import ParticipantFactory


def test_query_participant(graphql_client):
participant = ParticipantFactory()

response = graphql_client.query(
"""
query Participant($id: ID!, $conference: String!) {
participant(id: $id, conference: $conference) {
id
fullname
}
}
""",
variables={"id": participant.hashid, "conference": participant.conference.code},
)

assert response["data"]["participant"]["id"] == participant.hashid
assert response["data"]["participant"]["fullname"] == participant.user.fullname


def test_query_private_fields_of_own_user(graphql_client, user):
graphql_client.force_login(user)
participant = ParticipantFactory(
user=user,
speaker_availabilities={"test": "test"},
previous_talk_video="test",
speaker_level="level",
)

response = graphql_client.query(
"""query Participant($id: ID!, $conference: String!) {
participant(id: $id, conference: $conference) {
id
speakerAvailabilities
previousTalkVideo
speakerLevel
}
}
""",
variables={"id": participant.hashid, "conference": participant.conference.code},
)

assert response["data"]["participant"]["speakerAvailabilities"] == {"test": "test"}
assert response["data"]["participant"]["previousTalkVideo"] == "test"
assert response["data"]["participant"]["speakerLevel"] == "level"


def test_cannot_query_private_fields_of_other_user(graphql_client):
participant = ParticipantFactory()

response = graphql_client.query(
"""query Participant($id: ID!, $conference: String!) {
participant(id: $id, conference: $conference) {
id
speakerAvailabilities
previousTalkVideo
speakerLevel
}
}
""",
variables={"id": participant.hashid, "conference": participant.conference.code},
)

assert response["data"]["participant"]["speakerAvailabilities"] is None
assert response["data"]["participant"]["previousTalkVideo"] is None
assert response["data"]["participant"]["speakerLevel"] is None


def test_query_participant_with_wrong_conference(graphql_client):
participant = ParticipantFactory()

response = graphql_client.query(
"""
query Participant($id: ID!, $conference: String!) {
participant(id: $id, conference: $conference) {
id
fullname
}
}
""",
variables={"id": participant.hashid, "conference": ConferenceFactory().code},
)

assert response["data"]["participant"] is None


def test_query_participant_with_non_existent_id(graphql_client):
response = graphql_client.query(
"""
query Participant($id: ID!, $conference: String!) {
participant(id: $id, conference: $conference) {
id
fullname
}
}
""",
variables={"id": "abcabc", "conference": ConferenceFactory().code},
)

assert response["data"]["participant"] is None
42 changes: 31 additions & 11 deletions backend/api/participants/types.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
from typing import Optional
from typing import TYPE_CHECKING, Annotated

from submissions.models import Submission as SubmissionModel
from strawberry.scalars import JSON
import strawberry
from strawberry import ID

from api.submissions.permissions import CanSeeSubmissionPrivateFields

if TYPE_CHECKING:
from api.submissions.types import Submission


@strawberry.type
class Participant:
id: ID
user_id: ID
bio: str
website: str
photo: str | None
Expand All @@ -21,22 +24,39 @@ class Participant:
linkedin_url: str
facebook_url: str
mastodon_handle: str
speaker_id: strawberry.Private[int]
fullname: str
speaker_availabilities: JSON

_speaker_level: strawberry.Private[str]
_previous_talk_video: strawberry.Private[str]
_conference_id: strawberry.Private[int]
_user_id: strawberry.Private[int]
_speaker_availabilities: strawberry.Private[int]

@strawberry.field
def proposals(
self, info
) -> list[Annotated["Submission", strawberry.lazy("api.submissions.types")]]:
return SubmissionModel.objects.for_conference(self._conference_id).filter(
speaker_id=self._user_id,
status=SubmissionModel.STATUS.accepted,
)

@strawberry.field
def speaker_level(self, info) -> Optional[str]:
def speaker_availabilities(self, info) -> JSON | None:
if not CanSeeSubmissionPrivateFields().has_permission(self, info):
return None

return self._speaker_availabilities

@strawberry.field
def speaker_level(self, info) -> str | None:
if not CanSeeSubmissionPrivateFields().has_permission(self, info):
return None

return self._speaker_level

@strawberry.field
def previous_talk_video(self, info) -> Optional[str]:
def previous_talk_video(self, info) -> str | None:
if not CanSeeSubmissionPrivateFields().has_permission(self, info):
return None

Expand All @@ -46,20 +66,20 @@ def previous_talk_video(self, info) -> Optional[str]:
def from_model(cls, instance):
return cls(
id=instance.hashid,
user_id=instance.user_id,
speaker_id=instance.user_id,
fullname=instance.user.fullname,
photo=instance.photo_url,
photo_id=instance.photo_file_id,
bio=instance.bio,
website=instance.website,
public_profile=instance.public_profile,
_speaker_level=instance.speaker_level,
_previous_talk_video=instance.previous_talk_video,
twitter_handle=instance.twitter_handle,
instagram_handle=instance.instagram_handle,
linkedin_url=instance.linkedin_url,
facebook_url=instance.facebook_url,
mastodon_handle=instance.mastodon_handle,
speaker_availabilities=instance.speaker_availabilities or {},
_speaker_availabilities=instance.speaker_availabilities or {},
_conference_id=instance.conference_id,
_user_id=instance.user_id,
_speaker_level=instance.speaker_level,
_previous_talk_video=instance.previous_talk_video,
)
8 changes: 7 additions & 1 deletion backend/api/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,18 @@ def has_permission(self, source, info, **kwargs):
class CanSeeSubmissions(BasePermission):
message = "You need to have a ticket to see submissions"

def has_permission(self, conference, info):
def has_permission(self, conference, info, *args, **kwargs):
user = info.context.request.user

if kwargs.get("only_accepted", False):
return True

if not user.is_authenticated:
return False

if info.context._user_can_vote is not None:
return info.context._user_can_vote

return check_if_user_can_vote(user, conference)


Expand Down
2 changes: 1 addition & 1 deletion backend/api/schedule/types/slot.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ class ScheduleSlotType(Enum):

@strawberry.type
class ScheduleSlot:
id: strawberry.ID
hour: time
duration: int
type: ScheduleSlotType
id: strawberry.ID

@strawberry.field
def is_live(self) -> bool:
Expand Down
15 changes: 14 additions & 1 deletion backend/api/submissions/permissions.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from submissions.models import Submission
from strawberry.permission import BasePermission

from api.permissions import HasTokenPermission
Expand All @@ -13,6 +14,9 @@ def has_permission(self, source, info, **kwargs):
if HasTokenPermission().has_permission(source, info):
return True

if source.status == Submission.STATUS.accepted:
return True

if source.schedule_items.exists(): # pragma: no cover
return True

Expand Down Expand Up @@ -48,12 +52,21 @@ class CanSeeSubmissionPrivateFields(BasePermission):
message = "You can't see the private fields for this submission"

def has_permission(self, source, info):
from api.participants.types import Participant

user = info.context.request.user

if not user.is_authenticated:
return False

return user.is_staff or source.speaker_id == user.id
if isinstance(source, Submission):
source_user_id = source.speaker_id
elif isinstance(source, Participant):
source_user_id = source._user_id
else:
raise ValueError("Invalid source type")

return user.is_staff or source_user_id == user.id


class IsSubmissionSpeakerOrStaff(BasePermission):
Expand Down
28 changes: 21 additions & 7 deletions backend/api/submissions/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from api.context import Info
from api.submissions.permissions import CanSeeSubmissionRestrictedFields

from voting.helpers import check_if_user_can_vote
import strawberry

from api.permissions import CanSeeSubmissions, IsAuthenticated
Expand Down Expand Up @@ -34,7 +35,7 @@ def submission(self, info: Info, id: strawberry.ID) -> Submission | None:

return submission

@strawberry.field(permission_classes=[IsAuthenticated])
@strawberry.field()
def submissions(
self,
info: Info,
Expand All @@ -46,9 +47,10 @@ def submissions(
audience_levels: list[str] | None = None,
page: int | None = 1,
page_size: int | None = 50,
only_accepted: bool = False,
) -> Paginated[Submission] | None:
if page_size > 150:
raise ValueError("Page size cannot be greater than 150")
if page_size > 300:
raise ValueError("Page size cannot be greater than 300")

if page_size < 1:
raise ValueError("Page size must be greater than 0")
Expand All @@ -60,10 +62,17 @@ def submissions(
user = request.user
conference = ConferenceModel.objects.filter(code=code).first()

if not conference or not CanSeeSubmissions().has_permission(conference, info):
raise PermissionError("You need to have a ticket to see submissions")
if not only_accepted and not IsAuthenticated().has_permission(conference, info):
raise PermissionError("User not logged in")

info.context._user_can_vote = True
info.context._user_can_vote = (
check_if_user_can_vote(user, conference) if user.is_authenticated else False
)

if not conference or not CanSeeSubmissions().has_permission(
conference, info, only_accepted=only_accepted
):
raise PermissionError("You need to have a ticket to see submissions")

qs = conference.submissions.prefetch_related(
"type",
Expand All @@ -72,7 +81,12 @@ def submissions(
"languages",
"audience_level",
"tags",
).filter(status=SubmissionModel.STATUS.proposed)
)

if only_accepted:
qs = qs.filter(status=SubmissionModel.STATUS.accepted)
else:
qs = qs.filter(status=SubmissionModel.STATUS.proposed)

if languages:
qs = qs.filter(languages__code__in=languages)
Expand Down
Loading
Loading