From be8daa430e59856a4d0cc1c7ac6c65c58ee43e7f Mon Sep 17 00:00:00 2001 From: Santtu Pajukanta Date: Mon, 20 Jul 2026 00:31:44 +0300 Subject: [PATCH 1/3] feat(program_v2): schedule-item-specific Konsti signup links Add a `links` resolver to LimitedScheduleItemType mirroring the one on LimitedProgramType, with an `ownOnly` argument. The signup link is generated on the fly from the Konsti base URL when the `konsti` dimension is set and no signup annotation is present, so per-schedule-item signup links can be shown in the program guide and WP plugin. The Konsti base URL defaults to https://ropekonsti.fi via a DEFAULT_KONSTI_URL constant and can be overridden per event through the existing ProgramV2EventMeta.konsti_url field (tracon2026 uses https://konsti.tracon.fi). Program detail pages now render these schedule-item links on the schedule rows. fixes #801, fixes #993 Co-Authored-By: Claude Opus 4.8 --- kompassi-v2-frontend/src/__generated__/gql.ts | 6 +- .../src/__generated__/graphql.ts | 24 ++++- .../programs/[programSlug]/page.tsx | 55 ++++++---- .../management/commands/setup_tracon2026.py | 1 + kompassi/program_v2/graphql/program_links.py | 100 +++++++++++++++--- .../graphql/schedule_item_limited.py | 43 ++++++++ kompassi/program_v2/integrations/konsti.py | 15 ++- 7 files changed, 202 insertions(+), 42 deletions(-) diff --git a/kompassi-v2-frontend/src/__generated__/gql.ts b/kompassi-v2-frontend/src/__generated__/gql.ts index 8170a264f..788bbdf7c 100644 --- a/kompassi-v2-frontend/src/__generated__/gql.ts +++ b/kompassi-v2-frontend/src/__generated__/gql.ts @@ -135,7 +135,7 @@ type Documents = { "\n mutation CreateFeedback($input: ProgramFeedbackInput!) {\n createProgramFeedback(input: $input) {\n success\n }\n }\n": typeof types.CreateFeedbackDocument, "\n query ProgramFeedbackQuery($eventSlug: String!, $programSlug: String!) {\n event(slug: $eventSlug) {\n name\n program {\n program(slug: $programSlug) {\n title\n isAcceptingFeedback\n }\n }\n }\n }\n": typeof types.ProgramFeedbackQueryDocument, "\n fragment ProgramDetailAnnotation on ProgramAnnotationType {\n annotation {\n slug\n type\n title(lang: $locale)\n }\n value(lang: $locale)\n }\n": typeof types.ProgramDetailAnnotationFragmentDoc, - "\n query ProgramDetailQuery(\n $eventSlug: String!\n $programSlug: String!\n $locale: String\n ) {\n profile {\n program {\n scheduleItems(eventSlug: $eventSlug) {\n slug\n }\n }\n }\n\n event(slug: $eventSlug) {\n name\n slug\n timezone\n\n program {\n calendarExportLink\n\n program(slug: $programSlug) {\n title\n description\n cachedHosts\n isCancelled\n\n links(lang: $locale) {\n type\n href\n title\n }\n\n annotations(isShownInDetail: true) {\n ...ProgramDetailAnnotation\n }\n\n dimensions(isShownInDetail: true, publicOnly: true) {\n dimension {\n slug\n title(lang: $locale)\n }\n value {\n slug\n title(lang: $locale)\n }\n }\n scheduleItems {\n slug\n subtitle\n location(lang: $locale)\n startTime\n endTime\n }\n }\n }\n }\n }\n": typeof types.ProgramDetailQueryDocument, + "\n query ProgramDetailQuery(\n $eventSlug: String!\n $programSlug: String!\n $locale: String\n ) {\n profile {\n program {\n scheduleItems(eventSlug: $eventSlug) {\n slug\n }\n }\n }\n\n event(slug: $eventSlug) {\n name\n slug\n timezone\n\n program {\n calendarExportLink\n\n program(slug: $programSlug) {\n title\n description\n cachedHosts\n isCancelled\n\n links(lang: $locale) {\n type\n href\n title\n }\n\n annotations(isShownInDetail: true) {\n ...ProgramDetailAnnotation\n }\n\n dimensions(isShownInDetail: true, publicOnly: true) {\n dimension {\n slug\n title(lang: $locale)\n }\n value {\n slug\n title(lang: $locale)\n }\n }\n scheduleItems {\n slug\n subtitle\n location(lang: $locale)\n startTime\n endTime\n links(ownOnly: true, lang: $locale) {\n type\n href\n title\n }\n }\n }\n }\n }\n }\n": typeof types.ProgramDetailQueryDocument, "\n mutation UpdateQuota($input: UpdateQuotaInput!) {\n updateQuota(input: $input) {\n quota {\n id\n }\n }\n }\n": typeof types.UpdateQuotaDocument, "\n mutation DeleteQuota($input: DeleteQuotaInput!) {\n deleteQuota(input: $input) {\n id\n }\n }\n": typeof types.DeleteQuotaDocument, "\n fragment QuotaProduct on LimitedProductType {\n id\n title\n price\n countReserved\n }\n": typeof types.QuotaProductFragmentDoc, @@ -339,7 +339,7 @@ const documents: Documents = { "\n mutation CreateFeedback($input: ProgramFeedbackInput!) {\n createProgramFeedback(input: $input) {\n success\n }\n }\n": types.CreateFeedbackDocument, "\n query ProgramFeedbackQuery($eventSlug: String!, $programSlug: String!) {\n event(slug: $eventSlug) {\n name\n program {\n program(slug: $programSlug) {\n title\n isAcceptingFeedback\n }\n }\n }\n }\n": types.ProgramFeedbackQueryDocument, "\n fragment ProgramDetailAnnotation on ProgramAnnotationType {\n annotation {\n slug\n type\n title(lang: $locale)\n }\n value(lang: $locale)\n }\n": types.ProgramDetailAnnotationFragmentDoc, - "\n query ProgramDetailQuery(\n $eventSlug: String!\n $programSlug: String!\n $locale: String\n ) {\n profile {\n program {\n scheduleItems(eventSlug: $eventSlug) {\n slug\n }\n }\n }\n\n event(slug: $eventSlug) {\n name\n slug\n timezone\n\n program {\n calendarExportLink\n\n program(slug: $programSlug) {\n title\n description\n cachedHosts\n isCancelled\n\n links(lang: $locale) {\n type\n href\n title\n }\n\n annotations(isShownInDetail: true) {\n ...ProgramDetailAnnotation\n }\n\n dimensions(isShownInDetail: true, publicOnly: true) {\n dimension {\n slug\n title(lang: $locale)\n }\n value {\n slug\n title(lang: $locale)\n }\n }\n scheduleItems {\n slug\n subtitle\n location(lang: $locale)\n startTime\n endTime\n }\n }\n }\n }\n }\n": types.ProgramDetailQueryDocument, + "\n query ProgramDetailQuery(\n $eventSlug: String!\n $programSlug: String!\n $locale: String\n ) {\n profile {\n program {\n scheduleItems(eventSlug: $eventSlug) {\n slug\n }\n }\n }\n\n event(slug: $eventSlug) {\n name\n slug\n timezone\n\n program {\n calendarExportLink\n\n program(slug: $programSlug) {\n title\n description\n cachedHosts\n isCancelled\n\n links(lang: $locale) {\n type\n href\n title\n }\n\n annotations(isShownInDetail: true) {\n ...ProgramDetailAnnotation\n }\n\n dimensions(isShownInDetail: true, publicOnly: true) {\n dimension {\n slug\n title(lang: $locale)\n }\n value {\n slug\n title(lang: $locale)\n }\n }\n scheduleItems {\n slug\n subtitle\n location(lang: $locale)\n startTime\n endTime\n links(ownOnly: true, lang: $locale) {\n type\n href\n title\n }\n }\n }\n }\n }\n }\n": types.ProgramDetailQueryDocument, "\n mutation UpdateQuota($input: UpdateQuotaInput!) {\n updateQuota(input: $input) {\n quota {\n id\n }\n }\n }\n": types.UpdateQuotaDocument, "\n mutation DeleteQuota($input: DeleteQuotaInput!) {\n deleteQuota(input: $input) {\n id\n }\n }\n": types.DeleteQuotaDocument, "\n fragment QuotaProduct on LimitedProductType {\n id\n title\n price\n countReserved\n }\n": types.QuotaProductFragmentDoc, @@ -923,7 +923,7 @@ export function graphql(source: "\n fragment ProgramDetailAnnotation on Program /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "\n query ProgramDetailQuery(\n $eventSlug: String!\n $programSlug: String!\n $locale: String\n ) {\n profile {\n program {\n scheduleItems(eventSlug: $eventSlug) {\n slug\n }\n }\n }\n\n event(slug: $eventSlug) {\n name\n slug\n timezone\n\n program {\n calendarExportLink\n\n program(slug: $programSlug) {\n title\n description\n cachedHosts\n isCancelled\n\n links(lang: $locale) {\n type\n href\n title\n }\n\n annotations(isShownInDetail: true) {\n ...ProgramDetailAnnotation\n }\n\n dimensions(isShownInDetail: true, publicOnly: true) {\n dimension {\n slug\n title(lang: $locale)\n }\n value {\n slug\n title(lang: $locale)\n }\n }\n scheduleItems {\n slug\n subtitle\n location(lang: $locale)\n startTime\n endTime\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query ProgramDetailQuery(\n $eventSlug: String!\n $programSlug: String!\n $locale: String\n ) {\n profile {\n program {\n scheduleItems(eventSlug: $eventSlug) {\n slug\n }\n }\n }\n\n event(slug: $eventSlug) {\n name\n slug\n timezone\n\n program {\n calendarExportLink\n\n program(slug: $programSlug) {\n title\n description\n cachedHosts\n isCancelled\n\n links(lang: $locale) {\n type\n href\n title\n }\n\n annotations(isShownInDetail: true) {\n ...ProgramDetailAnnotation\n }\n\n dimensions(isShownInDetail: true, publicOnly: true) {\n dimension {\n slug\n title(lang: $locale)\n }\n value {\n slug\n title(lang: $locale)\n }\n }\n scheduleItems {\n slug\n subtitle\n location(lang: $locale)\n startTime\n endTime\n }\n }\n }\n }\n }\n"]; +export function graphql(source: "\n query ProgramDetailQuery(\n $eventSlug: String!\n $programSlug: String!\n $locale: String\n ) {\n profile {\n program {\n scheduleItems(eventSlug: $eventSlug) {\n slug\n }\n }\n }\n\n event(slug: $eventSlug) {\n name\n slug\n timezone\n\n program {\n calendarExportLink\n\n program(slug: $programSlug) {\n title\n description\n cachedHosts\n isCancelled\n\n links(lang: $locale) {\n type\n href\n title\n }\n\n annotations(isShownInDetail: true) {\n ...ProgramDetailAnnotation\n }\n\n dimensions(isShownInDetail: true, publicOnly: true) {\n dimension {\n slug\n title(lang: $locale)\n }\n value {\n slug\n title(lang: $locale)\n }\n }\n scheduleItems {\n slug\n subtitle\n location(lang: $locale)\n startTime\n endTime\n links(ownOnly: true, lang: $locale) {\n type\n href\n title\n }\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query ProgramDetailQuery(\n $eventSlug: String!\n $programSlug: String!\n $locale: String\n ) {\n profile {\n program {\n scheduleItems(eventSlug: $eventSlug) {\n slug\n }\n }\n }\n\n event(slug: $eventSlug) {\n name\n slug\n timezone\n\n program {\n calendarExportLink\n\n program(slug: $programSlug) {\n title\n description\n cachedHosts\n isCancelled\n\n links(lang: $locale) {\n type\n href\n title\n }\n\n annotations(isShownInDetail: true) {\n ...ProgramDetailAnnotation\n }\n\n dimensions(isShownInDetail: true, publicOnly: true) {\n dimension {\n slug\n title(lang: $locale)\n }\n value {\n slug\n title(lang: $locale)\n }\n }\n scheduleItems {\n slug\n subtitle\n location(lang: $locale)\n startTime\n endTime\n links(ownOnly: true, lang: $locale) {\n type\n href\n title\n }\n }\n }\n }\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/kompassi-v2-frontend/src/__generated__/graphql.ts b/kompassi-v2-frontend/src/__generated__/graphql.ts index 8747df2c6..52cf8638f 100644 --- a/kompassi-v2-frontend/src/__generated__/graphql.ts +++ b/kompassi-v2-frontend/src/__generated__/graphql.ts @@ -890,6 +890,8 @@ export type FullScheduleItemType = { isCancelled: Scalars['Boolean']['output']; /** Deprecated alias for `duration_minutes`. */ lengthMinutes: Scalars['Int']['output']; + /** Get the links associated with the schedule item. If types are not specified, all links are returned. With `ownOnly`, only links set directly on the schedule item are returned; otherwise links inherited from the program are included as well. */ + links: Array; location?: Maybe; program: LimitedProgramType; reservationsExcelExportLink: Scalars['String']['output']; @@ -922,6 +924,14 @@ export type FullScheduleItemTypeCachedDimensionsArgs = { }; +export type FullScheduleItemTypeLinksArgs = { + includeExpired?: InputMaybe; + lang?: InputMaybe; + ownOnly?: InputMaybe; + types?: InputMaybe>>; +}; + + export type FullScheduleItemTypeLocationArgs = { lang?: InputMaybe; }; @@ -1484,6 +1494,8 @@ export type LimitedScheduleItemType = { isPublic: Scalars['Boolean']['output']; /** Deprecated alias for `duration_minutes`. */ lengthMinutes: Scalars['Int']['output']; + /** Get the links associated with the schedule item. If types are not specified, all links are returned. With `ownOnly`, only links set directly on the schedule item are returned; otherwise links inherited from the program are included as well. */ + links: Array; location?: Maybe; reservationsExcelExportLink: Scalars['String']['output']; /** Convenience helper to get the value slug of the `room` dimension. NOTE: You should usually display `location` to users instead. */ @@ -1515,6 +1527,14 @@ export type LimitedScheduleItemTypeCachedDimensionsArgs = { }; +export type LimitedScheduleItemTypeLinksArgs = { + includeExpired?: InputMaybe; + lang?: InputMaybe; + ownOnly?: InputMaybe; + types?: InputMaybe>>; +}; + + export type LimitedScheduleItemTypeLocationArgs = { lang?: InputMaybe; }; @@ -3841,7 +3861,7 @@ export type ProgramDetailQueryQueryVariables = Exact<{ }>; -export type ProgramDetailQueryQuery = { __typename?: 'Query', profile?: { __typename?: 'OwnProfileType', program: { __typename?: 'ProgramV2ProfileMetaType', scheduleItems?: Array<{ __typename?: 'FullScheduleItemType', slug: string }> | null } } | null, event?: { __typename?: 'FullEventType', name: string, slug: string, timezone: string, program?: { __typename?: 'ProgramV2EventMetaType', calendarExportLink: string, program?: { __typename?: 'FullProgramType', title: string, description: string, cachedHosts: string, isCancelled: boolean, links: Array<{ __typename?: 'ProgramLink', type: ProgramLinkType, href: string, title: string }>, annotations: Array<{ __typename?: 'ProgramAnnotationType', value?: unknown | null, annotation: { __typename?: 'AnnotationType', slug: string, type: AnnotationDataType, title: string } }>, dimensions: Array<{ __typename?: 'ProgramDimensionValueType', dimension: { __typename?: 'FullDimensionType', slug: string, title?: string | null }, value: { __typename?: 'DimensionValueType', slug: string, title?: string | null } }>, scheduleItems: Array<{ __typename?: 'LimitedScheduleItemType', slug: string, subtitle: string, location?: string | null, startTime: string, endTime: string }> } | null } | null } | null }; +export type ProgramDetailQueryQuery = { __typename?: 'Query', profile?: { __typename?: 'OwnProfileType', program: { __typename?: 'ProgramV2ProfileMetaType', scheduleItems?: Array<{ __typename?: 'FullScheduleItemType', slug: string }> | null } } | null, event?: { __typename?: 'FullEventType', name: string, slug: string, timezone: string, program?: { __typename?: 'ProgramV2EventMetaType', calendarExportLink: string, program?: { __typename?: 'FullProgramType', title: string, description: string, cachedHosts: string, isCancelled: boolean, links: Array<{ __typename?: 'ProgramLink', type: ProgramLinkType, href: string, title: string }>, annotations: Array<{ __typename?: 'ProgramAnnotationType', value?: unknown | null, annotation: { __typename?: 'AnnotationType', slug: string, type: AnnotationDataType, title: string } }>, dimensions: Array<{ __typename?: 'ProgramDimensionValueType', dimension: { __typename?: 'FullDimensionType', slug: string, title?: string | null }, value: { __typename?: 'DimensionValueType', slug: string, title?: string | null } }>, scheduleItems: Array<{ __typename?: 'LimitedScheduleItemType', slug: string, subtitle: string, location?: string | null, startTime: string, endTime: string, links: Array<{ __typename?: 'ProgramLink', type: ProgramLinkType, href: string, title: string }> }> } | null } | null } | null }; export type UpdateQuotaMutationVariables = Exact<{ input: UpdateQuotaInput; @@ -4420,7 +4440,7 @@ export const UnmarkScheduleItemAsFavoriteDocument = {"kind":"Document","definiti export const ProgramListQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ProgramListQuery"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"locale"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filters"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"DimensionFilterInput"}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"hidePast"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"profile"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"program"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"scheduleItems"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"eventSlug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}}},{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filters"}}},{"kind":"Argument","name":{"kind":"Name","value":"hidePast"},"value":{"kind":"Variable","name":{"kind":"Name","value":"hidePast"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ScheduleItemList"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"event"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"slug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"timezone"}},{"kind":"Field","name":{"kind":"Name","value":"program"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"calendarExportLink"}},{"kind":"Field","name":{"kind":"Name","value":"isSchedulePublic"}},{"kind":"Field","alias":{"kind":"Name","value":"listFilters"},"name":{"kind":"Name","value":"dimensions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"isListFilter"},"value":{"kind":"BooleanValue","value":true}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"DimensionFilter"}}]}},{"kind":"Field","name":{"kind":"Name","value":"scheduleItems"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filters"}}},{"kind":"Argument","name":{"kind":"Name","value":"hidePast"},"value":{"kind":"Variable","name":{"kind":"Name","value":"hidePast"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ScheduleItemList"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ScheduleProgram"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LimitedProgramType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"cachedDimensions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"publicOnly"},"value":{"kind":"BooleanValue","value":true}},{"kind":"Argument","name":{"kind":"Name","value":"listFiltersOnly"},"value":{"kind":"BooleanValue","value":true}}]},{"kind":"Field","name":{"kind":"Name","value":"color"}},{"kind":"Field","name":{"kind":"Name","value":"isCancelled"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"DimensionFilterValue"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"DimensionValueType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"title"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"lang"},"value":{"kind":"Variable","name":{"kind":"Name","value":"locale"}}}]}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ScheduleItemList"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FullScheduleItemType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"location"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"lang"},"value":{"kind":"Variable","name":{"kind":"Name","value":"locale"}}}]},{"kind":"Field","name":{"kind":"Name","value":"subtitle"}},{"kind":"Field","name":{"kind":"Name","value":"startTime"}},{"kind":"Field","name":{"kind":"Name","value":"endTime"}},{"kind":"Field","name":{"kind":"Name","value":"program"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ScheduleProgram"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"DimensionFilter"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FullDimensionType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"title"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"lang"},"value":{"kind":"Variable","name":{"kind":"Name","value":"locale"}}}]},{"kind":"Field","name":{"kind":"Name","value":"isMultiValue"}},{"kind":"Field","name":{"kind":"Name","value":"isListFilter"}},{"kind":"Field","name":{"kind":"Name","value":"isKeyDimension"}},{"kind":"Field","name":{"kind":"Name","value":"values"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"lang"},"value":{"kind":"Variable","name":{"kind":"Name","value":"locale"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"DimensionFilterValue"}}]}}]}}]} as unknown as DocumentNode; export const CreateFeedbackDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateFeedback"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ProgramFeedbackInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createProgramFeedback"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"success"}}]}}]}}]} as unknown as DocumentNode; export const ProgramFeedbackQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ProgramFeedbackQuery"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"programSlug"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"event"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"slug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"program"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"program"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"slug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"programSlug"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"isAcceptingFeedback"}}]}}]}}]}}]}}]} as unknown as DocumentNode; -export const ProgramDetailQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ProgramDetailQuery"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"programSlug"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"locale"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"profile"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"program"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"scheduleItems"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"eventSlug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"event"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"slug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"timezone"}},{"kind":"Field","name":{"kind":"Name","value":"program"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"calendarExportLink"}},{"kind":"Field","name":{"kind":"Name","value":"program"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"slug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"programSlug"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"cachedHosts"}},{"kind":"Field","name":{"kind":"Name","value":"isCancelled"}},{"kind":"Field","name":{"kind":"Name","value":"links"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"lang"},"value":{"kind":"Variable","name":{"kind":"Name","value":"locale"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"href"}},{"kind":"Field","name":{"kind":"Name","value":"title"}}]}},{"kind":"Field","name":{"kind":"Name","value":"annotations"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"isShownInDetail"},"value":{"kind":"BooleanValue","value":true}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProgramDetailAnnotation"}}]}},{"kind":"Field","name":{"kind":"Name","value":"dimensions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"isShownInDetail"},"value":{"kind":"BooleanValue","value":true}},{"kind":"Argument","name":{"kind":"Name","value":"publicOnly"},"value":{"kind":"BooleanValue","value":true}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"dimension"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"title"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"lang"},"value":{"kind":"Variable","name":{"kind":"Name","value":"locale"}}}]}]}},{"kind":"Field","name":{"kind":"Name","value":"value"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"title"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"lang"},"value":{"kind":"Variable","name":{"kind":"Name","value":"locale"}}}]}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"scheduleItems"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"subtitle"}},{"kind":"Field","name":{"kind":"Name","value":"location"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"lang"},"value":{"kind":"Variable","name":{"kind":"Name","value":"locale"}}}]},{"kind":"Field","name":{"kind":"Name","value":"startTime"}},{"kind":"Field","name":{"kind":"Name","value":"endTime"}}]}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProgramDetailAnnotation"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ProgramAnnotationType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"annotation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"title"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"lang"},"value":{"kind":"Variable","name":{"kind":"Name","value":"locale"}}}]}]}},{"kind":"Field","name":{"kind":"Name","value":"value"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"lang"},"value":{"kind":"Variable","name":{"kind":"Name","value":"locale"}}}]}]}}]} as unknown as DocumentNode; +export const ProgramDetailQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ProgramDetailQuery"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"programSlug"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"locale"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"profile"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"program"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"scheduleItems"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"eventSlug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"event"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"slug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"timezone"}},{"kind":"Field","name":{"kind":"Name","value":"program"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"calendarExportLink"}},{"kind":"Field","name":{"kind":"Name","value":"program"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"slug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"programSlug"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"cachedHosts"}},{"kind":"Field","name":{"kind":"Name","value":"isCancelled"}},{"kind":"Field","name":{"kind":"Name","value":"links"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"lang"},"value":{"kind":"Variable","name":{"kind":"Name","value":"locale"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"href"}},{"kind":"Field","name":{"kind":"Name","value":"title"}}]}},{"kind":"Field","name":{"kind":"Name","value":"annotations"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"isShownInDetail"},"value":{"kind":"BooleanValue","value":true}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProgramDetailAnnotation"}}]}},{"kind":"Field","name":{"kind":"Name","value":"dimensions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"isShownInDetail"},"value":{"kind":"BooleanValue","value":true}},{"kind":"Argument","name":{"kind":"Name","value":"publicOnly"},"value":{"kind":"BooleanValue","value":true}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"dimension"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"title"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"lang"},"value":{"kind":"Variable","name":{"kind":"Name","value":"locale"}}}]}]}},{"kind":"Field","name":{"kind":"Name","value":"value"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"title"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"lang"},"value":{"kind":"Variable","name":{"kind":"Name","value":"locale"}}}]}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"scheduleItems"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"subtitle"}},{"kind":"Field","name":{"kind":"Name","value":"location"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"lang"},"value":{"kind":"Variable","name":{"kind":"Name","value":"locale"}}}]},{"kind":"Field","name":{"kind":"Name","value":"startTime"}},{"kind":"Field","name":{"kind":"Name","value":"endTime"}},{"kind":"Field","name":{"kind":"Name","value":"links"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"ownOnly"},"value":{"kind":"BooleanValue","value":true}},{"kind":"Argument","name":{"kind":"Name","value":"lang"},"value":{"kind":"Variable","name":{"kind":"Name","value":"locale"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"href"}},{"kind":"Field","name":{"kind":"Name","value":"title"}}]}}]}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProgramDetailAnnotation"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ProgramAnnotationType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"annotation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"title"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"lang"},"value":{"kind":"Variable","name":{"kind":"Name","value":"locale"}}}]}]}},{"kind":"Field","name":{"kind":"Name","value":"value"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"lang"},"value":{"kind":"Variable","name":{"kind":"Name","value":"locale"}}}]}]}}]} as unknown as DocumentNode; export const UpdateQuotaDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateQuota"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateQuotaInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateQuota"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"quota"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode; export const DeleteQuotaDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteQuota"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"DeleteQuotaInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteQuota"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode; export const AdminQuotaDetailPageDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"AdminQuotaDetailPage"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"quotaId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"event"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"slug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"tickets"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"quota"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"quotaId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"countReserved"}},{"kind":"Field","alias":{"kind":"Name","value":"quota"},"name":{"kind":"Name","value":"countTotal"}},{"kind":"Field","name":{"kind":"Name","value":"canDelete"}},{"kind":"Field","name":{"kind":"Name","value":"products"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"QuotaProduct"}}]}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"QuotaProduct"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LimitedProductType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"price"}},{"kind":"Field","name":{"kind":"Name","value":"countReserved"}}]}}]} as unknown as DocumentNode; diff --git a/kompassi-v2-frontend/src/app/[locale]/[eventSlug]/programs/[programSlug]/page.tsx b/kompassi-v2-frontend/src/app/[locale]/[eventSlug]/programs/[programSlug]/page.tsx index 45b633248..fb5719ac9 100644 --- a/kompassi-v2-frontend/src/app/[locale]/[eventSlug]/programs/[programSlug]/page.tsx +++ b/kompassi-v2-frontend/src/app/[locale]/[eventSlug]/programs/[programSlug]/page.tsx @@ -89,6 +89,11 @@ const query = graphql(` location(lang: $locale) startTime endTime + links(ownOnly: true, lang: $locale) { + type + href + title + } } } } @@ -145,6 +150,31 @@ function getLinkEmoji(type: ProgramLinkType) { } } +interface ProgramLinkFields { + type: ProgramLinkType; + href: string; + title: string; +} + +/// Renders a single program/schedule item link, internal links via next/link. +function ProgramLinkAnchor({ link }: { link: ProgramLinkFields }) { + const label = `${getLinkEmoji(link.type)} ${link.title}…`; + return link.href.startsWith(publicUrl) ? ( + + {label} + + ) : ( + + {label} + + ); +} + export default async function NewProgramPage(props: Props) { const params = await props.params; const { locale, eventSlug, programSlug } = params; @@ -222,6 +252,11 @@ export default async function NewProgramPage(props: Props) { )} + {scheduleItem.links.map((link, linkIndex) => ( + + + + ))} ))} @@ -229,25 +264,7 @@ export default async function NewProgramPage(props: Props) {
{program.links.map((link, index) => (
- {link.href.startsWith(publicUrl) ? ( - - {getLinkEmoji(link.type) + " "} - {link.title}… - - ) : ( - - {getLinkEmoji(link.type) + " "} - {link.title}… - - )} +
))}
diff --git a/kompassi/events/tracon2026/management/commands/setup_tracon2026.py b/kompassi/events/tracon2026/management/commands/setup_tracon2026.py index 2ff26666b..5426e87cd 100644 --- a/kompassi/events/tracon2026/management/commands/setup_tracon2026.py +++ b/kompassi/events/tracon2026/management/commands/setup_tracon2026.py @@ -352,6 +352,7 @@ def setup_program_v2(self): admin_group=admin_group, # NOTE: setting this shows program as published # guide_v2_embedded_url="https://2026.tracon.fi/opas/", + konsti_url="https://konsti.tracon.fi", contact_email="Traconin ohjelmatiimi ", is_accepting_feedback=True, default_registry=Registry.objects.get( diff --git a/kompassi/program_v2/graphql/program_links.py b/kompassi/program_v2/graphql/program_links.py index 9c726f760..aad317825 100644 --- a/kompassi/program_v2/graphql/program_links.py +++ b/kompassi/program_v2/graphql/program_links.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Self +from typing import TYPE_CHECKING, Self import graphene from django.conf import settings @@ -13,8 +13,8 @@ from ..models import Program -# imported for side effects (register object type used by django object type fields) -from .schedule_item_limited import LimitedScheduleItemType # noqa: F401 +if TYPE_CHECKING: + from ..models import ScheduleItem class ProgramLinkType(graphene.Enum): @@ -122,6 +122,34 @@ class ProgramLink(graphene.ObjectType): href = graphene.NonNull(graphene.String) title = graphene.NonNull(graphene.String) + @classmethod + def _build( + cls, + link_type: ProgramLinkType, + href: str | None, + annotations: dict, + language: str, + ) -> Self | None: + """ + Shared tail of `from_program` / `from_schedule_item`: given a resolved `href` + and the annotation source to read titles from, build the link (or None). + """ + if not href: + return None + + link_type_str = link_type.value # type: ignore[attr-defined] + link_annotation = f"internal:links:{link_type_str.lower()}" + + titles = get_message_in_language(DEFAULT_LINK_TITLES, language) or {} + title = ( + annotations.get(f"{link_annotation}:title:{language}") + or annotations.get(f"{link_annotation}:title:{DEFAULT_LANGUAGE}") + or annotations.get(f"{link_annotation}:title") + or titles.get(link_type_str, "") + ) + + return cls(type=link_type, href=href, title=title) # type: ignore[call-arg] + @classmethod def from_program( cls, @@ -143,7 +171,6 @@ def from_program( return None link_type_str = link_type.value # type: ignore[attr-defined] - title_specifier = "" link_annotation = f"internal:links:{link_type_str.lower()}" match link_type: @@ -175,18 +202,61 @@ def from_program( case _: href = program.annotations.get(link_annotation, "") - if not href: + return cls._build(link_type, href, program.annotations, language) + + @classmethod + def from_schedule_item( + cls, + request: HttpRequest, + schedule_item: ScheduleItem, + link_type: ProgramLinkType, + language: str = DEFAULT_LANGUAGE, + include_expired: bool = False, + own_only: bool = False, + ) -> Self | None: + """ + Schedule-item-specific counterpart of `from_program`. The signup link is + generated on the fly from the Konsti dimension when it is not otherwise set. + + With `own_only`, only links set directly on the schedule item (its own + `annotations`) are considered; otherwise links inherited from the program + (via `cached_combined_annotations`) are included as well. + """ + from ..integrations.konsti import get_konsti_base_url + + event = schedule_item.event + meta = event.program_v2_event_meta + + if meta is None: + # This should not happen, but appease the type checker return None - titles = get_message_in_language(DEFAULT_LINK_TITLES, language) or {} - title = ( - program.annotations.get(f"{link_annotation}:title:{language}") - or program.annotations.get(f"{link_annotation}:title:{DEFAULT_LANGUAGE}") - or program.annotations.get(f"{link_annotation}:title") - or titles.get(link_type_str, "") - ) + annotations = schedule_item.annotations if own_only else schedule_item.cached_combined_annotations + link_type_str = link_type.value # type: ignore[attr-defined] + link_annotation = f"internal:links:{link_type_str.lower()}" - if title_specifier: - title = f"{title} ({title_specifier})" + # Do not show time-sensitive links if the schedule item has ended + expired = not include_expired and schedule_item.cached_end_time and now() > schedule_item.cached_end_time - return cls(type=link_type, href=href, title=title) # type: ignore[call-arg] + match link_type: + case ProgramLinkType.SIGNUP: + if expired: + href = "" + else: + href = annotations.get(link_annotation, "") + if not href and schedule_item.cached_combined_dimensions.get("konsti"): + href = f"{get_konsti_base_url(meta)}/program/item/{schedule_item.slug}" + case ProgramLinkType.RESERVATION | ProgramLinkType.TICKETS | ProgramLinkType.REMOTE: + href = "" if expired else annotations.get(link_annotation, "") + case ( + ProgramLinkType.CALENDAR + | ProgramLinkType.GUIDE_V2_LIGHT + | ProgramLinkType.GUIDE_V2_EMBEDDED + | ProgramLinkType.FEEDBACK + ): + # These links are only meaningful at the program level. + href = "" + case _: + href = annotations.get(link_annotation, "") + + return cls._build(link_type, href, annotations, language) diff --git a/kompassi/program_v2/graphql/schedule_item_limited.py b/kompassi/program_v2/graphql/schedule_item_limited.py index 8f54ab7f4..ef68e5495 100644 --- a/kompassi/program_v2/graphql/schedule_item_limited.py +++ b/kompassi/program_v2/graphql/schedule_item_limited.py @@ -4,9 +4,11 @@ from graphene_django import DjangoObjectType from kompassi.core.utils.text_utils import normalize_whitespace +from kompassi.graphql_api.language import DEFAULT_LANGUAGE from kompassi.graphql_api.utils import resolve_local_datetime_field, resolve_localized_field, resolve_unix_seconds_field from ..models import ScheduleItem +from .program_links import ProgramLink, ProgramLinkType class LimitedScheduleItemType(DjangoObjectType): @@ -23,6 +25,47 @@ class Meta: from .cached_annotations import cached_annotations, resolve_cached_annotations from .cached_dimensions import cached_dimensions, resolve_cached_dimensions + @staticmethod + def resolve_links( + parent: ScheduleItem, + info, + types: list[ProgramLinkType] | None = None, + lang=DEFAULT_LANGUAGE, + include_expired: bool = False, + own_only: bool = False, + ): + """ + Get the links associated with the schedule item. If types are not specified, all links are + returned. With `ownOnly`, only links set directly on the schedule item are returned; + otherwise links inherited from the program are included as well. + """ + if types is None: + types = list(ProgramLinkType) + + return [ + schedule_item_link + for link_type in types + if ( + schedule_item_link := ProgramLink.from_schedule_item( + info.context, + parent, + link_type, + lang, + include_expired=include_expired, + own_only=own_only, + ) + ) + ] + + links = graphene.NonNull( + graphene.List(graphene.NonNull(ProgramLink)), + description=normalize_whitespace(resolve_links.__doc__ or ""), + types=graphene.List(ProgramLinkType), + lang=graphene.String(), + include_expired=graphene.Boolean(), + own_only=graphene.Boolean(), + ) + @staticmethod def resolve_duration_minutes(parent: ScheduleItem, info): return parent.duration.total_seconds() // 60 diff --git a/kompassi/program_v2/integrations/konsti.py b/kompassi/program_v2/integrations/konsti.py index 3a43ac89e..a928156ec 100644 --- a/kompassi/program_v2/integrations/konsti.py +++ b/kompassi/program_v2/integrations/konsti.py @@ -137,6 +137,17 @@ def UNSURE(x: str) -> str: ] +DEFAULT_KONSTI_URL = "https://ropekonsti.fi" + + +def get_konsti_base_url(meta) -> str: + """ + Base URL of the Konsti signup application for the event, used to form signup links. + Falls back to the default Konsti deployment if the event has not overridden it. + """ + return meta.konsti_url or DEFAULT_KONSTI_URL + + def get_konsti_signup_url(program: Program) -> str: """ Returns the Konsti signup URL for this program if it is set up for Konsti @@ -147,8 +158,6 @@ def get_konsti_signup_url(program: Program) -> str: if not ( # is already saved (cannot have annotations or dimensions otherwise) program.pk - # the event is set up for Konsti - and program.meta.konsti_url # this event is using Konsti and program.cached_dimensions.get("konsti", "") # this program is not a placeholder in Konsti @@ -159,4 +168,4 @@ def get_konsti_signup_url(program: Program) -> str: return "" sole_schedule_item = program.schedule_items.get() - return f"{program.meta.konsti_url}/program/item/{sole_schedule_item.slug}" + return f"{get_konsti_base_url(program.meta)}/program/item/{sole_schedule_item.slug}" From 5f922782a98ec4d35ea2b78bb9b2203198bbff71 Mon Sep 17 00:00:00 2001 From: Santtu Pajukanta Date: Mon, 20 Jul 2026 00:35:18 +0300 Subject: [PATCH 2/3] chore(program_v2): type-ignore some graphene funnities --- kompassi/program_v2/graphql/program_links.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/kompassi/program_v2/graphql/program_links.py b/kompassi/program_v2/graphql/program_links.py index aad317825..5f7a08abb 100644 --- a/kompassi/program_v2/graphql/program_links.py +++ b/kompassi/program_v2/graphql/program_links.py @@ -202,7 +202,12 @@ def from_program( case _: href = program.annotations.get(link_annotation, "") - return cls._build(link_type, href, program.annotations, language) + return cls._build( + link_type, # type: ignore + href, + program.annotations, + language, + ) @classmethod def from_schedule_item( @@ -259,4 +264,9 @@ def from_schedule_item( case _: href = annotations.get(link_annotation, "") - return cls._build(link_type, href, annotations, language) + return cls._build( + link_type, # type: ignore + href, + annotations, + language, + ) From 39b3484d431282da76500a25ac56c4867aef6533 Mon Sep 17 00:00:00 2001 From: Santtu Pajukanta Date: Mon, 20 Jul 2026 00:55:02 +0300 Subject: [PATCH 3/3] chore(program_v2): refactor konsti url handling --- kompassi/program_v2/graphql/program_links.py | 4 +-- kompassi/program_v2/integrations/konsti.py | 29 ++++++-------------- kompassi/program_v2/models/program.py | 5 ---- kompassi/program_v2/models/schedule_item.py | 5 ++++ 4 files changed, 16 insertions(+), 27 deletions(-) diff --git a/kompassi/program_v2/graphql/program_links.py b/kompassi/program_v2/graphql/program_links.py index 5f7a08abb..8af89a075 100644 --- a/kompassi/program_v2/graphql/program_links.py +++ b/kompassi/program_v2/graphql/program_links.py @@ -10,6 +10,7 @@ from kompassi.core.utils.locale_utils import get_message_in_language from kompassi.dimensions.models.annotation_dto import AnnotationDTO from kompassi.graphql_api.language import DEFAULT_LANGUAGE +from kompassi.program_v2.integrations.konsti import get_konsti_signup_url from ..models import Program @@ -227,7 +228,6 @@ def from_schedule_item( `annotations`) are considered; otherwise links inherited from the program (via `cached_combined_annotations`) are included as well. """ - from ..integrations.konsti import get_konsti_base_url event = schedule_item.event meta = event.program_v2_event_meta @@ -250,7 +250,7 @@ def from_schedule_item( else: href = annotations.get(link_annotation, "") if not href and schedule_item.cached_combined_dimensions.get("konsti"): - href = f"{get_konsti_base_url(meta)}/program/item/{schedule_item.slug}" + href = get_konsti_signup_url(schedule_item) case ProgramLinkType.RESERVATION | ProgramLinkType.TICKETS | ProgramLinkType.REMOTE: href = "" if expired else annotations.get(link_annotation, "") case ( diff --git a/kompassi/program_v2/integrations/konsti.py b/kompassi/program_v2/integrations/konsti.py index a928156ec..fef3fc221 100644 --- a/kompassi/program_v2/integrations/konsti.py +++ b/kompassi/program_v2/integrations/konsti.py @@ -7,7 +7,7 @@ from kompassi.dimensions.models.enums import AnnotationDataType if TYPE_CHECKING: - from kompassi.program_v2.models.program import Program + from kompassi.program_v2.models.program import ScheduleItem def UNSURE(x: str) -> str: @@ -140,32 +140,21 @@ def UNSURE(x: str) -> str: DEFAULT_KONSTI_URL = "https://ropekonsti.fi" -def get_konsti_base_url(meta) -> str: +def get_konsti_signup_url(schedule_item: ScheduleItem) -> str: """ - Base URL of the Konsti signup application for the event, used to form signup links. - Falls back to the default Konsti deployment if the event has not overridden it. + Returns the Konsti signup URL for this schedule item if it is set up for Konsti. """ - return meta.konsti_url or DEFAULT_KONSTI_URL + meta = schedule_item.meta + konsti_base_url = meta.konsti_url or DEFAULT_KONSTI_URL - -def get_konsti_signup_url(program: Program) -> str: - """ - Returns the Konsti signup URL for this program if it is set up for Konsti - and has a single schedule item. It will be put in the `internal:links:signup` annotation. - - TODO(#801) Should be separately on each schedule item - """ if not ( # is already saved (cannot have annotations or dimensions otherwise) - program.pk + schedule_item.pk # this event is using Konsti - and program.cached_dimensions.get("konsti", "") + and schedule_item.program.cached_dimensions.get("konsti", "") # this program is not a placeholder in Konsti - and not program.annotations.get("konsti:isPlaceholder", False) - # it has exactly one schedule item to determine the slug in Konsti - and program.schedule_items.count() == 1 + and not schedule_item.program.annotations.get("konsti:isPlaceholder", False) ): return "" - sole_schedule_item = program.schedule_items.get() - return f"{get_konsti_base_url(program.meta)}/program/item/{sole_schedule_item.slug}" + return f"{konsti_base_url}/program/item/{schedule_item.slug}" diff --git a/kompassi/program_v2/models/program.py b/kompassi/program_v2/models/program.py index 60851b4b9..335685e33 100644 --- a/kompassi/program_v2/models/program.py +++ b/kompassi/program_v2/models/program.py @@ -37,7 +37,6 @@ from kompassi.involvement.models.involvement import Involvement from ..dimensions import get_scheduled_dimension_value -from ..integrations.konsti import get_konsti_signup_url if TYPE_CHECKING: from dimensions.models.universe import Universe @@ -266,10 +265,6 @@ def with_annotations(self, update: CachedAnnotationsUpdate | CachedAnnotations | } ) - # TODO(#801) Should be separately on each schedule item - if not self.annotations.get("internal:links:signup") and (konsti_url := get_konsti_signup_url(self)): - self.annotations["internal:links:signup"] = konsti_url - self.annotations = compact_annotations(self.annotations) return self diff --git a/kompassi/program_v2/models/schedule_item.py b/kompassi/program_v2/models/schedule_item.py index 9e3440445..7f3f75147 100644 --- a/kompassi/program_v2/models/schedule_item.py +++ b/kompassi/program_v2/models/schedule_item.py @@ -30,6 +30,7 @@ if TYPE_CHECKING: from paikkala.models.programs import Program as PaikkalaProgram + from .meta import ProgramV2EventMeta from .schedule_item_dimension_value import ScheduleItemDimensionValue logger = logging.getLogger(__name__) @@ -337,3 +338,7 @@ def is_cancelled(self) -> bool: TODO(#726) Cancel single schedule item without cancelling the whole program. """ return bool(set(self.cached_combined_dimensions.get("state", [])).intersection({"cancelled", "rejected"})) + + @property + def meta(self) -> ProgramV2EventMeta: + return self.program.meta