Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

1899: Show freinet information #1926

Draft
wants to merge 1 commit into
base: 1762-add-agency-id-to-region
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions administration/src/bp-modules/regions/RegionController.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import styled from 'styled-components'

import { WhoAmIContext } from '../../WhoAmIProvider'
import { Role } from '../../generated/graphql'
import { ProjectConfigContext } from '../../project-configs/ProjectConfigContext'
import DataPrivacyCard from './DataPrivacyCard'
import RegionSettingsController from './RegionSettingsController'
import FreinetSettingsController from './freinet/FreinetSettingsController'

const RegionSettingsContainer = styled.div`
display: flex;
Expand All @@ -18,6 +20,7 @@ const RegionSettingsContainer = styled.div`

const RegionController = (): ReactElement => {
const { region, role } = useContext(WhoAmIContext).me!
const { freinetDataTransferEnabled, projectId } = useContext(ProjectConfigContext)
const { t } = useTranslation('errors')
if (!region || role !== Role.RegionAdmin) {
return <NonIdealState icon='cross' title={t('notAuthorized')} description={t('notAuthorizedForRegionSettings')} />
Expand All @@ -26,6 +29,7 @@ const RegionController = (): ReactElement => {
<RegionSettingsContainer>
<DataPrivacyCard />
<RegionSettingsController regionId={region.id} />
{freinetDataTransferEnabled && <FreinetSettingsController regionId={region.id} project={projectId} />}
</RegionSettingsContainer>
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { Button, Checkbox, H2 } from '@blueprintjs/core'
import React, { ReactElement, useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import styled from 'styled-components'

import { FreinetAgency } from '../../../generated/graphql'
import SettingsCard from '../../user-settings/SettingsCard'

const Headline = styled(H2)`
margin-bottom: 16px;
`

const ButtonContainer = styled.div`
text-align: right;
padding: 10px 0;
`

const Label = styled.span`
font-weight: bold;
`

const InformationContainer = styled.div`
margin: 16px 0;
`

type FreinetSettingsCardProps = {
agencyInformation: FreinetAgency
onSave: (dataTransferActivated: boolean) => void
loading: boolean
}

const FreinetSettingsCard = ({ agencyInformation, onSave, loading }: FreinetSettingsCardProps): ReactElement => {
const { t } = useTranslation('regionSettings')
const { agencyId, apiAccessKey, agencyName, dataTransferActivated: dbDataTransferActivated } = agencyInformation
const [dataTransferActivated, setDataTransferActivated] = useState(dbDataTransferActivated)
return (
<SettingsCard>
<Headline>{t('freinetHeadline')}</Headline>
<p>
<Trans i18nKey='regionSettings:freinetExplanation' />
</p>
<InformationContainer>
<div>
<Label>{t('freinetAgencyName')}: </Label>
<span>{agencyName}</span>
</div>
<div>
<Label>{t('freinetAgencyId')}: </Label>
<span>{agencyId}</span>
</div>
<div>
<Label>{t('freinetAgencyAccessKey')}: </Label>
<span>{apiAccessKey}</span>
</div>
</InformationContainer>
<Checkbox
checked={dataTransferActivated}
onChange={e => setDataTransferActivated(e.currentTarget.checked)}
label={t('freinetActivateDataTransferCheckbox')}
/>

<ButtonContainer>
<Button text={t('save')} intent='primary' onClick={() => onSave(dataTransferActivated)} loading={loading} />
</ButtonContainer>
</SettingsCard>
)
}

export default FreinetSettingsCard
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React, { ReactElement } from 'react'
import { useTranslation } from 'react-i18next'

import getMessageFromApolloError from '../../../errors/getMessageFromApolloError'
import { useGetFreinetAgencyByRegionIdQuery, useUpdateDataTransferToFreinetMutation } from '../../../generated/graphql'
import { useAppToaster } from '../../AppToaster'
import getQueryResult from '../../util/getQueryResult'
import FreinetSettingsCard from './FreinetSettingsCard'

type FreinetSettingsControllerProps = {
regionId: number
project: string
}

const FreinetSettingsController = ({ regionId, project }: FreinetSettingsControllerProps): ReactElement | null => {
const appToaster = useAppToaster()
const { t } = useTranslation('regionSettings')
const freinetQuery = useGetFreinetAgencyByRegionIdQuery({
variables: { regionId, project },
})
const [updateFreinetDataTransfer, { loading }] = useUpdateDataTransferToFreinetMutation({
onError: error => {
const { title } = getMessageFromApolloError(error, t)
appToaster?.show({ intent: 'danger', message: title })
},
onCompleted: () => {
appToaster?.show({ intent: 'success', message: t('freinetActivateDataTransferSuccessful') })
},
})

const freinetQueryResult = getQueryResult(freinetQuery, t)
if (!freinetQueryResult.successful) {
return freinetQueryResult.component
}

if (freinetQueryResult.data.agency == null) {
return null
}

const onSave = (dataTransferForFreinetActivated: boolean) => {
updateFreinetDataTransfer({
variables: {
regionId,
project,
dataTransferActivated: dataTransferForFreinetActivated,
},
})
}

return <FreinetSettingsCard agencyInformation={freinetQueryResult.data.agency} loading={loading} onSave={onSave} />
}

export default FreinetSettingsController
8 changes: 8 additions & 0 deletions administration/src/errors/GraphQlErrorMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,99 +32,107 @@
return {
title: t('errors:mailAddressAlreadyUsed'),
}
case GraphQlExceptionCode.FreinetAgencyNotFound:
return {
title: t('errors:freinetAgencyNotFound'),
}
case GraphQlExceptionCode.InvalidLink:
return {
title: t('errors:invalidLink'),
description: <InvalidLink />,
}
case GraphQlExceptionCode.InvalidCardHash:
return {
title: t('errors:invalidFormat'),
}
case GraphQlExceptionCode.InvalidCodeType:
return {
title: t('errors:invalidCodeType'),
}
case GraphQlExceptionCode.InvalidCredentials:
return {
title: t('errors:invalidCredentials'),
}
case GraphQlExceptionCode.InvalidFileType:
return {
title: t('errors:invalidFileType'),
}
case GraphQlExceptionCode.InvalidInput:
return {
title: t('errors:invalidInput'),
}
case GraphQlExceptionCode.InvalidFileSize:
return {
title: t('errors:invalidFileSize'),
}
case GraphQlExceptionCode.InvalidDataPolicySize:
return {
title: t('errors:invalidDataPolicySize', { maxSize: extensions.maxSize }),
}
case GraphQlExceptionCode.InvalidJson:
return {
title: t('errors:invalidJson'),
}
case GraphQlExceptionCode.InvalidNoteSize:
return {
title: t('errors:invalidNoteSize', { maxSize: extensions.maxSize }),
}
case GraphQlExceptionCode.InvalidPassword:
return {
title: t('errors:invalidPassword'),
}
case GraphQlExceptionCode.InvalidPasswordResetLink:
return {
title: t('errors:invalidPasswordResetLink'),
description: <InvalidPasswordResetLink />,
}
case GraphQlExceptionCode.InvalidQrCodeSize: {
const cardInfo = CardInfo.fromBinary(base64ToUint8Array(extensions.encodedCardInfoBase64!))
const codeTypeText =
extensions.codeType === CodeType.Dynamic
? t('errors:invalidQrCodeSize:dynamicType')
: t('errors:invalidQrCodeSize:staticType')
return {
title: t('errors:invalidQrCodeSize:title', { codeType: codeTypeText, fullName: cardInfo.fullName }),
}
}
case GraphQlExceptionCode.InvalidRole:
return {
title: t('errors:invalidRole'),
}
case GraphQlExceptionCode.UserEntitlementNotFound:
return {
title: t('errors:entitlementNotFound'),
}
case GraphQlExceptionCode.UserEntitlementExpired:
return {
title: t('errors:entitlementExpired'),
}
case GraphQlExceptionCode.MailNotSent:
return {
title: t('errors:mailNotSend', { recipient: extensions.recipient }),
}
case GraphQlExceptionCode.PasswordResetKeyExpired:
return {
title: t('errors:passwordResetKeyExpired'),
description: <PasswordResetKeyExpired />,
}
case GraphQlExceptionCode.RegionNotFound:
return {
title: t('errors:regionNotFound'),
}
case GraphQlExceptionCode.RegionNotActivatedForApplication:
return {
title: t('errors:regionNotActivatedForApplication'),
}
case GraphQlExceptionCode.RegionNotActivatedCardConfirmationMail:
return {
title: t('errors:regionNotActivatedForConfirmationMail'),
}
default:
return {
title: defaultError.title,
}

Check warning on line 135 in administration/src/errors/GraphQlErrorMap.tsx

View check run for this annotation

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (1762-add-agency-id-to-region)

❌ Getting worse: Complex Method

graphQlErrorMap increases in cyclomatic complexity from 26 to 27, threshold = 10. This function has many conditional statements (e.g. if, for, while), leading to lower code health. Avoid adding more conditionals and code to it without refactoring.
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
mutation updateDataTransferToFreinet($regionId: Int!, $project: String!, $dataTransferActivated: Boolean!) {
result: updateDataTransferToFreinet(
regionId: $regionId
project: $project
dataTransferActivated: $dataTransferActivated
)
}
1 change: 1 addition & 0 deletions administration/src/project-configs/bayern/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ const config: ProjectConfig = {
},
},
freinetCSVImportEnabled: true,
freinetDataTransferEnabled: true,
cardCreation: true,
selfServiceEnabled: false,
storesManagement: {
Expand Down
1 change: 1 addition & 0 deletions administration/src/project-configs/getProjectConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ export type ProjectConfig = {
csvExport: CsvExport
cardStatistics: CardStatistics
freinetCSVImportEnabled: boolean
freinetDataTransferEnabled: boolean
cardCreation: boolean
selfServiceEnabled: boolean
storesManagement: StoresManagementConfig
Expand Down
1 change: 1 addition & 0 deletions administration/src/project-configs/koblenz/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const config: ProjectConfig = {
},
cardStatistics: { enabled: false },
freinetCSVImportEnabled: false,
freinetDataTransferEnabled: false,
cardCreation: false,
selfServiceEnabled: true,
storesManagement: storesManagementConfig,
Expand Down
1 change: 1 addition & 0 deletions administration/src/project-configs/nuernberg/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ const config: ProjectConfig = {
},
cardStatistics: { enabled: false },
freinetCSVImportEnabled: false,
freinetDataTransferEnabled: false,
cardCreation: true,
selfServiceEnabled: false,
storesManagement: storesManagementConfig,
Expand Down
1 change: 1 addition & 0 deletions administration/src/project-configs/showcase/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const config: ProjectConfig = {
},
cardStatistics: { enabled: false },
freinetCSVImportEnabled: true,
freinetDataTransferEnabled: false,
cardCreation: true,
selfServiceEnabled: false,
storesManagement: {
Expand Down
12 changes: 10 additions & 2 deletions administration/src/util/translations/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,14 @@
"regionSettings": "Regionsspezifische Einstellungen",
"activatedForApplication": "Region ist für den neuen Beantragungsprozess freigeschaltet",
"activatedForCardConfirmationMail": "Nach der Erstellung einer Karte verschickt das System eine E-Mail-Bestätigung an den Antragsstellenden mit einem Link zur Vorab-Aktivierung",
"savedSettingsSuccessful": "Einstellungen wurden erfolgreich geändert."
"savedSettingsSuccessful": "Einstellungen wurden erfolgreich geändert.",
"freinetHeadline": "Freinet Datenübermittlung",
"freinetExplanation": "Hier können Sie die automatische Übermittlung von Personen- und Kartendaten für Ihre Region verwalten. <br/><br/> Die untenstehenden Freinet Informationen wurden automatisch ermittelt, bitte prüfen Sie ob diese korrekt sind, bevor Sie die Übermittlung aktivieren.",
"freinetAgencyName": "Agenturname",
"freinetAgencyId": "AgenturId",
"freinetAgencyAccessKey": "Zugangsschlüssel",
"freinetActivateDataTransferCheckbox": "Personen- und Kartendaten an Freinet übermitteln",
"freinetActivateDataTransferSuccessful": "Ihre Einstellungen zur Datenübermittlung wurden erfolgreich gespeichert."
},
"selfService": {
"welcome": "Herzlich Willkommen!",
Expand Down Expand Up @@ -515,7 +522,8 @@
"regionNotActivatedForApplication": "Für diese Region kann der zentrale Beantragungsprozess noch nicht genutzt werden, kontaktieren Sie bitte Ihre zuständige Behörde direkt.",
"regionNotActivatedForConfirmationMail": "Für diese Region ist das Senden von Bestätigungsmails gesperrt. Dies kann in den regionsspezifischen Einstellungen geändert werden.",
"pdfCreationError": "Etwas ist schiefgegangen beim Erstellen der PDF.",
"csvCreationError": "Etwas ist schiefgegangen beim Erstellen der CSV."
"csvCreationError": "Etwas ist schiefgegangen beim Erstellen der CSV.",
"freinetAgencyNotFound": "Diese Freinetagentur existiert nicht."
},
"applicationVerification": {
"withdrawMessageForVerifier": "Der Antrag wurde vom Antragsteller am {{date}} zurückgezogen.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,10 @@ object Authorizer {
return user.role == Role.REGION_ADMIN.db_value && ProjectEntity.find { Projects.project eq EAK_BAYERN_PROJECT }
.single().id.value == user.projectId.value && user.regionId?.value == regionId
}
fun mayUpdateFreinetAgencyInformationInRegion(user: AdministratorEntity, regionId: Int): Boolean {
return user.role == Role.REGION_ADMIN.db_value && ProjectEntity.find { Projects.project eq EAK_BAYERN_PROJECT }
.single().id.value == user.projectId.value && user.regionId?.value == regionId
}

fun mayAddApiTokensInProject(user: AdministratorEntity): Boolean {
return transaction {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package app.ehrenamtskarte.backend.exception.webservice.exceptions

import app.ehrenamtskarte.backend.exception.GraphQLBaseException
import app.ehrenamtskarte.backend.exception.webservice.schema.GraphQLExceptionCode

class FreinetAgencyNotFoundException : GraphQLBaseException(GraphQLExceptionCode.FREINET_AGENCY_NOT_FOUND)
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package app.ehrenamtskarte.backend.exception.webservice.schema

enum class GraphQLExceptionCode {
EMAIL_ALREADY_EXISTS,
FREINET_AGENCY_NOT_FOUND,
INVALID_INPUT,
INVALID_CARD_HASH,
INVALID_CODE_TYPE,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,8 @@ object FreinetAgencyRepository {
fun getFreinetAgencyByRegionId(regionId: Int): FreinetAgenciesEntity? {
return FreinetAgenciesEntity.find { FreinetAgencies.regionId eq regionId }.singleOrNull()
}

fun updateFreinetDataTransfer(freinetAgency: FreinetAgenciesEntity, dataTransferActivated: Boolean) {
freinetAgency.dataTransferActivated = dataTransferActivated
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package app.ehrenamtskarte.backend.freinet.webservice

import app.ehrenamtskarte.backend.auth.database.AdministratorEntity
import app.ehrenamtskarte.backend.auth.service.Authorizer
import app.ehrenamtskarte.backend.common.webservice.EAK_BAYERN_PROJECT
import app.ehrenamtskarte.backend.common.webservice.GraphQLContext
import app.ehrenamtskarte.backend.exception.service.ForbiddenException
import app.ehrenamtskarte.backend.exception.service.NotEakProjectException
import app.ehrenamtskarte.backend.exception.service.ProjectNotFoundException
import app.ehrenamtskarte.backend.exception.service.UnauthorizedException
import app.ehrenamtskarte.backend.exception.webservice.exceptions.FreinetAgencyNotFoundException
import app.ehrenamtskarte.backend.freinet.database.repos.FreinetAgencyRepository
import app.ehrenamtskarte.backend.projects.database.ProjectEntity
import app.ehrenamtskarte.backend.projects.database.Projects
import com.expediagroup.graphql.generator.annotations.GraphQLDescription
import graphql.schema.DataFetchingEnvironment
import org.jetbrains.exposed.sql.transactions.transaction

@Suppress("unused")
class FreinetAgencyMutationService {
@GraphQLDescription("Updates the data transfer to freinet. Works only for the EAK project.")
fun updateDataTransferToFreinet(dfe: DataFetchingEnvironment, regionId: Int, project: String, dataTransferActivated: Boolean): Boolean {
val context = dfe.getContext<GraphQLContext>()
val jwtPayload = context.enforceSignedIn()
transaction {
val admin =
AdministratorEntity.findById(jwtPayload.adminId)
?: throw UnauthorizedException()
val projectEntity = ProjectEntity.find { Projects.project eq project }.firstOrNull()
?: throw ProjectNotFoundException(project)

if (projectEntity.project != EAK_BAYERN_PROJECT) {
throw NotEakProjectException()
}
if (!Authorizer.mayUpdateFreinetAgencyInformationInRegion(admin, regionId)) {
throw ForbiddenException()
}
val freinetAgency = FreinetAgencyRepository.getFreinetAgencyByRegionId(regionId) ?: throw FreinetAgencyNotFoundException()
FreinetAgencyRepository.updateFreinetDataTransfer(freinetAgency, dataTransferActivated)
}
return true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ val freinetGraphQlParams = GraphQLParams(
dataLoaderRegistry = createRegistryFromNamedDataLoaders(),
queries = listOf(
TopLevelObject(FreinetAgencyQueryService())
)
),
mutations = listOf(TopLevelObject(FreinetAgencyMutationService()))
)
3 changes: 3 additions & 0 deletions specs/backend-api.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,8 @@ type Mutation {
updateApplicationNote(applicationId: Int!, noteText: String!): Boolean!
"Updates the data privacy policy of a region"
updateDataPrivacy(dataPrivacyText: String!, regionId: Int!): Boolean!
"Updates the data transfer to freinet. Works only for the EAK project."
updateDataTransferToFreinet(dataTransferActivated: Boolean!, project: String!, regionId: Int!): Boolean!
"Updates the notification settings"
updateNotificationSettings(notificationSettings: NotificationSettingsInput!, project: String!): Boolean!
"Updates the region specific settings"
Expand Down Expand Up @@ -325,6 +327,7 @@ enum GoldenCardEntitlementType {

enum GraphQLExceptionCode {
EMAIL_ALREADY_EXISTS
FREINET_AGENCY_NOT_FOUND
INVALID_CARD_HASH
INVALID_CODE_TYPE
INVALID_CREDENTIALS
Expand Down