diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4cc28bf20..ac60cfdf2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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 }} ############################################################################## diff --git a/backend/src/auth/RIGHTS.ts b/backend/src/auth/RIGHTS.ts index 4f144f1e9..6515f9e3b 100644 --- a/backend/src/auth/RIGHTS.ts +++ b/backend/src/auth/RIGHTS.ts @@ -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', diff --git a/backend/src/auth/ROLES.ts b/backend/src/auth/ROLES.ts index eabaf8e99..2f3b4e081 100644 --- a/backend/src/auth/ROLES.ts +++ b/backend/src/auth/ROLES.ts @@ -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 diff --git a/backend/src/graphql/model/OpenCreation.ts b/backend/src/graphql/model/OpenCreation.ts new file mode 100644 index 000000000..9ef08fd4a --- /dev/null +++ b/backend/src/graphql/model/OpenCreation.ts @@ -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 +} diff --git a/backend/src/graphql/model/User.ts b/backend/src/graphql/model/User.ts index 6e353f6a7..b4fdcae4f 100644 --- a/backend/src/graphql/model/User.ts +++ b/backend/src/graphql/model/User.ts @@ -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,7 +24,6 @@ export class User { this.isAdmin = user.isAdmin this.klickTipp = null this.hasElopage = null - this.creation = creation this.hideAmountGDD = user.hideAmountGDD this.hideAmountGDT = user.hideAmountGDT } @@ -34,9 +31,6 @@ export class User { @Field(() => Number) id: number - // `public_key` binary(32) DEFAULT NULL, - // `privkey` binary(80) DEFAULT NULL, - @Field(() => String) gradidoID: string @@ -62,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 @@ -84,8 +75,6 @@ export class User { @Field(() => Number, { nullable: true }) publisherId: number | null - // `passphrase` text COLLATE utf8mb4_unicode_ci DEFAULT NULL, - @Field(() => Date, { nullable: true }) isAdmin: Date | null @@ -94,7 +83,4 @@ export class User { @Field(() => Boolean, { nullable: true }) hasElopage: boolean | null - - @Field(() => [Decimal]) - creation: Decimal[] } diff --git a/backend/src/graphql/resolver/ContributionResolver.ts b/backend/src/graphql/resolver/ContributionResolver.ts index 3794546e2..7771c62ca 100644 --- a/backend/src/graphql/resolver/ContributionResolver.ts +++ b/backend/src/graphql/resolver/ContributionResolver.ts @@ -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, @@ -691,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 { + 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], + } + }) + } } diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index ed1bf3f47..c630c240a 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -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 { 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 { 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) diff --git a/backend/src/graphql/resolver/util/creations.ts b/backend/src/graphql/resolver/util/creations.ts index 54286d2aa..00137eaa1 100644 --- a/backend/src/graphql/resolver/util/creations.ts +++ b/backend/src/graphql/resolver/util/creations.ts @@ -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, ] } diff --git a/frontend/src/graphql/mutations.js b/frontend/src/graphql/mutations.js index 95d7b0c39..111eb7ab5 100644 --- a/frontend/src/graphql/mutations.js +++ b/frontend/src/graphql/mutations.js @@ -154,7 +154,6 @@ export const login = gql` hasElopage publisherId isAdmin - creation hideAmountGDD hideAmountGDT } diff --git a/frontend/src/graphql/queries.js b/frontend/src/graphql/queries.js index db6a68bf4..65fde8d1d 100644 --- a/frontend/src/graphql/queries.js +++ b/frontend/src/graphql/queries.js @@ -250,3 +250,13 @@ export const listContributionMessages = gql` } } ` + +export const openCreations = gql` + query { + openCreations { + year + month + amount + } + } +` diff --git a/frontend/src/pages/Community.spec.js b/frontend/src/pages/Community.spec.js index 62ed46cbb..7297876df 100644 --- a/frontend/src/pages/Community.spec.js +++ b/frontend/src/pages/Community.spec.js @@ -2,7 +2,7 @@ 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 { listContributions, listAllContributions } from '@/graphql/queries' import VueRouter from 'vue-router' import routes from '../routes/routes' @@ -13,6 +13,7 @@ localVue.use(VueRouter) const mockStoreDispach = jest.fn() const apolloQueryMock = jest.fn() const apolloMutationMock = jest.fn() +const apolloRefetchMock = jest.fn() const router = new VueRouter({ base: '/', @@ -39,6 +40,11 @@ describe('Community', () => { $apollo: { query: apolloQueryMock, mutate: apolloMutationMock, + queries: { + OpenCreations: { + refetch: apolloRefetchMock, + }, + }, }, $store: { dispatch: mockStoreDispach, @@ -207,10 +213,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 +297,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 +376,7 @@ describe('Community', () => { }) it('verifies the login (to get the new creations available)', () => { - expect(apolloQueryMock).toBeCalledWith({ - query: verifyLogin, - fetchPolicy: 'network-only', - }) + expect(apolloRefetchMock).toBeCalled() }) }) diff --git a/frontend/src/pages/Community.vue b/frontend/src/pages/Community.vue index 16af01ce3..1332ca9d7 100644 --- a/frontend/src/pages/Community.vue +++ b/frontend/src/pages/Community.vue @@ -5,8 +5,8 @@
@@ -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) @@ -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, diff --git a/frontend/src/pages/Send.spec.js b/frontend/src/pages/Send.spec.js index ba756f79d..63715a6a2 100644 --- a/frontend/src/pages/Send.spec.js +++ b/frontend/src/pages/Send.spec.js @@ -13,7 +13,7 @@ const navigatorClipboardMock = jest.fn() const localVue = global.localVue -describe.skip('Send', () => { +describe('Send', () => { let wrapper const propsData = { diff --git a/frontend/src/store/store.js b/frontend/src/store/store.js index 84fd82fd5..1cd874c06 100644 --- a/frontend/src/store/store.js +++ b/frontend/src/store/store.js @@ -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, }, diff --git a/frontend/src/store/store.test.js b/frontend/src/store/store.test.js index 5f40d7fa2..33fedd562 100644 --- a/frontend/src/store/store.test.js +++ b/frontend/src/store/store.test.js @@ -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()', () => {