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

967: Deactivate regions application process #971

Merged
merged 11 commits into from
May 23, 2023
Merged
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: 3 additions & 1 deletion administration/src/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ import Login from './bp-modules/auth/Login'
import ResetPasswordController from './bp-modules/auth/ResetPasswordController'
import CreateCardsController from './bp-modules/cards/CreateCardsController'
import HomeController from './bp-modules/home/HomeController'
import DataPrivacyPolicy from './bp-modules/regions/DataPrivacyPolicy'
import RegionsController from './bp-modules/regions/RegionController'
import DataPrivacyController from './bp-modules/regions/data-privacy-policy/DataPrivacyController'
import DataPrivacyPolicy from './bp-modules/regions/data-privacy-policy/DataPrivacyPolicy'
import UserSettingsController from './bp-modules/user-settings/UserSettingsController'
import ManageUsersController from './bp-modules/users/ManageUsersController'
import ApplicationApplicantController from './mui-modules/application-verification/ApplicationApplicantController'
Expand Down Expand Up @@ -68,6 +69,7 @@ const Router = () => {
...(projectConfig.applicationFeatureEnabled
? [
{ path: 'applications', element: <ApplicationsController /> },
{ path: 'region/data-privacy-policy', element: <DataPrivacyController /> },
// Currently, '/region' only allows to set the data privacy text for the application form
{ path: 'region', element: <RegionsController /> },
]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { Button, Checkbox, H2 } from '@blueprintjs/core'
import React, { ReactElement, useEffect, useState } from 'react'
import styled from 'styled-components'

import getMessageFromApolloError from '../../errors/getMessageFromApolloError'
import { useGetActivatedForApplicationQuery, useUpdateActivatedForApplicationMutation } from '../../generated/graphql'
import { useAppToaster } from '../AppToaster'
import SettingsCard from '../user-settings/SettingsCard'
import getQueryResult from '../util/getQueryResult'

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

const ActivatedForApplicationCard = ({ regionId }: { regionId: number }): ReactElement => {
const [activatedForApplication, setActivatedForApplication] = useState<boolean>(false)
const appToaster = useAppToaster()
const activatedForApplicationQuery = useGetActivatedForApplicationQuery({
variables: { regionId: regionId },
})

const [updateActivatedForApplication, { loading }] = useUpdateActivatedForApplicationMutation({
onError: error => {
const { title } = getMessageFromApolloError(error)
appToaster?.show({ intent: 'danger', message: title })
},
onCompleted: () => {
appToaster?.show({ intent: 'success', message: 'Einstellungen wurden erfolgreich geändert.' })
},
})
useEffect(() => {
const activatedForApplicationQueryResult = getQueryResult(activatedForApplicationQuery)
if (activatedForApplicationQueryResult.successful) {
const { activatedForApplication } = activatedForApplicationQueryResult.data.result
setActivatedForApplication(activatedForApplication)
}
}, [activatedForApplicationQuery])

const activatedForApplicationQueryResult = getQueryResult(activatedForApplicationQuery)
if (!activatedForApplicationQueryResult.successful) return activatedForApplicationQueryResult.component

return (
<SettingsCard>
<H2>Aktivierung Beantragungsprozess</H2>
<p>Hier können Sie festlegen, ob Ihre Region für den neuen Antragsprozess freigeschaltet ist.</p>
<Checkbox
checked={activatedForApplication}
onChange={e => setActivatedForApplication(e.currentTarget.checked)}
label='Region ist aktiviert'
/>
<ButtonContainer>
<Button
text={'Speichern'}
intent={'primary'}
onClick={() =>
updateActivatedForApplication({
variables: {
regionId,
activated: activatedForApplication,
},
})
}
loading={loading}
/>
</ButtonContainer>
</SettingsCard>
)
}

export default ActivatedForApplicationCard
23 changes: 23 additions & 0 deletions administration/src/bp-modules/regions/DataPrivacyCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Button, H2 } from '@blueprintjs/core'
import React, { ReactElement } from 'react'
import { useNavigate } from 'react-router-dom'

import SettingsCard from '../user-settings/SettingsCard'

const DataPrivacyCard = (): ReactElement => {
const navigate = useNavigate()
return (
<SettingsCard>
<H2>Erweiterung der Datenschutzerklärung</H2>
<p>
Hier können Sie die allgemeine Datenschutzerklärung um Inhalte, die speziell für Ihre Region relevant sind,
erweitern.
</p>
<div style={{ textAlign: 'right', padding: '10px 0' }}>
<Button text={'Öffnen'} intent={'primary'} onClick={() => navigate('/region/data-privacy-policy')} />
</div>
</SettingsCard>
)
}

export default DataPrivacyCard
36 changes: 19 additions & 17 deletions administration/src/bp-modules/regions/RegionController.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,21 @@
import { NonIdealState } from '@blueprintjs/core'
import React, { ReactElement, useContext } from 'react'
import styled from 'styled-components'

import { WhoAmIContext } from '../../WhoAmIProvider'
import { Role, useGetDataPolicyQuery } from '../../generated/graphql'
import getQueryResult from '../util/getQueryResult'
import RegionOverview from './RegionOverview'
import { Role } from '../../generated/graphql'
import ActivatedForApplicationCard from './ActivatedForApplicationCard'
import DataPrivacyCard from './DataPrivacyCard'

const RegionController = ({ regionId }: { regionId: number }) => {
const dataPolicyQuery = useGetDataPolicyQuery({
variables: { regionId: regionId },
onError: error => console.error(error),
})
const dataPolicyQueryResult = getQueryResult(dataPolicyQuery)
if (!dataPolicyQueryResult.successful) return dataPolicyQueryResult.component
const RegionSettingsContainer = styled.div`
display: flex;
width: 100%;
justify-content: center;
align-items: center;
flex-direction: column;
`

const dataPrivacyPolicy = dataPolicyQueryResult.data.dataPolicy.dataPrivacyPolicy ?? ''
return <RegionOverview dataPrivacyPolicy={dataPrivacyPolicy} regionId={regionId} />
}

const ControllerWithRegion = (): ReactElement => {
const RegionController = (): ReactElement => {
const { region, role } = useContext(WhoAmIContext).me!
if (!region || role !== Role.RegionAdmin) {
return (
Expand All @@ -29,8 +26,13 @@ const ControllerWithRegion = (): ReactElement => {
/>
)
} else {
return <RegionController regionId={region.id} />
return (
<RegionSettingsContainer>
<DataPrivacyCard />
<ActivatedForApplicationCard regionId={region.id} />
</RegionSettingsContainer>
)
}
}

export default ControllerWithRegion
export default RegionController
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { NonIdealState } from '@blueprintjs/core'
import React, { ReactElement, useContext } from 'react'

import { WhoAmIContext } from '../../../WhoAmIProvider'
import { Role, useGetDataPolicyQuery } from '../../../generated/graphql'
import getQueryResult from '../../util/getQueryResult'
import DataPrivacyOverview from './DataPrivacyOverview'

const DataPrivacyController = ({ regionId }: { regionId: number }) => {
const dataPolicyQuery = useGetDataPolicyQuery({
variables: { regionId: regionId },
onError: error => console.error(error),
})
const dataPolicyQueryResult = getQueryResult(dataPolicyQuery)
if (!dataPolicyQueryResult.successful) return dataPolicyQueryResult.component

const dataPrivacyPolicy = dataPolicyQueryResult.data.dataPolicy.dataPrivacyPolicy ?? ''

return <DataPrivacyOverview dataPrivacyPolicy={dataPrivacyPolicy} regionId={regionId} />
}

const DataPrivacyWithRegion = (): ReactElement => {
const { region, role } = useContext(WhoAmIContext).me!
if (!region || role !== Role.RegionAdmin) {
return (
<NonIdealState
icon='cross'
title='Fehlende Berechtigung'
description='Sie sind nicht berechtigt, Änderungen an der Datenschutzerklärung der Region vorzunehmen.'
/>
)
} else {
return <DataPrivacyController regionId={region.id} />
}
}

export default DataPrivacyWithRegion
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { Button, H3, TextArea } from '@blueprintjs/core'
import { Tooltip2 } from '@blueprintjs/popover2'
import React, { ReactElement, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import styled from 'styled-components'

import defaultErrorMap from '../../errors/DefaultErrorMap'
import getMessageFromApolloError from '../../errors/getMessageFromApolloError'
import { GraphQlExceptionCode, useUpdateDataPolicyMutation } from '../../generated/graphql'
import { useAppToaster } from '../AppToaster'
import ButtonBar from '../ButtonBar'
import defaultErrorMap from '../../../errors/DefaultErrorMap'
import getMessageFromApolloError from '../../../errors/getMessageFromApolloError'
import { GraphQlExceptionCode, useUpdateDataPolicyMutation } from '../../../generated/graphql'
import { useAppToaster } from '../../AppToaster'
import ButtonBar from '../../ButtonBar'

const Content = styled.div`
padding: 0 6rem;
Expand Down Expand Up @@ -39,7 +40,8 @@ type RegionOverviewProps = {

const MAX_CHARS = 20000

const RegionOverview = ({ dataPrivacyPolicy, regionId }: RegionOverviewProps): ReactElement => {
const DataPrivacyOverview = ({ dataPrivacyPolicy, regionId }: RegionOverviewProps): ReactElement => {
f1sh1918 marked this conversation as resolved.
Show resolved Hide resolved
const navigate = useNavigate()
const appToaster = useAppToaster()
const [dataPrivacyText, setDataPrivacyText] = useState<string>(dataPrivacyPolicy)
const [updateDataPrivacy, { loading }] = useUpdateDataPolicyMutation({
Expand Down Expand Up @@ -75,6 +77,7 @@ const RegionOverview = ({ dataPrivacyPolicy, regionId }: RegionOverviewProps): R
</CharCounter>
</Content>
<ButtonBar>
<Button icon='arrow-left' text='Zurück' onClick={() => navigate(-1)} />
<Tooltip2 disabled={!maxCharsExceeded} content={errorMessage}>
<Button
disabled={maxCharsExceeded}
Expand All @@ -89,4 +92,4 @@ const RegionOverview = ({ dataPrivacyPolicy, regionId }: RegionOverviewProps): R
</>
)
}
export default RegionOverview
export default DataPrivacyOverview
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { ReactElement, useContext } from 'react'
import styled from 'styled-components'

import { ProjectConfigContext } from '../../project-configs/ProjectConfigContext'
import { ProjectConfigContext } from '../../../project-configs/ProjectConfigContext'

const Container = styled.div`
max-width: 750px;
Expand Down
5 changes: 5 additions & 0 deletions administration/src/errors/DefaultErrorMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ const defaultErrorMap = (extensions?: ErrorExtensions): GraphQLErrorMessage => {
return {
title: 'Diese Region existiert nicht.',
}
case GraphQlExceptionCode.RegionNotActivatedForApplication:
return {
title:
'Für diese Region kann der zentrale Beantragungsprozess noch nicht genutzt werden, kontaktieren Sie bitte Ihre zuständige Behörde direkt.',
}
}
}

Expand Down
1 change: 1 addition & 0 deletions administration/src/graphql/auth/whoAmI.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ query whoAmI($project: String!) {
id
name
prefix
activatedForApplication
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
query getActivatedForApplication($regionId: Int!) {
result: regionByRegionId(regionId: $regionId) {
activatedForApplication
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ query getRegionByPostalCode($postalCode: String!, $project: String!) {
id
name
prefix
activatedForApplication
}
}
1 change: 1 addition & 0 deletions administration/src/graphql/regions/getRegions.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ query getRegions($project: String!) {
prefix
name
regionIdentifier
activatedForApplication
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
mutation updateActivatedForApplication($regionId: Int!, $activated: Boolean!) {
result: updateActivatedForApplication(regionId: $regionId, activated: $activated)
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ const ApplyController = (): React.ReactElement | null => {

if (!regionsQueryResult.successful) return regionsQueryResult.component

const regions = regionsQueryResult.data.regions
const regions = regionsQueryResult.data.regions.filter(region => region.activatedForApplication)

const submit = () => {
const validationResult = ApplicationForm.validate(state, { regions })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,15 @@ const RegionForm: Form<State, Options, ValidatedInput, AdditionalProps> = {
</a>
{` einsehen.`}
<br />
Eine Liste der teilnehmenden Landkreise und kreisfreien Städte finden Sie im Auswahlfeld unten.
{`Eine Liste der teilnehmenden Landkreise und kreisfreien Städte für den zentralen Beantragungsprozess finden Sie im Auswahlfeld unten.
Alle anderen Regionen können Sie `}
<a
href='https://www.lbe.bayern.de/engagement-anerkennen/ehrenamtskarte/voraussetzungen/index.php'
target='_blank'
rel='noreferrer'>
hier
</a>
{` direkt kontaktieren, um Ihre Ehrenamtskarte zu erhalten.`}
</Typography>
{renderAlert(state, postalCode, regionQuery)}
<SubForms.region.Component
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import app.ehrenamtskarte.backend.exception.service.UnauthorizedException
import app.ehrenamtskarte.backend.exception.webservice.exceptions.InvalidFileSizeException
import app.ehrenamtskarte.backend.exception.webservice.exceptions.InvalidFileTypeException
import app.ehrenamtskarte.backend.exception.webservice.exceptions.MailNotSentException
import app.ehrenamtskarte.backend.exception.webservice.exceptions.RegionNotActivatedForApplicationException
import app.ehrenamtskarte.backend.exception.webservice.exceptions.RegionNotFoundException
import app.ehrenamtskarte.backend.mail.Mailer
import app.ehrenamtskarte.backend.regions.database.repos.RegionsRepository
Expand All @@ -33,7 +34,10 @@ class EakApplicationMutationService {
val backendConfig = context.backendConfiguration
val projectConfig = backendConfig.projects.first { it.id == project }

transaction { RegionsRepository.findByIdInProject(project, regionId) } ?: throw RegionNotFoundException()
val region = transaction { RegionsRepository.findByIdInProject(project, regionId) } ?: throw RegionNotFoundException()
if (!region.activatedForApplication) {
throw RegionNotActivatedForApplicationException()
}
// Validate that all files are png, jpeg or pdf files and at most 5MB.
val allowedContentTypes = setOf("application/pdf", "image/png", "image/jpeg")
val maxFileSizeBytes = 5 * 1000 * 1000
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ object Authorizer {
)
}

fun mayUpdatePrivacyPolicyInRegion(user: AdministratorEntity, regionId: Int): Boolean {
fun mayUpdateSettingsInRegion(user: AdministratorEntity, regionId: Int): Boolean {
return user.regionId?.value == regionId && user.role == Role.REGION_ADMIN.db_value
}

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 RegionNotActivatedForApplicationException() : GraphQLBaseException(GraphQLExceptionCode.REGION_NOT_ACTIVATED_FOR_APPLICATION)
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ enum class GraphQLExceptionCode {
INVALID_ROLE,
MAIL_NOT_SENT,
PASSWORD_RESET_KEY_EXPIRED,
REGION_NOT_FOUND
REGION_NOT_FOUND,
REGION_NOT_ACTIVATED_FOR_APPLICATION
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ object MigrationsRegistry {
V0001_Baseline(),
V0002_DropCaseSensitiveEmailConstraint(),
V0003_AddFirstActivationDate(),
V0004_AddNotificationSettings()
V0004_AddNotificationSettings(),
V0005_AddRegionApplicationActivation()
)
}
Loading