Merge branch 'master' into e2e-test-setup

This commit is contained in:
Moriz Wahl 2022-09-22 10:01:35 +02:00 committed by GitHub
commit b34fd7e76f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
61 changed files with 901 additions and 157 deletions

View File

@ -4,8 +4,56 @@ All notable changes to this project will be documented in this file. Dates are d
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
#### [1.12.1](https://github.com/gradido/gradido/compare/1.12.0...1.12.1)
- fix: 🍰 Show Not Icons In `allContribution` List [`#2195`](https://github.com/gradido/gradido/pull/2195)
#### [1.12.0](https://github.com/gradido/gradido/compare/1.11.0...1.12.0)
> 12 September 2022
- release: v1.12.0 [`#2191`](https://github.com/gradido/gradido/pull/2191)
- if message empty else disabled button [`#2189`](https://github.com/gradido/gradido/pull/2189)
- messages show if Confirmed [`#2185`](https://github.com/gradido/gradido/pull/2185)
- text in messages smaller [`#2186`](https://github.com/gradido/gradido/pull/2186)
- feat: 🍰 Klicktipp retrieve not registered email [`#2181`](https://github.com/gradido/gradido/pull/2181)
- fix: 🍰 isModerator on messages to switch the messages side in the messages overview [`#2182`](https://github.com/gradido/gradido/pull/2182)
- Refactor locales for Nederlands [`#2174`](https://github.com/gradido/gradido/pull/2174)
- Add is moderator to contribution message [`#2180`](https://github.com/gradido/gradido/pull/2180)
- feat: 🍰 Moderator Cannot Answer Himself [`#2178`](https://github.com/gradido/gradido/pull/2178)
- refactor: Improve Statistics Query [`#2170`](https://github.com/gradido/gradido/pull/2170)
- fix: Remove Statistics from Wallet [`#2171`](https://github.com/gradido/gradido/pull/2171)
- feat: 🍰 Contribution Messages In Frontend [`#2164`](https://github.com/gradido/gradido/pull/2164)
- feat: 🚀 CRUD For Contribution Messages [`#2149`](https://github.com/gradido/gradido/pull/2149)
- fix: 🍰 Decay Calculation In Community Statistics [`#2167`](https://github.com/gradido/gradido/pull/2167)
- chore: 🍰 Remove Fetch Policy Network Only From Statistics [`#2159`](https://github.com/gradido/gradido/pull/2159)
- feat: 🍰 Remove Some Statistics Data From Frontend [`#2153`](https://github.com/gradido/gradido/pull/2153)
- feat: 🍰 Add Toogle Collaps On Language Name [`#2156`](https://github.com/gradido/gradido/pull/2156)
- 2145 corrections style for frontend [`#2147`](https://github.com/gradido/gradido/pull/2147)
- 2072 feature usecase contribution messaging [`#2073`](https://github.com/gradido/gradido/pull/2073)
- 2151 add hint to redeem link [`#2158`](https://github.com/gradido/gradido/pull/2158)
- 🍰 Create `contribution messages` table [`#2137`](https://github.com/gradido/gradido/pull/2137)
- feat: 🍰 Add The Languages French And Dutch [`#2138`](https://github.com/gradido/gradido/pull/2138)
- 1973 list open contribution links in the wallet [`#1975`](https://github.com/gradido/gradido/pull/1975)
- feat: 🍰 Admin Interface Displays Statistics [`#2124`](https://github.com/gradido/gradido/pull/2124)
- feat: Statistics Resolver [`#2041`](https://github.com/gradido/gradido/pull/2041)
- 2116 retrieve admin and moderators [`#2127`](https://github.com/gradido/gradido/pull/2127)
- 2125 feature gradido id: new column gradidoid in users table [`#2126`](https://github.com/gradido/gradido/pull/2126)
- 2119 new menu item gdt [`#2120`](https://github.com/gradido/gradido/pull/2120)
- feat: Migrate Contributions Table [`#2136`](https://github.com/gradido/gradido/pull/2136)
- chore: 🍰 Refactor Contribution Form Logic And Write Tests [`#2092`](https://github.com/gradido/gradido/pull/2092)
- fix: 🍰 Add `emailChecked` Before Changing `optIn` State & Log Error On klicktipp Middleware [`#2107`](https://github.com/gradido/gradido/pull/2107)
- Add RIGHTS.LIST_CONTRIBUTION_LINKS to ROLE_USER [`#2123`](https://github.com/gradido/gradido/pull/2123)
- 2121 translate locales to spanish [`#2122`](https://github.com/gradido/gradido/pull/2122)
- add formatter on input amount replace point and comma [`#2115`](https://github.com/gradido/gradido/pull/2115)
- remove required from form.memo [`#2114`](https://github.com/gradido/gradido/pull/2114)
- Fix pagination ellipsis [`#2104`](https://github.com/gradido/gradido/pull/2104)
#### [1.11.0](https://github.com/gradido/gradido/compare/1.10.1...1.11.0)
> 28 July 2022
- release: Version 1.11.0 [`#2103`](https://github.com/gradido/gradido/pull/2103)
- Fix navbar community item [`#2102`](https://github.com/gradido/gradido/pull/2102)
- Add validation date info to copied text after transaction link creation [`#2101`](https://github.com/gradido/gradido/pull/2101)
- Remove member area from menu (desktop and mobile), when user has no elopage account [`#2099`](https://github.com/gradido/gradido/pull/2099)

View File

@ -3,7 +3,7 @@
"description": "Administraion Interface for Gradido",
"main": "index.js",
"author": "Moriz Wahl",
"version": "1.11.0",
"version": "1.12.1",
"license": "Apache-2.0",
"private": false,
"scripts": {

View File

@ -1,6 +1,6 @@
<template>
<div class="contribution-messages-formular">
<div>
<div class="mt-5">
<b-form @submit.prevent="onSubmit" @reset.prevent="onReset">
<b-form-textarea
id="textarea"
@ -14,7 +14,9 @@
<b-button type="reset" variant="danger">{{ $t('form.cancel') }}</b-button>
</b-col>
<b-col class="text-right">
<b-button type="submit" variant="primary">{{ $t('form.submit') }}</b-button>
<b-button type="submit" variant="primary" :disabled="disabled">
{{ $t('form.submit') }}
</b-button>
</b-col>
</b-row>
</b-form>
@ -63,5 +65,13 @@ export default {
this.form.text = ''
},
},
computed: {
disabled() {
if (this.form.text !== '') {
return false
}
return true
},
},
}
</script>

View File

@ -1,6 +1,6 @@
<template>
<div class="contribution-messages-list-item">
<is-moderator v-if="isModerator" :message="message"></is-moderator>
<is-moderator v-if="message.isModerator" :message="message"></is-moderator>
<is-not-moderator v-else :message="message"></is-not-moderator>
</div>
</template>
@ -23,10 +23,5 @@ export default {
},
},
},
computed: {
isModerator() {
return this.$store.state.moderator.id === this.message.userId
},
},
}
</script>

View File

@ -5,7 +5,7 @@
<span class="ml-2 mr-2">{{ message.userFirstName }} {{ message.userLastName }}</span>
<span class="ml-2">{{ $d(new Date(message.createdAt), 'short') }}</span>
<small class="ml-4 text-success">{{ $t('moderator') }}</small>
<div class="mt-2 text-bold h4">{{ message.message }}</div>
<div class="mt-2">{{ message.message }}</div>
</div>
</div>
</template>

View File

@ -2,11 +2,9 @@
<div class="slot-is-not-moderator">
<div>
<b-avatar :text="initialLetters" variant="info"></b-avatar>
<span class="ml-2 mr-2 text-bold">
{{ message.userFirstName }} {{ message.userLastName }}
</span>
<span class="ml-2 mr-2">{{ message.userFirstName }} {{ message.userLastName }}</span>
<span class="ml-2">{{ $d(new Date(message.createdAt), 'short') }}</span>
<div class="mt-2 text-bold h4">{{ message.message }}</div>
<div class="mt-2">{{ message.message }}</div>
</div>
</div>
</template>

View File

@ -12,33 +12,42 @@
</b-button>
</template>
<template #cell(editCreation)="row">
<b-button
v-if="row.item.moderator"
variant="info"
size="md"
@click="rowToggleDetails(row, 0)"
class="mr-2"
>
<b-icon :icon="row.detailsShowing ? 'x' : 'pencil-square'" aria-label="Help"></b-icon>
</b-button>
<b-button v-else @click="rowToggleDetails(row, 0)">
<b-icon icon="chat-dots"></b-icon>
<b-icon
v-if="row.item.state === 'PENDING' && row.item.messageCount > 0"
icon="exclamation-circle-fill"
variant="warning"
></b-icon>
<b-icon
v-if="row.item.state === 'IN_PROGRESS' && row.item.messageCount > 0"
icon="question-diamond"
variant="light"
></b-icon>
</b-button>
<div v-if="$store.state.moderator.id !== row.item.userId">
<b-button
v-if="row.item.moderator"
variant="info"
size="md"
@click="rowToggleDetails(row, 0)"
class="mr-2"
>
<b-icon :icon="row.detailsShowing ? 'x' : 'pencil-square'" aria-label="Help"></b-icon>
</b-button>
<b-button v-else @click="rowToggleDetails(row, 0)">
<b-icon icon="chat-dots"></b-icon>
<b-icon
v-if="row.item.state === 'PENDING' && row.item.messageCount > 0"
icon="exclamation-circle-fill"
variant="warning"
></b-icon>
<b-icon
v-if="row.item.state === 'IN_PROGRESS' && row.item.messageCount > 0"
icon="question-diamond"
variant="light"
></b-icon>
</b-button>
</div>
</template>
<template #cell(confirm)="row">
<b-button variant="success" size="md" @click="$emit('show-overlay', row.item)" class="mr-2">
<b-icon icon="check" scale="2" variant=""></b-icon>
</b-button>
<div v-if="$store.state.moderator.id !== row.item.userId">
<b-button
variant="success"
size="md"
@click="$emit('show-overlay', row.item)"
class="mr-2"
>
<b-icon icon="check" scale="2" variant=""></b-icon>
</b-button>
</div>
</template>
<template #row-details="row">
<row-details

View File

@ -18,6 +18,7 @@ export const listContributionMessages = gql`
userFirstName
userLastName
userId
isModerator
}
}
}

View File

@ -6,6 +6,7 @@ export const listUnconfirmedContributions = gql`
id
firstName
lastName
userId
email
amount
memo

View File

@ -14,21 +14,23 @@ const apolloQueryMock = jest.fn().mockResolvedValue({
id: 1,
firstName: 'Bibi',
lastName: 'Bloxberg',
userId: 99,
email: 'bibi@bloxberg.de',
amount: 500,
memo: 'Danke für alles',
date: new Date(),
moderator: 2,
moderator: 1,
},
{
id: 2,
firstName: 'Räuber',
lastName: 'Hotzenplotz',
userId: 100,
email: 'raeuber@hotzenplotz.de',
amount: 1000000,
memo: 'Gut Ergattert',
date: new Date(),
moderator: 2,
moderator: 1,
},
],
},
@ -41,6 +43,15 @@ const mocks = {
$d: jest.fn((d) => d),
$store: {
commit: storeCommitMock,
state: {
moderator: {
firstName: 'Peter',
lastName: 'Lustig',
isAdmin: '2022-08-30T07:41:31.000Z',
id: 263,
language: 'de',
},
},
},
$apollo: {
query: apolloQueryMock,

View File

@ -1,6 +1,6 @@
{
"name": "gradido-backend",
"version": "1.11.0",
"version": "1.12.1",
"description": "Gradido unified backend providing an API-Service for Gradido Transactions",
"main": "src/index.ts",
"repository": "https://github.com/gradido/gradido/backend",
@ -14,7 +14,8 @@
"dev": "cross-env TZ=UTC nodemon -w src --ext ts --exec ts-node -r tsconfig-paths/register src/index.ts",
"lint": "eslint --max-warnings=0 --ext .js,.ts .",
"test": "cross-env TZ=UTC NODE_ENV=development jest --runInBand --coverage --forceExit --detectOpenHandles",
"seed": "cross-env TZ=UTC NODE_ENV=development ts-node -r tsconfig-paths/register src/seeds/index.ts"
"seed": "cross-env TZ=UTC NODE_ENV=development ts-node -r tsconfig-paths/register src/seeds/index.ts",
"klicktipp": "cross-env TZ=UTC NODE_ENV=development ts-node -r tsconfig-paths/register src/util/klicktipp.ts"
},
"dependencies": {
"@types/jest": "^27.0.2",

View File

@ -10,7 +10,7 @@ Decimal.set({
})
const constants = {
DB_VERSION: '0047-messages_tables',
DB_VERSION: '0048-add_is_moderator_to_contribution_messages',
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
LOG4JS_CONFIG: 'log4js-config.json',
// default log level on production should be info

View File

@ -13,6 +13,7 @@ export class ContributionMessage {
this.userFirstName = user.firstName
this.userLastName = user.lastName
this.userId = user.id
this.isModerator = contributionMessage.isModerator
}
@Field(() => Number)
@ -38,6 +39,9 @@ export class ContributionMessage {
@Field(() => Number, { nullable: true })
userId: number | null
@Field(() => Boolean)
isModerator: boolean
}
@ObjectType()
export class ContributionMessageListResult {

View File

@ -40,6 +40,7 @@ import Decimal from 'decimal.js-light'
import { Contribution } from '@entity/Contribution'
import { Transaction as DbTransaction } from '@entity/Transaction'
import { ContributionLink as DbContributionLink } from '@entity/ContributionLink'
import { sendContributionConfirmedEmail } from '@/mailer/sendContributionConfirmedEmail'
// mock account activation email to avoid console spam
jest.mock('@/mailer/sendAccountActivationEmail', () => {
@ -49,6 +50,14 @@ jest.mock('@/mailer/sendAccountActivationEmail', () => {
}
})
// mock account activation email to avoid console spam
jest.mock('@/mailer/sendContributionConfirmedEmail', () => {
return {
__esModule: true,
sendContributionConfirmedEmail: jest.fn(),
}
})
let mutate: any, query: any, con: any
let testEnv: any
@ -1450,6 +1459,20 @@ describe('AdminResolver', () => {
expect(transaction[0].linkedUserId).toEqual(null)
expect(transaction[0].typeId).toEqual(1)
})
it('calls sendContributionConfirmedEmail', async () => {
expect(sendContributionConfirmedEmail).toBeCalledWith(
expect.objectContaining({
contributionMemo: 'Herzlich Willkommen bei Gradido liebe Bibi!',
overviewURL: 'http://localhost/overview',
recipientEmail: 'bibi@bloxberg.de',
recipientFirstName: 'Bibi',
recipientLastName: 'Bloxberg',
senderFirstName: 'Peter',
senderLastName: 'Lustig',
}),
)
})
})
describe('confirm two creations one after the other quickly', () => {

View File

@ -66,6 +66,8 @@ import { ContributionMessage as DbContributionMessage } from '@entity/Contributi
import ContributionMessageArgs from '@arg/ContributionMessageArgs'
import { ContributionMessageType } from '@enum/MessageType'
import { ContributionMessage } from '@model/ContributionMessage'
import { sendContributionConfirmedEmail } from '@/mailer/sendContributionConfirmedEmail'
import { sendAddedContributionMessageEmail } from '@/mailer/sendAddedContributionMessageEmail'
// const EMAIL_OPT_IN_REGISTER = 1
// const EMAIL_OPT_UNKNOWN = 3 // elopage?
@ -470,6 +472,16 @@ export class AdminResolver {
await queryRunner.commitTransaction()
logger.info('creation commited successfuly.')
sendContributionConfirmedEmail({
senderFirstName: moderatorUser.firstName,
senderLastName: moderatorUser.lastName,
recipientFirstName: user.firstName,
recipientLastName: user.lastName,
recipientEmail: user.email,
contributionMemo: contribution.memo,
contributionAmount: contribution.amount,
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
})
} catch (e) {
await queryRunner.rollbackTransaction()
logger.error(`Creation was not successful: ${e}`)
@ -713,15 +725,22 @@ export class AdminResolver {
await queryRunner.startTransaction('READ UNCOMMITTED')
const contributionMessage = DbContributionMessage.create()
try {
const contribution = await Contribution.findOne({ id: contributionId })
const contribution = await Contribution.findOne({
where: { id: contributionId },
relations: ['user'],
})
if (!contribution) {
throw new Error('Contribution not found')
}
if (contribution.userId === user.id) {
throw new Error('Admin can not answer on own contribution')
}
contributionMessage.contributionId = contributionId
contributionMessage.createdAt = new Date()
contributionMessage.message = message
contributionMessage.userId = user.id
contributionMessage.type = ContributionMessageType.DIALOG
contributionMessage.isModerator = true
await queryRunner.manager.insert(DbContributionMessage, contributionMessage)
if (
@ -733,6 +752,18 @@ export class AdminResolver {
await queryRunner.manager.update(Contribution, { id: contributionId }, contribution)
}
await queryRunner.commitTransaction()
await sendAddedContributionMessageEmail({
senderFirstName: user.firstName,
senderLastName: user.lastName,
recipientFirstName: contribution.user.firstName,
recipientLastName: contribution.user.lastName,
recipientEmail: contribution.user.email,
senderEmail: user.email,
contributionMemo: contribution.memo,
message,
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
})
} catch (e) {
await queryRunner.rollbackTransaction()
logger.error(`ContributionMessage was not successful: ${e}`)

View File

@ -12,6 +12,14 @@ import { listContributionMessages, login } from '@/seeds/graphql/queries'
import { userFactory } from '@/seeds/factory/user'
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
import { peterLustig } from '@/seeds/users/peter-lustig'
import { sendAddedContributionMessageEmail } from '@/mailer/sendAddedContributionMessageEmail'
jest.mock('@/mailer/sendAddedContributionMessageEmail', () => {
return {
__esModule: true,
sendAddedContributionMessageEmail: jest.fn(),
}
})
let mutate: any, query: any, con: any
let testEnv: any
@ -93,6 +101,38 @@ describe('ContributionMessageResolver', () => {
}),
)
})
it('throws error when contribution.userId equals user.id', async () => {
await query({
query: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
})
const result2 = await mutate({
mutation: createContribution,
variables: {
amount: 100.0,
memo: 'Test env contribution',
creationDate: new Date().toString(),
},
})
await expect(
mutate({
mutation: adminCreateContributionMessage,
variables: {
contributionId: result2.data.createContribution.id,
message: 'Test',
},
}),
).resolves.toEqual(
expect.objectContaining({
errors: [
new GraphQLError(
'ContributionMessage was not successful: Error: Admin can not answer on own contribution',
),
],
}),
)
})
})
describe('valid input', () => {
@ -119,6 +159,20 @@ describe('ContributionMessageResolver', () => {
}),
)
})
it('calls sendAddedContributionMessageEmail', async () => {
expect(sendAddedContributionMessageEmail).toBeCalledWith({
senderFirstName: 'Peter',
senderLastName: 'Lustig',
recipientFirstName: 'Bibi',
recipientLastName: 'Bloxberg',
recipientEmail: 'bibi@bloxberg.de',
senderEmail: 'peter@lustig.de',
contributionMemo: 'Test env contribution',
message: 'Admin Test',
overviewURL: 'http://localhost/overview',
})
})
})
})
})

View File

@ -39,6 +39,7 @@ export class ContributionMessageResolver {
contributionMessage.message = message
contributionMessage.userId = user.id
contributionMessage.type = ContributionMessageType.DIALOG
contributionMessage.isModerator = false
await queryRunner.manager.insert(DbContributionMessage, contributionMessage)
if (contribution.contributionStatus === ContributionStatus.IN_PROGRESS) {

View File

@ -7,49 +7,48 @@ import { getConnection } from '@dbTools/typeorm'
import Decimal from 'decimal.js-light'
import { calculateDecay } from '@/util/decay'
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
@Resolver()
export class StatisticsResolver {
@Authorized([RIGHTS.COMMUNITY_STATISTICS])
@Query(() => CommunityStatistics)
async communityStatistics(): Promise<CommunityStatistics> {
const allUsers = await DbUser.find({ withDeleted: true })
let totalUsers = 0
let activeUsers = 0
let deletedUsers = 0
const allUsers = await DbUser.count({ withDeleted: true })
const totalUsers = await DbUser.count()
const deletedUsers = allUsers - totalUsers
let totalGradidoAvailable: Decimal = new Decimal(0)
let totalGradidoUnbookedDecayed: Decimal = new Decimal(0)
const receivedCallDate = new Date()
for (let i = 0; i < allUsers.length; i++) {
if (allUsers[i].deletedAt) {
deletedUsers++
} else {
totalUsers++
const lastTransaction = await DbTransaction.findOne({
where: { userId: allUsers[i].id },
order: { balanceDate: 'DESC' },
})
if (lastTransaction) {
activeUsers++
const decay = calculateDecay(
lastTransaction.balance,
lastTransaction.balanceDate,
receivedCallDate,
)
if (decay) {
totalGradidoAvailable = totalGradidoAvailable.plus(decay.balance.toString())
totalGradidoUnbookedDecayed = totalGradidoUnbookedDecayed.plus(decay.decay.toString())
}
}
}
}
const queryRunner = getConnection().createQueryRunner()
await queryRunner.connect()
const lastUserTransactions = await queryRunner.manager
.createQueryBuilder(DbUser, 'user')
.select('transaction.balance', 'balance')
.addSelect('transaction.balance_date', 'balanceDate')
.innerJoin(DbTransaction, 'transaction', 'user.id = transaction.user_id')
.where(
`transaction.balance_date = (SELECT MAX(t.balance_date) FROM transactions AS t WHERE t.user_id = user.id)`,
)
.orderBy('transaction.balance_date', 'DESC')
.addOrderBy('transaction.id', 'DESC')
.getRawMany()
const activeUsers = lastUserTransactions.length
lastUserTransactions.forEach(({ balance, balanceDate }) => {
const decay = calculateDecay(new Decimal(balance), new Date(balanceDate), receivedCallDate)
if (decay) {
totalGradidoAvailable = totalGradidoAvailable.plus(decay.balance.toString())
totalGradidoUnbookedDecayed = totalGradidoUnbookedDecayed.plus(decay.decay.toString())
}
})
const { totalGradidoCreated } = await queryRunner.manager
.createQueryBuilder()
.select('SUM(transaction.amount) AS totalGradidoCreated')

View File

@ -35,6 +35,7 @@ import Decimal from 'decimal.js-light'
import { BalanceResolver } from './BalanceResolver'
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const'
import { sendTransactionLinkRedeemedEmail } from '@/mailer/sendTransactionLinkRedeemed'
export const executeTransaction = async (
amount: Decimal,
@ -151,9 +152,21 @@ export const executeTransaction = async (
email: recipient.email,
senderEmail: sender.email,
amount,
memo,
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
})
if (transactionLink) {
await sendTransactionLinkRedeemedEmail({
senderFirstName: recipient.firstName,
senderLastName: recipient.lastName,
recipientFirstName: sender.firstName,
recipientLastName: sender.lastName,
email: sender.email,
senderEmail: recipient.email,
amount,
memo,
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
})
}
logger.info(`finished executeTransaction successfully`)
return true
}

View File

@ -19,6 +19,8 @@ import { contributionLinkFactory } from '@/seeds/factory/contributionLink'
import { ContributionLink } from '@model/ContributionLink'
// import { TransactionLink } from '@entity/TransactionLink'
import { EventProtocolType } from '@/event/EventProtocolType'
import { EventProtocol } from '@entity/EventProtocol'
import { logger } from '@test/testSetup'
import { validate as validateUUID, version as versionUUID } from 'uuid'
import { peterLustig } from '@/seeds/users/peter-lustig'
@ -169,6 +171,15 @@ describe('UserResolver', () => {
duration: expect.any(String),
})
})
it('stores the send confirmation event in the database', () => {
expect(EventProtocol.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventProtocolType.SEND_CONFIRMATION_EMAIL,
userId: user[0].id,
}),
)
})
})
describe('email already exists', () => {
@ -245,18 +256,26 @@ describe('UserResolver', () => {
mutation: setPassword,
variables: { code: emailOptIn, password: 'Aa12345_' },
})
// make Peter Lustig Admin
const peter = await User.findOneOrFail({ id: user[0].id })
peter.isAdmin = new Date()
await peter.save()
// date statement
const actualDate = new Date()
const futureDate = new Date() // Create a future day from the executed day
futureDate.setDate(futureDate.getDate() + 1)
// factory logs in as Peter Lustig
link = await contributionLinkFactory(testEnv, {
name: 'Dokumenta 2022',
memo: 'Vielen Dank für deinen Besuch bei der Dokumenta 2022',
amount: 200,
validFrom: new Date(2022, 5, 18),
validTo: new Date(2022, 8, 25),
validFrom: actualDate,
validTo: futureDate,
})
resetToken()
await mutate({
mutation: createUser,
@ -271,6 +290,15 @@ describe('UserResolver', () => {
}),
)
})
it('stores the account activated event in the database', () => {
expect(EventProtocol.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventProtocolType.ACTIVATE_ACCOUNT,
userId: user[0].id,
}),
)
})
})
/* A transaction link requires GDD on account
@ -383,6 +411,10 @@ bei Gradidio sei dabei!`,
}),
)
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('Password entered is lexically invalid')
})
})
describe('no valid optin code', () => {
@ -405,6 +437,10 @@ bei Gradidio sei dabei!`,
}),
)
})
it('logs the error found', () => {
expect(logger.error).toBeCalledWith('Could not login with emailVerificationCode')
})
})
})
@ -433,6 +469,10 @@ bei Gradidio sei dabei!`,
}),
)
})
it('logs the error found', () => {
expect(logger.error).toBeCalledWith('User with email=bibi@bloxberg.de does not exist')
})
})
describe('user is in database and correct login data', () => {
@ -475,6 +515,7 @@ bei Gradidio sei dabei!`,
describe('user is in database and wrong password', () => {
beforeAll(async () => {
await userFactory(testEnv, bibiBloxberg)
result = await query({ query: login, variables: { ...variables, password: 'wrong' } })
})
afterAll(async () => {
@ -482,14 +523,16 @@ bei Gradidio sei dabei!`,
})
it('returns an error', () => {
expect(
query({ query: login, variables: { ...variables, password: 'wrong' } }),
).resolves.toEqual(
expect(result).toEqual(
expect.objectContaining({
errors: [new GraphQLError('No user with this credentials')],
}),
)
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('The User has no valid credentials.')
})
})
})
@ -562,6 +605,8 @@ bei Gradidio sei dabei!`,
})
describe('authenticated', () => {
let user: User[]
const variables = {
email: 'bibi@bloxberg.de',
password: 'Aa12345_',
@ -569,6 +614,7 @@ bei Gradidio sei dabei!`,
beforeAll(async () => {
await query({ query: login, variables })
user = await User.find()
})
afterAll(() => {
@ -595,6 +641,15 @@ bei Gradidio sei dabei!`,
}),
)
})
it('stores the login event in the database', () => {
expect(EventProtocol.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventProtocolType.LOGIN,
userId: user[0].id,
}),
)
})
})
})
})
@ -649,13 +704,17 @@ bei Gradidio sei dabei!`,
})
describe('request reset password again', () => {
it('thows an error', async () => {
it('throws an error', async () => {
await expect(mutate({ mutation: forgotPassword, variables })).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('email already sent less than 10 minutes minutes ago')],
}),
)
})
it('logs the error found', () => {
expect(logger.error).toBeCalledWith(`email already sent less than 10 minutes minutes ago`)
})
})
})
})
@ -766,7 +825,7 @@ bei Gradidio sei dabei!`,
})
describe('language is not valid', () => {
it('thows an error', async () => {
it('throws an error', async () => {
await expect(
mutate({
mutation: updateUserInfos,
@ -780,6 +839,10 @@ bei Gradidio sei dabei!`,
}),
)
})
it('logs the error found', () => {
expect(logger.error).toBeCalledWith(`"not-valid" isn't a valid language`)
})
})
describe('password', () => {
@ -799,6 +862,10 @@ bei Gradidio sei dabei!`,
}),
)
})
it('logs the error found', () => {
expect(logger.error).toBeCalledWith(`Old password is invalid`)
})
})
describe('invalid new password', () => {
@ -821,6 +888,10 @@ bei Gradidio sei dabei!`,
}),
)
})
it('logs the error found', () => {
expect(logger.error).toBeCalledWith('newPassword does not fullfil the rules')
})
})
describe('correct old and new password', () => {
@ -840,7 +911,7 @@ bei Gradidio sei dabei!`,
)
})
it('can login wtih new password', async () => {
it('can login with new password', async () => {
await expect(
query({
query: login,
@ -860,7 +931,7 @@ bei Gradidio sei dabei!`,
)
})
it('cannot login wtih old password', async () => {
it('cannot login with old password', async () => {
await expect(
query({
query: login,
@ -875,6 +946,10 @@ bei Gradidio sei dabei!`,
}),
)
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('The User has no valid credentials.')
})
})
})
})

View File

@ -30,6 +30,7 @@ import {
EventRedeemRegister,
EventRegister,
EventSendConfirmationEmail,
EventActivateAccount,
} from '@/event/Event'
import { getUserCreation } from './util/creations'
import { UserRepository } from '@/typeorm/repository/User'
@ -273,7 +274,7 @@ export class UserResolver {
logger.info(`login with ${email}, ***, ${publisherId} ...`)
email = email.trim().toLowerCase()
const dbUser = await DbUser.findOneOrFail({ email }, { withDeleted: true }).catch(() => {
logger.error(`User with email=${email} does not exists`)
logger.error(`User with email=${email} does not exist`)
throw new Error('No user with this credentials')
})
if (dbUser.deletedAt) {
@ -389,7 +390,7 @@ export class UserResolver {
/* uncomment this, when you need the activation link on the console */
// In case EMails are disabled log the activation link for the user
if (!emailSent) {
logger.debug(`Email not send!`)
logger.debug(`Email not sent!`)
}
logger.info('createUser() faked and send multi registration mail...')
@ -548,6 +549,7 @@ export class UserResolver {
logger.info(`setPassword(${code}, ***)...`)
// Validate Password
if (!isPassword(password)) {
logger.error('Password entered is lexically invalid')
throw new Error(
'Please enter a valid password with at least 8 characters, upper and lower case letters, at least one number and one special character!',
)
@ -610,6 +612,8 @@ export class UserResolver {
await queryRunner.connect()
await queryRunner.startTransaction('READ UNCOMMITTED')
const event = new Event()
try {
// Save user
await queryRunner.manager.save(user).catch((error) => {
@ -618,6 +622,11 @@ export class UserResolver {
})
await queryRunner.commitTransaction()
const eventActivateAccount = new EventActivateAccount()
eventActivateAccount.userId = user.id
eventProtocol.writeEvent(event.setEventActivateAccount(eventActivateAccount))
logger.info('User data written successfully...')
} catch (e) {
await queryRunner.rollbackTransaction()
@ -727,6 +736,7 @@ export class UserResolver {
try {
await queryRunner.manager.save(userEntity).catch((error) => {
logger.error('error saving user: ' + error)
throw new Error('error saving user: ' + error)
})

View File

@ -0,0 +1,40 @@
import { sendAddedContributionMessageEmail } from './sendAddedContributionMessageEmail'
import { sendEMail } from './sendEMail'
jest.mock('./sendEMail', () => {
return {
__esModule: true,
sendEMail: jest.fn(),
}
})
describe('sendAddedContributionMessageEmail', () => {
beforeEach(async () => {
await sendAddedContributionMessageEmail({
senderFirstName: 'Peter',
senderLastName: 'Lustig',
recipientFirstName: 'Bibi',
recipientLastName: 'Bloxberg',
recipientEmail: 'bibi@bloxberg.de',
senderEmail: 'peter@lustig.de',
contributionMemo: 'Vielen herzlichen Dank für den neuen Hexenbesen!',
message: 'Was für ein Besen ist es geworden?',
overviewURL: 'http://localhost/overview',
})
})
it('calls sendEMail', () => {
expect(sendEMail).toBeCalledWith({
to: `Bibi Bloxberg <bibi@bloxberg.de>`,
subject: 'Gradido Frage zur Schöpfung',
text:
expect.stringContaining('Hallo Bibi Bloxberg') &&
expect.stringContaining('Peter Lustig') &&
expect.stringContaining(
'Du hast soeben zu deinem eingereichten Gradido Schöpfungsantrag "Vielen herzlichen Dank für den neuen Hexenbesen!" eine Rückfrage von Peter Lustig erhalten.',
) &&
expect.stringContaining('Was für ein Besen ist es geworden?') &&
expect.stringContaining('http://localhost/overview'),
})
})
})

View File

@ -0,0 +1,26 @@
import { backendLogger as logger } from '@/server/logger'
import { sendEMail } from './sendEMail'
import { contributionMessageReceived } from './text/contributionMessageReceived'
export const sendAddedContributionMessageEmail = (data: {
senderFirstName: string
senderLastName: string
recipientFirstName: string
recipientLastName: string
recipientEmail: string
senderEmail: string
contributionMemo: string
message: string
overviewURL: string
}): Promise<boolean> => {
logger.info(
`sendEmail(): to=${data.recipientFirstName} ${data.recipientLastName} <${data.recipientEmail}>,
subject=${contributionMessageReceived.de.subject},
text=${contributionMessageReceived.de.text(data)}`,
)
return sendEMail({
to: `${data.recipientFirstName} ${data.recipientLastName} <${data.recipientEmail}>`,
subject: contributionMessageReceived.de.subject,
text: contributionMessageReceived.de.text(data),
})
}

View File

@ -0,0 +1,39 @@
import Decimal from 'decimal.js-light'
import { sendContributionConfirmedEmail } from './sendContributionConfirmedEmail'
import { sendEMail } from './sendEMail'
jest.mock('./sendEMail', () => {
return {
__esModule: true,
sendEMail: jest.fn(),
}
})
describe('sendContributionConfirmedEmail', () => {
beforeEach(async () => {
await sendContributionConfirmedEmail({
senderFirstName: 'Peter',
senderLastName: 'Lustig',
recipientFirstName: 'Bibi',
recipientLastName: 'Bloxberg',
recipientEmail: 'bibi@bloxberg.de',
contributionMemo: 'Vielen herzlichen Dank für den neuen Hexenbesen!',
contributionAmount: new Decimal(200.0),
overviewURL: 'http://localhost/overview',
})
})
it('calls sendEMail', () => {
expect(sendEMail).toBeCalledWith({
to: 'Bibi Bloxberg <bibi@bloxberg.de>',
subject: 'Schöpfung wurde bestätigt',
text:
expect.stringContaining('Hallo Bibi Bloxberg') &&
expect.stringContaining(
'Dein Gradido Schöpfungsantrag "Vielen herzlichen Dank für den neuen Hexenbesen!" wurde soeben bestätigt.',
) &&
expect.stringContaining('Betrag: 200,00 GDD') &&
expect.stringContaining('Link zu deinem Konto: http://localhost/overview'),
})
})
})

View File

@ -0,0 +1,26 @@
import { backendLogger as logger } from '@/server/logger'
import Decimal from 'decimal.js-light'
import { sendEMail } from './sendEMail'
import { contributionConfirmed } from './text/contributionConfirmed'
export const sendContributionConfirmedEmail = (data: {
senderFirstName: string
senderLastName: string
recipientFirstName: string
recipientLastName: string
recipientEmail: string
contributionMemo: string
contributionAmount: Decimal
overviewURL: string
}): Promise<boolean> => {
logger.info(
`sendEmail(): to=${data.recipientFirstName} ${data.recipientLastName} <${data.recipientEmail}>,
subject=${contributionConfirmed.de.subject},
text=${contributionConfirmed.de.text(data)}`,
)
return sendEMail({
to: `${data.recipientFirstName} ${data.recipientLastName} <${data.recipientEmail}>`,
subject: contributionConfirmed.de.subject,
text: contributionConfirmed.de.text(data),
})
}

View File

@ -0,0 +1,44 @@
import { sendEMail } from './sendEMail'
import Decimal from 'decimal.js-light'
import { sendTransactionLinkRedeemedEmail } from './sendTransactionLinkRedeemed'
jest.mock('./sendEMail', () => {
return {
__esModule: true,
sendEMail: jest.fn(),
}
})
describe('sendTransactionLinkRedeemedEmail', () => {
beforeEach(async () => {
await sendTransactionLinkRedeemedEmail({
email: 'bibi@bloxberg.de',
senderFirstName: 'Peter',
senderLastName: 'Lustig',
recipientFirstName: 'Bibi',
recipientLastName: 'Bloxberg',
senderEmail: 'peter@lustig.de',
amount: new Decimal(42.0),
memo: 'Vielen Dank dass Du dabei bist',
overviewURL: 'http://localhost/overview',
})
})
it('calls sendEMail', () => {
expect(sendEMail).toBeCalledWith({
to: `Bibi Bloxberg <bibi@bloxberg.de>`,
subject: 'Gradido-Link wurde eingelöst',
text:
expect.stringContaining('Hallo Bibi Bloxberg') &&
expect.stringContaining(
'Peter Lustig (peter@lustig.de) hat soeben deinen Link eingelöst.',
) &&
expect.stringContaining('Betrag: 42,00 GDD,') &&
expect.stringContaining('Memo: Vielen Dank dass Du dabei bist') &&
expect.stringContaining(
'Details zur Transaktion findest du in deinem Gradido-Konto: http://localhost/overview',
) &&
expect.stringContaining('Bitte antworte nicht auf diese E-Mail!'),
})
})
})

View File

@ -0,0 +1,28 @@
import { backendLogger as logger } from '@/server/logger'
import Decimal from 'decimal.js-light'
import { sendEMail } from './sendEMail'
import { transactionLinkRedeemed } from './text/transactionLinkRedeemed'
export const sendTransactionLinkRedeemedEmail = (data: {
email: string
senderFirstName: string
senderLastName: string
recipientFirstName: string
recipientLastName: string
senderEmail: string
amount: Decimal
memo: string
overviewURL: string
}): Promise<boolean> => {
logger.info(
`sendEmail(): to=${data.recipientFirstName} ${data.recipientLastName},
<${data.email}>,
subject=${transactionLinkRedeemed.de.subject},
text=${transactionLinkRedeemed.de.text(data)}`,
)
return sendEMail({
to: `${data.recipientFirstName} ${data.recipientLastName} <${data.email}>`,
subject: transactionLinkRedeemed.de.subject,
text: transactionLinkRedeemed.de.text(data),
})
}

View File

@ -19,7 +19,6 @@ describe('sendTransactionReceivedEmail', () => {
email: 'peter@lustig.de',
senderEmail: 'bibi@bloxberg.de',
amount: new Decimal(42.0),
memo: 'Vielen herzlichen Dank für den neuen Hexenbesen!',
overviewURL: 'http://localhost/overview',
})
})
@ -33,7 +32,6 @@ describe('sendTransactionReceivedEmail', () => {
expect.stringContaining('42,00 GDD') &&
expect.stringContaining('Bibi Bloxberg') &&
expect.stringContaining('(bibi@bloxberg.de)') &&
expect.stringContaining('Vielen herzlichen Dank für den neuen Hexenbesen!') &&
expect.stringContaining('http://localhost/overview'),
})
})

View File

@ -11,7 +11,6 @@ export const sendTransactionReceivedEmail = (data: {
email: string
senderEmail: string
amount: Decimal
memo: string
overviewURL: string
}): Promise<boolean> => {
logger.info(

View File

@ -0,0 +1,31 @@
import Decimal from 'decimal.js-light'
export const contributionConfirmed = {
de: {
subject: 'Schöpfung wurde bestätigt',
text: (data: {
senderFirstName: string
senderLastName: string
recipientFirstName: string
recipientLastName: string
contributionMemo: string
contributionAmount: Decimal
overviewURL: string
}): string =>
`Hallo ${data.recipientFirstName} ${data.recipientLastName},
Dein eingereichter Gemeinwohl-Beitrag "${data.contributionMemo}" wurde soeben von ${
data.senderFirstName
} ${data.senderLastName} bestätigt.
Betrag: ${data.contributionAmount.toFixed(2).replace('.', ',')} GDD
Bitte antworte nicht auf diese E-Mail!
Mit freundlichen Grüßen,
dein Gradido-Team
Link zu deinem Konto: ${data.overviewURL}`,
},
}

View File

@ -0,0 +1,28 @@
export const contributionMessageReceived = {
de: {
subject: 'Gradido Frage zur Schöpfung',
text: (data: {
senderFirstName: string
senderLastName: string
recipientFirstName: string
recipientLastName: string
recipientEmail: string
senderEmail: string
contributionMemo: string
message: string
overviewURL: string
}): string =>
`Hallo ${data.recipientFirstName} ${data.recipientLastName},
du hast soeben zu deinem eingereichten Gemeinwohl-Beitrag "${data.contributionMemo}" eine Rückfrage von ${data.senderFirstName} ${data.senderLastName} erhalten.
Bitte beantworte die Rückfrage in deinem Gradido-Konto im Menü "Gemeinschaft" im Tab "Meine Beiträge zum Gemeinwohl"!
Link zu deinem Konto: ${data.overviewURL}
Bitte antworte nicht auf diese E-Mail!
Mit freundlichen Grüßen,
dein Gradido-Team`,
},
}

View File

@ -0,0 +1,33 @@
import Decimal from 'decimal.js-light'
export const transactionLinkRedeemed = {
de: {
subject: 'Gradido-Link wurde eingelöst',
text: (data: {
email: string
senderFirstName: string
senderLastName: string
recipientFirstName: string
recipientLastName: string
senderEmail: string
amount: Decimal
memo: string
overviewURL: string
}): string =>
`Hallo ${data.recipientFirstName} ${data.recipientLastName}
${data.senderFirstName} ${data.senderLastName} (${
data.senderEmail
}) hat soeben deinen Link eingelöst.
Betrag: ${data.amount.toFixed(2).replace('.', ',')} GDD,
Memo: ${data.memo}
Details zur Transaktion findest du in deinem Gradido-Konto: ${data.overviewURL}
Bitte antworte nicht auf diese E-Mail!
Mit freundlichen Grüßen,
dein Gradido-Team`,
},
}

View File

@ -11,7 +11,6 @@ export const transactionReceived = {
email: string
senderEmail: string
amount: Decimal
memo: string
overviewURL: string
}): string =>
`Hallo ${data.recipientFirstName} ${data.recipientLastName}
@ -19,16 +18,12 @@ export const transactionReceived = {
Du hast soeben ${data.amount.toFixed(2).replace('.', ',')} GDD von ${data.senderFirstName} ${
data.senderLastName
} (${data.senderEmail}) erhalten.
${data.senderFirstName} ${data.senderLastName} schreibt:
${data.memo}
Details zur Transaktion findest du in deinem Gradido-Konto: ${data.overviewURL}
Bitte antworte nicht auf diese E-Mail!
Mit freundlichen Grüßen,
dein Gradido-Team
Link zu deinem Konto: ${data.overviewURL}`,
dein Gradido-Team`,
},
}

View File

@ -0,0 +1,28 @@
import connection from '@/typeorm/connection'
import { getKlickTippUser } from '@/apis/KlicktippController'
import { User } from '@entity/User'
export async function retrieveNotRegisteredEmails(): Promise<string[]> {
const con = await connection()
if (!con) {
throw new Error('No connection to database')
}
const users = await User.find()
const notRegisteredUser = []
for (let i = 0; i < users.length; i++) {
const user = users[i]
try {
await getKlickTippUser(user.email)
} catch (err) {
notRegisteredUser.push(user.email)
// eslint-disable-next-line no-console
console.log(`${user.email}`)
}
}
await con.close()
// eslint-disable-next-line no-console
console.log('User die nicht bei KlickTipp vorhanden sind: ', notRegisteredUser)
return notRegisteredUser
}
retrieveNotRegisteredEmails()

View File

@ -0,0 +1,54 @@
import {
BaseEntity,
Column,
DeleteDateColumn,
Entity,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
} from 'typeorm'
import { Contribution } from '../Contribution'
import { User } from '../User'
@Entity('contribution_messages', {
engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci',
})
export class ContributionMessage extends BaseEntity {
@PrimaryGeneratedColumn('increment', { unsigned: true })
id: number
@Column({ name: 'contribution_id', unsigned: true, nullable: false })
contributionId: number
@ManyToOne(() => Contribution, (contribution) => contribution.messages)
@JoinColumn({ name: 'contribution_id' })
contribution: Contribution
@Column({ name: 'user_id', unsigned: true, nullable: false })
userId: number
@ManyToOne(() => User, (user) => user.messages)
@JoinColumn({ name: 'user_id' })
user: User
@Column({ length: 2000, nullable: false, collation: 'utf8mb4_unicode_ci' })
message: string
@Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP', name: 'created_at' })
createdAt: Date
@Column({ type: 'datetime', default: null, nullable: true, name: 'updated_at' })
updatedAt: Date
@DeleteDateColumn({ name: 'deleted_at' })
deletedAt: Date | null
@Column({ name: 'deleted_by', default: null, unsigned: true, nullable: true })
deletedBy: number
@Column({ length: 12, nullable: false, collation: 'utf8mb4_unicode_ci' })
type: string
@Column({ name: 'is_moderator', type: 'bool', nullable: false, default: false })
isModerator: boolean
}

View File

@ -1 +1 @@
export { ContributionMessage } from './0047-messages_tables/ContributionMessage'
export { ContributionMessage } from './0048-add_is_moderator_to_contribution_messages/ContributionMessage'

View File

@ -0,0 +1,12 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
/* eslint-disable @typescript-eslint/no-explicit-any */
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn(
`ALTER TABLE \`contribution_messages\` ADD COLUMN \`is_moderator\` boolean NOT NULL DEFAULT false;`,
)
}
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn(`ALTER TABLE \`contribution_messages\` DROP COLUMN \`is_moderator\`;`)
}

View File

@ -1,6 +1,6 @@
{
"name": "gradido-database",
"version": "1.11.0",
"version": "1.12.1",
"description": "Gradido Database Tool to execute database migrations",
"main": "src/index.ts",
"repository": "https://github.com/gradido/gradido/database",

View File

@ -61,7 +61,7 @@ EVENT_PROTOCOL_DISABLED=false
DATABASE_CONFIG_VERSION=v1.2022-03-18
# frontend
FRONTEND_CONFIG_VERSION=v2.2022-04-07
FRONTEND_CONFIG_VERSION=v3.2022-09-16
GRAPHQL_URI=https://stage1.gradido.net/graphql
ADMIN_AUTH_URL=https://stage1.gradido.net/admin/authenticate?token={token}
@ -77,6 +77,8 @@ META_KEYWORDS_DE="Grundeinkommen, Währung, Dankbarkeit, Schenk-Ökonomie, Natü
META_KEYWORDS_EN="Basic Income, Currency, Gratitude, Gift Economy, Natural Economy of Life, Economy, Ecology, Potential Development, Giving and Thanking, Cycle of Life, Monetary System"
META_AUTHOR="Bernd Hückstädt - Gradido-Akademie"
SUPPORT_MAIL=support@supportmail.com
# admin
ADMIN_CONFIG_VERSION=v1.2022-03-18

View File

@ -75,7 +75,7 @@ pm2 startup
sudo apt-get install -y certbot
sudo apt-get install -y python3-certbot-nginx
sudo certbot
> Enter email address (used for urgent renewal and security notices) > support@gradido.net
> Enter email address (used for urgent renewal and security notices) > e.g. support@supportmail.com
> Please read the Terms of Service at > Y
> Would you be willing, once your first certificate is successfully issued, to > N
> No names were found in your configuration files. Please enter in your domain > stage1.gradido.net

View File

@ -1,4 +1,4 @@
CONFIG_VERSION=v2.2022-04-07
CONFIG_VERSION=v3.2022-09-16
# Environment
DEFAULT_PUBLISHER_ID=2896
@ -21,4 +21,7 @@ META_DESCRIPTION_DE="Dankbarkeit ist die Währung der neuen Zeit. Immer mehr Men
META_DESCRIPTION_EN="Gratitude is the currency of the new age. More and more people are unleashing their potential and shaping a good future for all."
META_KEYWORDS_DE="Grundeinkommen, Währung, Dankbarkeit, Schenk-Ökonomie, Natürliche Ökonomie des Lebens, Ökonomie, Ökologie, Potenzialentfaltung, Schenken und Danken, Kreislauf des Lebens, Geldsystem"
META_KEYWORDS_EN="Basic Income, Currency, Gratitude, Gift Economy, Natural Economy of Life, Economy, Ecology, Potential Development, Giving and Thanking, Cycle of Life, Monetary System"
META_AUTHOR="Bernd Hückstädt - Gradido-Akademie"
META_AUTHOR="Bernd Hückstädt - Gradido-Akademie"
# Support Mail
SUPPORT_MAIL=support@supportmail.com

View File

@ -21,4 +21,7 @@ META_DESCRIPTION_DE=$META_DESCRIPTION_DE
META_DESCRIPTION_EN=$META_DESCRIPTION_EN
META_KEYWORDS_DE=$META_KEYWORDS_DE
META_KEYWORDS_EN=$META_KEYWORDS_EN
META_AUTHOR=$META_AUTHOR
META_AUTHOR=$META_AUTHOR
# Support Mail
SUPPORT_MAIL=$SUPPORT_MAIL

View File

@ -1,6 +1,6 @@
{
"name": "bootstrap-vue-gradido-wallet",
"version": "1.11.0",
"version": "1.12.1",
"private": true,
"scripts": {
"start": "node run/server.js",

View File

@ -14,7 +14,9 @@
<b-button type="reset" variant="danger">{{ $t('form.cancel') }}</b-button>
</b-col>
<b-col class="text-right">
<b-button type="submit" variant="primary">{{ $t('form.reply') }}</b-button>
<b-button type="submit" variant="primary" :disabled="disabled">
{{ $t('form.reply') }}
</b-button>
</b-col>
</b-row>
</b-form>
@ -63,5 +65,13 @@ export default {
this.form.text = ''
},
},
computed: {
disabled() {
if (this.form.text !== '') {
return false
}
return true
},
},
}
</script>

View File

@ -7,7 +7,6 @@
</b-container>
<contribution-messages-formular
v-if="['PENDING', 'IN_PROGRESS'].includes(state)"
class="mt-5"
:contributionId="contributionId"
@get-list-contribution-messages="getListContributionMessages"
@update-state="updateState"

View File

@ -4,7 +4,7 @@
<span class="ml-2 mr-2">{{ message.userFirstName }} {{ message.userLastName }}</span>
<span class="ml-2">{{ $d(new Date(message.createdAt), 'short') }}</span>
<small class="ml-4 text-success">{{ $t('community.moderator') }}</small>
<div class="mt-2 h3">{{ message.message }}</div>
<div class="mt-2">{{ message.message }}</div>
</div>
</template>
<script>

View File

@ -2,11 +2,9 @@
<div class="slot-is-not-moderator">
<div class="text-right">
<b-avatar :text="initialLetters" variant="info"></b-avatar>
<span class="ml-2 mr-2 text-bold">
{{ message.userFirstName }} {{ message.userLastName }}
</span>
<span class="ml-2 mr-2">{{ message.userFirstName }} {{ message.userLastName }}</span>
<span class="ml-2">{{ $d(new Date(message.createdAt), 'short') }}</span>
<div class="mt-2 h3">{{ message.message }}</div>
<div class="mt-2">{{ message.message }}</div>
</div>
</div>
</template>

View File

@ -4,6 +4,7 @@
<contribution-list-item
v-bind="item"
:contributionId="item.id"
:allContribution="allContribution"
@update-contribution-form="updateContributionForm"
@delete-contribution="deleteContribution"
@update-state="updateState"
@ -44,6 +45,11 @@ export default {
required: true,
},
pageSize: { type: Number, default: 25 },
allContribution: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {

View File

@ -27,12 +27,9 @@
</span>
</div>
<div class="mr-2">{{ memo }}</div>
<div
v-if="!['CONFIRMED', 'DELETED'].includes(state) && !firstName"
class="d-flex flex-row-reverse"
>
<div class="d-flex flex-row-reverse">
<div
v-if="!['CONFIRMED', 'DELETED'].includes(state)"
v-if="!['CONFIRMED', 'DELETED'].includes(state) && !allContribution"
class="pointer ml-5"
@click="
$emit('update-contribution-form', {
@ -46,7 +43,7 @@
<b-icon icon="pencil" class="h2"></b-icon>
</div>
<div
v-if="!['CONFIRMED', 'DELETED'].includes(state)"
v-if="!['CONFIRMED', 'DELETED'].includes(state) && !allContribution"
class="pointer"
@click="deleteContribution({ id })"
>
@ -144,6 +141,11 @@ export default {
type: Number,
required: true,
},
allContribution: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {

View File

@ -45,7 +45,7 @@ describe('LanguageSwitch', () => {
expect(wrapper.find('div.language-switch').exists()).toBeTruthy()
})
describe('with locales en, de and es', () => {
describe('with locales en, de, es, fr, and nl', () => {
describe('empty store', () => {
describe('navigator language is "en-US"', () => {
const languageGetter = jest.spyOn(navigator, 'language', 'get')
@ -94,11 +94,11 @@ describe('LanguageSwitch', () => {
describe('navigator language is "nl-NL"', () => {
const languageGetter = jest.spyOn(navigator, 'language', 'get')
it('shows Dutch as language ', async () => {
it('shows Nederlands as language ', async () => {
languageGetter.mockReturnValue('nl-NL')
wrapper.vm.setCurrentLanguage()
await wrapper.vm.$nextTick()
expect(wrapper.find('button.dropdown-toggle').text()).toBe('Holandés - nl')
expect(wrapper.find('button.dropdown-toggle').text()).toBe('Nederlands - nl')
})
})
@ -153,16 +153,16 @@ describe('LanguageSwitch', () => {
})
describe('language "nl" in store', () => {
it('shows Dutch as language', async () => {
it('shows Nederlands as language', async () => {
wrapper.vm.$store.state.language = 'nl'
wrapper.vm.setCurrentLanguage()
await wrapper.vm.$nextTick()
expect(wrapper.find('button.dropdown-toggle').text()).toBe('Holandés - nl')
expect(wrapper.find('button.dropdown-toggle').text()).toBe('Nederlands - nl')
})
})
describe('dropdown menu', () => {
it('has English and German as languages to choose', () => {
it('has five languages to choose from', () => {
expect(wrapper.findAll('li')).toHaveLength(5)
})
@ -174,16 +174,16 @@ describe('LanguageSwitch', () => {
expect(wrapper.findAll('li').at(1).text()).toBe('Deutsch')
})
it('has Español as second language to choose', () => {
it('has Español as third language to choose', () => {
expect(wrapper.findAll('li').at(2).text()).toBe('Español')
})
it('has French as second language to choose', () => {
it('has French as fourth language to choose', () => {
expect(wrapper.findAll('li').at(3).text()).toBe('Français')
})
it('has Dutch as second language to choose', () => {
expect(wrapper.findAll('li').at(4).text()).toBe('Holandés')
it('has Nederlands as fith language to choose', () => {
expect(wrapper.findAll('li').at(4).text()).toBe('Nederlands')
})
})
})

View File

@ -46,10 +46,11 @@ describe('LanguageSwitch', () => {
expect(wrapper.find('div.language-switch').exists()).toBe(true)
})
describe('with locales en and de', () => {
describe('with locales en, de, es, fr, and nl', () => {
describe('empty store', () => {
describe('navigator language is "en-US"', () => {
const languageGetter = jest.spyOn(navigator, 'language', 'get')
it('shows English as default navigator langauge', async () => {
languageGetter.mockReturnValue('en-US')
wrapper.vm.setCurrentLanguage()
@ -57,8 +58,10 @@ describe('LanguageSwitch', () => {
expect(wrapper.findAll('span.locales').at(0).text()).toBe('English')
})
})
describe('navigator language is "de-DE"', () => {
const languageGetter = jest.spyOn(navigator, 'language', 'get')
it('shows Deutsch as language ', async () => {
languageGetter.mockReturnValue('de-DE')
wrapper.vm.setCurrentLanguage()
@ -66,8 +69,10 @@ describe('LanguageSwitch', () => {
expect(wrapper.findAll('span.locales').at(1).text()).toBe('Deutsch')
})
})
describe('navigator language is "es-ES"', () => {
const languageGetter = jest.spyOn(navigator, 'language', 'get')
it('shows Español as language ', async () => {
languageGetter.mockReturnValue('es-ES')
wrapper.vm.setCurrentLanguage()
@ -75,8 +80,10 @@ describe('LanguageSwitch', () => {
expect(wrapper.findAll('span.locales').at(2).text()).toBe('Español')
})
})
describe('navigator language is "fr-FR"', () => {
const languageGetter = jest.spyOn(navigator, 'language', 'get')
it('shows French as language ', async () => {
languageGetter.mockReturnValue('fr-FR')
wrapper.vm.setCurrentLanguage()
@ -84,17 +91,21 @@ describe('LanguageSwitch', () => {
expect(wrapper.findAll('span.locales').at(3).text()).toBe('Français')
})
})
describe('navigator language is "nl-NL"', () => {
const languageGetter = jest.spyOn(navigator, 'language', 'get')
it('shows Dutch as language ', async () => {
it('shows Nederlands as language ', async () => {
languageGetter.mockReturnValue('nl-NL')
wrapper.vm.setCurrentLanguage()
await wrapper.vm.$nextTick()
expect(wrapper.findAll('span.locales').at(4).text()).toBe('Holandés')
expect(wrapper.findAll('span.locales').at(4).text()).toBe('Nederlands')
})
})
describe('navigator language is "it-IT" (not supported)', () => {
const languageGetter = jest.spyOn(navigator, 'language', 'get')
it('shows English as language ', async () => {
languageGetter.mockReturnValue('it-IT')
wrapper.vm.setCurrentLanguage()
@ -102,8 +113,10 @@ describe('LanguageSwitch', () => {
expect(wrapper.findAll('span.locales').at(0).text()).toBe('English')
})
})
describe('no navigator langauge', () => {
const languageGetter = jest.spyOn(navigator, 'language', 'get')
it('shows English as language ', async () => {
languageGetter.mockReturnValue(null)
wrapper.vm.setCurrentLanguage()
@ -112,6 +125,7 @@ describe('LanguageSwitch', () => {
})
})
})
describe('language "de" in store', () => {
it('shows Deutsch as language', async () => {
wrapper.vm.$store.state.language = 'de'
@ -120,6 +134,7 @@ describe('LanguageSwitch', () => {
expect(wrapper.findAll('span.locales').at(1).text()).toBe('English')
})
})
describe('language "es" in store', () => {
it('shows Español as language', async () => {
wrapper.vm.$store.state.language = 'es'
@ -128,6 +143,7 @@ describe('LanguageSwitch', () => {
expect(wrapper.findAll('span.locales').at(2).text()).toBe('Deutsch')
})
})
describe('language "fr" in store', () => {
it('shows French as language', async () => {
wrapper.vm.$store.state.language = 'fr'
@ -136,43 +152,77 @@ describe('LanguageSwitch', () => {
expect(wrapper.findAll('span.locales').at(3).text()).toBe('Español')
})
})
describe('language "nl" in store', () => {
it('shows Dutch as language', async () => {
it('shows Nederlands as language', async () => {
wrapper.vm.$store.state.language = 'nl'
wrapper.vm.setCurrentLanguage()
await wrapper.vm.$nextTick()
expect(wrapper.findAll('span.locales').at(4).text()).toBe('Français')
})
})
describe('language menu', () => {
it('has English, German and Español as languages to choose', () => {
beforeAll(async () => {
wrapper.vm.$store.state.language = 'en'
wrapper.vm.setCurrentLanguage()
await wrapper.vm.$nextTick()
})
it('has five languages to choose from', () => {
expect(wrapper.findAll('span.locales')).toHaveLength(5)
})
it('has English as first language to choose', () => {
expect(wrapper.findAll('span.locales').at(0).text()).toBe('Holandés')
expect(wrapper.findAll('span.locales').at(0).text()).toBe('English')
})
it('has German as second language to choose', () => {
expect(wrapper.findAll('span.locales').at(1).text()).toBe('English')
it('has Deutsch as second language to choose', () => {
expect(wrapper.findAll('span.locales').at(1).text()).toBe('Deutsch')
})
it('has Español as third language to choose', () => {
expect(wrapper.findAll('span.locales').at(2).text()).toBe('Deutsch')
expect(wrapper.findAll('span.locales').at(2).text()).toBe('Español')
})
it('has French as third language to choose', () => {
expect(wrapper.findAll('span.locales').at(3).text()).toBe('Español')
it('has Français as fourth language to choose', () => {
expect(wrapper.findAll('span.locales').at(3).text()).toBe('Français')
})
it('has Dutch as third language to choose', () => {
expect(wrapper.findAll('span.locales').at(4).text()).toBe('Français')
it('has Nederlands as fifth language to choose', () => {
expect(wrapper.findAll('span.locales').at(4).text()).toBe('Nederlands')
})
})
})
describe('calls the API', () => {
it("with locale 'de'", () => {
wrapper.findAll('span.locales').at(2).trigger('click')
wrapper.findAll('span.locales').at(1).trigger('click')
expect(updateUserInfosMutationMock).toBeCalledWith(
expect.objectContaining({ variables: { locale: 'de' } }),
)
})
it("with locale 'es'", () => {
wrapper.findAll('span.locales').at(2).trigger('click')
expect(updateUserInfosMutationMock).toBeCalledWith(
expect.objectContaining({ variables: { locale: 'es' } }),
)
})
it("with locale 'fr'", () => {
wrapper.findAll('span.locales').at(3).trigger('click')
expect(updateUserInfosMutationMock).toBeCalledWith(
expect.objectContaining({ variables: { locale: 'fr' } }),
)
})
it("with locale 'nl'", () => {
wrapper.findAll('span.locales').at(4).trigger('click')
expect(updateUserInfosMutationMock).toBeCalledWith(
expect.objectContaining({ variables: { locale: 'nl' } }),
)
})
})
})
})

View File

@ -8,7 +8,7 @@ const constants = {
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
CONFIG_VERSION: {
DEFAULT: 'DEFAULT',
EXPECTED: 'v2.2022-04-07',
EXPECTED: 'v3.2022-09-16',
CURRENT: '',
},
}
@ -60,6 +60,10 @@ const meta = {
META_AUTHOR: process.env.META_AUTHOR || 'Bernd Hückstädt - Gradido-Akademie',
}
const supportmail = {
SUPPORT_MAIL: process.env.SUPPORT_MAIL || 'support@supportmail.com',
}
// Check config version
constants.CONFIG_VERSION.CURRENT = process.env.CONFIG_VERSION || constants.CONFIG_VERSION.DEFAULT
if (
@ -79,6 +83,7 @@ const CONFIG = {
...endpoints,
...community,
...meta,
...supportmail,
}
module.exports = CONFIG

View File

@ -253,7 +253,7 @@
"en": "English",
"es": "Español",
"fr": "Français",
"nl": "Dutch",
"nl": "Nederlands",
"success": "Deine Sprache wurde erfolgreich geändert."
},
"name": {

View File

@ -253,7 +253,7 @@
"en": "English",
"es": "Español",
"fr": "Français",
"nl": "Holandés",
"nl": "Nederlands",
"success": "Your language has been successfully updated."
},
"name": {

View File

@ -254,8 +254,8 @@
"de": "Deutsch",
"en": "English",
"es": "Español",
"fr": "Frans",
"nl": "Holandés",
"fr": "Français",
"nl": "Nederlands",
"success": "Tu idioma ha sido cambiado con éxito."
},
"name": {

View File

@ -251,11 +251,11 @@
"settings": {
"language": {
"changeLanguage": "Changer la langue",
"de": "Allemand",
"en": "Anglais",
"es": "Espagnol",
"de": "Deutsch",
"en": "English",
"es": "Español",
"fr": "Français",
"nl": "Néerlandais",
"nl": "Nederlands",
"success": "Votre langue de préférence a bien été actualisée."
},
"name": {

View File

@ -24,7 +24,7 @@ const locales = [
enabled: true,
},
{
name: 'Holandés',
name: 'Nederlands',
code: 'nl',
iso: 'nl-NL',
enabled: true,

View File

@ -251,10 +251,10 @@
"settings": {
"language": {
"changeLanguage": "Taal veranderen",
"de": "Duits",
"en": "Engels",
"es": "Spaans",
"fr": "Frans",
"de": "Deutsch",
"en": "English",
"es": "Español",
"fr": "Français",
"nl": "Nederlands",
"success": "Jouw taal werd succesvol veranderd."
},

View File

@ -73,6 +73,7 @@
:contributionCount="contributionCountAll"
:showPagination="true"
:pageSize="pageSizeAll"
:allContribution="true"
/>
</b-tab>
</b-tabs>

View File

@ -83,7 +83,7 @@ export default {
countAdminUser: null,
itemsContributionLinks: [],
itemsAdminUser: [],
supportMail: 'support@supportemail.de',
supportMail: CONFIG.SUPPORT_MAIL,
membersCount: '1203',
totalUsers: null,
totalGradidoCreated: null,

View File

@ -1,6 +1,6 @@
{
"name": "gradido",
"version": "1.11.0",
"version": "1.12.1",
"description": "Gradido",
"main": "index.js",
"repository": "git@github.com:gradido/gradido.git",