diff --git a/graphql.schema.json b/graphql.schema.json index f2ab5f3d30a..bd537cfcfeb 100644 --- a/graphql.schema.json +++ b/graphql.schema.json @@ -9339,6 +9339,20 @@ }, "defaultValue": null }, + { + "name": "external_id", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, { "name": "name", "description": null, diff --git a/hasura/metadata/actions.graphql b/hasura/metadata/actions.graphql index 5245e6c5830..ec08c40a2d4 100644 --- a/hasura/metadata/actions.graphql +++ b/hasura/metadata/actions.graphql @@ -41,6 +41,7 @@ type Mutation { create_election( election_event_id: String! name: String! + external_id: String! presentation: jsonb description: String ): CreateElectionOutput diff --git a/packages/admin-portal/graphql.schema.json b/packages/admin-portal/graphql.schema.json index bac1e65739f..79c629b26b4 100644 --- a/packages/admin-portal/graphql.schema.json +++ b/packages/admin-portal/graphql.schema.json @@ -10215,6 +10215,22 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "external_id", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "name", "description": null, diff --git a/packages/admin-portal/src/components/ContestItem.tsx b/packages/admin-portal/src/components/ContestItem.tsx index f42e65c5dd9..954020242fe 100644 --- a/packages/admin-portal/src/components/ContestItem.tsx +++ b/packages/admin-portal/src/components/ContestItem.tsx @@ -2,6 +2,7 @@ // // SPDX-License-Identifier: AGPL-3.0-only +import {useAliasRenderer} from "@/hooks/useAliasRenderer" import {GET_AREA_WITH_AREA_CONTESTS} from "@/queries/GetAreaWithAreaContest" import {useQuery} from "@apollo/client" import styled from "@emotion/styled" @@ -26,8 +27,9 @@ interface ContestItemProps { export const ContestItem: React.FC = (props) => { const {record} = props + const aliasRendere = useAliasRenderer() const {data} = useGetOne("sequent_backend_contest", {id: record}) - return <>{data ? : null} + return <>{data ? : null} } diff --git a/packages/admin-portal/src/components/MiruExportWizard.tsx b/packages/admin-portal/src/components/MiruExportWizard.tsx index 1ad77572c8b..e5ee26eaa3a 100644 --- a/packages/admin-portal/src/components/MiruExportWizard.tsx +++ b/packages/admin-portal/src/components/MiruExportWizard.tsx @@ -55,12 +55,12 @@ import {useAtomValue} from "jotai" import {tallyQueryData} from "@/atoms/tally-candidates" import {CREATE_TRANSMISSION_PACKAGE} from "@/queries/CreateTransmissionPackage" import {GET_UPLOAD_URL} from "@/queries/GetUploadUrl" -import {translateElection} from "@sequentech/ui-core" import {ETasksExecution} from "@/types/tasksExecution" import {useWidgetStore} from "@/providers/WidgetsContextProvider" import {WidgetProps} from "@/components/Widget" import {CancelButton} from "@/resources/Tally/styles" import ArrowBackIosIcon from "@mui/icons-material/ArrowBackIos" +import {useAliasRenderer} from "@/hooks/useAliasRenderer" interface IMiruExportWizardProps {} @@ -86,6 +86,7 @@ export const MiruExportWizard: React.FC = ({}) => { const [passwordState, setPasswordState] = useState("") const [signatureId, setSignatureId] = useState("") const authContext = useContext(AuthContext) + const aliasRenderer = useAliasRenderer() const [addWidget, setWidgetTaskId, updateWidgetFail] = useWidgetStore() const {data: areaData} = useGetOne( @@ -556,13 +557,7 @@ export const MiruExportWizard: React.FC = ({}) => { [tallySessionData, tally] ) - const eventName = - (election && - (translateElection(election, "alias", i18n.language) || - translateElection(election, "name", i18n.language))) || - election?.alias || - election?.name || - "-" + const eventName = aliasRenderer(election) const canDownloadMiru = authContext.hasRole(IPermissions.MIRU_DOWNLOAD) const canSendMiru = authContext.hasRole(IPermissions.MIRU_SEND) diff --git a/packages/admin-portal/src/components/keys-ceremony/DownloadStep.tsx b/packages/admin-portal/src/components/keys-ceremony/DownloadStep.tsx index 86f83818185..7c8a697dbbb 100644 --- a/packages/admin-portal/src/components/keys-ceremony/DownloadStep.tsx +++ b/packages/admin-portal/src/components/keys-ceremony/DownloadStep.tsx @@ -19,6 +19,7 @@ import {WizardStyles} from "@/components/styles/WizardStyles" import {GET_PRIVATE_KEY} from "@/queries/GetPrivateKey" import {Dialog} from "@sequentech/ui-essentials" import {useNotify} from "react-admin" +import {useAliasRenderer} from "@/hooks/useAliasRenderer" export interface DownloadStepProps { electionEvent: Sequent_Backend_Election_Event @@ -40,6 +41,7 @@ export const DownloadStep: React.FC = ({ const [openConfirmationModal, setOpenConfirmationModal] = useState(false) const [errors, setErrors] = useState(null) const notify = useNotify() + const aliasRenderer = useAliasRenderer() const [checkboxState, setCheckboxState] = React.useState({ firstCheckbox: false, secondCheckbox: false, @@ -81,7 +83,7 @@ export const DownloadStep: React.FC = ({ const blob = new Blob([privateKey], {type: "text/plain"}) const blobUrl = window.URL.createObjectURL(blob) const username = authContext.username - const electionName = electionEvent.alias || electionEvent.name + const electionName = aliasRenderer(electionEvent.presentation) const fileName = `encrypted_private_key_trustee_${username}_${electionName}.txt` var tempLink = document.createElement("a") tempLink.href = blobUrl diff --git a/packages/admin-portal/src/components/menu/items/ElectionEvents.tsx b/packages/admin-portal/src/components/menu/items/ElectionEvents.tsx index bc8a22239e9..6756c73947e 100644 --- a/packages/admin-portal/src/components/menu/items/ElectionEvents.tsx +++ b/packages/admin-portal/src/components/menu/items/ElectionEvents.tsx @@ -21,6 +21,7 @@ import { IContest, IElection, ICandidate, + translateFromPresentation, } from "@sequentech/ui-core" import SearchIcon from "@mui/icons-material/Search" import { @@ -53,6 +54,7 @@ import { } from "@/queries/GetElectionEventsTree" import {useElectionEventTallyStore} from "@/providers/ElectionEventTallyProvider" import {sortCandidatesInContest, sortContestList, sortElectionList} from "@sequentech/ui-core" +import {useAliasRenderer} from "@/hooks/useAliasRenderer" const MenuItem = styled(Menu.Item)` color: ${adminTheme.palette.brandColor}; @@ -215,6 +217,7 @@ export default function ElectionEvents() { const [instantSearchInput, setInstantSearchInput] = useState("") const [searchInput, setSearchInput] = useState("") const navigate = useNavigate() + const aliasRenderer = useAliasRenderer() const [isArchivedElectionEvents, setArchivedElectionEvents] = useAtom( archivedElectionEventSelection @@ -468,6 +471,14 @@ export default function ElectionEvents() { openImportDrawer?.() } + const transformElectionEvent = (electionEvent: ElectionEventType): ElectionEventType => { + return { + ...electionEvent, + name: translateFromPresentation(electionEvent, "name", i18n.language) ?? "-", + alias: aliasRenderer(electionEvent), + } + } + const transformElectionsForSort = (elections: ElectionType[]): IElection[] => { return elections.map((election) => { return { @@ -475,6 +486,8 @@ export default function ElectionEvents() { tenant_id: tenantId || "", image_document_id: election.image_document_id ?? "", contests: [], + name: translateFromPresentation(election, "name", i18n.language) ?? "-", + alias: aliasRenderer(election), } }) } @@ -489,6 +502,8 @@ export default function ElectionEvents() { min_votes: 0, winning_candidates_num: 0, is_encrypted: false, + name: translateFromPresentation(contest, "name", i18n.language) ?? "-", + alias: aliasRenderer(contest), } }) } @@ -500,6 +515,8 @@ export default function ElectionEvents() { id: candidate.id, election_id: electionId || "", tenant_id: tenantId || "", + name: translateFromPresentation(candidate, "name", i18n.language) ?? "-", + alias: aliasRenderer(candidate), } }) } @@ -516,7 +533,7 @@ export default function ElectionEvents() { (electionEvent: ElectionEventType) => { const electionOrderType = electionEvent?.presentation?.elections_order return { - ...electionEvent, + ...transformElectionEvent(electionEvent), ...(electionEvent.id === electionEventId ? { active: true, diff --git a/packages/admin-portal/src/components/menu/items/election-events/TreeMenu.tsx b/packages/admin-portal/src/components/menu/items/election-events/TreeMenu.tsx index 36ceb2bceef..19ded4781b7 100644 --- a/packages/admin-portal/src/components/menu/items/election-events/TreeMenu.tsx +++ b/packages/admin-portal/src/components/menu/items/election-events/TreeMenu.tsx @@ -26,7 +26,7 @@ import {useActionPermissions} from "../use-tree-menu-hook" import {useTenantStore} from "@/providers/TenantContextProvider" import {NewResourceContext} from "@/providers/NewResourceProvider" import {adminTheme} from "@sequentech/ui-essentials" -import {translateElection} from "@sequentech/ui-core" +import {translateFromPresentation} from "@sequentech/ui-core" import {SettingsContext} from "@/providers/SettingsContextProvider" import {Box, Menu, MenuItem} from "@mui/material" import {MenuStyles, TreeMenuItemContainer} from "@/components/styles/Menu" @@ -35,6 +35,7 @@ import {useElectionEventTallyStore} from "@/providers/ElectionEventTallyProvider import {useCreateElectionEventStore} from "@/providers/CreateElectionEventContextProvider" import {useNavigate} from "react-router-dom" import RefreshIcon from "@mui/icons-material/Refresh" +import {useAliasRenderer} from "@/hooks/useAliasRenderer" export const mapAddResource: Record = { sequent_backend_election_event: "createResource.electionEvent", @@ -120,6 +121,7 @@ function TreeLeaves({ const {t, i18n} = useTranslation() const {openCreateDrawer, openImportDrawer} = useCreateElectionEventStore() const [anchorEl, setAnchorEl] = useState(null) + const aliasRenderer = useAliasRenderer() useEffect(() => { const dir = i18n.dir(i18n.language) @@ -209,13 +211,7 @@ function TreeLeaves({ parentData={resource} superParentData={parentData} id={resource.id} - name={ - translateElection(resource, "alias", i18n.language) || - translateElection(resource, "name", i18n.language) || - resource.alias || - resource.name || - "-" - } + name={aliasRenderer(resource)} treeResourceNames={treeResourceNames} isArchivedElectionEvents={isArchivedElectionEvents} fullPath={fillPath(resource)} diff --git a/packages/admin-portal/src/gql/gql.ts b/packages/admin-portal/src/gql/gql.ts index 28b3ecb035c..a7596fce4d1 100644 --- a/packages/admin-portal/src/gql/gql.ts +++ b/packages/admin-portal/src/gql/gql.ts @@ -15,7 +15,7 @@ import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/ const documents = { "\n mutation ChangeApplicationStatus(\n $election_event_id: String!\n $user_id: String!\n $id: String!\n $tenant_id: String\n $area_id: String\n $rejection_reason: String\n $rejection_message: String\n ) {\n ApplicationChangeStatus(\n body: {\n election_event_id: $election_event_id\n id: $id\n user_id: $user_id\n tenant_id: $tenant_id\n area_id: $area_id\n rejection_reason: $rejection_reason\n rejection_message: $rejection_message\n }\n ) {\n message\n error\n }\n }\n": types.ChangeApplicationStatusDocument, "\n mutation CheckPrivateKey(\n $electionEventId: String!\n $keysCeremonyId: String!\n $privateKeyBase64: String!\n ) {\n check_private_key(\n object: {\n election_event_id: $electionEventId\n keys_ceremony_id: $keysCeremonyId\n private_key_base64: $privateKeyBase64\n }\n ) {\n is_valid\n }\n }\n": types.CheckPrivateKeyDocument, - "\n mutation CreateElection(\n $electionEventId: String!\n $name: String!\n $presentation: jsonb\n $description: String\n ) {\n create_election(\n election_event_id: $electionEventId\n name: $name\n presentation: $presentation\n description: $description\n ) {\n id\n }\n }\n": types.CreateElectionDocument, + "\n mutation CreateElection(\n $electionEventId: String!\n $name: String!\n $externalId: String!\n $presentation: jsonb\n $description: String\n ) {\n create_election(\n election_event_id: $electionEventId\n name: $name\n external_id: $externalId\n presentation: $presentation\n description: $description\n ) {\n id\n }\n }\n": types.CreateElectionDocument, "\n mutation CreateKeysCeremony(\n $electionEventId: String!\n $threshold: Int!\n $trusteeNames: [String!]\n $electionId: String\n $name: String\n ) {\n create_keys_ceremony(\n object: {\n election_event_id: $electionEventId\n threshold: $threshold\n trustee_names: $trusteeNames\n election_id: $electionId\n name: $name\n }\n ) {\n keys_ceremony_id\n error_message\n }\n }\n": types.CreateKeysCeremonyDocument, "\n mutation InsertReport($object: sequent_backend_report_insert_input!) {\n insert_sequent_backend_report(objects: [$object]) {\n returning {\n id\n election_event_id\n tenant_id\n election_id\n report_type\n template_alias\n cron_config\n encryption_policy\n }\n affected_rows\n }\n }\n": types.InsertReportDocument, "\n mutation CreateRole($tenantId: String!, $role: KeycloakRole2!) {\n create_role(tenant_id: $tenantId, role: $role) {\n id\n name\n permissions\n access\n attributes\n client_roles\n }\n }\n": types.CreateRoleDocument, @@ -145,7 +145,7 @@ export function graphql(source: "\n mutation CheckPrivateKey(\n $elect /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "\n mutation CreateElection(\n $electionEventId: String!\n $name: String!\n $presentation: jsonb\n $description: String\n ) {\n create_election(\n election_event_id: $electionEventId\n name: $name\n presentation: $presentation\n description: $description\n ) {\n id\n }\n }\n"): (typeof documents)["\n mutation CreateElection(\n $electionEventId: String!\n $name: String!\n $presentation: jsonb\n $description: String\n ) {\n create_election(\n election_event_id: $electionEventId\n name: $name\n presentation: $presentation\n description: $description\n ) {\n id\n }\n }\n"]; +export function graphql(source: "\n mutation CreateElection(\n $electionEventId: String!\n $name: String!\n $externalId: String!\n $presentation: jsonb\n $description: String\n ) {\n create_election(\n election_event_id: $electionEventId\n name: $name\n external_id: $externalId\n presentation: $presentation\n description: $description\n ) {\n id\n }\n }\n"): (typeof documents)["\n mutation CreateElection(\n $electionEventId: String!\n $name: String!\n $externalId: String!\n $presentation: jsonb\n $description: String\n ) {\n create_election(\n election_event_id: $electionEventId\n name: $name\n external_id: $externalId\n presentation: $presentation\n description: $description\n ) {\n id\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/packages/admin-portal/src/gql/graphql.ts b/packages/admin-portal/src/gql/graphql.ts index 08e7e0fb011..2a5533e3fa7 100644 --- a/packages/admin-portal/src/gql/graphql.ts +++ b/packages/admin-portal/src/gql/graphql.ts @@ -1705,6 +1705,7 @@ export type Mutation_RootCreate_Ballot_ReceiptArgs = { export type Mutation_RootCreate_ElectionArgs = { description?: InputMaybe; election_event_id: Scalars['String']['input']; + external_id: Scalars['String']['input']; name: Scalars['String']['input']; presentation?: InputMaybe; }; @@ -20684,6 +20685,7 @@ export type CheckPrivateKeyMutation = { __typename?: 'mutation_root', check_priv export type CreateElectionMutationVariables = Exact<{ electionEventId: Scalars['String']['input']; name: Scalars['String']['input']; + externalId: Scalars['String']['input']; presentation?: InputMaybe; description?: InputMaybe; }>; @@ -21594,7 +21596,7 @@ export type LimitAccessByCountriesMutation = { __typename?: 'mutation_root', lim export const ChangeApplicationStatusDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ChangeApplicationStatus"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"election_event_id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"user_id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"tenant_id"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"area_id"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"rejection_reason"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"rejection_message"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"ApplicationChangeStatus"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"body"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"election_event_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"election_event_id"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"user_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"user_id"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"tenant_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"tenant_id"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"area_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"area_id"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"rejection_reason"},"value":{"kind":"Variable","name":{"kind":"Name","value":"rejection_reason"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"rejection_message"},"value":{"kind":"Variable","name":{"kind":"Name","value":"rejection_message"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}}]}}]} as unknown as DocumentNode; export const CheckPrivateKeyDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CheckPrivateKey"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"electionEventId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"keysCeremonyId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"privateKeyBase64"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"check_private_key"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"object"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"election_event_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"electionEventId"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"keys_ceremony_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"keysCeremonyId"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"private_key_base64"},"value":{"kind":"Variable","name":{"kind":"Name","value":"privateKeyBase64"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"is_valid"}}]}}]}}]} as unknown as DocumentNode; -export const CreateElectionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateElection"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"electionEventId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"name"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"presentation"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"jsonb"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"description"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"create_election"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"election_event_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"electionEventId"}}},{"kind":"Argument","name":{"kind":"Name","value":"name"},"value":{"kind":"Variable","name":{"kind":"Name","value":"name"}}},{"kind":"Argument","name":{"kind":"Name","value":"presentation"},"value":{"kind":"Variable","name":{"kind":"Name","value":"presentation"}}},{"kind":"Argument","name":{"kind":"Name","value":"description"},"value":{"kind":"Variable","name":{"kind":"Name","value":"description"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode; +export const CreateElectionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateElection"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"electionEventId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"name"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"externalId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"presentation"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"jsonb"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"description"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"create_election"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"election_event_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"electionEventId"}}},{"kind":"Argument","name":{"kind":"Name","value":"name"},"value":{"kind":"Variable","name":{"kind":"Name","value":"name"}}},{"kind":"Argument","name":{"kind":"Name","value":"external_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"externalId"}}},{"kind":"Argument","name":{"kind":"Name","value":"presentation"},"value":{"kind":"Variable","name":{"kind":"Name","value":"presentation"}}},{"kind":"Argument","name":{"kind":"Name","value":"description"},"value":{"kind":"Variable","name":{"kind":"Name","value":"description"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode; export const CreateKeysCeremonyDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateKeysCeremony"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"electionEventId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"threshold"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"trusteeNames"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"electionId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"name"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"create_keys_ceremony"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"object"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"election_event_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"electionEventId"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"threshold"},"value":{"kind":"Variable","name":{"kind":"Name","value":"threshold"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"trustee_names"},"value":{"kind":"Variable","name":{"kind":"Name","value":"trusteeNames"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"election_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"electionId"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"name"},"value":{"kind":"Variable","name":{"kind":"Name","value":"name"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"keys_ceremony_id"}},{"kind":"Field","name":{"kind":"Name","value":"error_message"}}]}}]}}]} as unknown as DocumentNode; export const InsertReportDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"InsertReport"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"object"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"sequent_backend_report_insert_input"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"insert_sequent_backend_report"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"objects"},"value":{"kind":"ListValue","values":[{"kind":"Variable","name":{"kind":"Name","value":"object"}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"returning"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"election_event_id"}},{"kind":"Field","name":{"kind":"Name","value":"tenant_id"}},{"kind":"Field","name":{"kind":"Name","value":"election_id"}},{"kind":"Field","name":{"kind":"Name","value":"report_type"}},{"kind":"Field","name":{"kind":"Name","value":"template_alias"}},{"kind":"Field","name":{"kind":"Name","value":"cron_config"}},{"kind":"Field","name":{"kind":"Name","value":"encryption_policy"}}]}},{"kind":"Field","name":{"kind":"Name","value":"affected_rows"}}]}}]}}]} as unknown as DocumentNode; export const CreateRoleDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateRole"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"tenantId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"role"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"KeycloakRole2"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"create_role"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"tenant_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"tenantId"}}},{"kind":"Argument","name":{"kind":"Name","value":"role"},"value":{"kind":"Variable","name":{"kind":"Name","value":"role"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"permissions"}},{"kind":"Field","name":{"kind":"Name","value":"access"}},{"kind":"Field","name":{"kind":"Name","value":"attributes"}},{"kind":"Field","name":{"kind":"Name","value":"client_roles"}}]}}]}}]} as unknown as DocumentNode; diff --git a/packages/admin-portal/src/hooks/useAliasRenderer.ts b/packages/admin-portal/src/hooks/useAliasRenderer.ts index 61a62bbeca5..40c02b0c4b6 100644 --- a/packages/admin-portal/src/hooks/useAliasRenderer.ts +++ b/packages/admin-portal/src/hooks/useAliasRenderer.ts @@ -2,22 +2,41 @@ // // SPDX-License-Identifier: AGPL-3.0-only -import {translateElection} from "@sequentech/ui-core" +import {translateFromPresentation} from "@sequentech/ui-core" import {useTranslation} from "react-i18next" +const isPlainObject = (value: unknown): value is Record => + typeof value === "object" && value !== null && !Array.isArray(value) + export function useAliasRenderer() { const {i18n} = useTranslation() - const aliasRenderer = (item: any) => { - if (!item) return "-" - - return ( - translateElection(item, "alias", i18n.language) || - translateElection(item, "name", i18n.language) || - item.alias || - item.name || + const aliasRenderer = (item: unknown) => { + const t = (x: any) => + translateFromPresentation(x, "alias", i18n.language) || + translateFromPresentation(x, "name", i18n.language) || + translateFromPresentation(x, "alias", "en") || + translateFromPresentation(x, "name", "en") || "-" - ) + + if (item == null) return "-" + + if (isPlainObject(item)) { + return t(item) + } + + const s = String(item).trim() + if (!s) return "-" + + if (s.startsWith("{")) { + try { + return t(JSON.parse(s)) + } catch { + return "-" + } + } + + return "-" } return aliasRenderer diff --git a/packages/admin-portal/src/queries/CreateElection.ts b/packages/admin-portal/src/queries/CreateElection.ts index 97a11e5f4e9..35519842cf4 100644 --- a/packages/admin-portal/src/queries/CreateElection.ts +++ b/packages/admin-portal/src/queries/CreateElection.ts @@ -7,12 +7,14 @@ export const CREATE_ELECTION = gql` mutation CreateElection( $electionEventId: String! $name: String! + $externalId: String! $presentation: jsonb $description: String ) { create_election( election_event_id: $electionEventId name: $name + external_id: $externalId presentation: $presentation description: $description ) { diff --git a/packages/admin-portal/src/queries/GetAreaWithAreaContest.ts b/packages/admin-portal/src/queries/GetAreaWithAreaContest.ts index 05778acc062..344223aeb9c 100644 --- a/packages/admin-portal/src/queries/GetAreaWithAreaContest.ts +++ b/packages/admin-portal/src/queries/GetAreaWithAreaContest.ts @@ -12,6 +12,7 @@ export const GET_AREA_WITH_AREA_CONTESTS = gql` contest { name alias + presentation } id } diff --git a/packages/admin-portal/src/resources/Candidate/CandidateDataForm.tsx b/packages/admin-portal/src/resources/Candidate/CandidateDataForm.tsx index 59461c88b79..b77889d67f4 100644 --- a/packages/admin-portal/src/resources/Candidate/CandidateDataForm.tsx +++ b/packages/admin-portal/src/resources/Candidate/CandidateDataForm.tsx @@ -162,9 +162,6 @@ export const CandidateDataForm: React.FC<{ if (!newCandidate.presentation.i18n.en.name && newCandidate.name) { newCandidate.presentation.i18n.en.name = newCandidate.name } - if (!newCandidate.presentation.i18n.en.name && newCandidate.name) { - newCandidate.presentation.i18n.en.name = newCandidate.name - } if (!newCandidate.presentation.i18n.en.alias && newCandidate.alias) { newCandidate.presentation.i18n.en.alias = newCandidate.alias } diff --git a/packages/admin-portal/src/resources/Contest/ContestTabs.tsx b/packages/admin-portal/src/resources/Contest/ContestTabs.tsx index c4fe53c21ce..4df6a33072b 100644 --- a/packages/admin-portal/src/resources/Contest/ContestTabs.tsx +++ b/packages/admin-portal/src/resources/Contest/ContestTabs.tsx @@ -9,8 +9,10 @@ import ElectionHeader from "../../components/ElectionHeader" import {EditContestData} from "./EditContestData" import {ListTallySheet} from "../TallySheet/ListTallySheet" import {TallySheetWizard, WizardSteps} from "../TallySheet/TallySheetWizard" +import {useAliasRenderer} from "@/hooks/useAliasRenderer" export const ContestTabs: React.FC = () => { + const aliasRenderer = useAliasRenderer() const record = useRecordContext() const [action, setAction] = useState(WizardSteps.List) @@ -27,7 +29,10 @@ export const ContestTabs: React.FC = () => { return ( <> - + @@ -38,36 +43,41 @@ export const ContestTabs: React.FC = () => { setAction(WizardSteps.List) }} > - {action === WizardSteps.List ? ( - - ) : action === WizardSteps.Start ? ( - - ) : action === WizardSteps.Edit ? ( - - ) : action === WizardSteps.Confirm ? ( - - ) : action === WizardSteps.View ? ( - - ) : null} + {record && + (action === WizardSteps.List ? ( + + ) : action === WizardSteps.Start ? ( + + ) : action === WizardSteps.Edit ? ( + + ) : action === WizardSteps.Confirm ? ( + + ) : action === WizardSteps.View ? ( + + ) : null)} diff --git a/packages/admin-portal/src/resources/Contest/EditContestDataForm.tsx b/packages/admin-portal/src/resources/Contest/EditContestDataForm.tsx index f27594a2538..b35a5517d84 100644 --- a/packages/admin-portal/src/resources/Contest/EditContestDataForm.tsx +++ b/packages/admin-portal/src/resources/Contest/EditContestDataForm.tsx @@ -435,9 +435,6 @@ export const ContestDataForm: React.FC = () => { if (!newContest.presentation.i18n.en.name && newContest.name) { newContest.presentation.i18n.en.name = newContest.name } - if (!newContest.presentation.i18n.en.name && newContest.name) { - newContest.presentation.i18n.en.name = newContest.name - } if (!newContest.presentation.i18n.en.alias && newContest.alias) { newContest.presentation.i18n.en.alias = newContest.alias } diff --git a/packages/admin-portal/src/resources/Election/CreateElection.tsx b/packages/admin-portal/src/resources/Election/CreateElection.tsx index bf81bbdb7d7..ada03bbb862 100644 --- a/packages/admin-portal/src/resources/Election/CreateElection.tsx +++ b/packages/admin-portal/src/resources/Election/CreateElection.tsx @@ -95,6 +95,7 @@ export const CreateElection: React.FC = () => { let electionSubmit = input0 as { name: string description?: string + external_id: string presentation: IElectionPresentation } let i18n = addDefaultTranslationsToElement(electionSubmit) @@ -123,6 +124,7 @@ export const CreateElection: React.FC = () => { const {data} = await createElection({ variables: { electionEventId: electionEventId, + externalId: electionSubmit.external_id, name: electionSubmit.name, presentation: electionSubmit.presentation, description: electionSubmit.description, @@ -151,8 +153,13 @@ export const CreateElection: React.FC = () => { > {t("common.resources.election")} {t("createResource.election")} - - + + + diff --git a/packages/admin-portal/src/resources/Election/ElectionData.tsx b/packages/admin-portal/src/resources/Election/ElectionData.tsx index 946741d4c96..2ef2edd2a80 100644 --- a/packages/admin-portal/src/resources/Election/ElectionData.tsx +++ b/packages/admin-portal/src/resources/Election/ElectionData.tsx @@ -70,17 +70,13 @@ export const EditElectionData: React.FC = () => { // is alll object, no change needed delete data.enabled_languages - // name, alias and description fields + // name and description fields const fromPresentationName = data?.presentation?.i18n?.en?.name || data?.presentation?.i18n[Object.keys(data.presentation.i18n)[0]].name || "" data.name = fromPresentationName - const fromPresentationAlias = - data?.presentation?.i18n?.en?.alias || - data?.presentation?.i18n[Object.keys(data.presentation.i18n)[0]].alias || - "" - data.alias = fromPresentationAlias + const fromPresentationDescription = data?.presentation?.i18n?.en?.description || data?.presentation?.i18n[Object.keys(data.presentation.i18n)[0]].description || diff --git a/packages/admin-portal/src/resources/Election/ElectionDataForm.tsx b/packages/admin-portal/src/resources/Election/ElectionDataForm.tsx index 1fcf553bd95..c9eb8c47654 100644 --- a/packages/admin-portal/src/resources/Election/ElectionDataForm.tsx +++ b/packages/admin-portal/src/resources/Election/ElectionDataForm.tsx @@ -272,9 +272,12 @@ export const ElectionDataForm: React.FC = () => { if (!temp.presentation?.i18n?.en) { temp.presentation.i18n.en = {} } - temp.presentation.i18n.en.name = temp.name - temp.presentation.i18n.en.alias = temp.alias - temp.presentation.i18n.en.description = temp.description + if (!temp.presentation.i18n.en.name) { + temp.presentation.i18n.en.name = temp.name + } + if (!temp.presentation.i18n.en.description && temp.description) { + temp.presentation.i18n.en.description = temp.description + } // receipts const template: {[key: string]: string | null} = {} @@ -577,6 +580,11 @@ export const ElectionDataForm: React.FC = () => { + {renderTabs(parsedValue)} diff --git a/packages/admin-portal/src/resources/Election/ElectionTabs.tsx b/packages/admin-portal/src/resources/Election/ElectionTabs.tsx index 84847d6516d..8d4c9456d9e 100644 --- a/packages/admin-portal/src/resources/Election/ElectionTabs.tsx +++ b/packages/admin-portal/src/resources/Election/ElectionTabs.tsx @@ -21,9 +21,10 @@ import {IPermissions} from "@/types/keycloak" import {EditElectionEventUsers} from "../ElectionEvent/EditElectionEventUsers" import {ResourceListStyles} from "@/components/styles/ResourceListStyles" import {Box, Typography} from "@mui/material" -import {EElectionEventLockedDown, i18n, translateElection} from "@sequentech/ui-core" +import {EElectionEventLockedDown} from "@sequentech/ui-core" import {EditElectionEventApprovals} from "../ElectionEvent/EditElectionEventApprovals" import {Tabs} from "@/components/Tabs" +import {useAliasRenderer} from "@/hooks/useAliasRenderer" export const ElectionTabs: React.FC = () => { const record = useRecordContext() @@ -33,6 +34,7 @@ export const ElectionTabs: React.FC = () => { const usersPermissionLabels = authContext.permissionLabels const [hasPermissionToViewElection, setHasPermissionToViewElection] = useState(true) const [open] = useSidebarState() + const aliasRenderer = useAliasRenderer() const isElectionEventLocked = record?.presentation?.locked_down == EElectionEventLockedDown.LOCKED_DOWN @@ -94,13 +96,7 @@ export const ElectionTabs: React.FC = () => { className="election-box" > import("@/components/dashboard/election-event/Dashboard")) @@ -81,6 +82,7 @@ export const ElectionEventTabs: React.FC = () => { record?.presentation?.locked_down == EElectionEventLockedDown.LOCKED_DOWN const {setTallyId} = useElectionEventTallyStore() const [open] = useSidebarState() + const aliasRenderer = useAliasRenderer() const showDashboard = authContext.isAuthorized( true, @@ -201,13 +203,7 @@ export const ElectionEventTabs: React.FC = () => { className="events-box" > = ({electionEventId}) => { label={t("reportsScreen.fields.electionId")} choices={elections?.map((election) => ({ id: election.id, - name: election.alias || election.name || "-", + name: aliasRenderer(election), }))} />, , diff --git a/packages/admin-portal/src/resources/ScheduledEvents/ListScheduledEvent.tsx b/packages/admin-portal/src/resources/ScheduledEvents/ListScheduledEvent.tsx index 6ad60973e4d..d17253fb5e3 100644 --- a/packages/admin-portal/src/resources/ScheduledEvents/ListScheduledEvent.tsx +++ b/packages/admin-portal/src/resources/ScheduledEvents/ListScheduledEvent.tsx @@ -277,7 +277,7 @@ const ListScheduledEvents: React.FC = ({electionEventId}) => { label={t("eventsScreen.fields.electionId")} choices={elections?.map((election) => ({ id: election.id, - name: election.alias || election.name || "-", + name: aliasRenderer(election), }))} onChange={(e: any) => { setEventScreenElectionId(e.target.value) diff --git a/packages/admin-portal/src/resources/Tally/TallyCeremony.tsx b/packages/admin-portal/src/resources/Tally/TallyCeremony.tsx index 78d4ed2258b..4f71cc1b654 100644 --- a/packages/admin-portal/src/resources/Tally/TallyCeremony.tsx +++ b/packages/admin-portal/src/resources/Tally/TallyCeremony.tsx @@ -290,6 +290,8 @@ export const TallyCeremony: React.FC = () => { }, [tallySession?.annotations?.[MIRU_TALLY_SESSION_ANNOTATION_KEY]]) const tallySessionDataRef = useRef(tallySessionData) + const electionEventName = record ? aliasRenderer(record) : "event" + useEffect(() => { tallySessionDataRef.current = tallySessionData }, [tallySessionData]) @@ -590,11 +592,17 @@ export const TallyCeremony: React.FC = () => { return documents ? { documents, - name: aliasRenderer(record) ?? "event", + name: electionEventName, class_type: "event", } : null - }, [resultsEventId, resultsEvent, resultsEvent?.[0]?.id, resultsEvent?.[0]?.name]) + }, [ + resultsEventId, + resultsEvent, + resultsEvent?.[0]?.id, + resultsEvent?.[0]?.name, + i18n.language, + ]) const handleMiruExportSuccess = (e: { election_id?: string @@ -1058,7 +1066,7 @@ export const TallyCeremony: React.FC = () => { electionEventId={ resultsEvent?.[0].election_event_id } - itemName={aliasRenderer(record) ?? "event"} + itemName={electionEventName} /> ) : null} diff --git a/packages/admin-portal/src/resources/Tally/TallyElectionsList.tsx b/packages/admin-portal/src/resources/Tally/TallyElectionsList.tsx index b36146ca0f9..40911a8aadf 100644 --- a/packages/admin-portal/src/resources/Tally/TallyElectionsList.tsx +++ b/packages/admin-portal/src/resources/Tally/TallyElectionsList.tsx @@ -60,7 +60,6 @@ export const TallyElectionsList: React.FC = (props) => ...election, rowId: index, id: election.id || "", - name: election.name, active: true, })) .filter((election) => diff --git a/packages/admin-portal/src/resources/Tally/TallyElectionsProgress.tsx b/packages/admin-portal/src/resources/Tally/TallyElectionsProgress.tsx index e7dd115f7db..e56384c1899 100644 --- a/packages/admin-portal/src/resources/Tally/TallyElectionsProgress.tsx +++ b/packages/admin-portal/src/resources/Tally/TallyElectionsProgress.tsx @@ -55,7 +55,6 @@ export const TallyElectionsProgress: React.FC = ({ ...election, rowId: index, id: election.id || "", - name: election.name, status: election.status || "", progress: 0, }) diff --git a/packages/admin-portal/src/resources/Tally/TallyElectionsResults.tsx b/packages/admin-portal/src/resources/Tally/TallyElectionsResults.tsx index 608826030de..21698221152 100644 --- a/packages/admin-portal/src/resources/Tally/TallyElectionsResults.tsx +++ b/packages/admin-portal/src/resources/Tally/TallyElectionsResults.tsx @@ -110,8 +110,8 @@ export const TallyElectionsResults: React.FC = (prop headerName: t("tally.table.elections"), flex: 1, editable: false, - valueGetter(params) { - return aliasRenderer(params.row) + renderCell: (props: GridRenderCellParams) => { + return aliasRenderer(props.row.presentation) }, }, { diff --git a/packages/admin-portal/src/resources/Tally/TallyResults.tsx b/packages/admin-portal/src/resources/Tally/TallyResults.tsx index d496be16644..3308fd017b0 100644 --- a/packages/admin-portal/src/resources/Tally/TallyResults.tsx +++ b/packages/admin-portal/src/resources/Tally/TallyResults.tsx @@ -32,7 +32,7 @@ const TallyResultsMemo: React.MemoExoticComponent> = (props: TallyResultsProps): React.JSX.Element => { const {tally, resultsEventId, onCreateTransmissionPackage, loading} = props - const {t} = useTranslation() + const {t, i18n} = useTranslation() const [value, setValue] = React.useState(0) const [electionsData, setElectionsData] = useState>([]) const [electionId, setElectionId] = useState(null) @@ -119,6 +119,10 @@ const TallyResultsMemo: React.MemoExoticComponent> = setValue(index) } + const getElectionAlias = (election: Sequent_Backend_Election) => { + return aliasRenderer(election.presentation) + } + const currentElection = useMemo(() => { return elections?.find((election) => election.id === electionId) }, [elections, electionId]) @@ -134,11 +138,17 @@ const TallyResultsMemo: React.MemoExoticComponent> = return documents ? { documents, - name: aliasRenderer(currentElection) ?? "election", + name: currentElection ? getElectionAlias(currentElection) : "election", class_type: "election", } : null - }, [resultsEventId, resultsElection, resultsElection?.[0]?.id, currentElection]) + }, [ + resultsEventId, + resultsElection, + resultsElection?.[0]?.id, + currentElection, + i18n.language, + ]) let areasDocuments: IResultDocumentsData[] | null = useMemo( () => @@ -213,7 +223,7 @@ const TallyResultsMemo: React.MemoExoticComponent> = {electionsData?.map((election, index) => ( tabClicked(election.id, index)} /> ))} @@ -222,7 +232,11 @@ const TallyResultsMemo: React.MemoExoticComponent> = = (pr const {t} = useTranslation() const {globalSettings} = useContext(SettingsContext) const tallyData = useAtomValue(tallyQueryData) + const aliasRenderer = useAliasRenderer() const candidates: Array | undefined = useMemo( () => @@ -114,6 +116,9 @@ export const TallyResultsCandidates: React.FC = (pr flex: 1, editable: false, align: "left", + renderCell: (props: GridRenderCellParams) => { + return aliasRenderer(props.row.presentation) + }, }, { field: "cast_votes", diff --git a/packages/admin-portal/src/resources/Tally/TallyResultsContestAreas.tsx b/packages/admin-portal/src/resources/Tally/TallyResultsContestAreas.tsx index b3f65e6736f..239e2347a02 100644 --- a/packages/admin-portal/src/resources/Tally/TallyResultsContestAreas.tsx +++ b/packages/admin-portal/src/resources/Tally/TallyResultsContestAreas.tsx @@ -41,7 +41,7 @@ export const TallyResultsContestAreas: React.FC = resultsEventId, tallySessionId, } = props - const {t} = reactI18next.useTranslation() + const {t, i18n} = reactI18next.useTranslation() const [value, setValue] = React.useState(0) const [areasData, setAreasData] = useState>([]) @@ -49,6 +49,7 @@ export const TallyResultsContestAreas: React.FC = const [selectedArea, setSelectedArea] = useState(null) const {globalSettings} = useContext(SettingsContext) const tallyData = useAtomValue(tallyQueryData) + const aliasRenderer = useAliasRenderer() const {canExportCeremony} = useKeysPermissions() @@ -123,6 +124,8 @@ export const TallyResultsContestAreas: React.FC = setSelectedArea(null) } + const contestName = contest ? aliasRenderer(contest) : "contest" + let documents: IResultDocumentsData | null = useMemo(() => { const documents = !!contestId && @@ -134,7 +137,7 @@ export const TallyResultsContestAreas: React.FC = return documents ? { documents, - name: contest?.name ?? "contest", + name: contestName, class_type: "contest-area", } : null @@ -144,11 +147,10 @@ export const TallyResultsContestAreas: React.FC = resultsContests, resultsContests?.[0]?.contest_id, resultsContests?.[0]?.area_id, - contest?.name, + contestName, + i18n.language, ]) - const aliasRenderer = useAliasRenderer() - return ( <> = ) : null} diff --git a/packages/admin-portal/src/resources/Tally/TallyResultsContests.tsx b/packages/admin-portal/src/resources/Tally/TallyResultsContests.tsx index 0d1424103b1..1fbbdf2bdf9 100644 --- a/packages/admin-portal/src/resources/Tally/TallyResultsContests.tsx +++ b/packages/admin-portal/src/resources/Tally/TallyResultsContests.tsx @@ -33,7 +33,8 @@ export const TallyResultsContest: React.FC = (props) = const [contestId, setContestId] = useState() const {globalSettings} = useContext(SettingsContext) - const {t} = reactI18next.useTranslation() + const {t, i18n} = reactI18next.useTranslation() + const aliasRenderer = useAliasRenderer() const [electionData, setElectionData] = useState(null) const [electionEventData, setElectionEventData] = useState(null) const [tenantData, setTenantData] = useState(null) @@ -139,11 +140,14 @@ export const TallyResultsContest: React.FC = (props) = } } - let contestName: string | undefined = useMemo( - () => - (contestId && contests?.find((contest) => contest.id === contestId)?.name) || undefined, - [contestId, contests] - ) + const contestName = useMemo(() => { + if (!contestId || !contests) return undefined + + const contest = contests.find((c) => c.id === contestId) + if (!contest?.presentation) return undefined + + return aliasRenderer(contest.presentation) + }, [contestId, contests, i18n.language]) let documents: IResultDocumentsData | null = useMemo(() => { const documents = @@ -159,7 +163,6 @@ export const TallyResultsContest: React.FC = (props) = } : null }, [contestId, resultsContests, resultsContests?.[0]?.documents, contestName]) - const aliasRenderer = useAliasRenderer() return ( <> diff --git a/packages/admin-portal/src/resources/Tally/TallyResultsGlobalCandidates.tsx b/packages/admin-portal/src/resources/Tally/TallyResultsGlobalCandidates.tsx index 283065b1bf4..75e159b3324 100644 --- a/packages/admin-portal/src/resources/Tally/TallyResultsGlobalCandidates.tsx +++ b/packages/admin-portal/src/resources/Tally/TallyResultsGlobalCandidates.tsx @@ -28,6 +28,7 @@ import {Sequent_Backend_Candidate_Extended} from "./types" import {formatPercentOne, isNumber} from "@sequentech/ui-core" import {useAtomValue} from "jotai" import {tallyQueryData} from "@/atoms/tally-candidates" +import {useAliasRenderer} from "@/hooks/useAliasRenderer" interface TallyResultsGlobalCandidatesProps { contestId: string @@ -55,6 +56,7 @@ export const TallyResultsGlobalCandidates: React.FC>([]) @@ -116,6 +118,9 @@ export const TallyResultsGlobalCandidates: React.FC) => { + return aliasRenderer(props.row.presentation) + }, }, { field: "cast_votes", diff --git a/packages/admin-portal/src/resources/TallySheet/EditTallySheet.tsx b/packages/admin-portal/src/resources/TallySheet/EditTallySheet.tsx index eaf09eeae44..2a9bf5dd1e8 100644 --- a/packages/admin-portal/src/resources/TallySheet/EditTallySheet.tsx +++ b/packages/admin-portal/src/resources/TallySheet/EditTallySheet.tsx @@ -35,6 +35,7 @@ import { EEnableCheckableLists, ICandidatePresentation, IContestPresentation, + translateFromPresentation, } from "@sequentech/ui-core" import {filterCandidateByCheckableLists} from "@/services/CandidatesFilter" import {uniq} from "lodash" @@ -68,7 +69,7 @@ const numbers = /^[0-9]+$/ export const EditTallySheet: React.FC = (props) => { const {tallySheet, contest, doCreatedTalySheet, submitRef} = props - const {t} = useTranslation() + const {t, i18n} = useTranslation() const [areasList, setAreasList] = useState([]) const [channel, setChannel] = React.useState(null) @@ -210,7 +211,11 @@ export const EditTallySheet: React.FC = (props) => { } const candidateTemp: ICandidateResultsExtended = { candidate_id: candidate.id, - name: candidate.name as string, + name: translateFromPresentation( + candidate, + "name", + i18n.language + ) as string, } if (contentTemp.candidate_results[candidate.id]) { candidateTemp.total_votes = @@ -265,7 +270,7 @@ export const EditTallySheet: React.FC = (props) => { } const candidateTemp: ICandidateResultsExtended = { candidate_id: candidate.id, - name: candidate.name as string, + name: translateFromPresentation(candidate, "name", i18n.language) as string, } candidatesTemp.push(candidateTemp) } diff --git a/packages/admin-portal/src/resources/TallySheet/ShowTallySheet.tsx b/packages/admin-portal/src/resources/TallySheet/ShowTallySheet.tsx index eca03aa1ee5..98fd0e397b1 100644 --- a/packages/admin-portal/src/resources/TallySheet/ShowTallySheet.tsx +++ b/packages/admin-portal/src/resources/TallySheet/ShowTallySheet.tsx @@ -27,7 +27,11 @@ import { } from "@mui/material" import {IAreaContestResults, ICandidateResults, IInvalidVotes} from "@/types/TallySheets" import {sortFunction} from "./utils" -import {EEnableCheckableLists, IContestPresentation} from "@sequentech/ui-core" +import { + EEnableCheckableLists, + IContestPresentation, + translateFromPresentation, +} from "@sequentech/ui-core" import {filterCandidateByCheckableLists} from "@/services/CandidatesFilter" const votingChannels = [ @@ -62,7 +66,7 @@ export const ShowTallySheet: React.FC = (props) => { const {contest, submitRef, tallySheet} = props const notify = useNotify() - const {t} = useTranslation() + const {t, i18n} = useTranslation() const [areasList, setAreasList] = useState([]) const [channel, setChannel] = React.useState(null) @@ -119,7 +123,7 @@ export const ShowTallySheet: React.FC = (props) => { } const candidateTemp: ICandidateResultsExtended = { candidate_id: candidate.id, - name: candidate.name, + name: translateFromPresentation(candidate, "name", i18n.language), total_votes: contentTemp.candidate_results?.[candidate.id]?.total_votes ?? 0, } @@ -152,7 +156,7 @@ export const ShowTallySheet: React.FC = (props) => { for (const candidate of candidates) { const candidateTemp: ICandidateResultsExtended = { candidate_id: candidate.id, - name: candidate.name, + name: translateFromPresentation(candidate, "name", i18n.language), } candidatesTemp.push(candidateTemp) } diff --git a/packages/admin-portal/src/resources/User/AuthrizedElectionsField.tsx b/packages/admin-portal/src/resources/User/AuthrizedElectionsField.tsx new file mode 100644 index 00000000000..3d4dc8a4083 --- /dev/null +++ b/packages/admin-portal/src/resources/User/AuthrizedElectionsField.tsx @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: 2025 Sequent Tech Inc +// +// SPDX-License-Identifier: AGPL-3.0-only +import React from "react" +import {useRecordContext, useGetList} from "react-admin" +import {Chip, Stack} from "@mui/material" +import {useAliasRenderer} from "@/hooks/useAliasRenderer" +import {useTranslation} from "react-i18next" +import {AUTHORIZED_ELECTION_IDS} from "./ListUsers" + +export const AuthorizedElectionsField = ({electionEventId}: {electionEventId?: string}) => { + const {t} = useTranslation() + const record = useRecordContext() + + const aliasRenderer = useAliasRenderer() + const ids: string[] = record?.attributes?.[AUTHORIZED_ELECTION_IDS] ?? [] + + const enabled = !!electionEventId && ids.length > 0 + + const {data} = useGetList( + "sequent_backend_election", + { + filter: { + election_event_id: electionEventId, + alias: { + format: "hasura-raw-query", + value: {_in: ids}, + }, + }, + pagination: {page: 1, perPage: 500}, + sort: {field: "id", order: "DESC"}, + }, + {enabled} + ) + + if (!enabled) return null + + return ( + + {data?.map((e: any) => ( + + ))} + + ) +} diff --git a/packages/admin-portal/src/resources/User/EditUserForm.tsx b/packages/admin-portal/src/resources/User/EditUserForm.tsx index 07f7ce2e93f..d7bbbbdf473 100644 --- a/packages/admin-portal/src/resources/User/EditUserForm.tsx +++ b/packages/admin-portal/src/resources/User/EditUserForm.tsx @@ -65,6 +65,7 @@ import {useUsersPermissions} from "./useUsersPermissions" import debounce from "lodash/debounce" import {CustomAutocompleteArrayInput} from "@sequentech/ui-essentials" import {useCustomNotify} from "@/hooks/useCustomNotify" +import {AUTHORIZED_ELECTION_IDS} from "./ListUsers" interface ListUserRolesProps { userId?: string @@ -569,6 +570,14 @@ export const EditUserForm: React.FC = ({ return {"name@_ilike,alias@_ilike": searched.current} } + const formattedElections = electionsList?.map((e) => { + return { + external_id: e.alias ?? "", + id: e.id, + name: aliasRenderer(e), + } + }) + const renderFormField = useCallback( (attr: UserProfileAttribute, index: number) => { if (attr.name) { @@ -740,7 +749,7 @@ export const EditUserForm: React.FC = ({ /> ) - } else if (attr.name.toLowerCase().includes("authorized-election-ids")) { + } else if (attr.name.toLowerCase().includes(AUTHORIZED_ELECTION_IDS)) { return ( <> = ({ label={getTranslationLabel(attr.name, attr.display_name, t)} className="elections-selector" fullWidth - choices={electionsList || []} - source="attributes.authorized-election-ids" - optionValue="alias" - optionText={aliasRenderer} + choices={formattedElections || []} + source={`attributes.${AUTHORIZED_ELECTION_IDS}`} + optionValue={"external_id"} + optionText={"name"} onChange={handleArraySelectChange} disabled={ !( diff --git a/packages/admin-portal/src/resources/User/ListUsers.tsx b/packages/admin-portal/src/resources/User/ListUsers.tsx index ac2b8b58a84..045bec1b331 100644 --- a/packages/admin-portal/src/resources/User/ListUsers.tsx +++ b/packages/admin-portal/src/resources/User/ListUsers.tsx @@ -90,6 +90,9 @@ import {Check, FilterAltOff} from "@mui/icons-material" import {useLocation} from "react-router-dom" import {getPreferenceKey} from "@/lib/helpers" import {isEqual} from "lodash" +import {AuthorizedElectionsField} from "./AuthrizedElectionsField" + +export const AUTHORIZED_ELECTION_IDS = "authorized-election-ids" const DataGridContainerStyle = styled(DatagridConfigurable, { shouldForwardProp: (prop) => prop !== "isOpenSideBar", // Prevent `isOpenSideBar` from being passed to the DOM @@ -890,6 +893,7 @@ export const ListUsers: React.FC = ({aside, electionEventId, ele const renderFields = (fields: UserProfileAttribute[]) => { const allFields = fields.map((attr) => { + if (attr.name === AUTHORIZED_ELECTION_IDS) return null if (attr.annotations?.inputType === "html5-date") { return ( = ({aside, electionEventId, ele } }, [isFetching, filtersChanged]) + const hasAuthorizedElectionIdsAttributes = useMemo( + () => + userAttributes?.get_user_profile_attributes.find( + (attr) => attr.name === AUTHORIZED_ELECTION_IDS + ), + [userAttributes?.get_user_profile_attributes] + ) + if (isLoading || (isFetching && filtersChanged)) { return } @@ -1051,6 +1063,16 @@ export const ListUsers: React.FC = ({aside, electionEventId, ele } /> )} + {electionEventId && hasAuthorizedElectionIdsAttributes && ( + ( + + )} + /> + )} {renderFields(listFields.attributesFields)} {electionEventId && ( election_event_id: Scalars["String"]["input"] + external_id: Scalars["String"]["input"] name: Scalars["String"]["input"] presentation?: InputMaybe } diff --git a/packages/harvest/src/routes/elections.rs b/packages/harvest/src/routes/elections.rs index 2419296d2d1..d5d5f4a8749 100644 --- a/packages/harvest/src/routes/elections.rs +++ b/packages/harvest/src/routes/elections.rs @@ -22,6 +22,7 @@ use windmill::services::import::import_election_event::upsert_b3_and_elog; pub struct CreateElectionInput { election_event_id: String, name: String, + external_id: String, presentation: ElectionPresentation, description: Option, } @@ -60,6 +61,7 @@ pub async fn create_election( &claims.hasura_claims.tenant_id, &body.election_event_id, &body.name, + &body.external_id, &body.presentation, body.description.clone(), ) diff --git a/packages/sequent-core/src/ballot_style.rs b/packages/sequent-core/src/ballot_style.rs index f9f9df59223..f938cfbd3db 100644 --- a/packages/sequent-core/src/ballot_style.rs +++ b/packages/sequent-core/src/ballot_style.rs @@ -9,6 +9,7 @@ use crate::ballot::{ }; use crate::serialization::deserialize_with_path::deserialize_value; +use crate::services::translations::{Alias, Name}; use crate::types::hasura::core as hasura_types; use anyhow::{anyhow, Context, Result}; use std::collections::HashMap; @@ -88,6 +89,8 @@ pub fn create_ballot_style( .map_err(|err| anyhow!("Error parsing election annotations {:?}", err))? .unwrap_or_default(); + let default_language = election.get_default_language(); + let contests: Vec = sorted_contests .into_iter() .map(|contest| { @@ -97,7 +100,11 @@ pub fn create_ballot_style( .filter(|c| c.contest_id == Some(contest.id.clone())) .collect::>(); - create_contest(contest, election_candidates) + create_contest( + contest, + election_candidates, + default_language.clone(), + ) }) .collect::>>()?; @@ -132,6 +139,7 @@ pub fn create_ballot_style( fn create_contest( contest: hasura_types::Contest, candidates: Vec, + default_language: String, ) -> Result { let mut sorted_candidates = candidates.clone(); sorted_candidates.sort_by_key(|k| k.id.clone()); @@ -163,17 +171,25 @@ fn create_contest( let alias_i18n = parse_i18n_field(&candidate_presentation.i18n, "alias"); + let candidate_name = name_i18n + .as_ref() + .and_then(|i18n| i18n.get(&default_language)) + .and_then(|name| name.clone()); + let candidate_alias = alias_i18n + .as_ref() + .and_then(|i18n| i18n.get(&default_language)) + .and_then(|alias| alias.clone()); Ok(ballot::Candidate { id: candidate.id.clone(), tenant_id: (candidate.tenant_id.clone()), election_event_id: (candidate.election_event_id.clone()), election_id: (contest.election_id.clone()), contest_id: (contest.id.clone()), - name: candidate.name.clone(), + name: candidate_name.clone(), name_i18n, description: candidate.description.clone(), description_i18n, - alias: candidate.alias.clone(), + alias: candidate_alias.clone(), alias_i18n: alias_i18n, candidate_type: candidate.r#type.clone(), presentation: Some(candidate_presentation), @@ -186,16 +202,25 @@ fn create_contest( }) .collect::>>()?; + let contest_name = name_i18n + .as_ref() + .and_then(|i18n| i18n.get(&default_language)) + .and_then(|name| name.clone()); + let contest_alias = alias_i18n + .as_ref() + .and_then(|i18n| i18n.get(&default_language)) + .and_then(|alias| alias.clone()); + Ok(ballot::Contest { id: contest.id.clone(), tenant_id: (contest.tenant_id), election_event_id: (contest.election_event_id), election_id: (contest.election_id.clone()), - name: contest.name, + name: contest_name, name_i18n, description: contest.description, description_i18n, - alias: contest.alias.clone(), + alias: contest_alias.clone(), alias_i18n, max_votes: (contest.max_votes.unwrap_or(0)), min_votes: (contest.min_votes.unwrap_or(0)), diff --git a/packages/sequent-core/src/services/translations.rs b/packages/sequent-core/src/services/translations.rs index 3b82b2bc6f2..2e34b0ddff0 100644 --- a/packages/sequent-core/src/services/translations.rs +++ b/packages/sequent-core/src/services/translations.rs @@ -13,83 +13,110 @@ use crate::{ pub const DEFAULT_LANG: &str = "en"; +fn parse_presentation

(presentation: &Option) -> Option

+where + P: for<'de> serde::Deserialize<'de>, +{ + let val = presentation.as_ref()?; + deserialize_value::

(val.clone()).ok() +} + +fn i18n_field( + i18n: &Option>>>, + language: &str, + field: &'static str, +) -> Option { + let i18n = i18n.as_ref()?; + + // Try requested language first, then default language. + i18n.get(language) + .and_then(|m| m.get(field)) + .cloned() + .flatten() + .or_else(|| { + i18n.get(DEFAULT_LANG) + .and_then(|m| m.get(field)) + .cloned() + .flatten() + }) +} + +pub trait Name { + fn get_name(&self, language: &str) -> String; +} + +pub trait Alias { + fn get_alias(&self, language: &str) -> String; +} + +/* ---------------------- ElectionEvent ---------------------- */ + impl ElectionEvent { /// Get the default language at Election Event level that´s configurable on /// the Admin portal pub fn get_default_language(&self) -> String { - let Some(presentation_val) = self.presentation.clone() else { - return DEFAULT_LANG.into(); - }; - let Ok(presentation) = - deserialize_value::(presentation_val) - else { - return DEFAULT_LANG.into(); - }; - let language_conf = presentation.language_conf.unwrap_or_default(); - let lang = language_conf - .default_language_code - .unwrap_or(DEFAULT_LANG.into()); - lang + parse_presentation::(&self.presentation) + .and_then(|p| p.language_conf) + .and_then(|lc| lc.default_language_code) + .unwrap_or_else(|| DEFAULT_LANG.into()) } pub fn get_contest_encryption_policy(&self) -> ContestEncryptionPolicy { - let Some(presentation_val) = self.presentation.clone() else { - return ContestEncryptionPolicy::default(); - }; - let Ok(presentation) = - deserialize_value::(presentation_val) - else { - return ContestEncryptionPolicy::default(); - }; - presentation.contest_encryption_policy.unwrap_or_default() + parse_presentation::(&self.presentation) + .and_then(|p| p.contest_encryption_policy) + .unwrap_or_default() } } -pub trait Name { - fn get_name(&self, default_language: &str) -> String; +impl Name for ElectionEvent { + fn get_name(&self, language: &str) -> String { + parse_presentation::(&self.presentation) + .and_then(|p| i18n_field(&p.i18n, language, "name")) + .unwrap_or_else(|| "-".into()) + } } -fn get_name_from_i18n( - i18n_ref: &Option>>>, - language: &str, -) -> Option { - let Some(i18n) = i18n_ref.clone() else { - return None; - }; - - let lang_name = if let Some(lang_i18n) = i18n.get(language) { - let alias = lang_i18n.get("alias").cloned().flatten(); - let name = lang_i18n.get("name").cloned().flatten(); - alias.or(name) - } else { - None - }; - let default_lang_name = if let Some(def_lang_i18n) = i18n.get(DEFAULT_LANG) - { - let alias = def_lang_i18n.get("alias").cloned().flatten(); - let name = def_lang_i18n.get("name").cloned().flatten(); - alias.or(name) - } else { - None - }; - lang_name.or(default_lang_name) +impl Alias for ElectionEvent { + fn get_alias(&self, language: &str) -> String { + let base = self.get_name(language); + + parse_presentation::(&self.presentation) + .and_then(|p| i18n_field(&p.i18n, language, "alias")) + .unwrap_or_else(|| base) + } +} + +/* ------------------------- Election ------------------------- */ + +impl Election { + pub fn get_default_language(&self) -> String { + parse_presentation::(&self.presentation) + .and_then(|p| p.language_conf) + .and_then(|lc| lc.default_language_code) + .unwrap_or_else(|| DEFAULT_LANG.into()) + } } impl Name for Election { fn get_name(&self, language: &str) -> String { - let base_name = self.name.clone(); - let Some(presentation_val) = self.presentation.clone() else { - return base_name; - }; - let Ok(presentation) = - deserialize_value::(presentation_val) - else { - return base_name; - }; - get_name_from_i18n(&presentation.i18n, language).unwrap_or(base_name) + parse_presentation::(&self.presentation) + .and_then(|p| i18n_field(&p.i18n, language, "name")) + .unwrap_or_else(|| "-".into()) } } +impl Alias for Election { + fn get_alias(&self, language: &str) -> String { + let base = self.get_name(language); + + parse_presentation::(&self.presentation) + .and_then(|p| i18n_field(&p.i18n, language, "alias")) + .unwrap_or_else(|| base) + } +} + +/* ===================== Contest ===================== */ + impl Name for Contest { fn get_name(&self, language: &str) -> String { let alias = self diff --git a/packages/step-cli/README.md b/packages/step-cli/README.md index de5eb62bd71..38d1aaeee08 100644 --- a/packages/step-cli/README.md +++ b/packages/step-cli/README.md @@ -38,9 +38,10 @@ Run ```step create-election-event --name --description --description --election-event-id ``` +Run ```step create-election --name --external-id --description --election-event-id ``` - name - the election name - required* +- external-id Unique Id for the election - required* - description - the election desciption - optional* - election_event_id - The associated election event id - required* diff --git a/packages/step-cli/src/commands/create_candidate.rs b/packages/step-cli/src/commands/create_candidate.rs index 8aec6285e08..d16598798fe 100644 --- a/packages/step-cli/src/commands/create_candidate.rs +++ b/packages/step-cli/src/commands/create_candidate.rs @@ -2,9 +2,12 @@ // // SPDX-License-Identifier: AGPL-3.0-only +use std::collections::HashMap; + use crate::{types::hasura_types::*, utils::read_config::read_config}; use clap::Args; use graphql_client::{GraphQLQuery, Response}; +use sequent_core::ballot::CandidatePresentation; #[derive(Args)] #[command(about = "Create a new candidate", long_about = None)] @@ -61,6 +64,13 @@ fn create_candidate( let config = read_config()?; let client = reqwest::blocking::Client::new(); + let mut presentation = CandidatePresentation::default(); + + presentation.i18n = Some(HashMap::from([( + "en".to_string(), + HashMap::from([("name".to_string(), Some(name.to_string()))]), + )])); + let variables = insert_candidate::Variables { name: name.to_string(), description: Some(description.to_string()), @@ -68,7 +78,7 @@ fn create_candidate( contest_id: contest_id.to_string(), tenant_id: config.tenant_id.clone(), - presentation: None, + presentation: Some(serde_json::to_value(presentation)?), }; let request_body = InsertCandidate::build_query(variables); diff --git a/packages/step-cli/src/commands/create_contest.rs b/packages/step-cli/src/commands/create_contest.rs index 698345089b7..09ed4036629 100644 --- a/packages/step-cli/src/commands/create_contest.rs +++ b/packages/step-cli/src/commands/create_contest.rs @@ -2,9 +2,12 @@ // // SPDX-License-Identifier: AGPL-3.0-only +use std::collections::HashMap; + use crate::{types::hasura_types::*, utils::read_config::read_config}; use clap::Args; use graphql_client::{GraphQLQuery, Response}; +use sequent_core::ballot::ContestPresentation; #[derive(Args)] #[command(about = "Create a new contest", long_about = None)] @@ -61,6 +64,13 @@ fn create_contest( let config = read_config()?; let client = reqwest::blocking::Client::new(); + let mut presentation = ContestPresentation::default(); + + presentation.i18n = Some(HashMap::from([( + "en".to_string(), + HashMap::from([("name".to_string(), Some(name.to_string()))]), + )])); + let variables = insert_contest::Variables { name: name.to_string(), description: Some(description.to_string()), @@ -68,7 +78,7 @@ fn create_contest( election_id: election_id.to_string(), tenant_id: config.tenant_id.clone(), - presentation: None, + presentation: Some(serde_json::to_value(presentation)?), max_votes: None, min_votes: None, winning_candidates_num: None, diff --git a/packages/step-cli/src/commands/create_election.rs b/packages/step-cli/src/commands/create_election.rs index 0b8475a1380..f0dc88a7cb4 100644 --- a/packages/step-cli/src/commands/create_election.rs +++ b/packages/step-cli/src/commands/create_election.rs @@ -2,9 +2,12 @@ // // SPDX-License-Identifier: AGPL-3.0-only +use std::collections::HashMap; + use crate::{types::hasura_types::*, utils::read_config::read_config}; use clap::Args; use graphql_client::{GraphQLQuery, Response}; +use sequent_core::ballot::ElectionPresentation; #[derive(Args)] #[command(about = "Create a new election", long_about = None)] @@ -17,6 +20,9 @@ pub struct CreateElection { #[arg(long, default_value = "")] description: String, + #[arg(long)] + external_id: String, + /// Election event id - the election event to be associated with #[arg(long)] election_event_id: String, @@ -32,7 +38,12 @@ pub struct InsertElection; impl CreateElection { pub fn run(&self) { - match create_election(&self.name, &self.description, &self.election_event_id) { + match create_election( + &self.name, + &self.external_id, + &self.description, + &self.election_event_id, + ) { Ok(id) => { println!("Success! Election created successfully! ID: {}", id); } @@ -45,17 +56,27 @@ impl CreateElection { fn create_election( name: &str, + external_id: &str, description: &str, election_event_id: &str, ) -> Result> { let config = read_config()?; let client = reqwest::blocking::Client::new(); + + let mut presentation = ElectionPresentation::default(); + + presentation.i18n = Some(HashMap::from([( + "en".to_string(), + HashMap::from([("name".to_string(), Some(name.to_string()))]), + )])); + let variables = insert_election::Variables { name: name.to_string(), + external_id: external_id.to_string(), description: Some(description.to_string()), election_event_id: election_event_id.to_string(), tenant_id: config.tenant_id.clone(), - presentation: None, + presentation: Some(serde_json::to_value(presentation)?), }; let request_body = InsertElection::build_query(variables); diff --git a/packages/step-cli/src/commands/create_election_event.rs b/packages/step-cli/src/commands/create_election_event.rs index 59db8ca3745..0209a9f0dcd 100644 --- a/packages/step-cli/src/commands/create_election_event.rs +++ b/packages/step-cli/src/commands/create_election_event.rs @@ -2,10 +2,13 @@ // // SPDX-License-Identifier: AGPL-3.0-only +use std::collections::HashMap; + use crate::types::hasura_types::*; use crate::utils::read_config::read_config; use clap::Args; use graphql_client::{GraphQLQuery, Response}; +use sequent_core::ballot::ElectionEventPresentation; use serde_json::Value; #[derive(Args, Debug)] @@ -63,6 +66,13 @@ fn create_election_event( let config = read_config()?; let client = reqwest::blocking::Client::new(); + let mut presentation = ElectionEventPresentation::default(); + + presentation.i18n = Some(HashMap::from([( + "en".to_string(), + HashMap::from([("name".to_string(), Some(name.to_string()))]), + )])); + let variables = create_election_event::Variables { election_event: create_election_event::CreateElectionEventInput { tenant_id: config.tenant_id.clone(), @@ -72,7 +82,7 @@ fn create_election_event( is_archived: Some(is_archived), id: None, - presentation: None, + presentation: Some(serde_json::to_value(presentation)?), created_at: None, updated_at: None, labels: None, diff --git a/packages/step-cli/src/graphql/insert_election.graphql b/packages/step-cli/src/graphql/insert_election.graphql index 77c8b944ddb..c17ec249c4d 100644 --- a/packages/step-cli/src/graphql/insert_election.graphql +++ b/packages/step-cli/src/graphql/insert_election.graphql @@ -1,8 +1,9 @@ -mutation InsertElection($tenant_id: uuid!, $election_event_id: uuid!, $name: String!, $description: String, $presentation: jsonb) { +mutation InsertElection($tenant_id: uuid!, $election_event_id: uuid!, $name: String!,$external_id: String!, $description: String, $presentation: jsonb) { insert_sequent_backend_election(objects: { tenant_id: $tenant_id, election_event_id: $election_event_id, name: $name, + external_id: $external_id, description: $description, presentation: $presentation }) { diff --git a/packages/ui-core/src/index.tsx b/packages/ui-core/src/index.tsx index 5167fa996b2..7681a524195 100644 --- a/packages/ui-core/src/index.tsx +++ b/packages/ui-core/src/index.tsx @@ -25,7 +25,7 @@ export {isNumber, isString, isArray, isNull, isUndefined} from "./utils/typechec export {downloadBlob, downloadUrl} from "./services/downloadBlob" export {shuffle, splitList, keyBy} from "./utils/array" export {normalizeWriteInText} from "./services/normalizeWriteInText" -export {translate, translateElection} from "./services/translate" +export {translate, translateFromPresentation} from "./services/translate" export * from "./types/ElectionEventPresentation" export * from "./services/percentFormatter" export * from "./services/wasm" diff --git a/packages/ui-core/src/services/translate.ts b/packages/ui-core/src/services/translate.ts index 9ec98eaeb76..1d268aa59fd 100644 --- a/packages/ui-core/src/services/translate.ts +++ b/packages/ui-core/src/services/translate.ts @@ -21,7 +21,14 @@ export const translate = ( return input[key] as string } -export const translateElection = (object: any, key: string, lang: string): string | undefined => { +export const translateFromPresentation = ( + object: any, + key: string, + lang: string +): string | undefined => { + if (object?.["i18n"]) { + return object["i18n"][lang]?.[key] || undefined + } if (object?.["presentation"]?.["i18n"]) { return object["presentation"]["i18n"][lang]?.[key] || object[key] || undefined } else { diff --git a/packages/voting-portal/graphql.schema.json b/packages/voting-portal/graphql.schema.json index bac1e65739f..79c629b26b4 100644 --- a/packages/voting-portal/graphql.schema.json +++ b/packages/voting-portal/graphql.schema.json @@ -10215,6 +10215,22 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "external_id", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "name", "description": null, diff --git a/packages/voting-portal/src/gql/graphql.ts b/packages/voting-portal/src/gql/graphql.ts index ad5eed8d8ad..91a6650402c 100644 --- a/packages/voting-portal/src/gql/graphql.ts +++ b/packages/voting-portal/src/gql/graphql.ts @@ -1705,6 +1705,7 @@ export type Mutation_RootCreate_Ballot_ReceiptArgs = { export type Mutation_RootCreate_ElectionArgs = { description?: InputMaybe; election_event_id: Scalars['String']['input']; + external_id: Scalars['String']['input']; name: Scalars['String']['input']; presentation?: InputMaybe; }; diff --git a/packages/voting-portal/src/routes/ElectionSelectionScreen.tsx b/packages/voting-portal/src/routes/ElectionSelectionScreen.tsx index f0573c7bfdb..f8e8461d281 100644 --- a/packages/voting-portal/src/routes/ElectionSelectionScreen.tsx +++ b/packages/voting-portal/src/routes/ElectionSelectionScreen.tsx @@ -9,7 +9,7 @@ import {Dialog, IconButton, PageLimit, SelectElection, theme} from "@sequentech/ import { isString, stringToHtml, - translateElection, + translateFromPresentation, EVotingStatus, IElectionEventStatus, isUndefined, @@ -171,7 +171,7 @@ const ElectionWrapper: React.FC = ({ 0} onClickToVote={canVote() ? onClickToVote : undefined} onClickBallotLocator={handleClickBallotLocator} diff --git a/packages/voting-portal/src/routes/StartScreen.tsx b/packages/voting-portal/src/routes/StartScreen.tsx index f4249f99b61..5d04a77fc60 100644 --- a/packages/voting-portal/src/routes/StartScreen.tsx +++ b/packages/voting-portal/src/routes/StartScreen.tsx @@ -5,7 +5,7 @@ import React, {useEffect, useState} from "react" import {Box, Typography} from "@mui/material" import {useTranslation} from "react-i18next" import {Dialog, PageLimit, theme} from "@sequentech/ui-essentials" -import {IElection, stringToHtml, translateElection} from "@sequentech/ui-core" +import {IElection, stringToHtml, translateFromPresentation} from "@sequentech/ui-core" import {styled} from "@mui/material/styles" import {Link as RouterLink, useLocation, useNavigate, useParams} from "react-router-dom" import Button from "@mui/material/Button" @@ -128,11 +128,13 @@ const StartScreen: React.FC = () => { - {translateElection(election, "name", i18n.language) ?? "-"} + {translateFromPresentation(election, "name", i18n.language) ?? "-"} {election.description ? ( - {stringToHtml(translateElection(election, "description", i18n.language) ?? "-")} + {stringToHtml( + translateFromPresentation(election, "description", i18n.language) ?? "-" + )} ) : null} {t("startScreen.instructionsTitle")} diff --git a/packages/voting-portal/src/routes/SupportMaterialsScreen.tsx b/packages/voting-portal/src/routes/SupportMaterialsScreen.tsx index 4c75f4df238..39044dc9662 100644 --- a/packages/voting-portal/src/routes/SupportMaterialsScreen.tsx +++ b/packages/voting-portal/src/routes/SupportMaterialsScreen.tsx @@ -6,7 +6,7 @@ import {Box, Button, Typography} from "@mui/material" import React, {useContext, useEffect, useState} from "react" import {useTranslation} from "react-i18next" import {PageLimit, theme} from "@sequentech/ui-essentials" -import {stringToHtml, translate, translateElection} from "@sequentech/ui-core" +import {stringToHtml, translate, translateFromPresentation} from "@sequentech/ui-core" import {styled} from "@mui/material/styles" import {TenantEventType} from ".." import {useAppDispatch, useAppSelector} from "../store/hooks" @@ -131,7 +131,7 @@ const SupportMaterialsScreen: React.FC = () => { {materialsTitles && - (translateElection( + (translateFromPresentation( materialsTitles, "materialsTitle", i18n.language @@ -142,7 +142,7 @@ const SupportMaterialsScreen: React.FC = () => { {stringToHtml( materialsTitles - ? translateElection( + ? translateFromPresentation( materialsTitles, "materialsSubtitle", i18n.language diff --git a/packages/voting-portal/src/routes/VotingScreen.tsx b/packages/voting-portal/src/routes/VotingScreen.tsx index c3728f45b05..a1145b3bc3f 100644 --- a/packages/voting-portal/src/routes/VotingScreen.tsx +++ b/packages/voting-portal/src/routes/VotingScreen.tsx @@ -12,7 +12,7 @@ import { check_voting_not_allowed_next_bool, stringToHtml, isUndefined, - translateElection, + translateFromPresentation, IContest, IAuditableMultiBallot, IAuditableSingleBallot, @@ -435,7 +435,7 @@ const VotingScreen: React.FC = () => { - {translateElection(election, "name", i18n.language) ?? "-"} + {translateFromPresentation(election, "name", i18n.language) ?? "-"} { variant="body2" sx={{color: theme.palette.customGrey.main}} > - {stringToHtml(translateElection(election, "description", i18n.language) ?? "-")} + {stringToHtml( + translateFromPresentation(election, "description", i18n.language) ?? "-" + )} ) : null} diff --git a/packages/windmill/src/postgres/election.rs b/packages/windmill/src/postgres/election.rs index 6bf8d7c6eb4..7037138c9bb 100644 --- a/packages/windmill/src/postgres/election.rs +++ b/packages/windmill/src/postgres/election.rs @@ -386,6 +386,7 @@ pub async fn create_election( tenant_id: &str, election_event_id: &str, name: &str, + external_id: &str, presentation: &ElectionPresentation, description: Option, ) -> Result { @@ -432,7 +433,7 @@ pub async fn create_election( &Uuid::parse_str(&tenant_id)?, &Uuid::parse_str(&election_event_id)?, &name.to_string(), - &name.to_string(), + &external_id.to_string(), &description, &presentation_value, &voting_channels_value, diff --git a/packages/windmill/src/services/ceremonies/insert_ballots.rs b/packages/windmill/src/services/ceremonies/insert_ballots.rs index 07e224f583f..29ed2e434da 100644 --- a/packages/windmill/src/services/ceremonies/insert_ballots.rs +++ b/packages/windmill/src/services/ceremonies/insert_ballots.rs @@ -100,7 +100,7 @@ pub async fn insert_ballots_messages( get_election_event_elections(&hasura_transaction, tenant_id, election_event_id) .await? .into_iter() - .filter_map(|election| election.alias.map(|x| (election.id.clone(), x))) + .filter_map(|election| election.external_id.map(|x| (election.id.clone(), x))) .collect(); // Collect all futures for parallel execution diff --git a/packages/windmill/src/services/ceremonies/velvet_tally.rs b/packages/windmill/src/services/ceremonies/velvet_tally.rs index edb1b3bb2d5..afd6d5488b4 100644 --- a/packages/windmill/src/services/ceremonies/velvet_tally.rs +++ b/packages/windmill/src/services/ceremonies/velvet_tally.rs @@ -23,7 +23,7 @@ use sequent_core::ballot_codec::PlaintextCodec; use sequent_core::serialization::deserialize_with_path::deserialize_value; use sequent_core::services::area_tree::TreeNodeArea; use sequent_core::services::s3; -use sequent_core::services::translations::Name; +use sequent_core::services::translations::{Alias, Name}; use sequent_core::signatures::ecies_encrypt::EciesKeyPair; use sequent_core::types::ceremonies::TallyType; use sequent_core::types::hasura::core::{ @@ -244,8 +244,7 @@ pub fn create_election_configs_blocking( // TODO: Refactor to just extract some Election Config with no subitems let election_name_opt = election_opt.map(|election| election.get_name(&default_lang)); - let election_alias_otp = - election_opt.map(|election| election.alias.clone().unwrap_or("".to_string())); + let election_alias_otp = election_opt.map(|election| election.get_alias(&default_lang)); let election_description = election_opt .map(|election| election.description.clone().unwrap_or("".to_string())) diff --git a/packages/windmill/src/services/consolidation/create_transmission_package_service.rs b/packages/windmill/src/services/consolidation/create_transmission_package_service.rs index 81f8b42be5d..3404a848361 100644 --- a/packages/windmill/src/services/consolidation/create_transmission_package_service.rs +++ b/packages/windmill/src/services/consolidation/create_transmission_package_service.rs @@ -40,6 +40,7 @@ use deadpool_postgres::{Client as DbClient, Transaction}; use sequent_core::ballot::Annotations; use sequent_core::serialization::deserialize_with_path::{deserialize_str, deserialize_value}; use sequent_core::services::date::ISO8601; +use sequent_core::services::translations::Name; use sequent_core::signatures::ecies_encrypt::generate_ecies_key_pair; use sequent_core::types::ceremonies::Log; use sequent_core::types::date_time::TimeZone; @@ -399,10 +400,11 @@ pub async fn create_transmission_package_service( } else { vec![] }; + let language = election.get_default_language(); logs.push(create_transmission_package_log( &now_local, election_id, - &election.name, + &election.get_name(&language), area_id, &area_name, )); diff --git a/packages/windmill/src/services/consolidation/send_transmission_package_service.rs b/packages/windmill/src/services/consolidation/send_transmission_package_service.rs index 7c6c500e149..f496b38f51b 100644 --- a/packages/windmill/src/services/consolidation/send_transmission_package_service.rs +++ b/packages/windmill/src/services/consolidation/send_transmission_package_service.rs @@ -33,6 +33,7 @@ use anyhow::{anyhow, Context, Result}; use chrono::{Local, Utc}; use deadpool_postgres::Client as DbClient; use reqwest::multipart; +use sequent_core::services::translations::Name; use sequent_core::util::temp_path::{generate_temp_file, get_file_size}; use sequent_core::{ ballot::Annotations, @@ -411,13 +412,14 @@ pub async fn send_transmission_package_service( let second_zip_folder_path = zip_output_temp_dir.path().join(&ccs_server.tag); let second_zip_path = second_zip_folder_path.join(format!("er_{}.zip", area_annotations.station_id)); + let language = election.get_default_language(); match send_package_to_ccs_server(&second_zip_path, ccs_server, false).await { Ok(_) => { let time_now = Local::now(); let new_log = send_transmission_package_to_ccs_log( &time_now, election_id, - &election.name, + &election.get_name(&language), area_id, &area_name, &ccs_server.name, @@ -451,7 +453,7 @@ pub async fn send_transmission_package_service( let new_log = error_sending_transmission_package_to_ccs_log( &time_now, election_id, - &election.name, + &election.get_name(&language), area_id, &area_name, &ccs_server.name, @@ -490,7 +492,7 @@ pub async fn send_transmission_package_service( let new_log = send_logs_to_ccs_log( &Local::now(), election_id, - &election.name, + &election.get_name(&language), area_id, &area_name, &ccs_server.name, @@ -512,7 +514,7 @@ pub async fn send_transmission_package_service( let new_log = error_sending_logs_to_ccs_log( &Local::now(), election_id, - &election.name, + &election.get_name(&language), area_id, &area_name, &ccs_server.name, diff --git a/packages/windmill/src/services/consolidation/upload_signature_service.rs b/packages/windmill/src/services/consolidation/upload_signature_service.rs index 152d845307d..f9446a38ce9 100644 --- a/packages/windmill/src/services/consolidation/upload_signature_service.rs +++ b/packages/windmill/src/services/consolidation/upload_signature_service.rs @@ -50,6 +50,7 @@ use anyhow::{anyhow, Context, Result}; use chrono::{Local, Utc}; use deadpool_postgres::{Client as DbClient, Transaction}; use reqwest::multipart; +use sequent_core::services::translations::Name; use sequent_core::util::temp_path::{generate_temp_file, get_file_size, read_temp_file}; use sequent_core::{ ballot::Annotations, @@ -434,12 +435,13 @@ pub async fn upload_transmission_package_signature_service( new_signatures.push(server_signature.clone()); // generate zip of zips let mut new_transmission_package_data = transmission_area_election.clone(); + let language = election.get_default_language(); new_transmission_package_data .logs .push(sign_transmission_package_log( &now_local, election_id, - &election.name, + &election.get_name(&language), area_id, &area_name, &sbei_user.miru_id, diff --git a/packages/windmill/src/services/election.rs b/packages/windmill/src/services/election.rs index 3d56ca7e390..f4f3bd14b19 100644 --- a/packages/windmill/src/services/election.rs +++ b/packages/windmill/src/services/election.rs @@ -4,6 +4,8 @@ use anyhow::{anyhow, Context, Result}; use deadpool_postgres::Client as DbClient; use deadpool_postgres::Transaction; +use sequent_core::services::translations::{Alias, Name}; +use sequent_core::types::hasura::core::Election; use sequent_core::types::keycloak::{User, VotesInfo}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -11,20 +13,26 @@ use tokio_postgres::row::Row; use tracing::{info, instrument}; use uuid::Uuid; +use crate::postgres::election::get_elections; + #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] pub struct ElectionHead { pub id: String, pub name: String, pub alias: Option, + pub external_id: Option, } -impl TryFrom for ElectionHead { +impl TryFrom for ElectionHead { type Error = anyhow::Error; - fn try_from(item: Row) -> Result { + fn try_from(item: Election) -> Result { + let default_language = item.get_default_language(); + let election = item.clone(); Ok(ElectionHead { - id: item.try_get::<_, Uuid>("id")?.to_string(), - name: item.get("name"), - alias: item.get("alias"), + id: election.id.clone(), + name: election.get_name(&default_language), + alias: Some(election.get_alias(&default_language)), + external_id: election.alias, // alias column act like external_id }) } } @@ -35,28 +43,12 @@ pub async fn get_election_event_elections( tenant_id: &str, election_event_id: &str, ) -> Result> { - let tenant_uuid: uuid::Uuid = Uuid::parse_str(tenant_id) - .map_err(|err| anyhow!("Error parsing tenant_id as UUID: {}", err))?; - let election_event_uuid: uuid::Uuid = Uuid::parse_str(election_event_id) - .map_err(|err| anyhow!("Error parsing election_event_id as UUID: {}", err))?; - let elections_statement = hasura_transaction - .prepare( - r#" - SELECT - id, name, alias - FROM sequent_backend.election - WHERE - tenant_id = $1 AND - election_event_id = $2; - "#, - ) - .await - .with_context(|| "Error preparing election statement")?; - let rows: Vec = hasura_transaction - .query(&elections_statement, &[&tenant_uuid, &election_event_uuid]) - .await - .with_context(|| "Error running the election query")?; - let elections = rows + let election_event_elections = + get_elections(hasura_transaction, tenant_id, election_event_id, None) + .await + .with_context(|| "Error get election event elections")?; + + let elections = election_event_elections .into_iter() .map(|row| -> Result { row.try_into() }) .collect::>>() diff --git a/packages/windmill/src/services/reports/audit_logs.rs b/packages/windmill/src/services/reports/audit_logs.rs index 7ea9c78bf0f..60295840e7f 100644 --- a/packages/windmill/src/services/reports/audit_logs.rs +++ b/packages/windmill/src/services/reports/audit_logs.rs @@ -49,6 +49,7 @@ use sequent_core::services::date::ISO8601; use sequent_core::services::keycloak::{self, get_event_realm, get_tenant_realm}; use sequent_core::services::pdf; use sequent_core::services::s3::get_minio_url; +use sequent_core::services::translations::Name; use sequent_core::types::hasura::core::{Election, TasksExecution}; use sequent_core::util::temp_path::*; use serde::{Deserialize, Serialize}; @@ -259,8 +260,9 @@ impl AuditLogsTemplate { .await .map_err(|err| anyhow!("Error at count_ballots_by_election: {err:?}"))?; + let language = election_event.get_default_language(); Ok(UserData { - election_event_title: election_event.name.clone(), + election_event_title: election_event.get_name(&language).clone(), election_dates, geographical_region, post, diff --git a/packages/windmill/src/services/reports/ov_not_yet_pre_enrolled_list.rs b/packages/windmill/src/services/reports/ov_not_yet_pre_enrolled_list.rs index 6f481f5ef8f..fc20077313e 100644 --- a/packages/windmill/src/services/reports/ov_not_yet_pre_enrolled_list.rs +++ b/packages/windmill/src/services/reports/ov_not_yet_pre_enrolled_list.rs @@ -37,6 +37,7 @@ use sequent_core::services::keycloak::{self, get_event_realm}; use sequent_core::services::pdf; use sequent_core::services::reports; use sequent_core::services::s3::get_minio_url; +use sequent_core::services::translations::Alias; use sequent_core::types::hasura::core::Election; use sequent_core::types::hasura::core::TasksExecution; use sequent_core::types::to_map::ToMap; @@ -417,11 +418,13 @@ impl TemplateRenderer for NotPreEnrolledListTemplate { .await .map_err(|err| anyhow!("Error at get_areas_by_election_id: {err:?}"))?; + let language = election.get_default_language(); + for area in election_areas.iter() { areas.push(UserDataArea { election_id: election_id.clone(), area_id: area.id.clone(), - election_title: election.alias.clone().unwrap_or(election.name.clone()), + election_title: election.get_alias(&language), election_dates: election_dates.clone(), post: election_general_data.post.clone(), area_name: area.clone().name.unwrap_or("-".to_string()), diff --git a/packages/windmill/src/services/reports/ov_not_yet_pre_enrolled_number.rs b/packages/windmill/src/services/reports/ov_not_yet_pre_enrolled_number.rs index eca1d6f7880..bb0e473d422 100644 --- a/packages/windmill/src/services/reports/ov_not_yet_pre_enrolled_number.rs +++ b/packages/windmill/src/services/reports/ov_not_yet_pre_enrolled_number.rs @@ -26,6 +26,7 @@ use sequent_core::ballot::StringifiedPeriodDates; use sequent_core::services::keycloak::get_event_realm; use sequent_core::services::pdf; use sequent_core::services::s3::get_minio_url; +use sequent_core::services::translations::Alias; use sequent_core::types::hasura::core::Election; use sequent_core::util::temp_path::get_public_assets_path_env_var; use serde::{Deserialize, Serialize}; @@ -157,10 +158,8 @@ impl TemplateRenderer for NumOVNotPreEnrolledReport { .map_err(|e| anyhow::anyhow!("Error in get_elections: {}", e))?, }; - let election_title: String = election_event - .alias - .clone() - .unwrap_or(election_event.name.clone()); + let language = election_event.get_default_language(); + let election_title: String = election_event.get_alias(&language); let scheduled_events = find_scheduled_event_by_election_event_id( &hasura_transaction, @@ -202,7 +201,8 @@ impl TemplateRenderer for NumOVNotPreEnrolledReport { let election_dates = get_election_dates(&election, scheduled_events.clone()) .map_err(|e| anyhow::anyhow!("Error getting election dates {e}"))?; - let election_name = election.alias.clone().unwrap_or(election.name.clone()); + let language = election.get_default_language(); + let election_name = election.get_alias(&language); let election_areas = get_areas_by_election_id( &hasura_transaction, diff --git a/packages/windmill/src/services/reports/ov_pre_enrolled_approved.rs b/packages/windmill/src/services/reports/ov_pre_enrolled_approved.rs index 7d3eaf5fd48..32a662b6a82 100644 --- a/packages/windmill/src/services/reports/ov_pre_enrolled_approved.rs +++ b/packages/windmill/src/services/reports/ov_pre_enrolled_approved.rs @@ -36,6 +36,7 @@ use rayon::ThreadPoolBuilder; use sequent_core::ballot::StringifiedPeriodDates; use sequent_core::services::keycloak::{self, get_event_realm}; use sequent_core::services::s3::get_minio_url; +use sequent_core::services::translations::Alias; use sequent_core::services::{pdf, reports}; use sequent_core::types::hasura::core::Election; use sequent_core::types::hasura::core::TasksExecution; @@ -485,11 +486,13 @@ impl TemplateRenderer for PreEnrolledVoterTemplate { .await .map_err(|err| anyhow!("Error at get_areas_by_election_id: {err:?}"))?; + let language = election.get_default_language(); + for area in election_areas.iter() { areas.push(UserDataArea { election_id: election_id.clone(), area_id: area.id.clone(), - election_title: election.alias.clone().unwrap_or(election.name.clone()), + election_title: election.get_alias(&language), election_dates: election_dates.clone(), post: election_general_data.post.clone(), area_name: area.clone().name.unwrap_or("-".to_string()), diff --git a/packages/windmill/src/services/reports/ov_turnout_per_aboard_status_sex.rs b/packages/windmill/src/services/reports/ov_turnout_per_aboard_status_sex.rs index ba5e43eff67..6fecb537737 100644 --- a/packages/windmill/src/services/reports/ov_turnout_per_aboard_status_sex.rs +++ b/packages/windmill/src/services/reports/ov_turnout_per_aboard_status_sex.rs @@ -24,6 +24,7 @@ use sequent_core::ballot::StringifiedPeriodDates; use sequent_core::services::keycloak::get_event_realm; use sequent_core::services::pdf; use sequent_core::services::s3::get_minio_url; +use sequent_core::services::translations::Alias; use sequent_core::types::hasura::core::Election; use sequent_core::util::temp_path::*; use serde::{Deserialize, Serialize}; @@ -241,11 +242,11 @@ impl TemplateRenderer for OVTurnoutPerAboardAndSexReport { }) .collect(); + let language = election_event.get_default_language(); + let election_title: String = election_event.get_alias(&language); + Ok(UserData { - election_event_title: election_event - .alias - .clone() - .unwrap_or(election_event.name.clone()), + election_event_title: election_title, regions: regions, elections: elections_data, execution_annotations: ExecutionAnnotations { diff --git a/packages/windmill/src/services/reports/ov_turnout_per_aboard_status_sex_percentage.rs b/packages/windmill/src/services/reports/ov_turnout_per_aboard_status_sex_percentage.rs index bce9d814754..2878e561166 100644 --- a/packages/windmill/src/services/reports/ov_turnout_per_aboard_status_sex_percentage.rs +++ b/packages/windmill/src/services/reports/ov_turnout_per_aboard_status_sex_percentage.rs @@ -25,6 +25,7 @@ use sequent_core::ballot::StringifiedPeriodDates; use sequent_core::services::keycloak::get_event_realm; use sequent_core::services::pdf; use sequent_core::services::s3::get_minio_url; +use sequent_core::services::translations::Alias; use sequent_core::types::hasura::core::Election; use sequent_core::util::temp_path::*; use serde::{Deserialize, Serialize}; @@ -344,11 +345,11 @@ impl TemplateRenderer for OVTurnoutPerAboardAndSexPercentageReport { let regions: Vec = region_map.into_values().collect(); + let language = election_event.get_default_language(); + let election_title: String = election_event.get_alias(&language); + Ok(UserData { - election_event_title: election_event - .alias - .clone() - .unwrap_or(election_event.name.clone()), + election_event_title: election_title, regions: regions, elections: elections_data, execution_annotations: ExecutionAnnotations { diff --git a/packages/windmill/src/services/reports/ov_turnout_percentage.rs b/packages/windmill/src/services/reports/ov_turnout_percentage.rs index 8f59e483f32..2eff5084ab9 100644 --- a/packages/windmill/src/services/reports/ov_turnout_percentage.rs +++ b/packages/windmill/src/services/reports/ov_turnout_percentage.rs @@ -22,6 +22,7 @@ use sequent_core::ballot::StringifiedPeriodDates; use sequent_core::services::keycloak::get_event_realm; use sequent_core::services::pdf; use sequent_core::services::s3::get_minio_url; +use sequent_core::services::translations::Alias; use sequent_core::types::scheduled_event::generate_voting_period_dates; use sequent_core::util::temp_path::*; use serde::{Deserialize, Serialize}; @@ -308,11 +309,12 @@ impl TemplateRenderer for OVTurnoutPercentageReport { calc_percentage(overall_total_female_voted, overall_total_female_registered); let percentage_total = calc_percentage(overall_total_voted, overall_total_registered); + let language = election.get_default_language(); Ok(UserData { areas, election: UserDataElection { election_dates, - election_title: election.alias.unwrap_or(election.name).clone(), + election_title: election.get_alias(&language), post: election_general_data.post, inspectors: vec![], overall_total: UserDataStats { diff --git a/packages/windmill/src/services/reports/ov_who_voted.rs b/packages/windmill/src/services/reports/ov_who_voted.rs index c5c95dab351..137945deb08 100644 --- a/packages/windmill/src/services/reports/ov_who_voted.rs +++ b/packages/windmill/src/services/reports/ov_who_voted.rs @@ -27,6 +27,7 @@ use rayon::ThreadPoolBuilder; use sequent_core::ballot::StringifiedPeriodDates; use sequent_core::services::keycloak::get_event_realm; use sequent_core::services::s3::get_minio_url; +use sequent_core::services::translations::Alias; use sequent_core::services::{pdf, reports}; use sequent_core::types::hasura::core::Election; use sequent_core::types::hasura::core::TasksExecution; @@ -451,6 +452,9 @@ impl TemplateRenderer for OVUsersWhoVotedTemplate { ) .await .map_err(|err| anyhow!("Error at get_areas_by_election_id: {err:?}"))?; + + let language = election.get_default_language(); + for area in election_areas.iter() { // Fetch voters data for each area (using the has_voted filter) let voters_filters = FilterListVoters { @@ -478,7 +482,7 @@ impl TemplateRenderer for OVUsersWhoVotedTemplate { .map_err(|e| anyhow!("Error getting voters data: {}", e))?; let area_name = area.clone().name.unwrap_or("-".to_string()); areas.push(UserDataArea { - election_title: election.alias.clone().unwrap_or(election.name.clone()), + election_title: election.get_alias(&language), election_dates: election_dates.clone(), post: election_general_data.post.clone(), area_name, diff --git a/packages/windmill/src/services/reports/ov_with_voting_status.rs b/packages/windmill/src/services/reports/ov_with_voting_status.rs index 85486cad797..b95bb1bdcb0 100644 --- a/packages/windmill/src/services/reports/ov_with_voting_status.rs +++ b/packages/windmill/src/services/reports/ov_with_voting_status.rs @@ -33,6 +33,7 @@ use rayon::ThreadPoolBuilder; use sequent_core::ballot::StringifiedPeriodDates; use sequent_core::services::keycloak::{self, get_event_realm}; use sequent_core::services::s3::get_minio_url; +use sequent_core::services::translations::Alias; use sequent_core::services::{pdf, reports}; use sequent_core::types::hasura::core::Election; use sequent_core::types::hasura::core::TasksExecution; @@ -477,11 +478,13 @@ impl TemplateRenderer for OVWithVotingStatusTemplate { .await .map_err(|err| anyhow!("Error at get_areas_by_election_id: {err:?}"))?; + let language = election.get_default_language(); + for area in election_areas.iter() { areas.push(UserDataArea { election_id: election_id.clone(), area_id: area.id.clone(), - election_title: election.alias.clone().unwrap_or(election.name.clone()), + election_title: election.get_alias(&language), election_dates: election_dates.clone(), post: election_general_data.post.clone(), area_name: area.clone().name.unwrap_or("-".to_string()), diff --git a/packages/windmill/src/services/reports/ovcs_events.rs b/packages/windmill/src/services/reports/ovcs_events.rs index d4ffcf4cc98..9909e81bb39 100644 --- a/packages/windmill/src/services/reports/ovcs_events.rs +++ b/packages/windmill/src/services/reports/ovcs_events.rs @@ -26,6 +26,7 @@ use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; use deadpool_postgres::Transaction; use sequent_core::services::pdf; +use sequent_core::services::translations::Alias; use sequent_core::types::ceremonies::TallyType; use sequent_core::util::temp_path::get_public_assets_path_env_var; use sequent_core::{ @@ -285,11 +286,11 @@ impl TemplateRenderer for OVCSEventsTemplate { .map(|(name, events)| Region { name, events }) .collect(); + let language = election_event.get_default_language(); + let election_title: String = election_event.get_alias(&language); + Ok(UserData { - election_event_title: election_event - .alias - .clone() - .unwrap_or(election_event.name.clone()), + election_event_title: election_title, execution_annotations: ExecutionAnnotations { date_printed, report_hash, diff --git a/packages/windmill/src/services/reports/ovcs_information.rs b/packages/windmill/src/services/reports/ovcs_information.rs index 0548ecc4606..b577811966f 100644 --- a/packages/windmill/src/services/reports/ovcs_information.rs +++ b/packages/windmill/src/services/reports/ovcs_information.rs @@ -21,6 +21,7 @@ use sequent_core::ballot::StringifiedPeriodDates; use sequent_core::services::keycloak::get_event_realm; use sequent_core::services::pdf; use sequent_core::services::s3::get_minio_url; +use sequent_core::services::translations::Alias; use sequent_core::util::temp_path::*; use serde::{Deserialize, Serialize}; use tracing::{info, instrument}; @@ -159,7 +160,8 @@ impl TemplateRenderer for OVCSInformationTemplate { .map_err(|err| anyhow!("Error at get_areas_by_election_id: {err:?}"))?; let date_printed = get_date_and_time(); - let election_title = election_event.name.clone(); + let language = election_event.get_default_language(); + let election_title: String = election_event.get_alias(&language); // Fetch election's voting periods let scheduled_events = find_scheduled_event_by_election_event_id( diff --git a/packages/windmill/src/services/reports/ovcs_statistics.rs b/packages/windmill/src/services/reports/ovcs_statistics.rs index 6e5a45a2cb4..e86b341a526 100644 --- a/packages/windmill/src/services/reports/ovcs_statistics.rs +++ b/packages/windmill/src/services/reports/ovcs_statistics.rs @@ -27,6 +27,7 @@ use sequent_core::ballot::StringifiedPeriodDates; use sequent_core::services::keycloak::get_event_realm; use sequent_core::services::pdf; use sequent_core::services::s3::get_minio_url; +use sequent_core::services::translations::Alias; use sequent_core::types::hasura::core::Election; use sequent_core::util::temp_path::*; use serde::{Deserialize, Serialize}; @@ -289,9 +290,11 @@ impl TemplateRenderer for OVCSStatisticsTemplate { .or_insert_with(Vec::new) .push(area_stat); } + + let language = election.get_default_language(); elections_data.push(UserElectionData { election_dates, - election_title: election.alias.unwrap_or(election.name).clone(), + election_title: election.get_alias(&language), }); } @@ -311,8 +314,11 @@ impl TemplateRenderer for OVCSStatisticsTemplate { .await .map_err(|err| anyhow!("Error at counting all disapproved applications: {err}"))?; + let language = election_event.get_default_language(); + let election_title: String = election_event.get_alias(&language); + Ok(UserData { - election_event_title: election_event.alias.unwrap_or(election_event.name).clone(), + election_event_title: election_title, execution_annotations: ExecutionAnnotations { date_printed, report_hash, diff --git a/packages/windmill/src/services/reports/overseas_voters.rs b/packages/windmill/src/services/reports/overseas_voters.rs index 3ed540e5bc8..85139e0fa31 100644 --- a/packages/windmill/src/services/reports/overseas_voters.rs +++ b/packages/windmill/src/services/reports/overseas_voters.rs @@ -26,20 +26,20 @@ use rayon::ThreadPoolBuilder; use sequent_core::ballot::StringifiedPeriodDates; use sequent_core::services::keycloak::{self, get_event_realm}; use sequent_core::services::s3::get_minio_url; +use sequent_core::services::translations::Alias; use sequent_core::services::{pdf, reports}; use sequent_core::types::hasura::core::TasksExecution; -use sequent_core::util::temp_path::*; -use serde::{Deserialize, Serialize}; -use tracing::{debug, info, instrument}; - use sequent_core::types::templates::ReportExtraConfig; use sequent_core::types::to_map::ToMap; +use sequent_core::util::temp_path::*; +use serde::{Deserialize, Serialize}; use std::fmt::Debug; use std::fs; use std::path::Path; use tempfile::tempdir; use tempfile::{NamedTempFile, TempPath}; use tokio::runtime::Runtime; +use tracing::{debug, info, instrument}; use crate::services::celery_app::get_worker_threads; use crate::services::consolidation::aes_256_cbc_encrypt::encrypt_file_aes_256_cbc; @@ -491,6 +491,8 @@ impl TemplateRenderer for OverseasVotersReport { .await .map_err(|err| anyhow!("Error at get_areas_by_election_id: {err:?}"))?; + let language = election.get_default_language(); + for area in election_areas.iter() { let area_general_data = extract_area_data(&area, election_event_annotations.sbei_users.clone()) @@ -500,7 +502,7 @@ impl TemplateRenderer for OverseasVotersReport { areas.push(UserDataArea { election_id: election_id.clone(), area_id: area.id.clone(), - election_title: election.alias.clone().unwrap_or(election.name.clone()), + election_title: election.get_alias(&language), election_dates: election_dates.clone(), station_name: election_general_data.precinct_code.clone(), station_id: election_general_data.pollcenter_code.clone(), diff --git a/packages/windmill/src/services/reports/pre_enrolled_ov_but_disapproved.rs b/packages/windmill/src/services/reports/pre_enrolled_ov_but_disapproved.rs index 2d45b601494..4532a5e5776 100644 --- a/packages/windmill/src/services/reports/pre_enrolled_ov_but_disapproved.rs +++ b/packages/windmill/src/services/reports/pre_enrolled_ov_but_disapproved.rs @@ -22,6 +22,7 @@ use futures::executor::block_on; use sequent_core::ballot::StringifiedPeriodDates; use sequent_core::services::keycloak::{self, get_event_realm}; use sequent_core::services::s3::get_minio_url; +use sequent_core::services::translations::Alias; use sequent_core::services::{pdf, reports}; use sequent_core::types::hasura::core::Election; use sequent_core::types::hasura::core::TasksExecution; @@ -469,11 +470,13 @@ impl TemplateRenderer for PreEnrolledDisapprovedTemplate { .await .map_err(|err| anyhow!("Error at get_areas_by_election_id: {err:?}"))?; + let language = election.get_default_language(); + for area in election_areas.iter() { areas.push(UserDataArea { election_id: election_id.clone(), area_id: area.id.clone(), - election_title: election.alias.clone().unwrap_or(election.name.clone()), + election_title: election.get_alias(&language), election_dates: election_dates.clone(), post: election_general_data.post.clone(), area_name: area.clone().name.unwrap_or("-".to_string()), diff --git a/packages/windmill/src/services/reports/pre_enrolled_ov_subject_to_manual_validation.rs b/packages/windmill/src/services/reports/pre_enrolled_ov_subject_to_manual_validation.rs index 530eb7fcb76..f04641a94e7 100644 --- a/packages/windmill/src/services/reports/pre_enrolled_ov_subject_to_manual_validation.rs +++ b/packages/windmill/src/services/reports/pre_enrolled_ov_subject_to_manual_validation.rs @@ -33,6 +33,7 @@ use rayon::ThreadPoolBuilder; use sequent_core::ballot::StringifiedPeriodDates; use sequent_core::services::keycloak::{self, get_event_realm}; use sequent_core::services::s3::get_minio_url; +use sequent_core::services::translations::Alias; use sequent_core::services::{pdf, reports}; use sequent_core::types::hasura::core::Election; use sequent_core::types::hasura::core::TasksExecution; @@ -463,11 +464,13 @@ impl TemplateRenderer for PreEnrolledManualUsersTemplate { .await .map_err(|err| anyhow!("Error at get_areas_by_election_id: {err:?}"))?; + let language = election.get_default_language(); + for area in election_areas.iter() { areas.push(UserDataArea { election_id: election_id.clone(), area_id: area.id.clone(), - election_title: election.alias.clone().unwrap_or(election.name.clone()), + election_title: election.get_alias(&language), election_dates: election_dates.clone(), post: election_general_data.post.clone(), area_name: area.clone().name.unwrap_or("-".to_string()), diff --git a/packages/windmill/src/services/reports/report_variables.rs b/packages/windmill/src/services/reports/report_variables.rs index 4bc4c74257e..547720e583f 100644 --- a/packages/windmill/src/services/reports/report_variables.rs +++ b/packages/windmill/src/services/reports/report_variables.rs @@ -20,6 +20,7 @@ use crate::types::miru_plugin::MiruSbeiUser; use anyhow::{anyhow, Context, Result}; use deadpool_postgres::Transaction; use sequent_core::ballot::StringifiedPeriodDates; +use sequent_core::services::translations::Alias; use sequent_core::types::hasura::core::{Area, Election, ElectionEvent}; use sequent_core::types::keycloak::AREA_ID_ATTR_NAME; use sequent_core::types::scheduled_event::ScheduledEvent; @@ -448,9 +449,10 @@ pub async fn process_elections( let election_dates = get_election_dates(&election, scheduled_events.clone()) .map_err(|e| anyhow::anyhow!("Error getting election dates {e}"))?; + let language = election.get_default_language(); elections_data.push(UserDataElection { election_dates, - election_name: election.alias.unwrap_or(election.name), + election_name: election.get_alias(&language), election_annotations: election_general_data, }); } diff --git a/packages/windmill/src/services/reports/status.rs b/packages/windmill/src/services/reports/status.rs index 5abc0b54b44..b435686d158 100644 --- a/packages/windmill/src/services/reports/status.rs +++ b/packages/windmill/src/services/reports/status.rs @@ -24,6 +24,7 @@ use sequent_core::serialization::deserialize_with_path::deserialize_value; use sequent_core::services::keycloak::get_event_realm; use sequent_core::services::pdf; use sequent_core::services::s3::get_minio_url; +use sequent_core::services::translations::Name; use sequent_core::util::temp_path::*; use sequent_core::{ballot::ElectionStatus, ballot::VotingStatus}; use serde::{Deserialize, Serialize}; @@ -193,7 +194,9 @@ impl TemplateRenderer for StatusTemplate { let election_dates = get_election_dates(&election, scheduled_events) .map_err(|e| anyhow::anyhow!("Error getting election dates {e}"))?; let date_printed = get_date_and_time(); - let election_title = election_event.name.clone(); + + let language = election_event.get_default_language(); + let election_title: String = election_event.get_name(&language); let app_hash = get_app_hash(); let app_version = get_app_version(); diff --git a/packages/windmill/src/services/reports/transmission_report.rs b/packages/windmill/src/services/reports/transmission_report.rs index da7b10e783e..eb6618b485a 100644 --- a/packages/windmill/src/services/reports/transmission_report.rs +++ b/packages/windmill/src/services/reports/transmission_report.rs @@ -26,6 +26,7 @@ use sequent_core::ballot::StringifiedPeriodDates; use sequent_core::services::keycloak::get_event_realm; use sequent_core::services::pdf; use sequent_core::services::s3::get_minio_url; +use sequent_core::services::translations::Name; use sequent_core::types::scheduled_event::generate_voting_period_dates; use sequent_core::util::temp_path::*; use serde::{Deserialize, Serialize}; @@ -168,7 +169,8 @@ impl TemplateRenderer for TransmissionReport { .await?; let date_printed = get_date_and_time(); - let election_title = election_event.name.clone(); + let language = election_event.get_default_language(); + let election_title: String = election_event.get_name(&language); // get election instace let election = match get_election_by_id( diff --git a/packages/windmill/src/tasks/import_candidates.rs b/packages/windmill/src/tasks/import_candidates.rs index 8f9e4431c09..b3e4cee6398 100644 --- a/packages/windmill/src/tasks/import_candidates.rs +++ b/packages/windmill/src/tasks/import_candidates.rs @@ -4,6 +4,7 @@ use crate::postgres::candidate::insert_candidates; use crate::postgres::contest::export_contests; +use crate::postgres::election_event::get_election_event_by_id; use crate::services::tasks_execution::*; use crate::{ postgres::document::get_document, @@ -13,11 +14,12 @@ use anyhow::{anyhow, Context, Result}; use deadpool_postgres::Client as DbClient; use encoding_rs::WINDOWS_1252; use encoding_rs_io::DecodeReaderBytesBuilder; -use sequent_core::ballot::ContestPresentation; +use sequent_core::ballot::{CandidatePresentation, ContestPresentation}; use sequent_core::serialization::deserialize_with_path::deserialize_value; use sequent_core::types::hasura::core::Contest; use sequent_core::types::hasura::core::{Candidate, TasksExecution}; use sequent_core::util::integrity_check::{integrity_check, HashFileVerifyError}; +use std::collections::HashMap; use std::io::BufReader; use std::io::Seek; use tracing::{event, info, instrument, Level}; @@ -260,11 +262,6 @@ fn get_contest_from_postcode(contests: &Vec, postcode: &str) -> Result< if let Some(&contest_name) = contest_map.get(postcode) { // Find the contest with the matching alias for contest in contests { - if let Some(alias) = contest.alias.clone() { - if alias == contest_name.to_string() { - return Ok(Some(contest.id.clone())); - } - } if let Some(presentation) = contest.presentation.clone() { let contest_presentation: ContestPresentation = deserialize_value(presentation)?; if let Some(i18n) = contest_presentation.i18n.clone() { @@ -375,6 +372,10 @@ pub async fn import_candidates_task( .has_headers(false) .from_reader(transcoded_reader); + let election_event = + get_election_event_by_id(&hasura_transaction, &tenant_id, &election_event_id).await?; + let default_lang = election_event.get_default_language(); + let mut candidates: Vec = vec![]; for result in rdr.records() { match result.with_context(|| "Error reading CSV record") { @@ -389,6 +390,23 @@ pub async fn import_candidates_task( let Some(contest_id) = contest_id_opt else { continue; }; + + let mut presentation = CandidatePresentation::new(); + + let lang = default_lang.as_str(); + + presentation + .i18n + .get_or_insert_with(HashMap::new) + .entry(lang.to_string()) + .or_insert_with(HashMap::new) + .insert( + "name".to_string(), + Some(format!("{name_on_ballot} ({ext})")), + ); + + let presentation_json = serde_json::to_value(&presentation)?; + let candidate = Candidate { id: Uuid::new_v4().to_string(), tenant_id: tenant_id.clone(), @@ -402,7 +420,7 @@ pub async fn import_candidates_task( alias: None, description: None, r#type: None, - presentation: None, + presentation: Some(presentation_json), is_public: Some(true), image_document_id: None, }; diff --git a/packages/windmill/src/tasks/send_template.rs b/packages/windmill/src/tasks/send_template.rs index 003a71beb52..e133a0bfdb6 100644 --- a/packages/windmill/src/tasks/send_template.rs +++ b/packages/windmill/src/tasks/send_template.rs @@ -4,17 +4,17 @@ use crate::postgres::area::get_elections_by_area; use crate::postgres::election_event::get_election_event_by_id_if_exist; use crate::services::celery_app::get_celery_app; +use crate::services::database::{get_hasura_pool, get_keycloak_pool, PgConfig}; use crate::services::election_event_board::get_election_event_board; use crate::services::election_event_statistics::update_election_event_statistics; use crate::services::election_statistics::update_election_statistics; use crate::services::electoral_log::ElectoralLog; use crate::services::providers::{email_sender::EmailSender, sms_sender::SmsSender}; use crate::services::users::{list_users, list_users_with_vote_info, ListUsersFilter}; -use crate::types::error::Result; - -use crate::services::database::{get_hasura_pool, get_keycloak_pool, PgConfig}; use crate::types::error::Error; +use crate::types::error::Result; use deadpool_postgres::{Client as DbClient, Transaction}; +use sequent_core::services::translations::Name; use anyhow::{anyhow, Context}; use aws_sdk_sesv2::types::{Body, Content, Destination, EmailContent, Message as AwsMessage}; @@ -61,11 +61,12 @@ fn get_variables( ); variables.insert("tenant_id".to_string(), json!(tenant_id.clone())); if let Some(ref election_event) = election_event { + let language = election_event.get_default_language(); variables.insert( "election_event".to_string(), json!({ "id": election_event.id.clone(), - "name": election_event.name.clone(), + "name": election_event.get_name(&language).clone(), }), ); variables.insert(