Skip to content

Commit 644f3fd

Browse files
authored
Add ability to show accepted speakers/proposals (#4354)
1 parent 0745dfe commit 644f3fd

File tree

48 files changed

+1025
-460
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+1025
-460
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from typing import Self
2+
import strawberry
3+
from api.cms.page.registry import register_page_block
4+
import enum
5+
6+
7+
@strawberry.enum
8+
class DynamicContentDisplaySectionSource(enum.Enum):
9+
speakers = "speakers"
10+
keynoters = "keynoters"
11+
proposals = "proposals"
12+
13+
14+
@register_page_block()
15+
@strawberry.type
16+
class DynamicContentDisplaySection:
17+
id: strawberry.ID
18+
source: DynamicContentDisplaySectionSource
19+
20+
@classmethod
21+
def from_block(cls, block) -> Self:
22+
return cls(
23+
id=block.id,
24+
source=DynamicContentDisplaySectionSource(block.value["source"]),
25+
)

backend/api/participants/queries.py

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import json
2-
from typing import Optional
32
from django.conf import settings
43
import strawberry
54
from strawberry.tools import create_type
@@ -15,20 +14,13 @@
1514

1615

1716
@strawberry.field
18-
def participant(
19-
info: Info, user_id: strawberry.ID, conference: str
20-
) -> Optional[Participant]:
21-
user = info.context.request.user
22-
decoded_id = decode_hashid(user_id, salt=settings.USER_ID_HASH_SALT, min_length=6)
17+
def participant(info: Info, id: strawberry.ID, conference: str) -> Participant | None:
18+
decoded_id = decode_hashid(id, salt=settings.USER_ID_HASH_SALT, min_length=6)
2319
participant = ParticipantModel.objects.filter(
24-
conference__code=conference, user_id=decoded_id
20+
conference__code=conference, id=decoded_id
2521
).first()
2622

27-
if not participant or (
28-
not participant.public_profile and (not user or participant.user_id != user.id)
29-
):
30-
# Profile doesn't exist, or
31-
# Profile is not public, and the person requesting it is not the owner
23+
if not participant:
3224
return None
3325

3426
return Participant.from_model(participant)
@@ -37,7 +29,7 @@ def participant(
3729
@strawberry.field
3830
def ticket_id_to_user_hashid(
3931
ticket_id: strawberry.ID, conference_code: str
40-
) -> Optional[str]:
32+
) -> str | None:
4133
conference = Conference.objects.filter(code=conference_code).first()
4234
decoded_ticket_id = decode_hashid(ticket_id)
4335
order_position = pretix.get_order_position(conference, decoded_ticket_id)

backend/api/participants/tests/__init__.py

Whitespace-only changes.
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
from conferences.tests.factories import ConferenceFactory
2+
from participants.tests.factories import ParticipantFactory
3+
4+
5+
def test_query_participant(graphql_client):
6+
participant = ParticipantFactory()
7+
8+
response = graphql_client.query(
9+
"""
10+
query Participant($id: ID!, $conference: String!) {
11+
participant(id: $id, conference: $conference) {
12+
id
13+
fullname
14+
}
15+
}
16+
""",
17+
variables={"id": participant.hashid, "conference": participant.conference.code},
18+
)
19+
20+
assert response["data"]["participant"]["id"] == participant.hashid
21+
assert response["data"]["participant"]["fullname"] == participant.user.fullname
22+
23+
24+
def test_query_private_fields_of_own_user(graphql_client, user):
25+
graphql_client.force_login(user)
26+
participant = ParticipantFactory(
27+
user=user,
28+
speaker_availabilities={"test": "test"},
29+
previous_talk_video="test",
30+
speaker_level="level",
31+
)
32+
33+
response = graphql_client.query(
34+
"""query Participant($id: ID!, $conference: String!) {
35+
participant(id: $id, conference: $conference) {
36+
id
37+
speakerAvailabilities
38+
previousTalkVideo
39+
speakerLevel
40+
}
41+
}
42+
""",
43+
variables={"id": participant.hashid, "conference": participant.conference.code},
44+
)
45+
46+
assert response["data"]["participant"]["speakerAvailabilities"] == {"test": "test"}
47+
assert response["data"]["participant"]["previousTalkVideo"] == "test"
48+
assert response["data"]["participant"]["speakerLevel"] == "level"
49+
50+
51+
def test_cannot_query_private_fields_of_other_user(graphql_client):
52+
participant = ParticipantFactory()
53+
54+
response = graphql_client.query(
55+
"""query Participant($id: ID!, $conference: String!) {
56+
participant(id: $id, conference: $conference) {
57+
id
58+
speakerAvailabilities
59+
previousTalkVideo
60+
speakerLevel
61+
}
62+
}
63+
""",
64+
variables={"id": participant.hashid, "conference": participant.conference.code},
65+
)
66+
67+
assert response["data"]["participant"]["speakerAvailabilities"] is None
68+
assert response["data"]["participant"]["previousTalkVideo"] is None
69+
assert response["data"]["participant"]["speakerLevel"] is None
70+
71+
72+
def test_query_participant_with_wrong_conference(graphql_client):
73+
participant = ParticipantFactory()
74+
75+
response = graphql_client.query(
76+
"""
77+
query Participant($id: ID!, $conference: String!) {
78+
participant(id: $id, conference: $conference) {
79+
id
80+
fullname
81+
}
82+
}
83+
""",
84+
variables={"id": participant.hashid, "conference": ConferenceFactory().code},
85+
)
86+
87+
assert response["data"]["participant"] is None
88+
89+
90+
def test_query_participant_with_non_existent_id(graphql_client):
91+
response = graphql_client.query(
92+
"""
93+
query Participant($id: ID!, $conference: String!) {
94+
participant(id: $id, conference: $conference) {
95+
id
96+
fullname
97+
}
98+
}
99+
""",
100+
variables={"id": "abcabc", "conference": ConferenceFactory().code},
101+
)
102+
103+
assert response["data"]["participant"] is None

backend/api/participants/types.py

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
1-
from typing import Optional
1+
from typing import TYPE_CHECKING, Annotated
22

3+
from submissions.models import Submission as SubmissionModel
34
from strawberry.scalars import JSON
45
import strawberry
56
from strawberry import ID
67

78
from api.submissions.permissions import CanSeeSubmissionPrivateFields
89

10+
if TYPE_CHECKING:
11+
from api.submissions.types import Submission
12+
913

1014
@strawberry.type
1115
class Participant:
1216
id: ID
13-
user_id: ID
1417
bio: str
1518
website: str
1619
photo: str | None
@@ -21,22 +24,39 @@ class Participant:
2124
linkedin_url: str
2225
facebook_url: str
2326
mastodon_handle: str
24-
speaker_id: strawberry.Private[int]
2527
fullname: str
26-
speaker_availabilities: JSON
2728

2829
_speaker_level: strawberry.Private[str]
2930
_previous_talk_video: strawberry.Private[str]
31+
_conference_id: strawberry.Private[int]
32+
_user_id: strawberry.Private[int]
33+
_speaker_availabilities: strawberry.Private[int]
34+
35+
@strawberry.field
36+
def proposals(
37+
self, info
38+
) -> list[Annotated["Submission", strawberry.lazy("api.submissions.types")]]:
39+
return SubmissionModel.objects.for_conference(self._conference_id).filter(
40+
speaker_id=self._user_id,
41+
status=SubmissionModel.STATUS.accepted,
42+
)
3043

3144
@strawberry.field
32-
def speaker_level(self, info) -> Optional[str]:
45+
def speaker_availabilities(self, info) -> JSON | None:
46+
if not CanSeeSubmissionPrivateFields().has_permission(self, info):
47+
return None
48+
49+
return self._speaker_availabilities
50+
51+
@strawberry.field
52+
def speaker_level(self, info) -> str | None:
3353
if not CanSeeSubmissionPrivateFields().has_permission(self, info):
3454
return None
3555

3656
return self._speaker_level
3757

3858
@strawberry.field
39-
def previous_talk_video(self, info) -> Optional[str]:
59+
def previous_talk_video(self, info) -> str | None:
4060
if not CanSeeSubmissionPrivateFields().has_permission(self, info):
4161
return None
4262

@@ -46,20 +66,20 @@ def previous_talk_video(self, info) -> Optional[str]:
4666
def from_model(cls, instance):
4767
return cls(
4868
id=instance.hashid,
49-
user_id=instance.user_id,
50-
speaker_id=instance.user_id,
5169
fullname=instance.user.fullname,
5270
photo=instance.photo_url,
5371
photo_id=instance.photo_file_id,
5472
bio=instance.bio,
5573
website=instance.website,
5674
public_profile=instance.public_profile,
57-
_speaker_level=instance.speaker_level,
58-
_previous_talk_video=instance.previous_talk_video,
5975
twitter_handle=instance.twitter_handle,
6076
instagram_handle=instance.instagram_handle,
6177
linkedin_url=instance.linkedin_url,
6278
facebook_url=instance.facebook_url,
6379
mastodon_handle=instance.mastodon_handle,
64-
speaker_availabilities=instance.speaker_availabilities or {},
80+
_speaker_availabilities=instance.speaker_availabilities or {},
81+
_conference_id=instance.conference_id,
82+
_user_id=instance.user_id,
83+
_speaker_level=instance.speaker_level,
84+
_previous_talk_video=instance.previous_talk_video,
6585
)

backend/api/permissions.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,18 @@ def has_permission(self, source, info, **kwargs):
3838
class CanSeeSubmissions(BasePermission):
3939
message = "You need to have a ticket to see submissions"
4040

41-
def has_permission(self, conference, info):
41+
def has_permission(self, conference, info, *args, **kwargs):
4242
user = info.context.request.user
4343

44+
if kwargs.get("only_accepted", False):
45+
return True
46+
4447
if not user.is_authenticated:
4548
return False
4649

50+
if info.context._user_can_vote is not None:
51+
return info.context._user_can_vote
52+
4753
return check_if_user_can_vote(user, conference)
4854

4955

backend/api/schedule/types/slot.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ class ScheduleSlotType(Enum):
1616

1717
@strawberry.type
1818
class ScheduleSlot:
19+
id: strawberry.ID
1920
hour: time
2021
duration: int
2122
type: ScheduleSlotType
22-
id: strawberry.ID
2323

2424
@strawberry.field
2525
def is_live(self) -> bool:

backend/api/submissions/permissions.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from submissions.models import Submission
12
from strawberry.permission import BasePermission
23

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

17+
if source.status == Submission.STATUS.accepted:
18+
return True
19+
1620
if source.schedule_items.exists(): # pragma: no cover
1721
return True
1822

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

5054
def has_permission(self, source, info):
55+
from api.participants.types import Participant
56+
5157
user = info.context.request.user
5258

5359
if not user.is_authenticated:
5460
return False
5561

56-
return user.is_staff or source.speaker_id == user.id
62+
if isinstance(source, Submission):
63+
source_user_id = source.speaker_id
64+
elif isinstance(source, Participant):
65+
source_user_id = source._user_id
66+
else:
67+
raise ValueError("Invalid source type")
68+
69+
return user.is_staff or source_user_id == user.id
5770

5871

5972
class IsSubmissionSpeakerOrStaff(BasePermission):

backend/api/submissions/schema.py

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from api.context import Info
33
from api.submissions.permissions import CanSeeSubmissionRestrictedFields
44

5+
from voting.helpers import check_if_user_can_vote
56
import strawberry
67

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

3536
return submission
3637

37-
@strawberry.field(permission_classes=[IsAuthenticated])
38+
@strawberry.field()
3839
def submissions(
3940
self,
4041
info: Info,
@@ -46,9 +47,10 @@ def submissions(
4647
audience_levels: list[str] | None = None,
4748
page: int | None = 1,
4849
page_size: int | None = 50,
50+
only_accepted: bool = False,
4951
) -> Paginated[Submission] | None:
50-
if page_size > 150:
51-
raise ValueError("Page size cannot be greater than 150")
52+
if page_size > 300:
53+
raise ValueError("Page size cannot be greater than 300")
5254

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

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

66-
info.context._user_can_vote = True
68+
info.context._user_can_vote = (
69+
check_if_user_can_vote(user, conference) if user.is_authenticated else False
70+
)
71+
72+
if not conference or not CanSeeSubmissions().has_permission(
73+
conference, info, only_accepted=only_accepted
74+
):
75+
raise PermissionError("You need to have a ticket to see submissions")
6776

6877
qs = conference.submissions.prefetch_related(
6978
"type",
@@ -72,7 +81,12 @@ def submissions(
7281
"languages",
7382
"audience_level",
7483
"tags",
75-
).filter(status=SubmissionModel.STATUS.proposed)
84+
)
85+
86+
if only_accepted:
87+
qs = qs.filter(status=SubmissionModel.STATUS.accepted)
88+
else:
89+
qs = qs.filter(status=SubmissionModel.STATUS.proposed)
7690

7791
if languages:
7892
qs = qs.filter(languages__code__in=languages)

0 commit comments

Comments
 (0)