mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
Merge branch 'master' into fix-semaphore-deadlock-on-error
This commit is contained in:
commit
fd2563f171
3
.github/workflows/lint_pr.yml
vendored
3
.github/workflows/lint_pr.yml
vendored
@ -29,6 +29,9 @@ jobs:
|
||||
admin
|
||||
database
|
||||
release
|
||||
federation
|
||||
workflow
|
||||
docker
|
||||
other
|
||||
# Configure that a scope must always be provided.
|
||||
requireScope: true
|
||||
|
||||
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@ -437,7 +437,7 @@ jobs:
|
||||
report_name: Coverage Frontend
|
||||
type: lcov
|
||||
result_path: ./coverage/lcov.info
|
||||
min_coverage: 89
|
||||
min_coverage: 91
|
||||
token: ${{ github.token }}
|
||||
|
||||
##############################################################################
|
||||
@ -527,7 +527,7 @@ jobs:
|
||||
report_name: Coverage Backend
|
||||
type: lcov
|
||||
result_path: ./backend/coverage/lcov.info
|
||||
min_coverage: 76
|
||||
min_coverage: 78
|
||||
token: ${{ github.token }}
|
||||
|
||||
##########################################################################
|
||||
|
||||
6
.vscode/extensions.json
vendored
6
.vscode/extensions.json
vendored
@ -3,6 +3,10 @@
|
||||
"streetsidesoftware.code-spell-checker",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"hediet.vscode-drawio"
|
||||
"hediet.vscode-drawio",
|
||||
"streetsidesoftware.code-spell-checker-german",
|
||||
"mtxr.sqltools",
|
||||
"mtxr.sqltools-driver-mysql",
|
||||
"jcbuisson.vue"
|
||||
]
|
||||
}
|
||||
17
.vscode/settings.json
vendored
17
.vscode/settings.json
vendored
@ -1,3 +1,18 @@
|
||||
{
|
||||
"git.ignoreLimitWarning": true
|
||||
"git.ignoreLimitWarning": true,
|
||||
"sqltools.connections": [
|
||||
{
|
||||
"mysqlOptions": {
|
||||
"authProtocol": "default"
|
||||
},
|
||||
"previewLimit": 50,
|
||||
"server": "localhost",
|
||||
"port": 3306,
|
||||
"driver": "MariaDB",
|
||||
"name": "localhost",
|
||||
"database": "gradido_community",
|
||||
"username": "root",
|
||||
"password": ""
|
||||
}
|
||||
],
|
||||
}
|
||||
@ -4141,9 +4141,9 @@ caniuse-api@^3.0.0:
|
||||
lodash.uniq "^4.5.0"
|
||||
|
||||
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000844, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001271:
|
||||
version "1.0.30001354"
|
||||
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001354.tgz"
|
||||
integrity sha512-mImKeCkyGDAHNywYFA4bqnLAzTUvVkqPvhY4DV47X+Gl2c5Z8c3KNETnXp14GQt11LvxE8AwjzGxJ+rsikiOzg==
|
||||
version "1.0.30001442"
|
||||
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001442.tgz"
|
||||
integrity sha512-239m03Pqy0hwxYPYR5JwOIxRJfLTWtle9FV8zosfV5pHg+/51uD4nxcUlM8+mWWGfwKtt8lJNHnD3cWw9VZ6ow==
|
||||
|
||||
capture-exit@^2.0.0:
|
||||
version "2.0.0"
|
||||
|
||||
@ -66,5 +66,5 @@ EVENT_PROTOCOL_DISABLED=false
|
||||
# if you set the value of FEDERATION_DHT_TOPIC, the DHT hyperswarm will start to announce and listen
|
||||
# on an hash created from this topic
|
||||
# FEDERATION_DHT_TOPIC=GRADIDO_HUB
|
||||
# FEDERATION_DHT_SEED=64ebcb0e3ad547848fef4197c6e2332f
|
||||
# FEDERATION_DHT_SEED=64ebcb0e3ad547848fef4197c6e2332f
|
||||
# FEDERATION_COMMUNITY_URL=http://localhost:4000/api
|
||||
|
||||
@ -31,7 +31,6 @@
|
||||
"express": "^4.17.1",
|
||||
"graphql": "^15.5.1",
|
||||
"i18n": "^0.15.1",
|
||||
"jest": "^27.2.4",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"log4js": "^6.4.6",
|
||||
|
||||
@ -35,6 +35,7 @@ export enum RIGHTS {
|
||||
SEARCH_ADMIN_USERS = 'SEARCH_ADMIN_USERS',
|
||||
CREATE_CONTRIBUTION_MESSAGE = 'CREATE_CONTRIBUTION_MESSAGE',
|
||||
LIST_ALL_CONTRIBUTION_MESSAGES = 'LIST_ALL_CONTRIBUTION_MESSAGES',
|
||||
OPEN_CREATIONS = 'OPEN_CREATIONS',
|
||||
// Admin
|
||||
SEARCH_USERS = 'SEARCH_USERS',
|
||||
SET_USER_ROLE = 'SET_USER_ROLE',
|
||||
|
||||
@ -33,6 +33,7 @@ export const ROLE_USER = new Role('user', [
|
||||
RIGHTS.COMMUNITY_STATISTICS,
|
||||
RIGHTS.CREATE_CONTRIBUTION_MESSAGE,
|
||||
RIGHTS.LIST_ALL_CONTRIBUTION_MESSAGES,
|
||||
RIGHTS.OPEN_CREATIONS,
|
||||
])
|
||||
export const ROLE_ADMIN = new Role('admin', Object.values(RIGHTS)) // all rights
|
||||
|
||||
|
||||
@ -10,14 +10,14 @@ Decimal.set({
|
||||
})
|
||||
|
||||
const constants = {
|
||||
DB_VERSION: '0058-add_communities_table',
|
||||
DB_VERSION: '0059-add_hide_amount_to_users',
|
||||
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
|
||||
LOG_LEVEL: process.env.LOG_LEVEL || 'info',
|
||||
CONFIG_VERSION: {
|
||||
DEFAULT: 'DEFAULT',
|
||||
EXPECTED: 'v14.2022-11-22',
|
||||
EXPECTED: 'v14.2022-12-22',
|
||||
CURRENT: '',
|
||||
},
|
||||
}
|
||||
@ -70,11 +70,13 @@ const email = {
|
||||
EMAIL: process.env.EMAIL === 'true' || false,
|
||||
EMAIL_TEST_MODUS: process.env.EMAIL_TEST_MODUS === 'true' || false,
|
||||
EMAIL_TEST_RECEIVER: process.env.EMAIL_TEST_RECEIVER || 'stage1@gradido.net',
|
||||
EMAIL_USERNAME: process.env.EMAIL_USERNAME || 'gradido_email',
|
||||
EMAIL_USERNAME: process.env.EMAIL_USERNAME || '',
|
||||
EMAIL_SENDER: process.env.EMAIL_SENDER || 'info@gradido.net',
|
||||
EMAIL_PASSWORD: process.env.EMAIL_PASSWORD || 'xxx',
|
||||
EMAIL_SMTP_URL: process.env.EMAIL_SMTP_URL || 'gmail.com',
|
||||
EMAIL_SMTP_PORT: process.env.EMAIL_SMTP_PORT || '587',
|
||||
EMAIL_PASSWORD: process.env.EMAIL_PASSWORD || '',
|
||||
EMAIL_SMTP_URL: process.env.EMAIL_SMTP_URL || 'mailserver',
|
||||
EMAIL_SMTP_PORT: process.env.EMAIL_SMTP_PORT || '1025',
|
||||
// eslint-disable-next-line no-unneeded-ternary
|
||||
EMAIL_TLS: process.env.EMAIL_TLS === 'false' ? false : true,
|
||||
EMAIL_LINK_VERIFICATION:
|
||||
process.env.EMAIL_LINK_VERIFICATION || 'http://localhost/checkEmail/{optin}{code}',
|
||||
EMAIL_LINK_SETPASSWORD:
|
||||
|
||||
@ -41,7 +41,7 @@ export const sendEmailTranslated = async (params: {
|
||||
host: CONFIG.EMAIL_SMTP_URL,
|
||||
port: Number(CONFIG.EMAIL_SMTP_PORT),
|
||||
secure: false, // true for 465, false for other ports
|
||||
requireTLS: true,
|
||||
requireTLS: CONFIG.EMAIL_TLS,
|
||||
auth: {
|
||||
user: CONFIG.EMAIL_USERNAME,
|
||||
pass: CONFIG.EMAIL_PASSWORD,
|
||||
|
||||
@ -327,18 +327,20 @@ describe('sendEmailVariants', () => {
|
||||
to: 'Peter Lustig <peter@lustig.de>',
|
||||
from: 'Gradido (do not answer) <info@gradido.net>',
|
||||
attachments: [],
|
||||
subject: 'Gradido: Your common good contribution was confirmed',
|
||||
subject: 'Gradido: Your contribution to the common good was confirmed',
|
||||
html: expect.any(String),
|
||||
text: expect.stringContaining('GRADIDO: YOUR COMMON GOOD CONTRIBUTION WAS CONFIRMED'),
|
||||
text: expect.stringContaining(
|
||||
'GRADIDO: YOUR CONTRIBUTION TO THE COMMON GOOD WAS CONFIRMED',
|
||||
),
|
||||
}),
|
||||
})
|
||||
expect(result.originalMessage.html).toContain('<!DOCTYPE html>')
|
||||
expect(result.originalMessage.html).toContain('<html lang="en">')
|
||||
expect(result.originalMessage.html).toContain(
|
||||
'<title>Gradido: Your common good contribution was confirmed</title>',
|
||||
'<title>Gradido: Your contribution to the common good was confirmed</title>',
|
||||
)
|
||||
expect(result.originalMessage.html).toContain(
|
||||
'>Gradido: Your common good contribution was confirmed</h1>',
|
||||
'>Gradido: Your contribution to the common good was confirmed</h1>',
|
||||
)
|
||||
expect(result.originalMessage.html).toContain('Hello Peter Lustig')
|
||||
expect(result.originalMessage.html).toContain(
|
||||
|
||||
@ -19,4 +19,10 @@ export default class UpdateUserInfosArgs {
|
||||
|
||||
@Field({ nullable: true })
|
||||
passwordNew?: string
|
||||
|
||||
@Field({ nullable: true })
|
||||
hideAmountGDD?: boolean
|
||||
|
||||
@Field({ nullable: true })
|
||||
hideAmountGDT?: boolean
|
||||
}
|
||||
|
||||
14
backend/src/graphql/model/OpenCreation.ts
Normal file
14
backend/src/graphql/model/OpenCreation.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { ObjectType, Field, Int } from 'type-graphql'
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
@ObjectType()
|
||||
export class OpenCreation {
|
||||
@Field(() => Int)
|
||||
month: number
|
||||
|
||||
@Field(() => Int)
|
||||
year: number
|
||||
|
||||
@Field(() => Decimal)
|
||||
amount: Decimal
|
||||
}
|
||||
@ -1,13 +1,11 @@
|
||||
import { ObjectType, Field } from 'type-graphql'
|
||||
import { KlickTipp } from './KlickTipp'
|
||||
import { User as dbUser } from '@entity/User'
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { FULL_CREATION_AVAILABLE } from '../resolver/const/const'
|
||||
import { UserContact } from './UserContact'
|
||||
|
||||
@ObjectType()
|
||||
export class User {
|
||||
constructor(user: dbUser, creation: Decimal[] = FULL_CREATION_AVAILABLE) {
|
||||
constructor(user: dbUser) {
|
||||
this.id = user.id
|
||||
this.gradidoID = user.gradidoID
|
||||
this.alias = user.alias
|
||||
@ -26,15 +24,13 @@ export class User {
|
||||
this.isAdmin = user.isAdmin
|
||||
this.klickTipp = null
|
||||
this.hasElopage = null
|
||||
this.creation = creation
|
||||
this.hideAmountGDD = user.hideAmountGDD
|
||||
this.hideAmountGDT = user.hideAmountGDT
|
||||
}
|
||||
|
||||
@Field(() => Number)
|
||||
id: number
|
||||
|
||||
// `public_key` binary(32) DEFAULT NULL,
|
||||
// `privkey` binary(80) DEFAULT NULL,
|
||||
|
||||
@Field(() => String)
|
||||
gradidoID: string
|
||||
|
||||
@ -60,9 +56,6 @@ export class User {
|
||||
@Field(() => Date, { nullable: true })
|
||||
deletedAt: Date | null
|
||||
|
||||
// `password` bigint(20) unsigned DEFAULT 0,
|
||||
// `email_hash` binary(32) DEFAULT NULL,
|
||||
|
||||
@Field(() => Date)
|
||||
createdAt: Date
|
||||
|
||||
@ -72,12 +65,16 @@ export class User {
|
||||
@Field(() => String)
|
||||
language: string
|
||||
|
||||
@Field(() => Boolean)
|
||||
hideAmountGDD: boolean
|
||||
|
||||
@Field(() => Boolean)
|
||||
hideAmountGDT: boolean
|
||||
|
||||
// This is not the users publisherId, but the one of the users who recommend him
|
||||
@Field(() => Number, { nullable: true })
|
||||
publisherId: number | null
|
||||
|
||||
// `passphrase` text COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
|
||||
@Field(() => Date, { nullable: true })
|
||||
isAdmin: Date | null
|
||||
|
||||
@ -86,7 +83,4 @@ export class User {
|
||||
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
hasElopage: boolean | null
|
||||
|
||||
@Field(() => [Decimal])
|
||||
creation: Decimal[]
|
||||
}
|
||||
|
||||
@ -32,7 +32,7 @@ export class BalanceResolver {
|
||||
|
||||
const lastTransaction = context.lastTransaction
|
||||
? context.lastTransaction
|
||||
: await dbTransaction.findOne({ userId: user.id }, { order: { balanceDate: 'DESC' } })
|
||||
: await dbTransaction.findOne({ userId: user.id }, { order: { id: 'DESC' } })
|
||||
|
||||
logger.debug(`lastTransaction=${lastTransaction}`)
|
||||
|
||||
|
||||
@ -1146,13 +1146,21 @@ describe('ContributionResolver', () => {
|
||||
const now = new Date()
|
||||
|
||||
beforeAll(async () => {
|
||||
creation = await creationFactory(testEnv, {
|
||||
email: 'peter@lustig.de',
|
||||
amount: 400,
|
||||
memo: 'Herzlich Willkommen bei Gradido!',
|
||||
creationDate: contributionDateFormatter(
|
||||
new Date(now.getFullYear(), now.getMonth() - 1, 1),
|
||||
),
|
||||
await mutate({
|
||||
mutation: adminCreateContribution,
|
||||
variables: {
|
||||
email: 'peter@lustig.de',
|
||||
amount: 400,
|
||||
memo: 'Herzlich Willkommen bei Gradido!',
|
||||
creationDate: contributionDateFormatter(
|
||||
new Date(now.getFullYear(), now.getMonth() - 1, 1),
|
||||
),
|
||||
},
|
||||
})
|
||||
creation = await Contribution.findOneOrFail({
|
||||
where: {
|
||||
memo: 'Herzlich Willkommen bei Gradido!',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@ -1879,6 +1887,10 @@ describe('ContributionResolver', () => {
|
||||
new Date(now.getFullYear(), now.getMonth() - 2, 1),
|
||||
),
|
||||
})
|
||||
await query({
|
||||
query: login,
|
||||
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||
})
|
||||
})
|
||||
|
||||
it('returns true', async () => {
|
||||
@ -1935,6 +1947,23 @@ describe('ContributionResolver', () => {
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
describe('confirm same contribution again', () => {
|
||||
it('throws an error', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: confirmContribution,
|
||||
variables: {
|
||||
id: creation ? creation.id : -1,
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('Contribution already confirmd.')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('confirm two creations one after the other quickly', () => {
|
||||
@ -1959,6 +1988,10 @@ describe('ContributionResolver', () => {
|
||||
new Date(now.getFullYear(), now.getMonth() - 2, 1),
|
||||
),
|
||||
})
|
||||
await query({
|
||||
query: login,
|
||||
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||
})
|
||||
})
|
||||
|
||||
it('throws no error for the second confirmation', async () => {
|
||||
|
||||
@ -11,8 +11,9 @@ import { Transaction as DbTransaction } from '@entity/Transaction'
|
||||
import { AdminCreateContributions } from '@model/AdminCreateContributions'
|
||||
import { AdminUpdateContribution } from '@model/AdminUpdateContribution'
|
||||
import { Contribution, ContributionListResult } from '@model/Contribution'
|
||||
import { UnconfirmedContribution } from '@model/UnconfirmedContribution'
|
||||
import { Decay } from '@model/Decay'
|
||||
import { OpenCreation } from '@model/OpenCreation'
|
||||
import { UnconfirmedContribution } from '@model/UnconfirmedContribution'
|
||||
import { TransactionTypeId } from '@enum/TransactionTypeId'
|
||||
import { Order } from '@enum/Order'
|
||||
import { ContributionType } from '@enum/ContributionType'
|
||||
@ -27,6 +28,7 @@ import { RIGHTS } from '@/auth/RIGHTS'
|
||||
import { Context, getUser, getClientTimezoneOffset } from '@/server/context'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
import {
|
||||
getCreationDates,
|
||||
getUserCreation,
|
||||
getUserCreations,
|
||||
validateContribution,
|
||||
@ -553,108 +555,116 @@ export class ContributionResolver {
|
||||
@Arg('id', () => Int) id: number,
|
||||
@Ctx() context: Context,
|
||||
): Promise<boolean> {
|
||||
const clientTimezoneOffset = getClientTimezoneOffset(context)
|
||||
const contribution = await DbContribution.findOne(id)
|
||||
if (!contribution) {
|
||||
logger.error(`Contribution not found for given id: ${id}`)
|
||||
throw new Error('Contribution not found to given id.')
|
||||
}
|
||||
const moderatorUser = getUser(context)
|
||||
if (moderatorUser.id === contribution.userId) {
|
||||
logger.error('Moderator can not confirm own contribution')
|
||||
throw new Error('Moderator can not confirm own contribution')
|
||||
}
|
||||
const user = await DbUser.findOneOrFail(
|
||||
{ id: contribution.userId },
|
||||
{ withDeleted: true, relations: ['emailContact'] },
|
||||
)
|
||||
if (user.deletedAt) {
|
||||
logger.error('This user was deleted. Cannot confirm a contribution.')
|
||||
throw new Error('This user was deleted. Cannot confirm a contribution.')
|
||||
}
|
||||
const creations = await getUserCreation(contribution.userId, clientTimezoneOffset, false)
|
||||
validateContribution(
|
||||
creations,
|
||||
contribution.amount,
|
||||
contribution.contributionDate,
|
||||
clientTimezoneOffset,
|
||||
)
|
||||
|
||||
// acquire lock
|
||||
const releaseLock = await TRANSACTIONS_LOCK.acquire()
|
||||
|
||||
const receivedCallDate = new Date()
|
||||
const queryRunner = getConnection().createQueryRunner()
|
||||
await queryRunner.connect()
|
||||
await queryRunner.startTransaction('REPEATABLE READ') // 'READ COMMITTED')
|
||||
try {
|
||||
const lastTransaction = await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
.select('transaction')
|
||||
.from(DbTransaction, 'transaction')
|
||||
.where('transaction.userId = :id', { id: contribution.userId })
|
||||
.orderBy('transaction.id', 'DESC')
|
||||
.getOne()
|
||||
logger.info('lastTransaction ID', lastTransaction ? lastTransaction.id : 'undefined')
|
||||
|
||||
let newBalance = new Decimal(0)
|
||||
let decay: Decay | null = null
|
||||
if (lastTransaction) {
|
||||
decay = calculateDecay(
|
||||
lastTransaction.balance,
|
||||
lastTransaction.balanceDate,
|
||||
receivedCallDate,
|
||||
)
|
||||
newBalance = decay.balance
|
||||
const clientTimezoneOffset = getClientTimezoneOffset(context)
|
||||
const contribution = await DbContribution.findOne(id)
|
||||
if (!contribution) {
|
||||
logger.error(`Contribution not found for given id: ${id}`)
|
||||
throw new Error('Contribution not found to given id.')
|
||||
}
|
||||
newBalance = newBalance.add(contribution.amount.toString())
|
||||
if (contribution.confirmedAt) {
|
||||
logger.error(`Contribution already confirmd: ${id}`)
|
||||
throw new Error('Contribution already confirmd.')
|
||||
}
|
||||
const moderatorUser = getUser(context)
|
||||
if (moderatorUser.id === contribution.userId) {
|
||||
logger.error('Moderator can not confirm own contribution')
|
||||
throw new Error('Moderator can not confirm own contribution')
|
||||
}
|
||||
const user = await DbUser.findOneOrFail(
|
||||
{ id: contribution.userId },
|
||||
{ withDeleted: true, relations: ['emailContact'] },
|
||||
)
|
||||
if (user.deletedAt) {
|
||||
logger.error('This user was deleted. Cannot confirm a contribution.')
|
||||
throw new Error('This user was deleted. Cannot confirm a contribution.')
|
||||
}
|
||||
const creations = await getUserCreation(contribution.userId, clientTimezoneOffset, false)
|
||||
validateContribution(
|
||||
creations,
|
||||
contribution.amount,
|
||||
contribution.contributionDate,
|
||||
clientTimezoneOffset,
|
||||
)
|
||||
|
||||
const transaction = new DbTransaction()
|
||||
transaction.typeId = TransactionTypeId.CREATION
|
||||
transaction.memo = contribution.memo
|
||||
transaction.userId = contribution.userId
|
||||
transaction.previous = lastTransaction ? lastTransaction.id : null
|
||||
transaction.amount = contribution.amount
|
||||
transaction.creationDate = contribution.contributionDate
|
||||
transaction.balance = newBalance
|
||||
transaction.balanceDate = receivedCallDate
|
||||
transaction.decay = decay ? decay.decay : new Decimal(0)
|
||||
transaction.decayStart = decay ? decay.start : null
|
||||
await queryRunner.manager.insert(DbTransaction, transaction)
|
||||
const receivedCallDate = new Date()
|
||||
const queryRunner = getConnection().createQueryRunner()
|
||||
await queryRunner.connect()
|
||||
await queryRunner.startTransaction('REPEATABLE READ') // 'READ COMMITTED')
|
||||
try {
|
||||
const lastTransaction = await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
.select('transaction')
|
||||
.from(DbTransaction, 'transaction')
|
||||
.where('transaction.userId = :id', { id: contribution.userId })
|
||||
.orderBy('transaction.id', 'DESC')
|
||||
.getOne()
|
||||
logger.info('lastTransaction ID', lastTransaction ? lastTransaction.id : 'undefined')
|
||||
|
||||
contribution.confirmedAt = receivedCallDate
|
||||
contribution.confirmedBy = moderatorUser.id
|
||||
contribution.transactionId = transaction.id
|
||||
contribution.contributionStatus = ContributionStatus.CONFIRMED
|
||||
await queryRunner.manager.update(DbContribution, { id: contribution.id }, contribution)
|
||||
let newBalance = new Decimal(0)
|
||||
let decay: Decay | null = null
|
||||
if (lastTransaction) {
|
||||
decay = calculateDecay(
|
||||
lastTransaction.balance,
|
||||
lastTransaction.balanceDate,
|
||||
receivedCallDate,
|
||||
)
|
||||
newBalance = decay.balance
|
||||
}
|
||||
newBalance = newBalance.add(contribution.amount.toString())
|
||||
|
||||
await queryRunner.commitTransaction()
|
||||
logger.info('creation commited successfuly.')
|
||||
sendContributionConfirmedEmail({
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
email: user.emailContact.email,
|
||||
language: user.language,
|
||||
senderFirstName: moderatorUser.firstName,
|
||||
senderLastName: moderatorUser.lastName,
|
||||
contributionMemo: contribution.memo,
|
||||
contributionAmount: contribution.amount,
|
||||
})
|
||||
} catch (e) {
|
||||
await queryRunner.rollbackTransaction()
|
||||
logger.error('Creation was not successful', e)
|
||||
throw new Error('Creation was not successful.')
|
||||
const transaction = new DbTransaction()
|
||||
transaction.typeId = TransactionTypeId.CREATION
|
||||
transaction.memo = contribution.memo
|
||||
transaction.userId = contribution.userId
|
||||
transaction.previous = lastTransaction ? lastTransaction.id : null
|
||||
transaction.amount = contribution.amount
|
||||
transaction.creationDate = contribution.contributionDate
|
||||
transaction.balance = newBalance
|
||||
transaction.balanceDate = receivedCallDate
|
||||
transaction.decay = decay ? decay.decay : new Decimal(0)
|
||||
transaction.decayStart = decay ? decay.start : null
|
||||
await queryRunner.manager.insert(DbTransaction, transaction)
|
||||
|
||||
contribution.confirmedAt = receivedCallDate
|
||||
contribution.confirmedBy = moderatorUser.id
|
||||
contribution.transactionId = transaction.id
|
||||
contribution.contributionStatus = ContributionStatus.CONFIRMED
|
||||
await queryRunner.manager.update(DbContribution, { id: contribution.id }, contribution)
|
||||
|
||||
await queryRunner.commitTransaction()
|
||||
logger.info('creation commited successfuly.')
|
||||
sendContributionConfirmedEmail({
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
email: user.emailContact.email,
|
||||
language: user.language,
|
||||
senderFirstName: moderatorUser.firstName,
|
||||
senderLastName: moderatorUser.lastName,
|
||||
contributionMemo: contribution.memo,
|
||||
contributionAmount: contribution.amount,
|
||||
})
|
||||
} catch (e) {
|
||||
await queryRunner.rollbackTransaction()
|
||||
logger.error('Creation was not successful', e)
|
||||
throw new Error('Creation was not successful.')
|
||||
} finally {
|
||||
await queryRunner.release()
|
||||
}
|
||||
|
||||
const event = new Event()
|
||||
const eventContributionConfirm = new EventContributionConfirm()
|
||||
eventContributionConfirm.userId = user.id
|
||||
eventContributionConfirm.amount = contribution.amount
|
||||
eventContributionConfirm.contributionId = contribution.id
|
||||
await eventProtocol.writeEvent(event.setEventContributionConfirm(eventContributionConfirm))
|
||||
} finally {
|
||||
await queryRunner.release()
|
||||
releaseLock()
|
||||
}
|
||||
|
||||
const event = new Event()
|
||||
const eventContributionConfirm = new EventContributionConfirm()
|
||||
eventContributionConfirm.userId = user.id
|
||||
eventContributionConfirm.amount = contribution.amount
|
||||
eventContributionConfirm.contributionId = contribution.id
|
||||
await eventProtocol.writeEvent(event.setEventContributionConfirm(eventContributionConfirm))
|
||||
return true
|
||||
}
|
||||
|
||||
@ -683,4 +693,23 @@ export class ContributionResolver {
|
||||
)
|
||||
// return userTransactions.map((t) => new Transaction(t, new User(user), communityUser))
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.OPEN_CREATIONS])
|
||||
@Query(() => [OpenCreation])
|
||||
async openCreations(
|
||||
@Arg('userId', () => Int, { nullable: true }) userId: number | null,
|
||||
@Ctx() context: Context,
|
||||
): Promise<OpenCreation[]> {
|
||||
const id = userId || getUser(context).id
|
||||
const clientTimezoneOffset = getClientTimezoneOffset(context)
|
||||
const creationDates = getCreationDates(clientTimezoneOffset)
|
||||
const creations = await getUserCreation(id, clientTimezoneOffset)
|
||||
return creationDates.map((date, index) => {
|
||||
return {
|
||||
month: date.getMonth(),
|
||||
year: date.getFullYear(),
|
||||
amount: creations[index],
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -211,7 +211,7 @@ export class TransactionResolver {
|
||||
// find current balance
|
||||
const lastTransaction = await dbTransaction.findOne(
|
||||
{ userId: user.id },
|
||||
{ order: { balanceDate: 'DESC' }, relations: ['contribution'] },
|
||||
{ order: { id: 'DESC' }, relations: ['contribution'] },
|
||||
)
|
||||
logger.debug(`lastTransaction=${lastTransaction}`)
|
||||
|
||||
|
||||
@ -63,6 +63,7 @@ jest.mock('@/emails/sendEmailVariants', () => {
|
||||
})
|
||||
|
||||
/*
|
||||
|
||||
jest.mock('@/apis/KlicktippController', () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
@ -132,6 +133,8 @@ describe('UserResolver', () => {
|
||||
{
|
||||
id: expect.any(Number),
|
||||
gradidoID: expect.any(String),
|
||||
hideAmountGDD: expect.any(Boolean),
|
||||
hideAmountGDT: expect.any(Boolean),
|
||||
alias: null,
|
||||
emailContact: expect.any(UserContact), // 'peter@lustig.de',
|
||||
emailId: expect.any(Number),
|
||||
|
||||
@ -58,7 +58,7 @@ import {
|
||||
EventSendConfirmationEmail,
|
||||
EventActivateAccount,
|
||||
} from '@/event/Event'
|
||||
import { getUserCreation, getUserCreations } from './util/creations'
|
||||
import { getUserCreations } from './util/creations'
|
||||
import { isValidPassword } from '@/password/EncryptorUtils'
|
||||
import { FULL_CREATION_AVAILABLE } from './const/const'
|
||||
import { encryptPassword, verifyPassword } from '@/password/PasswordEncryptor'
|
||||
@ -114,9 +114,8 @@ export class UserResolver {
|
||||
async verifyLogin(@Ctx() context: Context): Promise<User> {
|
||||
logger.info('verifyLogin...')
|
||||
// TODO refactor and do not have duplicate code with login(see below)
|
||||
const clientTimezoneOffset = getClientTimezoneOffset(context)
|
||||
const userEntity = getUser(context)
|
||||
const user = new User(userEntity, await getUserCreation(userEntity.id, clientTimezoneOffset))
|
||||
const user = new User(userEntity)
|
||||
// Elopage Status & Stored PublisherId
|
||||
user.hasElopage = await this.hasElopage(context)
|
||||
|
||||
@ -132,7 +131,6 @@ export class UserResolver {
|
||||
@Ctx() context: Context,
|
||||
): Promise<User> {
|
||||
logger.info(`login with ${email}, ***, ${publisherId} ...`)
|
||||
const clientTimezoneOffset = getClientTimezoneOffset(context)
|
||||
email = email.trim().toLowerCase()
|
||||
const dbUser = await findUserByEmail(email)
|
||||
if (dbUser.deletedAt) {
|
||||
@ -163,7 +161,7 @@ export class UserResolver {
|
||||
logger.addContext('user', dbUser.id)
|
||||
logger.debug('validation of login credentials successful...')
|
||||
|
||||
const user = new User(dbUser, await getUserCreation(dbUser.id, clientTimezoneOffset))
|
||||
const user = new User(dbUser)
|
||||
logger.debug(`user= ${JSON.stringify(user, null, 2)}`)
|
||||
|
||||
i18n.setLocale(user.language)
|
||||
@ -567,7 +565,15 @@ export class UserResolver {
|
||||
@Mutation(() => Boolean)
|
||||
async updateUserInfos(
|
||||
@Args()
|
||||
{ firstName, lastName, language, password, passwordNew }: UpdateUserInfosArgs,
|
||||
{
|
||||
firstName,
|
||||
lastName,
|
||||
language,
|
||||
password,
|
||||
passwordNew,
|
||||
hideAmountGDD,
|
||||
hideAmountGDT,
|
||||
}: UpdateUserInfosArgs,
|
||||
@Ctx() context: Context,
|
||||
): Promise<boolean> {
|
||||
logger.info(`updateUserInfos(${firstName}, ${lastName}, ${language}, ***, ***)...`)
|
||||
@ -609,6 +615,15 @@ export class UserResolver {
|
||||
userEntity.password = encryptPassword(userEntity, passwordNew)
|
||||
}
|
||||
|
||||
// Save hideAmountGDD value
|
||||
if (hideAmountGDD !== undefined) {
|
||||
userEntity.hideAmountGDD = hideAmountGDD
|
||||
}
|
||||
// Save hideAmountGDT value
|
||||
if (hideAmountGDT !== undefined) {
|
||||
userEntity.hideAmountGDT = hideAmountGDT
|
||||
}
|
||||
|
||||
const queryRunner = getConnection().createQueryRunner()
|
||||
await queryRunner.connect()
|
||||
await queryRunner.startTransaction('REPEATABLE READ')
|
||||
|
||||
@ -101,15 +101,19 @@ export const getUserCreation = async (
|
||||
}
|
||||
|
||||
const getCreationMonths = (timezoneOffset: number): number[] => {
|
||||
return getCreationDates(timezoneOffset).map((date) => date.getMonth() + 1)
|
||||
}
|
||||
|
||||
export const getCreationDates = (timezoneOffset: number): Date[] => {
|
||||
const clientNow = new Date()
|
||||
clientNow.setTime(clientNow.getTime() - timezoneOffset * 60 * 1000)
|
||||
logger.info(
|
||||
`getCreationMonths -- offset: ${timezoneOffset} -- clientNow: ${clientNow.toISOString()}`,
|
||||
)
|
||||
return [
|
||||
new Date(clientNow.getFullYear(), clientNow.getMonth() - 2, 1).getMonth() + 1,
|
||||
new Date(clientNow.getFullYear(), clientNow.getMonth() - 1, 1).getMonth() + 1,
|
||||
clientNow.getMonth() + 1,
|
||||
new Date(clientNow.getFullYear(), clientNow.getMonth() - 2, 1),
|
||||
new Date(clientNow.getFullYear(), clientNow.getMonth() - 1, 1),
|
||||
clientNow,
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@ -21,7 +21,7 @@
|
||||
},
|
||||
"contributionConfirmed": {
|
||||
"commonGoodContributionConfirmed": "Your public good contribution “{contributionMemo}” has just been confirmed by {senderFirstName} {senderLastName} and credited to your Gradido account.",
|
||||
"subject": "Gradido: Your common good contribution was confirmed"
|
||||
"subject": "Gradido: Your contribution to the common good was confirmed"
|
||||
},
|
||||
"contributionRejected": {
|
||||
"commonGoodContributionRejected": "Your public good contribution “{contributionMemo}” was rejected by {senderFirstName} {senderLastName}.",
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
import { login, adminCreateContribution, confirmContribution } from '@/seeds/graphql/mutations'
|
||||
import { login, createContribution, confirmContribution } from '@/seeds/graphql/mutations'
|
||||
import { CreationInterface } from '@/seeds/creation/CreationInterface'
|
||||
import { ApolloServerTestClient } from 'apollo-server-testing'
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
@ -19,43 +18,27 @@ export const creationFactory = async (
|
||||
creation: CreationInterface,
|
||||
): Promise<Contribution | void> => {
|
||||
const { mutate } = client
|
||||
logger.trace('creationFactory...')
|
||||
await mutate({ mutation: login, variables: { email: 'peter@lustig.de', password: 'Aa12345_' } })
|
||||
logger.trace('creationFactory... after login')
|
||||
// TODO it would be nice to have this mutation return the id
|
||||
await mutate({ mutation: adminCreateContribution, variables: { ...creation } })
|
||||
logger.trace('creationFactory... after adminCreateContribution')
|
||||
await mutate({ mutation: login, variables: { email: creation.email, password: 'Aa12345_' } })
|
||||
|
||||
const user = await findUserByEmail(creation.email) // userContact.user
|
||||
const {
|
||||
data: { createContribution: contribution },
|
||||
} = await mutate({ mutation: createContribution, variables: { ...creation } })
|
||||
|
||||
const pendingCreation = await Contribution.findOneOrFail({
|
||||
where: { userId: user.id, amount: creation.amount },
|
||||
order: { createdAt: 'DESC' },
|
||||
})
|
||||
logger.trace(
|
||||
'creationFactory... after Contribution.findOneOrFail pendingCreation=',
|
||||
pendingCreation,
|
||||
)
|
||||
if (creation.confirmed) {
|
||||
logger.trace('creationFactory... creation.confirmed=', creation.confirmed)
|
||||
await mutate({ mutation: confirmContribution, variables: { id: pendingCreation.id } })
|
||||
logger.trace('creationFactory... after confirmContribution')
|
||||
const confirmedCreation = await Contribution.findOneOrFail({ id: pendingCreation.id })
|
||||
logger.trace(
|
||||
'creationFactory... after Contribution.findOneOrFail confirmedCreation=',
|
||||
confirmedCreation,
|
||||
)
|
||||
const user = await findUserByEmail(creation.email) // userContact.user
|
||||
|
||||
await mutate({ mutation: login, variables: { email: 'peter@lustig.de', password: 'Aa12345_' } })
|
||||
await mutate({ mutation: confirmContribution, variables: { id: contribution.id } })
|
||||
const confirmedContribution = await Contribution.findOneOrFail({ id: contribution.id })
|
||||
|
||||
if (creation.moveCreationDate) {
|
||||
logger.trace('creationFactory... creation.moveCreationDate=', creation.moveCreationDate)
|
||||
const transaction = await Transaction.findOneOrFail({
|
||||
where: { userId: user.id, creationDate: new Date(creation.creationDate) },
|
||||
order: { balanceDate: 'DESC' },
|
||||
})
|
||||
logger.trace('creationFactory... after Transaction.findOneOrFail transaction=', transaction)
|
||||
|
||||
if (transaction.decay.equals(0) && transaction.creationDate) {
|
||||
confirmedCreation.contributionDate = new Date(
|
||||
confirmedContribution.contributionDate = new Date(
|
||||
nMonthsBefore(transaction.creationDate, creation.moveCreationDate),
|
||||
)
|
||||
transaction.creationDate = new Date(
|
||||
@ -64,17 +47,11 @@ export const creationFactory = async (
|
||||
transaction.balanceDate = new Date(
|
||||
nMonthsBefore(transaction.balanceDate, creation.moveCreationDate),
|
||||
)
|
||||
logger.trace('creationFactory... before transaction.save transaction=', transaction)
|
||||
await transaction.save()
|
||||
logger.trace(
|
||||
'creationFactory... before confirmedCreation.save confirmedCreation=',
|
||||
confirmedCreation,
|
||||
)
|
||||
await confirmedCreation.save()
|
||||
await confirmedContribution.save()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.trace('creationFactory... pendingCreation=', pendingCreation)
|
||||
return pendingCreation
|
||||
return contribution
|
||||
}
|
||||
}
|
||||
|
||||
@ -31,6 +31,8 @@ export const updateUserInfos = gql`
|
||||
$password: String
|
||||
$passwordNew: String
|
||||
$locale: String
|
||||
$hideAmountGDD: Boolean
|
||||
$hideAmountGDT: Boolean
|
||||
) {
|
||||
updateUserInfos(
|
||||
firstName: $firstName
|
||||
@ -38,6 +40,8 @@ export const updateUserInfos = gql`
|
||||
password: $password
|
||||
passwordNew: $passwordNew
|
||||
language: $locale
|
||||
hideAmountGDD: $hideAmountGDD
|
||||
hideAmountGDT: $hideAmountGDT
|
||||
)
|
||||
}
|
||||
`
|
||||
|
||||
@ -23,8 +23,8 @@ const setHeadersPlugin = {
|
||||
|
||||
const filterVariables = (variables: any) => {
|
||||
const vars = clonedeep(variables)
|
||||
if (vars.password) vars.password = '***'
|
||||
if (vars.passwordNew) vars.passwordNew = '***'
|
||||
if (vars && vars.password) vars.password = '***'
|
||||
if (vars && vars.passwordNew) vars.passwordNew = '***'
|
||||
return vars
|
||||
}
|
||||
|
||||
|
||||
@ -18,6 +18,8 @@ const communityDbUser: dbUser = {
|
||||
lastName: 'Akademie',
|
||||
deletedAt: null,
|
||||
password: BigInt(0),
|
||||
hideAmountGDD: false,
|
||||
hideAmountGDT: false,
|
||||
// emailHash: Buffer.from(''),
|
||||
createdAt: new Date(),
|
||||
// emailChecked: false,
|
||||
|
||||
@ -1913,9 +1913,9 @@ camelcase@^6.2.0:
|
||||
integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==
|
||||
|
||||
caniuse-lite@^1.0.30001264:
|
||||
version "1.0.30001418"
|
||||
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001418.tgz"
|
||||
integrity sha512-oIs7+JL3K9JRQ3jPZjlH6qyYDp+nBTCais7hjh0s+fuBwufc7uZ7hPYMXrDOJhV360KGMTcczMRObk0/iMqZRg==
|
||||
version "1.0.30001442"
|
||||
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001442.tgz"
|
||||
integrity sha512-239m03Pqy0hwxYPYR5JwOIxRJfLTWtle9FV8zosfV5pHg+/51uD4nxcUlM8+mWWGfwKtt8lJNHnD3cWw9VZ6ow==
|
||||
|
||||
chacha20-universal@^1.0.4:
|
||||
version "1.0.4"
|
||||
|
||||
@ -6,7 +6,7 @@ import {
|
||||
DeleteDateColumn,
|
||||
OneToOne,
|
||||
} from 'typeorm'
|
||||
import { User } from './User'
|
||||
import { User } from '../User'
|
||||
|
||||
@Entity('user_contacts', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
|
||||
export class UserContact extends BaseEntity {
|
||||
|
||||
118
database/entity/0059-add_hide_amount_to_users/User.ts
Normal file
118
database/entity/0059-add_hide_amount_to_users/User.ts
Normal file
@ -0,0 +1,118 @@
|
||||
import {
|
||||
BaseEntity,
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
DeleteDateColumn,
|
||||
OneToMany,
|
||||
JoinColumn,
|
||||
OneToOne,
|
||||
} from 'typeorm'
|
||||
import { Contribution } from '../Contribution'
|
||||
import { ContributionMessage } from '../ContributionMessage'
|
||||
import { UserContact } from '../UserContact'
|
||||
|
||||
@Entity('users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
|
||||
export class User extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('increment', { unsigned: true })
|
||||
id: number
|
||||
|
||||
@Column({
|
||||
name: 'gradido_id',
|
||||
length: 36,
|
||||
nullable: false,
|
||||
collation: 'utf8mb4_unicode_ci',
|
||||
})
|
||||
gradidoID: string
|
||||
|
||||
@Column({
|
||||
name: 'alias',
|
||||
length: 20,
|
||||
nullable: true,
|
||||
default: null,
|
||||
collation: 'utf8mb4_unicode_ci',
|
||||
})
|
||||
alias: string
|
||||
|
||||
@OneToOne(() => UserContact, (emailContact: UserContact) => emailContact.user)
|
||||
@JoinColumn({ name: 'email_id' })
|
||||
emailContact: UserContact
|
||||
|
||||
@Column({ name: 'email_id', type: 'int', unsigned: true, nullable: true, default: null })
|
||||
emailId: number | null
|
||||
|
||||
@Column({
|
||||
name: 'first_name',
|
||||
length: 255,
|
||||
nullable: true,
|
||||
default: null,
|
||||
collation: 'utf8mb4_unicode_ci',
|
||||
})
|
||||
firstName: string
|
||||
|
||||
@Column({
|
||||
name: 'last_name',
|
||||
length: 255,
|
||||
nullable: true,
|
||||
default: null,
|
||||
collation: 'utf8mb4_unicode_ci',
|
||||
})
|
||||
lastName: string
|
||||
|
||||
@Column({ name: 'created_at', default: () => 'CURRENT_TIMESTAMP', nullable: false })
|
||||
createdAt: Date
|
||||
|
||||
@DeleteDateColumn({ name: 'deleted_at', nullable: true })
|
||||
deletedAt: Date | null
|
||||
|
||||
@Column({ type: 'bigint', default: 0, unsigned: true })
|
||||
password: BigInt
|
||||
|
||||
@Column({
|
||||
name: 'password_encryption_type',
|
||||
type: 'int',
|
||||
unsigned: true,
|
||||
nullable: false,
|
||||
default: 0,
|
||||
})
|
||||
passwordEncryptionType: number
|
||||
|
||||
@Column({ length: 4, default: 'de', collation: 'utf8mb4_unicode_ci', nullable: false })
|
||||
language: string
|
||||
|
||||
@Column({ type: 'bool', default: false })
|
||||
hideAmountGDD: boolean
|
||||
|
||||
@Column({ type: 'bool', default: false })
|
||||
hideAmountGDT: boolean
|
||||
|
||||
@Column({ name: 'is_admin', type: 'datetime', nullable: true, default: null })
|
||||
isAdmin: Date | null
|
||||
|
||||
@Column({ name: 'referrer_id', type: 'int', unsigned: true, nullable: true, default: null })
|
||||
referrerId?: number | null
|
||||
|
||||
@Column({
|
||||
name: 'contribution_link_id',
|
||||
type: 'int',
|
||||
unsigned: true,
|
||||
nullable: true,
|
||||
default: null,
|
||||
})
|
||||
contributionLinkId?: number | null
|
||||
|
||||
@Column({ name: 'publisher_id', default: 0 })
|
||||
publisherId: number
|
||||
|
||||
@OneToMany(() => Contribution, (contribution) => contribution.user)
|
||||
@JoinColumn({ name: 'user_id' })
|
||||
contributions?: Contribution[]
|
||||
|
||||
@OneToMany(() => ContributionMessage, (message) => message.user)
|
||||
@JoinColumn({ name: 'user_id' })
|
||||
messages?: ContributionMessage[]
|
||||
|
||||
@OneToMany(() => UserContact, (userContact: UserContact) => userContact.user)
|
||||
@JoinColumn({ name: 'user_id' })
|
||||
userContacts?: UserContact[]
|
||||
}
|
||||
@ -1 +1 @@
|
||||
export { User } from './0057-clear_old_password_junk/User'
|
||||
export { User } from './0059-add_hide_amount_to_users/User'
|
||||
|
||||
12
database/migrations/0059-add_hide_amount_to_users.ts
Normal file
12
database/migrations/0059-add_hide_amount_to_users.ts
Normal 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 users ADD COLUMN hideAmountGDD bool DEFAULT false;')
|
||||
await queryFn('ALTER TABLE users ADD COLUMN hideAmountGDT bool DEFAULT false;')
|
||||
}
|
||||
|
||||
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||
await queryFn('ALTER TABLE users DROP COLUMN hideAmountGDD;')
|
||||
await queryFn('ALTER TABLE users DROP COLUMN hideAmountGDT;')
|
||||
}
|
||||
@ -27,11 +27,10 @@ COMMUNITY_DESCRIPTION="Gradido Development Stage1 Test Community"
|
||||
COMMUNITY_SUPPORT_MAIL=support@supportmail.com
|
||||
|
||||
# backend
|
||||
BACKEND_CONFIG_VERSION=v13.2022-12-20
|
||||
BACKEND_CONFIG_VERSION=v14.2022-12-22
|
||||
|
||||
JWT_EXPIRES_IN=10m
|
||||
GDT_API_URL=https://gdt.gradido.net
|
||||
ENV_NAME=stage1
|
||||
|
||||
TYPEORM_LOGGING_RELATIVE_PATH=../deployment/bare_metal/log/typeorm.backend.log
|
||||
|
||||
@ -64,7 +63,7 @@ EVENT_PROTOCOL_DISABLED=false
|
||||
# if you set the value of FEDERATION_DHT_TOPIC, the DHT hyperswarm will start to announce and listen
|
||||
# on an hash created from this topic
|
||||
# FEDERATION_DHT_TOPIC=GRADIDO_HUB
|
||||
# FEDERATION_DHT_SEED=64ebcb0e3ad547848fef4197c6e2332f
|
||||
# FEDERATION_DHT_SEED=64ebcb0e3ad547848fef4197c6e2332f
|
||||
|
||||
# database
|
||||
DATABASE_CONFIG_VERSION=v1.2022-03-18
|
||||
|
||||
@ -231,3 +231,32 @@ This opens the `crontab` in edit-mode and insert the following entry:
|
||||
```bash
|
||||
0 4 * * * find /tmp -name "yarn--*" -ctime +1 -exec rm -r {} \; > /dev/null
|
||||
```
|
||||
|
||||
## Define Cronjob To start backup script automatically
|
||||
|
||||
At least at production stage we need a daily backup of our database. This can be done by adding a cronjob
|
||||
to start the existing backup.sh script.
|
||||
|
||||
### On production / stage3 / stage2
|
||||
|
||||
To check for existing cronjobs for the `gradido` user, please
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
crontab -l
|
||||
```
|
||||
|
||||
This show all existing entries of the crontab for user `gradido`
|
||||
|
||||
To install/add the cronjob for a daily backup at 3:00am please
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
crontab -e
|
||||
```
|
||||
and insert the following line
|
||||
```bash
|
||||
0 3 * * * ~/gradido/deployment/bare_metal/backup.sh
|
||||
```
|
||||
|
||||
@ -112,6 +112,17 @@ services:
|
||||
volumes:
|
||||
- /sessions
|
||||
|
||||
########################################################
|
||||
# MAILSERVER TO FAKE SMTP ##############################
|
||||
########################################################
|
||||
mailserver:
|
||||
image: maildev/maildev
|
||||
ports:
|
||||
- 1080:1080
|
||||
- 1025:1025
|
||||
networks:
|
||||
- external-net
|
||||
|
||||
volumes:
|
||||
frontend_node_modules:
|
||||
admin_node_modules:
|
||||
|
||||
@ -81,6 +81,17 @@ services:
|
||||
nginx:
|
||||
image: gradido/nginx:test
|
||||
|
||||
########################################################
|
||||
# MAILSERVER TO FAKE SMTP ##############################
|
||||
########################################################
|
||||
mailserver:
|
||||
image: maildev/maildev
|
||||
ports:
|
||||
- 1080:1080
|
||||
- 1025:1025
|
||||
networks:
|
||||
- external-net
|
||||
|
||||
networks:
|
||||
external-net:
|
||||
internal-net:
|
||||
|
||||
@ -0,0 +1,103 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import OpenCreationsAmount from './OpenCreationsAmount.vue'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
describe('OpenCreationsAmount', () => {
|
||||
let wrapper
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
$d: jest.fn((date, formatter = null) => {
|
||||
return { date, formatter }
|
||||
}),
|
||||
}
|
||||
|
||||
const thisMonth = new Date()
|
||||
const lastMonth = new Date(thisMonth.getFullYear(), thisMonth.getMonth() - 1)
|
||||
|
||||
const propsData = {
|
||||
minimalDate: lastMonth,
|
||||
maxGddLastMonth: 400,
|
||||
maxGddThisMonth: 600,
|
||||
}
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(OpenCreationsAmount, {
|
||||
localVue,
|
||||
mocks,
|
||||
propsData,
|
||||
})
|
||||
}
|
||||
|
||||
describe('mount', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('renders the component', () => {
|
||||
expect(wrapper.find('div.appBoxShadow').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('renders two dates', () => {
|
||||
expect(mocks.$d).toBeCalledTimes(2)
|
||||
})
|
||||
|
||||
it('renders the date of last month', () => {
|
||||
expect(mocks.$d).toBeCalledWith(lastMonth, 'monthAndYear')
|
||||
})
|
||||
|
||||
it('renders the date of this month', () => {
|
||||
expect(mocks.$d).toBeCalledWith(expect.any(Date), 'monthAndYear')
|
||||
})
|
||||
|
||||
describe('open creations for both months', () => {
|
||||
it('renders submitted contributions text', () => {
|
||||
expect(mocks.$t).toBeCalledWith('contribution.submit')
|
||||
})
|
||||
|
||||
it('does not render max reached text', () => {
|
||||
expect(mocks.$t).not.toBeCalledWith('maxReached')
|
||||
})
|
||||
|
||||
it('renders submitted hours last month', () => {
|
||||
expect(wrapper.findAll('div.row').at(1).findAll('div.col').at(2).text()).toBe('30 h')
|
||||
})
|
||||
|
||||
it('renders available hours last month', () => {
|
||||
expect(wrapper.findAll('div.row').at(1).findAll('div.col').at(3).text()).toBe('20 h')
|
||||
})
|
||||
|
||||
it('renders submitted hours this month', () => {
|
||||
expect(wrapper.findAll('div.row').at(2).findAll('div.col').at(2).text()).toBe('20 h')
|
||||
})
|
||||
|
||||
it('renders available hours this month', () => {
|
||||
expect(wrapper.findAll('div.row').at(2).findAll('div.col').at(3).text()).toBe('30 h')
|
||||
})
|
||||
})
|
||||
|
||||
describe('no creations available for last month', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.setProps({ maxGddLastMonth: 0 })
|
||||
})
|
||||
|
||||
it('renders submitted contributions text', () => {
|
||||
expect(mocks.$t).toBeCalledWith('contribution.submit')
|
||||
})
|
||||
|
||||
it('renders max reached text', () => {
|
||||
expect(mocks.$t).toBeCalledWith('maxReached')
|
||||
})
|
||||
|
||||
it('renders submitted hours last month', () => {
|
||||
expect(wrapper.findAll('div.row').at(1).findAll('div.col').at(2).text()).toBe('50 h')
|
||||
})
|
||||
|
||||
it('renders available hours last month', () => {
|
||||
expect(wrapper.findAll('div.row').at(1).findAll('div.col').at(3).text()).toBe('0 h')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -14,9 +14,9 @@
|
||||
{{ maxGddLastMonth > 0 ? $t('contribution.submit') : $t('maxReached') }}
|
||||
</b-col>
|
||||
<b-col class="d-none d-md-inline text-197 text-center">
|
||||
{{ (1000 - maxGddLastMonth) / 20 }} {{ $t('h') }}
|
||||
{{ hoursSubmittedLastMonth }} {{ $t('h') }}
|
||||
</b-col>
|
||||
<b-col class="text-4 text-center">{{ maxGddLastMonth / 20 }} {{ $t('h') }}</b-col>
|
||||
<b-col class="text-4 text-center">{{ hoursAvailableLastMonth }} {{ $t('h') }}</b-col>
|
||||
</b-row>
|
||||
|
||||
<b-row class="font-weight-bold">
|
||||
@ -25,9 +25,9 @@
|
||||
{{ maxGddThisMonth > 0 ? $t('contribution.submit') : $t('maxReached') }}
|
||||
</b-col>
|
||||
<b-col class="d-none d-md-inline text-197 text-center">
|
||||
{{ (1000 - maxGddThisMonth) / 20 }} {{ $t('h') }}
|
||||
{{ hoursSubmittedThisMonth }} {{ $t('h') }}
|
||||
</b-col>
|
||||
<b-col class="text-4 text-center">{{ maxGddThisMonth / 20 }} {{ $t('h') }}</b-col>
|
||||
<b-col class="text-4 text-center">{{ hoursAvailableThisMonth }} {{ $t('h') }}</b-col>
|
||||
</b-row>
|
||||
</div>
|
||||
</div>
|
||||
@ -40,5 +40,19 @@ export default {
|
||||
maxGddLastMonth: { type: Number, required: true },
|
||||
maxGddThisMonth: { type: Number, required: true },
|
||||
},
|
||||
computed: {
|
||||
hoursSubmittedThisMonth() {
|
||||
return (1000 - this.maxGddThisMonth) / 20
|
||||
},
|
||||
hoursSubmittedLastMonth() {
|
||||
return (1000 - this.maxGddLastMonth) / 20
|
||||
},
|
||||
hoursAvailableThisMonth() {
|
||||
return this.maxGddThisMonth / 20
|
||||
},
|
||||
hoursAvailableLastMonth() {
|
||||
return this.maxGddLastMonth / 20
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -37,7 +37,7 @@
|
||||
</b-row>
|
||||
<b-row class="mt-5 p-5">
|
||||
<b-col>
|
||||
<b-button @click="$emit('on-reset')">{{ $t('back') }}</b-button>
|
||||
<b-button @click="$emit('on-back')">{{ $t('back') }}</b-button>
|
||||
</b-col>
|
||||
<b-col class="text-right">
|
||||
<b-button
|
||||
|
||||
@ -41,7 +41,7 @@
|
||||
</b-row>
|
||||
<b-row class="mt-5 p-5">
|
||||
<b-col>
|
||||
<b-button @click="$emit('on-reset')">{{ $t('back') }}</b-button>
|
||||
<b-button @click="$emit('on-back')">{{ $t('back') }}</b-button>
|
||||
</b-col>
|
||||
<b-col class="text-right">
|
||||
<b-button
|
||||
|
||||
@ -322,7 +322,7 @@ Die ganze Welt bezwingen.“`)
|
||||
[
|
||||
{
|
||||
email: 'someone@watches.tv',
|
||||
amount: '87.23',
|
||||
amount: 87.23,
|
||||
memo: 'Long enough',
|
||||
selected: 'send',
|
||||
},
|
||||
|
||||
@ -1,112 +1,115 @@
|
||||
<template>
|
||||
<b-row class="transaction-form">
|
||||
<b-col cols="12">
|
||||
<b-card class="appBoxShadow gradido-border-radius" body-class="p-3">
|
||||
<validation-observer v-slot="{ handleSubmit }" ref="formValidator">
|
||||
<b-form role="form" @submit.prevent="handleSubmit(onSubmit)" @reset="onReset">
|
||||
<b-form-radio-group v-model="radioSelected" class="container">
|
||||
<b-row class="mb-4">
|
||||
<b-col cols="12" lg="6">
|
||||
<b-row class="bg-248 gradido-border-radius pt-lg-2 mr-lg-2">
|
||||
<b-col cols="10" @click="radioSelected = sendTypes.send" class="pointer">
|
||||
{{ $t('send_gdd') }}
|
||||
</b-col>
|
||||
<b-col cols="2">
|
||||
<b-form-radio
|
||||
name="shipping"
|
||||
size="lg"
|
||||
:value="sendTypes.send"
|
||||
stacked
|
||||
class="custom-radio-button pointer"
|
||||
></b-form-radio>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-col>
|
||||
<div class="transaction-form">
|
||||
<b-row>
|
||||
<b-col cols="12">
|
||||
<b-card class="appBoxShadow gradido-border-radius" body-class="p-3">
|
||||
<validation-observer v-slot="{ handleSubmit }" ref="formValidator">
|
||||
<b-form role="form" @submit.prevent="handleSubmit(onSubmit)" @reset="onReset">
|
||||
<b-form-radio-group v-model="radioSelected" class="container">
|
||||
<b-row class="mb-4">
|
||||
<b-col cols="12" lg="6">
|
||||
<b-row class="bg-248 gradido-border-radius pt-lg-2 mr-lg-2">
|
||||
<b-col cols="10" @click="radioSelected = sendTypes.send" class="pointer">
|
||||
{{ $t('send_gdd') }}
|
||||
</b-col>
|
||||
<b-col cols="2">
|
||||
<b-form-radio
|
||||
name="shipping"
|
||||
size="lg"
|
||||
:value="sendTypes.send"
|
||||
stacked
|
||||
class="custom-radio-button pointer"
|
||||
></b-form-radio>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-col>
|
||||
<b-col>
|
||||
<b-row class="bg-248 gradido-border-radius pt-lg-2 ml-lg-2 mt-2 mt-lg-0">
|
||||
<b-col cols="10" @click="radioSelected = sendTypes.link" class="pointer">
|
||||
{{ $t('send_per_link') }}
|
||||
</b-col>
|
||||
<b-col cols="2" class="pointer">
|
||||
<b-form-radio
|
||||
name="shipping"
|
||||
:value="sendTypes.link"
|
||||
size="lg"
|
||||
class="custom-radio-button"
|
||||
></b-form-radio>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<div class="mt-4 mb-4" v-if="radioSelected === sendTypes.link">
|
||||
<h2 class="alert-heading">{{ $t('gdd_per_link.header') }}</h2>
|
||||
<div>
|
||||
{{ $t('gdd_per_link.choose-amount') }}
|
||||
</div>
|
||||
</div>
|
||||
</b-form-radio-group>
|
||||
<b-row>
|
||||
<b-col>
|
||||
<b-row class="bg-248 gradido-border-radius pt-lg-2 ml-lg-2 mt-2 mt-lg-0">
|
||||
<b-col cols="10" @click="radioSelected = sendTypes.link" class="pointer">
|
||||
{{ $t('send_per_link') }}
|
||||
<b-row>
|
||||
<b-col cols="12">
|
||||
<div v-if="radioSelected === sendTypes.send">
|
||||
<input-email
|
||||
:name="$t('form.recipient')"
|
||||
:label="$t('form.recipient')"
|
||||
:placeholder="$t('form.email')"
|
||||
v-model="form.email"
|
||||
:disabled="isBalanceDisabled"
|
||||
@onValidation="onValidation"
|
||||
/>
|
||||
</div>
|
||||
</b-col>
|
||||
<b-col cols="2" class="pointer">
|
||||
<b-form-radio
|
||||
name="shipping"
|
||||
:value="sendTypes.link"
|
||||
size="lg"
|
||||
class="custom-radio-button"
|
||||
></b-form-radio>
|
||||
<b-col cols="12" lg="6">
|
||||
<input-amount
|
||||
v-model="form.amount"
|
||||
:name="$t('form.amount')"
|
||||
:label="$t('form.amount')"
|
||||
:placeholder="'0.01'"
|
||||
:rules="{ required: true, gddSendAmount: [0.01, balance] }"
|
||||
typ="TransactionForm"
|
||||
:disabled="isBalanceDisabled"
|
||||
></input-amount>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<div class="mt-4 mb-4" v-if="radioSelected === sendTypes.link">
|
||||
<h2 class="alert-heading">{{ $t('gdd_per_link.header') }}</h2>
|
||||
<div>
|
||||
{{ $t('gdd_per_link.choose-amount') }}
|
||||
</div>
|
||||
<b-row>
|
||||
<b-col>
|
||||
<input-textarea
|
||||
v-model="form.memo"
|
||||
:name="$t('form.message')"
|
||||
:label="$t('form.message')"
|
||||
:placeholder="$t('form.message')"
|
||||
:rules="{ required: true, min: 5, max: 255 }"
|
||||
:disabled="isBalanceDisabled"
|
||||
/>
|
||||
</b-col>
|
||||
</b-row>
|
||||
<div v-if="!!isBalanceDisabled" class="text-danger mt-5">
|
||||
{{ $t('form.no_gdd_available') }}
|
||||
</div>
|
||||
</b-form-radio-group>
|
||||
<b-row>
|
||||
<b-col>
|
||||
<b-row>
|
||||
<b-col cols="12">
|
||||
<div v-if="radioSelected === sendTypes.send">
|
||||
<input-email
|
||||
:name="$t('form.recipient')"
|
||||
:label="$t('form.recipient')"
|
||||
:placeholder="$t('form.email')"
|
||||
v-model="form.email"
|
||||
:disabled="isBalanceDisabled"
|
||||
/>
|
||||
</div>
|
||||
</b-col>
|
||||
<b-col cols="12" lg="6">
|
||||
<input-amount
|
||||
v-model="form.amount"
|
||||
:name="$t('form.amount')"
|
||||
:label="$t('form.amount')"
|
||||
:placeholder="'0.01'"
|
||||
:rules="{ required: true, gddSendAmount: [0.01, balance] }"
|
||||
typ="TransactionForm"
|
||||
:disabled="isBalanceDisabled"
|
||||
></input-amount>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<b-row>
|
||||
<b-col>
|
||||
<input-textarea
|
||||
v-model="form.memo"
|
||||
:name="$t('form.message')"
|
||||
:label="$t('form.message')"
|
||||
:placeholder="$t('form.message')"
|
||||
:rules="{ required: true, min: 5, max: 255 }"
|
||||
:disabled="isBalanceDisabled"
|
||||
/>
|
||||
</b-col>
|
||||
</b-row>
|
||||
<div v-if="!!isBalanceDisabled" class="text-danger mt-5">
|
||||
{{ $t('form.no_gdd_available') }}
|
||||
</div>
|
||||
<b-row v-else class="test-buttons mt-5">
|
||||
<b-col>
|
||||
<b-button type="reset" variant="secondary" @click="onReset">
|
||||
{{ $t('form.reset') }}
|
||||
</b-button>
|
||||
</b-col>
|
||||
<b-col class="text-right">
|
||||
<b-button type="submit" variant="gradido">
|
||||
{{ $t('form.check_now') }}
|
||||
</b-button>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-form>
|
||||
</validation-observer>
|
||||
</b-card>
|
||||
</b-col>
|
||||
</b-row>
|
||||
<b-row v-else class="test-buttons mt-5">
|
||||
<b-col>
|
||||
<b-button type="reset" variant="secondary" @click="onReset">
|
||||
{{ $t('form.reset') }}
|
||||
</b-button>
|
||||
</b-col>
|
||||
<b-col class="text-right">
|
||||
<b-button type="submit" variant="gradido">
|
||||
{{ $t('form.check_now') }}
|
||||
</b-button>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-form>
|
||||
</validation-observer>
|
||||
</b-card>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { SEND_TYPES } from '@/pages/Send.vue'
|
||||
@ -140,11 +143,14 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onValidation() {
|
||||
this.$refs.formValidator.validate()
|
||||
},
|
||||
onSubmit() {
|
||||
this.$emit('set-transaction', {
|
||||
selected: this.radioSelected,
|
||||
email: this.form.email,
|
||||
amount: this.form.amount,
|
||||
amount: Number(this.form.amount.replace(',', '.')),
|
||||
memo: this.form.memo,
|
||||
})
|
||||
},
|
||||
@ -153,6 +159,7 @@ export default {
|
||||
this.form.email = ''
|
||||
this.form.amount = ''
|
||||
this.form.memo = ''
|
||||
this.$refs.formValidator.validate()
|
||||
},
|
||||
setNewRecipientEmail() {
|
||||
this.form.email = this.recipientEmail ? this.recipientEmail : this.form.email
|
||||
@ -177,6 +184,9 @@ export default {
|
||||
created() {
|
||||
this.setNewRecipientEmail()
|
||||
},
|
||||
mounted() {
|
||||
if (this.form.email !== '') this.$refs.formValidator.validate()
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
<div class="text-center">
|
||||
<div><figure-qr-code :link="link" /></div>
|
||||
<div>
|
||||
<b-button variant="secondary" @click="$emit('on-reset')" class="mt-4" data-test="close-btn">
|
||||
<b-button variant="secondary" @click="$emit('on-back')" class="mt-4" data-test="close-btn">
|
||||
{{ $t('form.close') }}
|
||||
</b-button>
|
||||
</div>
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
<div v-else>{{ errorResult }}</div>
|
||||
</div>
|
||||
<p class="text-center mt-5">
|
||||
<b-button variant="secondary" @click="$emit('on-reset')">
|
||||
<b-button variant="secondary" @click="$emit('on-back')">
|
||||
{{ $t('form.close') }}
|
||||
</b-button>
|
||||
</p>
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
{{ $t('form.send_transaction_success') }}
|
||||
</div>
|
||||
<div class="text-center mt-5">
|
||||
<b-button variant="primary" @click="$emit('on-reset')">{{ $t('form.close') }}</b-button>
|
||||
<b-button variant="primary" @click="$emit('on-back')">{{ $t('form.close') }}</b-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -22,6 +22,7 @@
|
||||
@focus="amountFocused = true"
|
||||
@blur="normalizeAmount(true)"
|
||||
:disabled="disabled"
|
||||
autocomplete="off"
|
||||
></b-form-input>
|
||||
|
||||
<b-form-invalid-feedback v-bind="ariaMsg">
|
||||
@ -63,7 +64,7 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentValue: '',
|
||||
currentValue: this.value,
|
||||
amountValue: 0.0,
|
||||
amountFocused: false,
|
||||
}
|
||||
|
||||
@ -21,6 +21,7 @@
|
||||
@focus="emailFocused = true"
|
||||
@blur="normalizeEmail()"
|
||||
:disabled="disabled"
|
||||
autocomplete="off"
|
||||
></b-form-input>
|
||||
<b-form-invalid-feedback v-bind="ariaMsg">
|
||||
{{ errors[0] }}
|
||||
@ -40,11 +41,11 @@ export default {
|
||||
}
|
||||
},
|
||||
},
|
||||
name: { type: String, default: 'Email' },
|
||||
label: { type: String, default: 'Email' },
|
||||
placeholder: { type: String, default: 'Email' },
|
||||
value: { required: true, type: String, default: '' },
|
||||
disabled: { required: false, type: Boolean, default: false },
|
||||
name: { type: String, required: true },
|
||||
label: { type: String, required: true },
|
||||
placeholder: { type: String, required: true },
|
||||
value: { type: String, required: true },
|
||||
disabled: { type: Boolean, required: false, default: false },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@ -62,7 +63,10 @@ export default {
|
||||
this.$emit('input', this.currentValue)
|
||||
},
|
||||
value() {
|
||||
if (this.value !== this.currentValue) this.currentValue = this.value
|
||||
if (this.value !== this.currentValue) {
|
||||
this.currentValue = this.value
|
||||
}
|
||||
this.$emit('onValidation')
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
||||
@ -58,7 +58,7 @@ describe('InputTextarea', () => {
|
||||
})
|
||||
|
||||
it('has the value ""', () => {
|
||||
expect(wrapper.vm.currentValue).toEqual('')
|
||||
expect(wrapper.vm.currentValue).toEqual('Long enough')
|
||||
})
|
||||
|
||||
it('has the label "input-field-label"', () => {
|
||||
@ -72,9 +72,8 @@ describe('InputTextarea', () => {
|
||||
|
||||
describe('input value changes', () => {
|
||||
it('emits input with new value', async () => {
|
||||
await wrapper.find('textarea').setValue('Long enough')
|
||||
expect(wrapper.emitted('input')).toBeTruthy()
|
||||
expect(wrapper.emitted('input')).toEqual([['Long enough']])
|
||||
await wrapper.find('textarea').setValue('New Text')
|
||||
expect(wrapper.emitted('input')).toEqual([['New Text']])
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -41,7 +41,7 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentValue: '',
|
||||
currentValue: this.value,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
||||
@ -12,14 +12,20 @@
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</b-button>
|
||||
</b-navbar-brand>
|
||||
<b-img class="sheet-img position-absolute zindex1" :src="sheet"></b-img>
|
||||
<router-link to="/settings" class="d-block d-lg-none zindex1000">
|
||||
|
||||
<router-link to="/settings" class="d-block d-lg-none">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="mr-3">
|
||||
<avatar :username="username.username" :color="'#fff'" :size="61"></avatar>
|
||||
<avatar
|
||||
:username="username.username"
|
||||
:initials="username.initials"
|
||||
:color="'#fff'"
|
||||
:size="61"
|
||||
></avatar>
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
<b-img class="sheet-img position-absolute zindex-1" :src="sheet"></b-img>
|
||||
<b-collapse id="nav-collapse" is-nav class="ml-5">
|
||||
<b-navbar-nav class="ml-auto" right>
|
||||
<div class="mb-2">
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
<b-icon icon="layers" aria-hidden="true"></b-icon>
|
||||
<span class="ml-2">{{ $t('gdt.gdt') }}</span>
|
||||
</b-nav-item>
|
||||
<b-nav-item to="/community#my" class="" active-class="activeRoute">
|
||||
<b-nav-item to="/community" class="" active-class="activeRoute">
|
||||
<b-icon icon="people" aria-hidden="true"></b-icon>
|
||||
<span class="ml-2">{{ $t('creation') }}</span>
|
||||
</b-nav-item>
|
||||
|
||||
@ -1,20 +1,31 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import GddAmount from './GddAmount'
|
||||
import { updateUserInfos } from '@/graphql/mutations'
|
||||
import flushPromises from 'flush-promises'
|
||||
|
||||
import { toastErrorSpy, toastSuccessSpy } from '@test/testSetup'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
const mockAPICall = jest.fn()
|
||||
const storeCommitMock = jest.fn()
|
||||
|
||||
const state = {
|
||||
language: 'en',
|
||||
hideAmountGDD: false,
|
||||
}
|
||||
|
||||
const mocks = {
|
||||
$store: {
|
||||
state,
|
||||
commit: storeCommitMock,
|
||||
},
|
||||
$i18n: {
|
||||
locale: 'en',
|
||||
},
|
||||
$t: jest.fn((t) => t),
|
||||
$apollo: {
|
||||
mutate: mockAPICall,
|
||||
},
|
||||
}
|
||||
|
||||
const propsData = {
|
||||
@ -38,5 +49,89 @@ describe('GddAmount', () => {
|
||||
it('renders the component gdd-amount', () => {
|
||||
expect(wrapper.find('div.gdd-amount').exists()).toBe(true)
|
||||
})
|
||||
|
||||
describe('API throws exception', () => {
|
||||
beforeEach(async () => {
|
||||
mockAPICall.mockRejectedValue({
|
||||
message: 'Ouch',
|
||||
})
|
||||
jest.clearAllMocks()
|
||||
await wrapper.find('div.border-left svg').trigger('click')
|
||||
await flushPromises()
|
||||
})
|
||||
|
||||
it('toasts an error message', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith('Ouch')
|
||||
})
|
||||
})
|
||||
|
||||
describe('API call successful', () => {
|
||||
beforeEach(async () => {
|
||||
mockAPICall.mockResolvedValue({
|
||||
data: {
|
||||
updateUserInfos: {
|
||||
validValues: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
jest.clearAllMocks()
|
||||
await wrapper.find('div.border-left svg').trigger('click')
|
||||
await flushPromises()
|
||||
})
|
||||
|
||||
it('calls the API', () => {
|
||||
expect(mockAPICall).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
mutation: updateUserInfos,
|
||||
variables: {
|
||||
hideAmountGDD: true,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('commits hideAmountGDD to store', () => {
|
||||
expect(storeCommitMock).toBeCalledWith('hideAmountGDD', true)
|
||||
})
|
||||
|
||||
it('toasts a success message', () => {
|
||||
expect(toastSuccessSpy).toBeCalledWith('settings.showAmountGDD')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('second call to API', () => {
|
||||
beforeEach(async () => {
|
||||
mockAPICall.mockResolvedValue({
|
||||
data: {
|
||||
updateUserInfos: {
|
||||
validValues: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
jest.clearAllMocks()
|
||||
wrapper.vm.$store.state.hideAmountGDD = true
|
||||
await wrapper.find('div.border-left svg').trigger('click')
|
||||
await flushPromises()
|
||||
})
|
||||
|
||||
it('calls the API', () => {
|
||||
expect(mockAPICall).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
mutation: updateUserInfos,
|
||||
variables: {
|
||||
hideAmountGDD: false,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('commits hideAmountGDD to store', () => {
|
||||
expect(storeCommitMock).toBeCalledWith('hideAmountGDD', false)
|
||||
})
|
||||
|
||||
it('toasts a success message', () => {
|
||||
expect(toastSuccessSpy).toBeCalledWith('settings.hideAmountGDD')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -39,7 +39,7 @@
|
||||
<b-icon
|
||||
:icon="hideAmount ? 'eye-slash' : 'eye'"
|
||||
class="mr-3 gradido-global-border-color-accent pointer hover-icon"
|
||||
@click="$store.commit('hideAmountGDD', !hideAmount)"
|
||||
@click="updateHideAmountGDD"
|
||||
></b-icon>
|
||||
</b-col>
|
||||
</b-row>
|
||||
@ -47,6 +47,8 @@
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { updateUserInfos } from '@/graphql/mutations'
|
||||
|
||||
export default {
|
||||
name: 'GddAmount',
|
||||
props: {
|
||||
@ -60,5 +62,27 @@ export default {
|
||||
return this.$store.state.hideAmountGDD
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async updateHideAmountGDD() {
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: updateUserInfos,
|
||||
variables: {
|
||||
hideAmountGDD: !this.hideAmount,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
this.$store.commit('hideAmountGDD', !this.hideAmount)
|
||||
if (!this.hideAmount) {
|
||||
this.toastSuccess(this.$t('settings.showAmountGDD'))
|
||||
} else {
|
||||
this.toastSuccess(this.$t('settings.hideAmountGDD'))
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toastError(error.message)
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -1,19 +1,30 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import GdtAmount from './GdtAmount'
|
||||
import { updateUserInfos } from '@/graphql/mutations'
|
||||
import flushPromises from 'flush-promises'
|
||||
|
||||
import { toastErrorSpy, toastSuccessSpy } from '@test/testSetup'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
const mockAPICall = jest.fn()
|
||||
const storeCommitMock = jest.fn()
|
||||
|
||||
const state = {
|
||||
language: 'en',
|
||||
hideAmountGDT: false,
|
||||
}
|
||||
|
||||
const mocks = {
|
||||
$store: {
|
||||
state,
|
||||
commit: storeCommitMock,
|
||||
},
|
||||
$i18n: {
|
||||
locale: 'en',
|
||||
},
|
||||
$apollo: {
|
||||
mutate: mockAPICall,
|
||||
},
|
||||
$t: jest.fn((t) => t),
|
||||
$n: jest.fn((n) => n),
|
||||
}
|
||||
@ -39,5 +50,89 @@ describe('GdtAmount', () => {
|
||||
it('renders the component gdt-amount', () => {
|
||||
expect(wrapper.find('div.gdt-amount').exists()).toBe(true)
|
||||
})
|
||||
|
||||
describe('API throws exception', () => {
|
||||
beforeEach(async () => {
|
||||
mockAPICall.mockRejectedValue({
|
||||
message: 'Ouch',
|
||||
})
|
||||
jest.clearAllMocks()
|
||||
await wrapper.find('div.border-left svg').trigger('click')
|
||||
await flushPromises()
|
||||
})
|
||||
|
||||
it('toasts an error message', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith('Ouch')
|
||||
})
|
||||
})
|
||||
|
||||
describe('API call successful', () => {
|
||||
beforeEach(async () => {
|
||||
mockAPICall.mockResolvedValue({
|
||||
data: {
|
||||
updateUserInfos: {
|
||||
validValues: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
jest.clearAllMocks()
|
||||
await wrapper.find('div.border-left svg').trigger('click')
|
||||
await flushPromises()
|
||||
})
|
||||
|
||||
it('calls the API', () => {
|
||||
expect(mockAPICall).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
mutation: updateUserInfos,
|
||||
variables: {
|
||||
hideAmountGDT: true,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('commits hideAmountGDT to store', () => {
|
||||
expect(storeCommitMock).toBeCalledWith('hideAmountGDT', true)
|
||||
})
|
||||
|
||||
it('toasts a success message', () => {
|
||||
expect(toastSuccessSpy).toBeCalledWith('settings.showAmountGDT')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('second call to API', () => {
|
||||
beforeEach(async () => {
|
||||
mockAPICall.mockResolvedValue({
|
||||
data: {
|
||||
updateUserInfos: {
|
||||
validValues: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
jest.clearAllMocks()
|
||||
wrapper.vm.$store.state.hideAmountGDT = true
|
||||
await wrapper.find('div.border-left svg').trigger('click')
|
||||
await flushPromises()
|
||||
})
|
||||
|
||||
it('calls the API', () => {
|
||||
expect(mockAPICall).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
mutation: updateUserInfos,
|
||||
variables: {
|
||||
hideAmountGDT: false,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('commits hideAmountGDT to store', () => {
|
||||
expect(storeCommitMock).toBeCalledWith('hideAmountGDT', false)
|
||||
})
|
||||
|
||||
it('toasts a success message', () => {
|
||||
expect(toastSuccessSpy).toBeCalledWith('settings.hideAmountGDT')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -34,7 +34,7 @@
|
||||
<b-icon
|
||||
:icon="hideAmount ? 'eye-slash' : 'eye'"
|
||||
class="mr-3 gradido-global-border-color-accent pointer hover-icon"
|
||||
@click="$store.commit('hideAmountGDT', !hideAmount)"
|
||||
@click="updateHideAmountGDT"
|
||||
></b-icon>
|
||||
</b-col>
|
||||
</b-row>
|
||||
@ -42,6 +42,8 @@
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { updateUserInfos } from '@/graphql/mutations'
|
||||
|
||||
export default {
|
||||
name: 'GdtAmount',
|
||||
props: {
|
||||
@ -54,5 +56,27 @@ export default {
|
||||
return this.$store.state.hideAmountGDT
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async updateHideAmountGDT() {
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: updateUserInfos,
|
||||
variables: {
|
||||
hideAmountGDT: !this.hideAmount,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
this.$store.commit('hideAmountGDT', !this.hideAmount)
|
||||
if (!this.hideAmount) {
|
||||
this.toastSuccess(this.$t('settings.showAmountGDT'))
|
||||
} else {
|
||||
this.toastSuccess(this.$t('settings.hideAmountGDT'))
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toastError(error.message)
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -1,20 +1,20 @@
|
||||
<template>
|
||||
<div class="nav-community">
|
||||
<div class="nav-community container">
|
||||
<b-row class="nav-row">
|
||||
<b-col cols="12" lg="4" md="4">
|
||||
<b-btn active-class="btn-active" block variant="link" to="#edit">
|
||||
<b-col cols="12" lg="4" md="4" class="px-0">
|
||||
<b-btn active-class="btn-active" block variant="link" to="/community#edit">
|
||||
<b-icon icon="pencil" class="mr-2" />
|
||||
{{ $t('community.submitContribution') }}
|
||||
</b-btn>
|
||||
</b-col>
|
||||
<b-col cols="12" lg="4" md="4">
|
||||
<b-btn active-class="btn-active" block variant="link" to="#my">
|
||||
<b-col cols="12" lg="4" md="4" class="px-0">
|
||||
<b-btn active-class="btn-active" block variant="link" to="/community#my">
|
||||
<b-icon icon="person" class="mr-2" />
|
||||
{{ $t('community.myContributions') }}
|
||||
</b-btn>
|
||||
</b-col>
|
||||
<b-col cols="12" lg="4" md="4">
|
||||
<b-btn active-class="btn-active" block variant="link" to="#all">
|
||||
<b-col cols="12" lg="4" md="4" class="px-0">
|
||||
<b-btn active-class="btn-active" block variant="link" to="/community#all">
|
||||
<b-icon icon="people" class="mr-2" />
|
||||
{{ $t('community.community') }}
|
||||
</b-btn>
|
||||
|
||||
@ -24,6 +24,7 @@
|
||||
:size="72"
|
||||
:color="'#fff'"
|
||||
:username="`${transaction.linkedUser.firstName} ${transaction.linkedUser.lastName}`"
|
||||
:initials="`${transaction.linkedUser.firstName[0]}${transaction.linkedUser.lastName[0]}`"
|
||||
></avatar>
|
||||
</div>
|
||||
</b-col>
|
||||
|
||||
@ -31,6 +31,8 @@ export const updateUserInfos = gql`
|
||||
$password: String
|
||||
$passwordNew: String
|
||||
$locale: String
|
||||
$hideAmountGDD: Boolean
|
||||
$hideAmountGDT: Boolean
|
||||
) {
|
||||
updateUserInfos(
|
||||
firstName: $firstName
|
||||
@ -38,6 +40,8 @@ export const updateUserInfos = gql`
|
||||
password: $password
|
||||
passwordNew: $passwordNew
|
||||
language: $locale
|
||||
hideAmountGDD: $hideAmountGDD
|
||||
hideAmountGDT: $hideAmountGDT
|
||||
)
|
||||
}
|
||||
`
|
||||
@ -150,7 +154,8 @@ export const login = gql`
|
||||
hasElopage
|
||||
publisherId
|
||||
isAdmin
|
||||
creation
|
||||
hideAmountGDD
|
||||
hideAmountGDT
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@ -14,6 +14,8 @@ export const verifyLogin = gql`
|
||||
publisherId
|
||||
isAdmin
|
||||
creation
|
||||
hideAmountGDD
|
||||
hideAmountGDT
|
||||
}
|
||||
}
|
||||
`
|
||||
@ -248,3 +250,13 @@ export const listContributionMessages = gql`
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const openCreations = gql`
|
||||
query {
|
||||
openCreations {
|
||||
year
|
||||
month
|
||||
amount
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@ -269,6 +269,8 @@
|
||||
"warningText": "Bist du noch da?"
|
||||
},
|
||||
"settings": {
|
||||
"hideAmountGDD": "Dein GDD Betrag ist versteckt.",
|
||||
"hideAmountGDT": "Dein GDT Betrag ist versteckt.",
|
||||
"language": {
|
||||
"changeLanguage": "Sprache ändern",
|
||||
"de": "Deutsch",
|
||||
@ -301,7 +303,9 @@
|
||||
"text": "Speichere nun dein neues Passwort, mit dem du dich zukünftig in deinem Gradido-Konto anmelden kannst."
|
||||
},
|
||||
"subtitle": "Wenn du dein Passwort vergessen hast, kannst du es hier zurücksetzen."
|
||||
}
|
||||
},
|
||||
"showAmountGDD": "Dein GDD Betrag ist sichtbar.",
|
||||
"showAmountGDT": "Dein GDT Betrag ist sichtbar."
|
||||
},
|
||||
"signin": "Anmelden",
|
||||
"signup": "Registrieren",
|
||||
|
||||
@ -269,6 +269,8 @@
|
||||
"warningText": "Are you still there?"
|
||||
},
|
||||
"settings": {
|
||||
"hideAmountGDD": "Your GDD amount is hidden.",
|
||||
"hideAmountGDT": "Your GDT amount is hidden.",
|
||||
"language": {
|
||||
"changeLanguage": "Change language",
|
||||
"de": "Deutsch",
|
||||
@ -301,7 +303,9 @@
|
||||
"text": "Now save your new password, which you can use to log in to your Gradido account in the future."
|
||||
},
|
||||
"subtitle": "If you have forgotten your password, you can reset it here."
|
||||
}
|
||||
},
|
||||
"showAmountGDD": "Your GDD amount is visible.",
|
||||
"showAmountGDT": "Your GDT amount is visible."
|
||||
},
|
||||
"signin": "Sign in",
|
||||
"signup": "Sign up",
|
||||
|
||||
@ -2,33 +2,14 @@ import { mount } from '@vue/test-utils'
|
||||
import Community from './Community'
|
||||
import { toastErrorSpy, toastSuccessSpy } from '@test/testSetup'
|
||||
import { createContribution, updateContribution, deleteContribution } from '@/graphql/mutations'
|
||||
import { listContributions, listAllContributions, verifyLogin } from '@/graphql/queries'
|
||||
|
||||
import VueRouter from 'vue-router'
|
||||
import routes from '../routes/routes'
|
||||
import { listContributions, listAllContributions } from '@/graphql/queries'
|
||||
|
||||
const localVue = global.localVue
|
||||
localVue.use(VueRouter)
|
||||
|
||||
const mockStoreDispach = jest.fn()
|
||||
const apolloQueryMock = jest.fn()
|
||||
const apolloMutationMock = jest.fn()
|
||||
|
||||
const router = new VueRouter({
|
||||
base: '/',
|
||||
routes,
|
||||
linkActiveClass: 'active',
|
||||
mode: 'history',
|
||||
// scrollBehavior: (to, from, savedPosition) => {
|
||||
// if (savedPosition) {
|
||||
// return savedPosition
|
||||
// }
|
||||
// if (to.hash) {
|
||||
// return { selector: to.hash }
|
||||
// }
|
||||
// return { x: 0, y: 0 }
|
||||
// },
|
||||
})
|
||||
const apolloRefetchMock = jest.fn()
|
||||
|
||||
describe('Community', () => {
|
||||
let wrapper
|
||||
@ -39,6 +20,11 @@ describe('Community', () => {
|
||||
$apollo: {
|
||||
query: apolloQueryMock,
|
||||
mutate: apolloMutationMock,
|
||||
queries: {
|
||||
OpenCreations: {
|
||||
refetch: apolloRefetchMock,
|
||||
},
|
||||
},
|
||||
},
|
||||
$store: {
|
||||
dispatch: mockStoreDispach,
|
||||
@ -49,12 +35,17 @@ describe('Community', () => {
|
||||
$i18n: {
|
||||
locale: 'en',
|
||||
},
|
||||
$router: {
|
||||
push: jest.fn(),
|
||||
},
|
||||
$route: {
|
||||
hash: 'my',
|
||||
},
|
||||
}
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(Community, {
|
||||
localVue,
|
||||
router,
|
||||
mocks,
|
||||
})
|
||||
}
|
||||
@ -207,10 +198,7 @@ describe('Community', () => {
|
||||
})
|
||||
|
||||
it('verifies the login (to get the new creations available)', () => {
|
||||
expect(apolloQueryMock).toBeCalledWith({
|
||||
query: verifyLogin,
|
||||
fetchPolicy: 'network-only',
|
||||
})
|
||||
expect(apolloRefetchMock).toBeCalled()
|
||||
})
|
||||
|
||||
it('set all data to the default values)', () => {
|
||||
@ -294,10 +282,7 @@ describe('Community', () => {
|
||||
})
|
||||
|
||||
it('verifies the login (to get the new creations available)', () => {
|
||||
expect(apolloQueryMock).toBeCalledWith({
|
||||
query: verifyLogin,
|
||||
fetchPolicy: 'network-only',
|
||||
})
|
||||
expect(apolloRefetchMock).toBeCalled()
|
||||
})
|
||||
|
||||
it('set all data to the default values)', () => {
|
||||
@ -376,10 +361,7 @@ describe('Community', () => {
|
||||
})
|
||||
|
||||
it('verifies the login (to get the new creations available)', () => {
|
||||
expect(apolloQueryMock).toBeCalledWith({
|
||||
query: verifyLogin,
|
||||
fetchPolicy: 'network-only',
|
||||
})
|
||||
expect(apolloRefetchMock).toBeCalled()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -5,8 +5,8 @@
|
||||
<b-tab no-body>
|
||||
<open-creations-amount
|
||||
:minimalDate="minimalDate"
|
||||
:maxGddThisMonth="maxGddThisMonth"
|
||||
:maxGddLastMonth="maxGddLastMonth"
|
||||
:maxGddLastMonth="maxForMonths[0]"
|
||||
:maxGddThisMonth="maxForMonths[1]"
|
||||
/>
|
||||
<div class="mb-3"></div>
|
||||
<contribution-form
|
||||
@ -15,8 +15,8 @@
|
||||
v-model="form"
|
||||
:isThisMonth="isThisMonth"
|
||||
:minimalDate="minimalDate"
|
||||
:maxGddLastMonth="maxGddLastMonth"
|
||||
:maxGddThisMonth="maxGddThisMonth"
|
||||
:maxGddLastMonth="maxForMonths[0]"
|
||||
:maxGddThisMonth="maxForMonths[1]"
|
||||
/>
|
||||
</b-tab>
|
||||
<b-tab no-body>
|
||||
@ -52,7 +52,7 @@ import OpenCreationsAmount from '@/components/Contributions/OpenCreationsAmount.
|
||||
import ContributionForm from '@/components/Contributions/ContributionForm.vue'
|
||||
import ContributionList from '@/components/Contributions/ContributionList.vue'
|
||||
import { createContribution, updateContribution, deleteContribution } from '@/graphql/mutations'
|
||||
import { listContributions, listAllContributions, verifyLogin } from '@/graphql/queries'
|
||||
import { listContributions, listAllContributions, openCreations } from '@/graphql/queries'
|
||||
|
||||
export default {
|
||||
name: 'Community',
|
||||
@ -82,6 +82,7 @@ export default {
|
||||
},
|
||||
updateAmount: '',
|
||||
maximalDate: new Date(),
|
||||
openCreations: [],
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@ -90,6 +91,23 @@ export default {
|
||||
this.hashLink = this.$route.hash
|
||||
})
|
||||
},
|
||||
apollo: {
|
||||
OpenCreations: {
|
||||
query() {
|
||||
return openCreations
|
||||
},
|
||||
fetchPolicy: 'network-only',
|
||||
variables() {
|
||||
return {}
|
||||
},
|
||||
update({ openCreations }) {
|
||||
this.openCreations = openCreations
|
||||
},
|
||||
error({ message }) {
|
||||
this.toastError(message)
|
||||
},
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
$route(to, from) {
|
||||
this.tabIndex = this.tabLinkHashes.findIndex((hashLink) => hashLink === to.hash)
|
||||
@ -120,17 +138,20 @@ export default {
|
||||
formDate.getMonth() === this.maximalDate.getMonth()
|
||||
)
|
||||
},
|
||||
maxGddLastMonth() {
|
||||
amountToAdd() {
|
||||
// when existing contribution is edited, the amount is added back on top of the amount
|
||||
return this.form.id && !this.isThisMonth
|
||||
? parseInt(this.$store.state.creation[1]) + parseInt(this.updateAmount)
|
||||
: parseInt(this.$store.state.creation[1])
|
||||
if (this.form.id) return parseInt(this.updateAmount)
|
||||
return 0
|
||||
},
|
||||
maxGddThisMonth() {
|
||||
// when existing contribution is edited, the amount is added back on top of the amount
|
||||
return this.form.id && this.isThisMonth
|
||||
? parseInt(this.$store.state.creation[2]) + parseInt(this.updateAmount)
|
||||
: parseInt(this.$store.state.creation[2])
|
||||
maxForMonths() {
|
||||
const formDate = new Date(this.form.date)
|
||||
if (this.openCreations && this.openCreations.length)
|
||||
return this.openCreations.slice(1).map((creation) => {
|
||||
if (creation.year === formDate.getFullYear() && creation.month === formDate.getMonth())
|
||||
return parseInt(creation.amount) + this.amountToAdd
|
||||
return parseInt(creation.amount)
|
||||
})
|
||||
return [0, 0]
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
@ -160,7 +181,7 @@ export default {
|
||||
currentPage: this.currentPage,
|
||||
pageSize: this.pageSize,
|
||||
})
|
||||
this.verifyLogin()
|
||||
this.$apollo.queries.OpenCreations.refetch()
|
||||
})
|
||||
.catch((err) => {
|
||||
this.toastError(err.message)
|
||||
@ -188,7 +209,7 @@ export default {
|
||||
currentPage: this.currentPage,
|
||||
pageSize: this.pageSize,
|
||||
})
|
||||
this.verifyLogin()
|
||||
this.$apollo.queries.OpenCreations.refetch()
|
||||
})
|
||||
.catch((err) => {
|
||||
this.toastError(err.message)
|
||||
@ -213,7 +234,7 @@ export default {
|
||||
currentPage: this.currentPage,
|
||||
pageSize: this.pageSize,
|
||||
})
|
||||
this.verifyLogin()
|
||||
this.$apollo.queries.OpenCreations.refetch()
|
||||
})
|
||||
.catch((err) => {
|
||||
this.toastError(err.message)
|
||||
@ -259,7 +280,7 @@ export default {
|
||||
if (this.items.find((item) => item.state === 'IN_PROGRESS')) {
|
||||
this.tabIndex = 1
|
||||
if (this.$route.hash !== '#my') {
|
||||
this.$router.push({ path: '#my' })
|
||||
this.$router.push({ path: '/community#my' })
|
||||
}
|
||||
this.toastInfo('Du hast eine Rückfrage auf eine Contribution. Bitte beantworte diese!')
|
||||
}
|
||||
@ -268,22 +289,6 @@ export default {
|
||||
this.toastError(err.message)
|
||||
})
|
||||
},
|
||||
verifyLogin() {
|
||||
this.$apollo
|
||||
.query({
|
||||
query: verifyLogin,
|
||||
fetchPolicy: 'network-only',
|
||||
})
|
||||
.then((result) => {
|
||||
const {
|
||||
data: { verifyLogin },
|
||||
} = result
|
||||
this.$store.dispatch('login', verifyLogin)
|
||||
})
|
||||
.catch(() => {
|
||||
this.$emit('logout')
|
||||
})
|
||||
},
|
||||
updateContributionForm(item) {
|
||||
this.form.id = item.id
|
||||
this.form.date = item.contributionDate
|
||||
@ -303,8 +308,6 @@ export default {
|
||||
},
|
||||
|
||||
created() {
|
||||
// verifyLogin is important at this point so that creation is updated on reload if they are deleted in a session in the admin area.
|
||||
this.verifyLogin()
|
||||
this.updateListContributions({
|
||||
currentPage: this.currentPage,
|
||||
pageSize: this.pageSize,
|
||||
@ -315,6 +318,7 @@ export default {
|
||||
})
|
||||
this.updateTransactions(0)
|
||||
this.tabIndex = 1
|
||||
this.$router.push({ path: '/community#my' })
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -58,11 +58,11 @@ describe('ForgotPassword', () => {
|
||||
})
|
||||
|
||||
it('has the label "Email"', () => {
|
||||
expect(form.find('label').text()).toEqual('Email')
|
||||
expect(form.find('label').text()).toEqual('form.email')
|
||||
})
|
||||
|
||||
it('has the placeholder "Email"', () => {
|
||||
expect(form.find('input').attributes('placeholder')).toEqual('Email')
|
||||
expect(form.find('input').attributes('placeholder')).toEqual('form.email')
|
||||
})
|
||||
|
||||
it('has a submit button', () => {
|
||||
|
||||
@ -6,7 +6,12 @@
|
||||
<b-col>
|
||||
<validation-observer ref="observer" v-slot="{ handleSubmit }">
|
||||
<b-form role="form" @submit.prevent="handleSubmit(onSubmit)">
|
||||
<input-email v-model="form.email"></input-email>
|
||||
<input-email
|
||||
v-model="form.email"
|
||||
:name="$t('form.email')"
|
||||
:label="$t('form.email')"
|
||||
:placeholder="$t('form.email')"
|
||||
></input-email>
|
||||
<div class="text-center">
|
||||
<b-button type="submit" variant="gradido">
|
||||
{{ $t('settings.password.send_now') }}
|
||||
|
||||
@ -76,7 +76,7 @@ describe('Login', () => {
|
||||
})
|
||||
|
||||
it('has an Email input field', () => {
|
||||
expect(wrapper.find('input[placeholder="Email"]').exists()).toBe(true)
|
||||
expect(wrapper.find('div[data-test="input-email"]').find('input').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('has an Password input field', () => {
|
||||
@ -110,7 +110,10 @@ describe('Login', () => {
|
||||
describe('valid data', () => {
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
await wrapper.find('input[placeholder="Email"]').setValue('user@example.org')
|
||||
await wrapper
|
||||
.find('div[data-test="input-email"]')
|
||||
.find('input')
|
||||
.setValue('user@example.org')
|
||||
await wrapper.find('input[placeholder="form.password"]').setValue('1234')
|
||||
await flushPromises()
|
||||
apolloMutateMock.mockResolvedValue({
|
||||
@ -159,7 +162,10 @@ describe('Login', () => {
|
||||
code: 'some-code',
|
||||
}
|
||||
wrapper = Wrapper()
|
||||
await wrapper.find('input[placeholder="Email"]').setValue('user@example.org')
|
||||
await wrapper
|
||||
.find('div[data-test="input-email"]')
|
||||
.find('input')
|
||||
.setValue('user@example.org')
|
||||
await wrapper.find('input[placeholder="form.password"]').setValue('1234')
|
||||
await flushPromises()
|
||||
await wrapper.find('form').trigger('submit')
|
||||
@ -180,7 +186,10 @@ describe('Login', () => {
|
||||
})
|
||||
wrapper = Wrapper()
|
||||
jest.clearAllMocks()
|
||||
await wrapper.find('input[placeholder="Email"]').setValue('user@example.org')
|
||||
await wrapper
|
||||
.find('div[data-test="input-email"]')
|
||||
.find('input')
|
||||
.setValue('user@example.org')
|
||||
await wrapper.find('input[placeholder="form.password"]').setValue('1234')
|
||||
await flushPromises()
|
||||
await wrapper.find('form').trigger('submit')
|
||||
|
||||
@ -5,7 +5,14 @@
|
||||
<validation-observer ref="observer" v-slot="{ handleSubmit }">
|
||||
<b-form @submit.stop.prevent="handleSubmit(onSubmit)">
|
||||
<b-row>
|
||||
<b-col sm="12" md="12" lg="6"><input-email v-model="form.email"></input-email></b-col>
|
||||
<b-col sm="12" md="12" lg="6">
|
||||
<input-email
|
||||
v-model="form.email"
|
||||
:name="$t('form.email')"
|
||||
:label="$t('form.email')"
|
||||
:placeholder="$t('form.email')"
|
||||
></input-email>
|
||||
</b-col>
|
||||
<b-col sm="12" md="12" lg="6">
|
||||
<input-password
|
||||
:label="$t('form.password')"
|
||||
|
||||
@ -65,7 +65,7 @@ describe('Register', () => {
|
||||
})
|
||||
|
||||
it('has email input fields', () => {
|
||||
expect(wrapper.find('#Email-input-field').exists()).toBe(true)
|
||||
expect(wrapper.find('div[data-test="input-email"]').find('input').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('has 1 checkbox input fields', () => {
|
||||
@ -107,7 +107,10 @@ describe('Register', () => {
|
||||
wrapper.find('#registerLastname').setValue('Mustermann')
|
||||
})
|
||||
it('has disabled submit button when missing input checked box', () => {
|
||||
wrapper.find('#Email-input-field').setValue('max.mustermann@gradido.net')
|
||||
wrapper
|
||||
.find('div[data-test="input-email"]')
|
||||
.find('input')
|
||||
.setValue('max.mustermann@gradido.net')
|
||||
expect(wrapper.find('button[type="submit"]').attributes('disabled')).toBe('disabled')
|
||||
})
|
||||
|
||||
@ -121,7 +124,10 @@ describe('Register', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.find('#registerFirstname').setValue('Max')
|
||||
wrapper.find('#registerLastname').setValue('Mustermann')
|
||||
wrapper.find('#Email-input-field').setValue('max.mustermann@gradido.net')
|
||||
wrapper
|
||||
.find('div[data-test="input-email"]')
|
||||
.find('input')
|
||||
.setValue('max.mustermann@gradido.net')
|
||||
wrapper.find('#registerCheckbox').setChecked()
|
||||
})
|
||||
|
||||
@ -211,7 +217,10 @@ describe('Register', () => {
|
||||
wrapper = Wrapper()
|
||||
wrapper.find('#registerFirstname').setValue('Max')
|
||||
wrapper.find('#registerLastname').setValue('Mustermann')
|
||||
wrapper.find('#Email-input-field').setValue('max.mustermann@gradido.net')
|
||||
wrapper
|
||||
.find('div[data-test="input-email"]')
|
||||
.find('input')
|
||||
.setValue('max.mustermann@gradido.net')
|
||||
wrapper.find('#registerCheckbox').setChecked()
|
||||
await wrapper.find('form').trigger('submit')
|
||||
await flushPromises()
|
||||
|
||||
@ -59,7 +59,14 @@
|
||||
</b-col>
|
||||
</b-row>
|
||||
<b-row>
|
||||
<b-col><input-email v-model="form.email"></input-email></b-col>
|
||||
<b-col>
|
||||
<input-email
|
||||
v-model="form.email"
|
||||
:name="$t('form.email')"
|
||||
:label="$t('form.email')"
|
||||
:placeholder="$t('form.email')"
|
||||
></input-email>
|
||||
</b-col>
|
||||
</b-row>
|
||||
<div class="my-4">
|
||||
<b-form-checkbox
|
||||
|
||||
@ -13,7 +13,7 @@ const navigatorClipboardMock = jest.fn()
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
describe.skip('Send', () => {
|
||||
describe('Send', () => {
|
||||
let wrapper
|
||||
|
||||
const propsData = {
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
:amount="transactionData.amount"
|
||||
:memo="transactionData.memo"
|
||||
@send-transaction="sendTransaction"
|
||||
@on-reset="onReset"
|
||||
@on-back="onBack"
|
||||
></transaction-confirmation-send>
|
||||
</template>
|
||||
<template #transactionConfirmationLink>
|
||||
@ -26,17 +26,17 @@
|
||||
:memo="transactionData.memo"
|
||||
:loading="loading"
|
||||
@send-transaction="sendTransaction"
|
||||
@on-reset="onReset"
|
||||
@on-back="onBack"
|
||||
></transaction-confirmation-link>
|
||||
</template>
|
||||
<template #transactionResultSendSuccess>
|
||||
<transaction-result-send-success @on-reset="onReset"></transaction-result-send-success>
|
||||
<transaction-result-send-success @on-back="onBack"></transaction-result-send-success>
|
||||
</template>
|
||||
<template #transactionResultSendError>
|
||||
<transaction-result-send-error
|
||||
:error="error"
|
||||
:errorResult="errorResult"
|
||||
@on-reset="onReset"
|
||||
@on-back="onBack"
|
||||
></transaction-result-send-error>
|
||||
</template>
|
||||
<template #transactionResultLink>
|
||||
@ -45,7 +45,7 @@
|
||||
:amount="amount"
|
||||
:memo="memo"
|
||||
:validUntil="validUntil"
|
||||
@on-reset="onReset"
|
||||
@on-back="onBack"
|
||||
></transaction-result-link>
|
||||
</template>
|
||||
</gdd-send>
|
||||
@ -169,8 +169,9 @@ export default {
|
||||
}
|
||||
this.loading = false
|
||||
},
|
||||
onReset() {
|
||||
onBack() {
|
||||
this.currentTransactionStep = TRANSACTION_STEPS.transactionForm
|
||||
this.$mount()
|
||||
},
|
||||
updateTransactions(pagination) {
|
||||
this.$emit('update-transactions', pagination)
|
||||
|
||||
@ -85,7 +85,7 @@ export default {
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
gdt() {
|
||||
currentPage() {
|
||||
if (this.gdt) {
|
||||
this.updateGdt()
|
||||
}
|
||||
|
||||
@ -47,9 +47,6 @@ export const mutations = {
|
||||
hasElopage: (state, hasElopage) => {
|
||||
state.hasElopage = hasElopage
|
||||
},
|
||||
creation: (state, creation) => {
|
||||
state.creation = creation
|
||||
},
|
||||
hideAmountGDD: (state, hideAmountGDD) => {
|
||||
state.hideAmountGDD = !!hideAmountGDD
|
||||
},
|
||||
@ -69,7 +66,6 @@ export const actions = {
|
||||
commit('hasElopage', data.hasElopage)
|
||||
commit('publisherId', data.publisherId)
|
||||
commit('isAdmin', data.isAdmin)
|
||||
commit('creation', data.creation)
|
||||
commit('hideAmountGDD', data.hideAmountGDD)
|
||||
commit('hideAmountGDT', data.hideAmountGDT)
|
||||
},
|
||||
@ -83,7 +79,6 @@ export const actions = {
|
||||
commit('hasElopage', false)
|
||||
commit('publisherId', null)
|
||||
commit('isAdmin', false)
|
||||
commit('creation', null)
|
||||
commit('hideAmountGDD', false)
|
||||
commit('hideAmountGDT', true)
|
||||
localStorage.clear()
|
||||
@ -111,7 +106,6 @@ try {
|
||||
newsletterState: null,
|
||||
hasElopage: false,
|
||||
publisherId: null,
|
||||
creation: null,
|
||||
hideAmountGDD: null,
|
||||
hideAmountGDT: null,
|
||||
},
|
||||
|
||||
@ -30,7 +30,6 @@ const {
|
||||
publisherId,
|
||||
isAdmin,
|
||||
hasElopage,
|
||||
creation,
|
||||
hideAmountGDD,
|
||||
hideAmountGDT,
|
||||
} = mutations
|
||||
@ -143,14 +142,6 @@ describe('Vuex store', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('creation', () => {
|
||||
it('sets the state of creation', () => {
|
||||
const state = { creation: null }
|
||||
creation(state, true)
|
||||
expect(state.creation).toEqual(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('hideAmountGDD', () => {
|
||||
it('sets the state of hideAmountGDD', () => {
|
||||
const state = { hideAmountGDD: false }
|
||||
@ -183,14 +174,13 @@ describe('Vuex store', () => {
|
||||
hasElopage: false,
|
||||
publisherId: 1234,
|
||||
isAdmin: true,
|
||||
creation: ['1000', '1000', '1000'],
|
||||
hideAmountGDD: false,
|
||||
hideAmountGDT: true,
|
||||
}
|
||||
|
||||
it('calls eleven commits', () => {
|
||||
login({ commit, state }, commitedData)
|
||||
expect(commit).toHaveBeenCalledTimes(11)
|
||||
expect(commit).toHaveBeenCalledTimes(10)
|
||||
})
|
||||
|
||||
it('commits email', () => {
|
||||
@ -233,19 +223,14 @@ describe('Vuex store', () => {
|
||||
expect(commit).toHaveBeenNthCalledWith(8, 'isAdmin', true)
|
||||
})
|
||||
|
||||
it('commits creation', () => {
|
||||
login({ commit, state }, commitedData)
|
||||
expect(commit).toHaveBeenNthCalledWith(9, 'creation', ['1000', '1000', '1000'])
|
||||
})
|
||||
|
||||
it('commits hideAmountGDD', () => {
|
||||
login({ commit, state }, commitedData)
|
||||
expect(commit).toHaveBeenNthCalledWith(10, 'hideAmountGDD', false)
|
||||
expect(commit).toHaveBeenNthCalledWith(9, 'hideAmountGDD', false)
|
||||
})
|
||||
|
||||
it('commits hideAmountGDT', () => {
|
||||
login({ commit, state }, commitedData)
|
||||
expect(commit).toHaveBeenNthCalledWith(11, 'hideAmountGDT', true)
|
||||
expect(commit).toHaveBeenNthCalledWith(10, 'hideAmountGDT', true)
|
||||
})
|
||||
})
|
||||
|
||||
@ -255,7 +240,7 @@ describe('Vuex store', () => {
|
||||
|
||||
it('calls eleven commits', () => {
|
||||
logout({ commit, state })
|
||||
expect(commit).toHaveBeenCalledTimes(11)
|
||||
expect(commit).toHaveBeenCalledTimes(10)
|
||||
})
|
||||
|
||||
it('commits token', () => {
|
||||
@ -298,19 +283,14 @@ describe('Vuex store', () => {
|
||||
expect(commit).toHaveBeenNthCalledWith(8, 'isAdmin', false)
|
||||
})
|
||||
|
||||
it('commits creation', () => {
|
||||
logout({ commit, state })
|
||||
expect(commit).toHaveBeenNthCalledWith(9, 'creation', null)
|
||||
})
|
||||
|
||||
it('commits hideAmountGDD', () => {
|
||||
logout({ commit, state })
|
||||
expect(commit).toHaveBeenNthCalledWith(10, 'hideAmountGDD', false)
|
||||
expect(commit).toHaveBeenNthCalledWith(9, 'hideAmountGDD', false)
|
||||
})
|
||||
|
||||
it('commits hideAmountGDT', () => {
|
||||
logout({ commit, state })
|
||||
expect(commit).toHaveBeenNthCalledWith(11, 'hideAmountGDT', true)
|
||||
expect(commit).toHaveBeenNthCalledWith(10, 'hideAmountGDT', true)
|
||||
})
|
||||
// how to get this working?
|
||||
it.skip('calls localStorage.clear()', () => {
|
||||
|
||||
@ -4578,9 +4578,9 @@ caniuse-api@^3.0.0:
|
||||
lodash.uniq "^4.5.0"
|
||||
|
||||
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001181, caniuse-lite@^1.0.30001280, caniuse-lite@^1.0.30001286:
|
||||
version "1.0.30001439"
|
||||
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001439.tgz"
|
||||
integrity sha512-1MgUzEkoMO6gKfXflStpYgZDlFM7M/ck/bgfVCACO5vnAf0fXoNVHdWtqGU+MYca+4bL9Z5bpOVmR33cWW9G2A==
|
||||
version "1.0.30001442"
|
||||
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001442.tgz"
|
||||
integrity sha512-239m03Pqy0hwxYPYR5JwOIxRJfLTWtle9FV8zosfV5pHg+/51uD4nxcUlM8+mWWGfwKtt8lJNHnD3cWw9VZ6ow==
|
||||
|
||||
capture-exit@^2.0.0:
|
||||
version "2.0.0"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user