diff --git a/backend/api/users/tests/test_booked_schedule_items.py b/backend/api/users/tests/test_booked_schedule_items.py
new file mode 100644
index 0000000000..64e68d5fe8
--- /dev/null
+++ b/backend/api/users/tests/test_booked_schedule_items.py
@@ -0,0 +1,133 @@
+import datetime
+
+from schedule.tests.factories import (
+ DayFactory,
+ ScheduleItemAttendeeFactory,
+ ScheduleItemFactory,
+ SlotFactory,
+)
+from submissions.tests.factories import SubmissionFactory
+from users.tests.factories import UserFactory
+import pytest
+
+from schedule.models import ScheduleItem
+
+pytestmark = pytest.mark.django_db
+
+
+def _bookable_schedule_item(attendees_total_capacity: int = 30):
+ submission = SubmissionFactory()
+ return ScheduleItemFactory(
+ status=ScheduleItem.STATUS.confirmed,
+ submission=submission,
+ type=ScheduleItem.TYPES.training,
+ conference=submission.conference,
+ attendees_total_capacity=attendees_total_capacity,
+ slot=SlotFactory(
+ day=DayFactory(
+ day=datetime.date(2020, 10, 10),
+ conference=submission.conference,
+ ),
+ hour=datetime.time(10, 10, 0),
+ duration=30,
+ ),
+ )
+
+
+def test_get_booked_schedule_items(graphql_client, user):
+ graphql_client.force_login(user)
+
+ booked_item = _bookable_schedule_item()
+ _bookable_schedule_item()
+ ScheduleItemAttendeeFactory(schedule_item=booked_item, user_id=user.id)
+
+ response = graphql_client.query(
+ """query($conference: String!) {
+ me {
+ bookedScheduleItems(conference: $conference) {
+ id
+ title
+ slug
+ start
+ end
+ }
+ }
+ }""",
+ variables={"conference": booked_item.conference.code},
+ )
+
+ booked_schedule_items = response["data"]["me"]["bookedScheduleItems"]
+ assert len(booked_schedule_items) == 1
+ assert booked_schedule_items[0]["id"] == str(booked_item.id)
+ assert booked_schedule_items[0]["slug"] == booked_item.slug
+ assert booked_schedule_items[0]["start"] is not None
+ assert booked_schedule_items[0]["end"] is not None
+
+
+def test_booked_schedule_items_excludes_items_without_slot(graphql_client, user):
+ graphql_client.force_login(user)
+
+ submission = SubmissionFactory()
+ unscheduled_workshop = ScheduleItemFactory(
+ status=ScheduleItem.STATUS.confirmed,
+ submission=submission,
+ type=ScheduleItem.TYPES.training,
+ conference=submission.conference,
+ attendees_total_capacity=30,
+ slot=None,
+ )
+ ScheduleItemAttendeeFactory(schedule_item=unscheduled_workshop, user_id=user.id)
+
+ response = graphql_client.query(
+ """query($conference: String!) {
+ me {
+ bookedScheduleItems(conference: $conference) {
+ id
+ start
+ end
+ }
+ }
+ }""",
+ variables={"conference": unscheduled_workshop.conference.code},
+ )
+
+ assert response["data"]["me"]["bookedScheduleItems"] == []
+
+
+def test_booked_schedule_items_excludes_other_users_bookings(graphql_client, user):
+ graphql_client.force_login(user)
+
+ booked_item = _bookable_schedule_item()
+ other_user = UserFactory()
+ ScheduleItemAttendeeFactory(schedule_item=booked_item, user_id=other_user.id)
+
+ response = graphql_client.query(
+ """query($conference: String!) {
+ me {
+ bookedScheduleItems(conference: $conference) {
+ id
+ }
+ }
+ }""",
+ variables={"conference": booked_item.conference.code},
+ )
+
+ assert response["data"]["me"]["bookedScheduleItems"] == []
+
+
+def test_booked_schedule_items_requires_authentication(graphql_client):
+ schedule_item = _bookable_schedule_item()
+ ScheduleItemAttendeeFactory(schedule_item=schedule_item)
+
+ response = graphql_client.query(
+ """query($conference: String!) {
+ me {
+ bookedScheduleItems(conference: $conference) {
+ id
+ }
+ }
+ }""",
+ variables={"conference": schedule_item.conference.code},
+ )
+
+ assert response["errors"][0]["message"] == "User not logged in"
diff --git a/backend/api/users/types.py b/backend/api/users/types.py
index de731308de..de296f647e 100644
--- a/backend/api/users/types.py
+++ b/backend/api/users/types.py
@@ -3,6 +3,7 @@
from api.permissions import IsAuthenticated
from django.conf import settings
+from django.db.models import Prefetch
from django.urls import reverse
from api.billing.types import BillingAddress
from api.visa.types import InvitationLetterRequest
@@ -26,6 +27,8 @@
from api.helpers.ids import encode_hashid
from badges.roles import ConferenceRole, get_conference_roles_for_user
from association_membership.models import Membership
+from api.schedule.types import ScheduleItem
+from schedule.models import Room, ScheduleItem as ScheduleItemModel
from schedule.models import ScheduleItemStar as ScheduleItemStarModel
from submissions.models import Submission as SubmissionModel
from billing.models import BillingAddress as BillingAddressModel
@@ -111,6 +114,22 @@ def starred_schedule_items(
).values_list("schedule_item_id", flat=True)
return stars
+ @strawberry.field(permission_classes=[IsAuthenticated])
+ def booked_schedule_items(self, info: Info, conference: str) -> list[ScheduleItem]:
+ return list(
+ ScheduleItemModel.objects.filter(
+ conference__code=conference,
+ attendees__user_id=self.id,
+ slot__isnull=False,
+ )
+ .distinct()
+ .select_related("slot", "slot__day", "language")
+ .prefetch_related(
+ Prefetch("rooms", queryset=Room.objects.only("id", "name", "type"))
+ )
+ .order_by("slot__day__day", "slot__hour")
+ )
+
@strawberry.field
def grant(self, info: Info, conference: str) -> Grant | None:
grant = GrantModel.objects.filter(
diff --git a/frontend/src/components/my-workshops-profile-page-handler/index.tsx b/frontend/src/components/my-workshops-profile-page-handler/index.tsx
new file mode 100644
index 0000000000..2325d428d7
--- /dev/null
+++ b/frontend/src/components/my-workshops-profile-page-handler/index.tsx
@@ -0,0 +1,41 @@
+import { Heading, Page, Section } from "@python-italia/pycon-styleguide";
+import React from "react";
+import { FormattedMessage } from "react-intl";
+
+import { useMyProfileWithBookedWorkshopsQuery } from "~/types";
+
+import { MetaTags } from "../meta-tags";
+import { MyWorkshopsTable } from "./my-workshops-table";
+import { NoWorkshops } from "./no-workshops";
+
+export const MyWorkshopsProfilePageHandler = () => {
+ const {
+ data: {
+ me: { bookedScheduleItems },
+ },
+ } = useMyProfileWithBookedWorkshopsQuery({
+ variables: {
+ conference: process.env.conferenceCode,
+ },
+ });
+
+ return (
+