From 88436c3cf7cfdfbc9e9b61dfee7e393297eeef36 Mon Sep 17 00:00:00 2001 From: Alina Beck Date: Mon, 11 Nov 2019 12:05:09 +0300 Subject: [PATCH 01/21] add donation info with hardcoded values --- webapp/pages/index.vue | 65 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 3 deletions(-) diff --git a/webapp/pages/index.vue b/webapp/pages/index.vue index 91e2de55f..cdc95933c 100644 --- a/webapp/pages/index.vue +++ b/webapp/pages/index.vue @@ -4,7 +4,18 @@ - + +
+
+

Donations for November

+
+
+ 500 of 15.000 € +
+ + Donate now + +
From 768f5f66315783eaaca16de03bf93dd456c8ac07 Mon Sep 17 00:00:00 2001 From: Alina Beck Date: Mon, 11 Nov 2019 12:31:10 +0300 Subject: [PATCH 02/21] extract progress bar into separate component --- webapp/components/ProgressBar/ProgressBar.vue | 76 +++++++++++++++++++ webapp/pages/index.vue | 53 +++---------- 2 files changed, 85 insertions(+), 44 deletions(-) create mode 100644 webapp/components/ProgressBar/ProgressBar.vue diff --git a/webapp/components/ProgressBar/ProgressBar.vue b/webapp/components/ProgressBar/ProgressBar.vue new file mode 100644 index 000000000..93030c47b --- /dev/null +++ b/webapp/components/ProgressBar/ProgressBar.vue @@ -0,0 +1,76 @@ + + + + + diff --git a/webapp/pages/index.vue b/webapp/pages/index.vue index cdc95933c..d9944a5dd 100644 --- a/webapp/pages/index.vue +++ b/webapp/pages/index.vue @@ -6,12 +6,13 @@
-
-

Donations for November

-
-
- 500 of 15.000 € -
+ + Donate now @@ -74,6 +75,7 @@ import HcPostCard from '~/components/PostCard/PostCard.vue' import HcLoadMore from '~/components/LoadMore.vue' import MasonryGrid from '~/components/MasonryGrid/MasonryGrid.vue' import MasonryGridItem from '~/components/MasonryGrid/MasonryGridItem.vue' +import ProgressBar from '~/components/ProgressBar/ProgressBar.vue' import { mapGetters, mapMutations } from 'vuex' import { filterPosts } from '~/graphql/PostQuery.js' import PostMutations from '~/graphql/PostMutations' @@ -86,6 +88,7 @@ export default { HcEmpty, MasonryGrid, MasonryGridItem, + ProgressBar, }, data() { const { hashtag = null } = this.$route.query @@ -286,42 +289,4 @@ export default { align-items: flex-end; height: 100%; } - -.progress-bar { - position: relative; - height: 100%; - width: 240px; - margin-right: $space-x-small; -} - -.progress-bar__title { - position: absolute; - top: -2px; - left: $space-xx-small; - margin: 0; -} - -.progress-bar__goal { - position: absolute; - bottom: 0; - left: 0; - height: 37.5px; // styleguide-button-size - background-color: $color-neutral-100; - border-radius: $border-radius-base; -} - -.progress-bar__progress { - position: absolute; - bottom: 1px; - left: 0; - height: 35.5px; // styleguide-button-size - background-color: $color-yellow; - border-radius: $border-radius-base; -} - -.progress-bar__label { - position: absolute; - top: 50%; - left: $space-xx-small; -} From f6de44b337a0395ecb345092a4506cbc0045303a Mon Sep 17 00:00:00 2001 From: Alina Beck Date: Mon, 11 Nov 2019 13:06:09 +0300 Subject: [PATCH 03/21] place donation info in separate component compute title and label according to selected locale and donation values --- .../components/DonationInfo/DonationInfo.vue | 50 +++++++++++++++++++ webapp/locales/de.json | 9 +++- webapp/locales/en.json | 15 ++++-- webapp/pages/index.vue | 23 ++------- 4 files changed, 70 insertions(+), 27 deletions(-) create mode 100644 webapp/components/DonationInfo/DonationInfo.vue diff --git a/webapp/components/DonationInfo/DonationInfo.vue b/webapp/components/DonationInfo/DonationInfo.vue new file mode 100644 index 000000000..cbcc0571e --- /dev/null +++ b/webapp/components/DonationInfo/DonationInfo.vue @@ -0,0 +1,50 @@ + + + + + diff --git a/webapp/locales/de.json b/webapp/locales/de.json index 34cd948ed..3aaab5544 100644 --- a/webapp/locales/de.json +++ b/webapp/locales/de.json @@ -62,6 +62,11 @@ } } }, + "donations": { + "donations-for": "Spenden für", + "donate-now": "Jetzt spenden", + "amount-of-total": "{amount} von {total} € erreicht" + }, "maintenance": { "title": "Human Connection befindet sich in der Wartung", "explanation": "Zurzeit führen wir einige geplante Wartungsarbeiten durch, bitte versuch es später erneut.", @@ -674,7 +679,7 @@ "terms-of-service": { "title": "Nutzungsbedingungen", "description": "Die folgenden Nutzungsbedingungen sind Basis für die Nutzung unseres Netzwerkes. Beim Registrieren musst Du sie anerkennen und wir werden Dich auch später über ggf. stattfindende Änderungen informieren. Das Human Connection Netzwerk wird in Deutschland betrieben und unterliegt daher deutschem Recht. Gerichtsstand ist Kirchheim / Teck. Zu Details schau in unser Impressum: https://human-connection.org/impressum " - }, + }, "use-and-license" : { "title": "Nutzung und Lizenz", "description": "Sind Inhalte, die Du bei uns einstellst, durch Rechte am geistigen Eigentum geschützt, erteilst Du uns eine nicht-exklusive, übertragbare, unterlizenzierbare und weltweite Lizenz für die Nutzung dieser Inhalte für die Bereitstellung in unserem Netzwerk. Diese Lizenz endet, sobald Du Deine Inhalte oder Deinen ganzen Account löscht. Bedenke, dass andere Deine Inhalte weiter teilen können und wir diese nicht löschen können." @@ -702,6 +707,6 @@ "addition" : { "title": "Zusätzliche machen wir regelmäßig Veranstaltungen, wo Du auch Eindrücke wiedergeben und Fragen stellen kannst. Du findest eine aktuelle Übersicht hier:", "description": " https://human-connection.org/veranstaltungen/ " - } + } } } diff --git a/webapp/locales/en.json b/webapp/locales/en.json index d3b4e8edc..7c8ddbe4c 100644 --- a/webapp/locales/en.json +++ b/webapp/locales/en.json @@ -63,6 +63,11 @@ } } }, + "donations": { + "donations-for": "Donations for", + "donate-now": "Donate now", + "amount-of-total": "{amount} of {total} € collected" + }, "maintenance": { "title": "Human Connection is under maintenance", "explanation": "At the moment we are doing some scheduled maintenance, please try again later.", @@ -675,7 +680,7 @@ "terms-of-service": { "title": "Terms of Service", "description": "The following terms of use form the basis for the use of our network. When you register, you must accept them and we will inform you later about any changes that may take place. The Human Connection Network is operated in Germany and is therefore subject to German law. Place of jurisdiction is Kirchheim / Teck. For details see our imprint: https://human-connection.org/imprint " - }, + }, "use-and-license" : { "title": "Use and License", "description": "If any content you post to us is protected by intellectual property rights, you grant us a non-exclusive, transferable, sublicensable, worldwide license to use such content for posting to our network. This license expires when you delete your content or your entire account. Remember that others may share your content and we cannot delete it." @@ -703,10 +708,10 @@ "addition" : { "title": "In addition, we regularly hold events where you can also share your impressions and ask questions. You can find a current overview here:", "description": " https://human-connection.org/events/ " - } + } } } - - - + + + diff --git a/webapp/pages/index.vue b/webapp/pages/index.vue index d9944a5dd..7105d8c74 100644 --- a/webapp/pages/index.vue +++ b/webapp/pages/index.vue @@ -5,18 +5,7 @@ - +
From d15a1a8105f178cd77bc58168ee90320e7a92db2 Mon Sep 17 00:00:00 2001 From: Alina Beck Date: Mon, 11 Nov 2019 15:57:29 +0300 Subject: [PATCH 05/21] style donations info for mobile --- webapp/components/DonationInfo/DonationInfo.vue | 7 +++++++ webapp/components/ProgressBar/ProgressBar.vue | 15 ++++++++++++++- webapp/pages/index.vue | 9 +++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/webapp/components/DonationInfo/DonationInfo.vue b/webapp/components/DonationInfo/DonationInfo.vue index cbcc0571e..f05258341 100644 --- a/webapp/components/DonationInfo/DonationInfo.vue +++ b/webapp/components/DonationInfo/DonationInfo.vue @@ -46,5 +46,12 @@ export default { display: flex; align-items: flex-end; height: 100%; + + @media (max-width: 546px) { + width: 100%; + height: 50%; + justify-content: flex-end; + margin-bottom: $space-x-small; + } } diff --git a/webapp/components/ProgressBar/ProgressBar.vue b/webapp/components/ProgressBar/ProgressBar.vue index 93030c47b..6041bd359 100644 --- a/webapp/components/ProgressBar/ProgressBar.vue +++ b/webapp/components/ProgressBar/ProgressBar.vue @@ -1,8 +1,8 @@ @@ -39,6 +39,15 @@ export default { height: 100%; width: 240px; margin-right: $space-x-small; + + @media (max-width: 680px) { + width: 180px; + } + + @media (max-width: 546px) { + flex-basis: 50%; + flex-grow: 1; + } } .progress-bar__title { @@ -46,6 +55,10 @@ export default { top: -2px; left: $space-xx-small; margin: 0; + + @media (max-width: 546px) { + top: $space-xx-small; + } } .progress-bar__goal { diff --git a/webapp/pages/index.vue b/webapp/pages/index.vue index 7105d8c74..d1ff9a2ad 100644 --- a/webapp/pages/index.vue +++ b/webapp/pages/index.vue @@ -265,11 +265,20 @@ export default { .sorting-dropdown { width: 250px; position: relative; + + @media (max-width: 680px) { + width: 180px; + } } .top-info-bar { display: flex; justify-content: space-between; align-items: flex-end; + + @media (max-width: 546px) { + grid-row-end: span 3 !important; + flex-direction: column; + } } From 61cb7dd7c2cc230f99eb4d8ebacc131acbf5d262 Mon Sep 17 00:00:00 2001 From: Alina Beck Date: Mon, 11 Nov 2019 16:28:39 +0300 Subject: [PATCH 06/21] add ProgressBar tests --- .../ProgressBar/ProgressBar.spec.js | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 webapp/components/ProgressBar/ProgressBar.spec.js diff --git a/webapp/components/ProgressBar/ProgressBar.spec.js b/webapp/components/ProgressBar/ProgressBar.spec.js new file mode 100644 index 000000000..6fb6f1666 --- /dev/null +++ b/webapp/components/ProgressBar/ProgressBar.spec.js @@ -0,0 +1,65 @@ +import { mount } from '@vue/test-utils' +import ProgressBar from './ProgressBar' + +describe('ProgessBar.vue', () => { + let propsData + + beforeEach(() => { + propsData = { + goal: 50000, + progress: 10000, + } + }) + + const Wrapper = () => mount(ProgressBar, { propsData }) + + describe('given only goal and progress', () => { + it('renders no title', () => { + expect( + Wrapper() + .find('.progress-bar__title') + .exists(), + ).toBe(false) + }) + + it('renders no label', () => { + expect( + Wrapper() + .find('.progress-bar__label') + .exists(), + ).toBe(false) + }) + + it('calculates the progress bar width as a percentage of the goal', () => { + expect(Wrapper().vm.progressBarWidth).toBe('width: 20%;') + }) + }) + + describe('given a title', () => { + beforeEach(() => { + propsData.title = 'This is progress' + }) + + it('renders the title', () => { + expect( + Wrapper() + .find('.progress-bar__title') + .text(), + ).toBe('This is progress') + }) + }) + + describe('given a label', () => { + beforeEach(() => { + propsData.label = 'Going well' + }) + + it('renders the label', () => { + expect( + Wrapper() + .find('.progress-bar__label') + .text(), + ).toBe('Going well') + }) + }) +}) From ffb3ff896e9649cdde7b96430b921be29529a791 Mon Sep 17 00:00:00 2001 From: Alina Beck Date: Mon, 11 Nov 2019 17:36:04 +0300 Subject: [PATCH 07/21] add tests for DonationInfo component (wip) --- .../DonationInfo/DonationInfo.spec.js | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 webapp/components/DonationInfo/DonationInfo.spec.js diff --git a/webapp/components/DonationInfo/DonationInfo.spec.js b/webapp/components/DonationInfo/DonationInfo.spec.js new file mode 100644 index 000000000..64fd432e8 --- /dev/null +++ b/webapp/components/DonationInfo/DonationInfo.spec.js @@ -0,0 +1,55 @@ +import { mount, createLocalVue } from '@vue/test-utils' +import Styleguide from '@human-connection/styleguide' +import DonationInfo from './DonationInfo.vue' + +const localVue = createLocalVue() +localVue.use(Styleguide) + +const mockDate = new Date(2019, 11, 6) +global.Date = jest.fn(() => mockDate) + +describe('DonationInfo.vue', () => { + const mocks = { + $t: jest.fn(string => string), + $i18n: { + locale: () => 'de', + }, + } + + const propsData = { + goal: 50000, + progress: 10000, + } + + const Wrapper = () => mount(DonationInfo, { propsData, mocks, localVue }) + + it('includes a link to the Human Connection donations website', () => { + expect( + Wrapper() + .find('a') + .attributes('href'), + ).toBe('https://human-connection.org/spenden/') + }) + + it('displays a call to action button', () => { + expect( + Wrapper() + .find('.ds-button') + .text(), + ).toBe('donations.donate-now') + }) + + it('creates a title from the current month and a translation string', () => { + mocks.$t = jest.fn(() => 'Spenden für') + expect(Wrapper().vm.title).toBe('Spenden für Dezember') + }) + + it('creates a label from the given amounts and a translation string', () => { + Wrapper() + expect(mocks.$t.mock.calls[1][0]).toBe('donations.amount-of-total') + expect(mocks.$t.mock.calls[1][1]).toBe({ + amount: '10.000', + total: '50.000', + }) + }) +}) From 835a098731fba4d40a741f08a9ca4681c412d436 Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Mon, 11 Nov 2019 18:14:33 +0100 Subject: [PATCH 08/21] Set up backend for DonationsInfo Co-authored-by: Mike Aono --- .../src/middleware/permissionsMiddleware.js | 2 + backend/src/models/Donations.js | 14 ++ backend/src/models/index.js | 1 + backend/src/schema/index.js | 2 + backend/src/schema/resolvers/donations.js | 31 ++++ .../src/schema/resolvers/donations.spec.js | 172 ++++++++++++++++++ backend/src/schema/types/type/Donations.gql | 14 ++ backend/src/seed/factories/donations.js | 18 ++ backend/src/seed/factories/index.js | 2 + 9 files changed, 256 insertions(+) create mode 100644 backend/src/models/Donations.js create mode 100644 backend/src/schema/resolvers/donations.js create mode 100644 backend/src/schema/resolvers/donations.spec.js create mode 100644 backend/src/schema/types/type/Donations.gql create mode 100644 backend/src/seed/factories/donations.js diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index a0116a439..d312bc112 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -135,6 +135,7 @@ const permissions = shield( blockedUsers: isAuthenticated, notifications: isAuthenticated, profilePagePosts: or(onlyEnabledContent, isModerator), + Donations: isAuthenticated, }, Mutation: { '*': deny, @@ -177,6 +178,7 @@ const permissions = shield( VerifyEmailAddress: isAuthenticated, pinPost: isAdmin, unpinPost: isAdmin, + UpdateDonations: isAdmin, }, User: { email: or(isMyOwn, isAdmin), diff --git a/backend/src/models/Donations.js b/backend/src/models/Donations.js new file mode 100644 index 000000000..45e06e1d4 --- /dev/null +++ b/backend/src/models/Donations.js @@ -0,0 +1,14 @@ +import uuid from 'uuid/v4' + +module.exports = { + id: { type: 'string', primary: true, default: uuid }, + goal: { type: 'number' }, + progress: { type: 'number' }, + createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, + updatedAt: { + type: 'string', + isoDate: true, + required: true, + default: () => new Date().toISOString(), + }, +} diff --git a/backend/src/models/index.js b/backend/src/models/index.js index 08362b69f..bd89ddc51 100644 --- a/backend/src/models/index.js +++ b/backend/src/models/index.js @@ -12,4 +12,5 @@ export default { Category: require('./Category.js'), Tag: require('./Tag.js'), Location: require('./Location.js'), + Donations: require('./Donations.js'), } diff --git a/backend/src/schema/index.js b/backend/src/schema/index.js index 95fa9ef61..b1bd36451 100644 --- a/backend/src/schema/index.js +++ b/backend/src/schema/index.js @@ -24,6 +24,7 @@ export default applyScalars( 'SocialMedia', 'NOTIFIED', 'REPORTED', + 'Donations', ], // add 'User' here as soon as possible }, @@ -44,6 +45,7 @@ export default applyScalars( 'EMOTED', 'NOTIFIED', 'REPORTED', + 'Donations', ], // add 'User' here as soon as possible }, diff --git a/backend/src/schema/resolvers/donations.js b/backend/src/schema/resolvers/donations.js new file mode 100644 index 000000000..1c7aee8d4 --- /dev/null +++ b/backend/src/schema/resolvers/donations.js @@ -0,0 +1,31 @@ +export default { + Mutation: { + UpdateDonations: async (_parent, params, context, _resolveInfo) => { + const { driver } = context + const session = driver.session() + let donations + const writeTxResultPromise = session.writeTransaction(async txc => { + const updateDonationsTransactionResponse = await txc.run( + ` + MATCH (donations:Donations {id: $params.id}) + SET donations += $params + SET donations.updatedAt = toString(datetime()) + RETURN donations + `, + { params }, + ) + return updateDonationsTransactionResponse.records.map( + record => record.get('donations').properties, + ) + }) + try { + const txResult = await writeTxResultPromise + if (!txResult[0]) return null + donations = txResult[0] + } finally { + session.close() + } + return donations + }, + }, +} diff --git a/backend/src/schema/resolvers/donations.spec.js b/backend/src/schema/resolvers/donations.spec.js new file mode 100644 index 000000000..a5264ee3a --- /dev/null +++ b/backend/src/schema/resolvers/donations.spec.js @@ -0,0 +1,172 @@ +import { createTestClient } from 'apollo-server-testing' +import Factory from '../../seed/factories' +import { gql } from '../../jest/helpers' +import { neode as getNeode, getDriver } from '../../bootstrap/neo4j' +import createServer from '../../server' + +let mutate, query, authenticatedUser, variables +const factory = Factory() +const instance = getNeode() +const driver = getDriver() + +const updateDonationsMutation = gql` + mutation($id: ID!, $goal: Int, $progress: Int) { + UpdateDonations(id: $id, goal: $goal, progress: $progress) { + goal + progress + createdAt + updatedAt + } + } +` +const donationsQuery = gql` + query { + Donations { + goal + progress + } + } +` + +describe('donations', () => { + let currentUser, newlyCreatedDonations + beforeAll(async () => { + await factory.cleanDatabase() + authenticatedUser = undefined + const { server } = createServer({ + context: () => { + return { + driver, + neode: instance, + user: authenticatedUser, + } + }, + }) + mutate = createTestClient(server).mutate + query = createTestClient(server).query + }) + + beforeEach(async () => { + variables = {} + newlyCreatedDonations = await factory.create('Donations', { id: 'total-donations' }) + }) + + afterEach(async () => { + await factory.cleanDatabase() + }) + + describe('query for donations', () => { + describe('unauthenticated', () => { + it('throws authorization error', async () => { + authenticatedUser = undefined + await expect(query({ query: donationsQuery, variables })).resolves.toMatchObject({ + errors: [{ message: 'Not Authorised!' }], + }) + }) + + describe('authenticated', () => { + beforeEach(async () => { + currentUser = await factory.create('User', { + id: 'normal-user', + role: 'user', + }) + authenticatedUser = await currentUser.toJson() + }) + + it('returns the current Donations info', async () => { + await expect(query({ query: donationsQuery, variables })).resolves.toMatchObject({ + data: { Donations: [{ goal: 15000, progress: 0 }] }, + }) + }) + }) + }) + }) + + describe('update donations', () => { + beforeEach(() => { + variables = { id: 'total-donations', goal: 20000, progress: 3000 } + }) + + describe('unauthenticated', () => { + it('throws authorization error', async () => { + authenticatedUser = undefined + await expect( + mutate({ mutation: updateDonationsMutation, variables }), + ).resolves.toMatchObject({ + errors: [{ message: 'Not Authorised!' }], + }) + }) + + describe('authenticated', () => { + describe('as a normal user', () => { + beforeEach(async () => { + currentUser = await factory.create('User', { + id: 'normal-user', + role: 'user', + }) + authenticatedUser = await currentUser.toJson() + }) + + it('throws authorization error', async () => { + await expect( + mutate({ mutation: updateDonationsMutation, variables }), + ).resolves.toMatchObject({ + data: { UpdateDonations: null }, + errors: [{ message: 'Not Authorised!' }], + }) + }) + }) + + describe('as a moderator', () => { + beforeEach(async () => { + currentUser = await factory.create('User', { + id: 'moderator', + role: 'moderator', + }) + authenticatedUser = await currentUser.toJson() + }) + + it('throws authorization error', async () => { + await expect( + mutate({ mutation: updateDonationsMutation, variables }), + ).resolves.toMatchObject({ + data: { UpdateDonations: null }, + errors: [{ message: 'Not Authorised!' }], + }) + }) + }) + + describe('as an admin', () => { + beforeEach(async () => { + currentUser = await factory.create('User', { + id: 'admin', + role: 'admin', + }) + authenticatedUser = await currentUser.toJson() + }) + + it('updates Donations info', async () => { + await expect( + mutate({ mutation: updateDonationsMutation, variables }), + ).resolves.toMatchObject({ + data: { UpdateDonations: { goal: 20000, progress: 3000 } }, + errors: undefined, + }) + }) + + it('updates the updatedAt attribute', async () => { + newlyCreatedDonations = await newlyCreatedDonations.toJson() + const { + data: { UpdateDonations }, + } = await mutate({ mutation: updateDonationsMutation, variables }) + expect(newlyCreatedDonations.updatedAt).toBeTruthy() + expect(Date.parse(newlyCreatedDonations.updatedAt)).toEqual(expect.any(Number)) + expect(UpdateDonations.updatedAt).toBeTruthy() + expect(Date.parse(UpdateDonations.updatedAt)).toEqual(expect.any(Number)) + expect(newlyCreatedDonations.updatedAt).not.toEqual(UpdateDonations.updatedAt) + }) + }) + }) + }) + }) +}) diff --git a/backend/src/schema/types/type/Donations.gql b/backend/src/schema/types/type/Donations.gql new file mode 100644 index 000000000..88e5edd2d --- /dev/null +++ b/backend/src/schema/types/type/Donations.gql @@ -0,0 +1,14 @@ +type Donations { + goal: Int! + progress: Int! + createdAt: String + updatedAt: String +} + +type Query { + Donations: [Donations] +} + +type Mutation { + UpdateDonations(id: ID!, goal: Int, progress: Int): Donations +} \ No newline at end of file diff --git a/backend/src/seed/factories/donations.js b/backend/src/seed/factories/donations.js new file mode 100644 index 000000000..e22cdb6d7 --- /dev/null +++ b/backend/src/seed/factories/donations.js @@ -0,0 +1,18 @@ +import uuid from 'uuid/v4' + +export default function create() { + return { + factory: async ({ args, neodeInstance }) => { + const defaults = { + id: uuid(), + goal: 15000, + progress: 0, + } + args = { + ...defaults, + ...args, + } + return neodeInstance.create('Donations', args) + }, + } +} diff --git a/backend/src/seed/factories/index.js b/backend/src/seed/factories/index.js index 2c96c8698..5054155fc 100644 --- a/backend/src/seed/factories/index.js +++ b/backend/src/seed/factories/index.js @@ -8,6 +8,7 @@ import createTag from './tags.js' import createSocialMedia from './socialMedia.js' import createLocation from './locations.js' import createEmailAddress from './emailAddresses.js' +import createDonations from './donations.js' import createUnverifiedEmailAddresss from './unverifiedEmailAddresses.js' const factories = { @@ -21,6 +22,7 @@ const factories = { Location: createLocation, EmailAddress: createEmailAddress, UnverifiedEmailAddress: createUnverifiedEmailAddresss, + Donations: createDonations, } export const cleanDatabase = async (options = {}) => { From afb443ac0df3ccd2b037e2a98f2e282cd1138384 Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Mon, 11 Nov 2019 19:14:06 +0100 Subject: [PATCH 09/21] Update resolver to match one Donations node - there should only ever be one Donations node to start off with, if that changes in the future for some reason, then we'd need to look into changing the match to something that makes more sense. --- backend/src/schema/resolvers/donations.js | 7 ++++++- backend/src/schema/resolvers/donations.spec.js | 8 ++++---- backend/src/schema/types/type/Donations.gql | 3 ++- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/backend/src/schema/resolvers/donations.js b/backend/src/schema/resolvers/donations.js index 1c7aee8d4..4057748e0 100644 --- a/backend/src/schema/resolvers/donations.js +++ b/backend/src/schema/resolvers/donations.js @@ -1,13 +1,18 @@ +import uuid from 'uuid/v4' + export default { Mutation: { UpdateDonations: async (_parent, params, context, _resolveInfo) => { const { driver } = context const session = driver.session() let donations + params.id = params.id || uuid() + const writeTxResultPromise = session.writeTransaction(async txc => { const updateDonationsTransactionResponse = await txc.run( ` - MATCH (donations:Donations {id: $params.id}) + MATCH (donations:Donations) + WITH donations LIMIT 1 SET donations += $params SET donations.updatedAt = toString(datetime()) RETURN donations diff --git a/backend/src/schema/resolvers/donations.spec.js b/backend/src/schema/resolvers/donations.spec.js index a5264ee3a..9c62a7dda 100644 --- a/backend/src/schema/resolvers/donations.spec.js +++ b/backend/src/schema/resolvers/donations.spec.js @@ -10,8 +10,8 @@ const instance = getNeode() const driver = getDriver() const updateDonationsMutation = gql` - mutation($id: ID!, $goal: Int, $progress: Int) { - UpdateDonations(id: $id, goal: $goal, progress: $progress) { + mutation($goal: Int, $progress: Int) { + UpdateDonations(goal: $goal, progress: $progress) { goal progress createdAt @@ -48,7 +48,7 @@ describe('donations', () => { beforeEach(async () => { variables = {} - newlyCreatedDonations = await factory.create('Donations', { id: 'total-donations' }) + newlyCreatedDonations = await factory.create('Donations') }) afterEach(async () => { @@ -84,7 +84,7 @@ describe('donations', () => { describe('update donations', () => { beforeEach(() => { - variables = { id: 'total-donations', goal: 20000, progress: 3000 } + variables = { goal: 20000, progress: 3000 } }) describe('unauthenticated', () => { diff --git a/backend/src/schema/types/type/Donations.gql b/backend/src/schema/types/type/Donations.gql index 88e5edd2d..39cfe9b71 100644 --- a/backend/src/schema/types/type/Donations.gql +++ b/backend/src/schema/types/type/Donations.gql @@ -1,4 +1,5 @@ type Donations { + id: ID! goal: Int! progress: Int! createdAt: String @@ -10,5 +11,5 @@ type Query { } type Mutation { - UpdateDonations(id: ID!, goal: Int, progress: Int): Donations + UpdateDonations(goal: Int, progress: Int): Donations } \ No newline at end of file From 7d95809f6eacb0974eb4950b541f84a50491b862 Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Mon, 11 Nov 2019 19:15:39 +0100 Subject: [PATCH 10/21] Update test to ensure currency is shown by locale --- .../DonationInfo/DonationInfo.spec.js | 50 ++++++++++++------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/webapp/components/DonationInfo/DonationInfo.spec.js b/webapp/components/DonationInfo/DonationInfo.spec.js index 64fd432e8..34be14836 100644 --- a/webapp/components/DonationInfo/DonationInfo.spec.js +++ b/webapp/components/DonationInfo/DonationInfo.spec.js @@ -9,17 +9,19 @@ const mockDate = new Date(2019, 11, 6) global.Date = jest.fn(() => mockDate) describe('DonationInfo.vue', () => { - const mocks = { - $t: jest.fn(string => string), - $i18n: { - locale: () => 'de', - }, - } - - const propsData = { - goal: 50000, - progress: 10000, - } + let mocks, propsData + beforeEach(() => { + mocks = { + $t: jest.fn(string => string), + $i18n: { + locale: () => 'de', + }, + } + propsData = { + goal: 50000, + progress: 10000, + } + }) const Wrapper = () => mount(DonationInfo, { propsData, mocks, localVue }) @@ -44,12 +46,26 @@ describe('DonationInfo.vue', () => { expect(Wrapper().vm.title).toBe('Spenden für Dezember') }) - it('creates a label from the given amounts and a translation string', () => { - Wrapper() - expect(mocks.$t.mock.calls[1][0]).toBe('donations.amount-of-total') - expect(mocks.$t.mock.calls[1][1]).toBe({ - amount: '10.000', - total: '50.000', + describe('given german locale', () => { + it('creates a label from the given amounts and a translation string', () => { + Wrapper() + expect(mocks.$t.mock.calls[1][0]).toBe('donations.amount-of-total') + expect(mocks.$t.mock.calls[1][1]).toStrictEqual({ + amount: '10.000', + total: '50.000', + }) + }) + }) + + describe('given english locale', () => { + it('creates a label from the given amounts and a translation string', () => { + mocks.$i18n.locale = () => 'en' + Wrapper() + expect(mocks.$t.mock.calls[1][0]).toBe('donations.amount-of-total') + expect(mocks.$t.mock.calls[1][1]).toStrictEqual({ + amount: '10,000', + total: '50,000', + }) }) }) }) From f51ad1131b68c2ef97f8a9c7889061ae5fc83807 Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Mon, 11 Nov 2019 19:16:07 +0100 Subject: [PATCH 11/21] UpdateDonations from admin/donations @alina-beck I'm not 100% satisfied with this at the moment. I think that the admin could have the goal and progress initialized from a query for the Donations, and be able to update if one of them changed... what do you think? --- webapp/graphql/Donations.js | 14 ++++++++++++++ webapp/pages/admin/donations.vue | 21 +++++++++++++++++---- 2 files changed, 31 insertions(+), 4 deletions(-) create mode 100644 webapp/graphql/Donations.js diff --git a/webapp/graphql/Donations.js b/webapp/graphql/Donations.js new file mode 100644 index 000000000..472218458 --- /dev/null +++ b/webapp/graphql/Donations.js @@ -0,0 +1,14 @@ +import gql from 'graphql-tag' + +export const UpdateDonations = () => { + return gql` + mutation($goal: Int, $progress: Int) { + UpdateDonations(goal: $goal, progress: $progress) { + id + goal + progress + updatedAt + } + } + ` +} diff --git a/webapp/pages/admin/donations.vue b/webapp/pages/admin/donations.vue index 5043ceaec..2eb7b92f9 100644 --- a/webapp/pages/admin/donations.vue +++ b/webapp/pages/admin/donations.vue @@ -16,19 +16,32 @@ From 6a98e5759fee9cb7d1460133cff4c443af16f35f Mon Sep 17 00:00:00 2001 From: Alina Beck Date: Tue, 12 Nov 2019 13:50:45 +0300 Subject: [PATCH 15/21] fix lint errors in backend --- backend/src/seed/seed-db.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/seed/seed-db.js b/backend/src/seed/seed-db.js index 8ce08ff12..e3f6eea66 100644 --- a/backend/src/seed/seed-db.js +++ b/backend/src/seed/seed-db.js @@ -923,7 +923,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] }) }), ) - + await factory.create('Donations') /* eslint-disable-next-line no-console */ console.log('Seeded Data...') From 71bcf814db90336363aa2020b5a0e636defce433 Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Tue, 12 Nov 2019 12:08:19 +0100 Subject: [PATCH 16/21] Fix incorrect destructuring, parseInt - graphql schema expects an integer --- webapp/pages/admin/donations.vue | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/webapp/pages/admin/donations.vue b/webapp/pages/admin/donations.vue index 9bc1279f4..22bd43e81 100644 --- a/webapp/pages/admin/donations.vue +++ b/webapp/pages/admin/donations.vue @@ -29,13 +29,13 @@ export default { }, methods: { submit() { - const { goal, progress } = this + const { goal, progress } = this.formData this.$apollo .mutate({ mutation: UpdateDonations(), variables: { - goal, - progress, + goal: parseInt(goal), + progress: parseInt(progress), }, }) .then(() => { From 982a02fc4f9ee163741da78dc3fb52b48981fde2 Mon Sep 17 00:00:00 2001 From: Alina Beck Date: Tue, 12 Nov 2019 14:24:32 +0300 Subject: [PATCH 17/21] query donations data in donation info component --- .../DonationInfo/DonationInfo.spec.js | 53 +++++++++++-------- .../components/DonationInfo/DonationInfo.vue | 28 ++++++---- 2 files changed, 49 insertions(+), 32 deletions(-) diff --git a/webapp/components/DonationInfo/DonationInfo.spec.js b/webapp/components/DonationInfo/DonationInfo.spec.js index 34be14836..ca719c19a 100644 --- a/webapp/components/DonationInfo/DonationInfo.spec.js +++ b/webapp/components/DonationInfo/DonationInfo.spec.js @@ -9,7 +9,8 @@ const mockDate = new Date(2019, 11, 6) global.Date = jest.fn(() => mockDate) describe('DonationInfo.vue', () => { - let mocks, propsData + let mocks, wrapper + beforeEach(() => { mocks = { $t: jest.fn(string => string), @@ -17,13 +18,9 @@ describe('DonationInfo.vue', () => { locale: () => 'de', }, } - propsData = { - goal: 50000, - progress: 10000, - } }) - const Wrapper = () => mount(DonationInfo, { propsData, mocks, localVue }) + const Wrapper = () => mount(DonationInfo, { mocks, localVue }) it('includes a link to the Human Connection donations website', () => { expect( @@ -46,25 +43,37 @@ describe('DonationInfo.vue', () => { expect(Wrapper().vm.title).toBe('Spenden für Dezember') }) - describe('given german locale', () => { - it('creates a label from the given amounts and a translation string', () => { - Wrapper() - expect(mocks.$t.mock.calls[1][0]).toBe('donations.amount-of-total') - expect(mocks.$t.mock.calls[1][1]).toStrictEqual({ - amount: '10.000', - total: '50.000', + describe('mount with data', () => { + beforeEach(() => { + wrapper = Wrapper() + wrapper.setData({ goal: 50000, progress: 10000 }) + }) + + describe('given german locale', () => { + it('creates a label from the given amounts and a translation string', () => { + expect(mocks.$t).toBeCalledWith( + 'donations.amount-of-total', + expect.objectContaining({ + amount: '10.000', + total: '50.000', + }), + ) }) }) - }) - describe('given english locale', () => { - it('creates a label from the given amounts and a translation string', () => { - mocks.$i18n.locale = () => 'en' - Wrapper() - expect(mocks.$t.mock.calls[1][0]).toBe('donations.amount-of-total') - expect(mocks.$t.mock.calls[1][1]).toStrictEqual({ - amount: '10,000', - total: '50,000', + describe('given english locale', () => { + beforeEach(() => { + mocks.$i18n.locale = () => 'en' + }) + + it('creates a label from the given amounts and a translation string', () => { + expect(mocks.$t).toBeCalledWith( + 'donations.amount-of-total', + expect.objectContaining({ + amount: '10,000', + total: '50,000', + }), + ) }) }) }) diff --git a/webapp/components/DonationInfo/DonationInfo.vue b/webapp/components/DonationInfo/DonationInfo.vue index f05258341..947f4bdbc 100644 --- a/webapp/components/DonationInfo/DonationInfo.vue +++ b/webapp/components/DonationInfo/DonationInfo.vue @@ -8,22 +8,18 @@ From a215093bc3d55f3d0b8b762e70fd64664c89b0f6 Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Tue, 12 Nov 2019 13:26:30 +0100 Subject: [PATCH 18/21] Make Donations reactive Co-authored-by: @alina-beck --- backend/src/schema/resolvers/donations.js | 4 ---- backend/src/schema/resolvers/donations.spec.js | 2 ++ webapp/graphql/Donations.js | 1 + webapp/pages/admin/donations.vue | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/backend/src/schema/resolvers/donations.js b/backend/src/schema/resolvers/donations.js index 4057748e0..88149077d 100644 --- a/backend/src/schema/resolvers/donations.js +++ b/backend/src/schema/resolvers/donations.js @@ -1,13 +1,9 @@ -import uuid from 'uuid/v4' - export default { Mutation: { UpdateDonations: async (_parent, params, context, _resolveInfo) => { const { driver } = context const session = driver.session() let donations - params.id = params.id || uuid() - const writeTxResultPromise = session.writeTransaction(async txc => { const updateDonationsTransactionResponse = await txc.run( ` diff --git a/backend/src/schema/resolvers/donations.spec.js b/backend/src/schema/resolvers/donations.spec.js index 9c62a7dda..327688d3a 100644 --- a/backend/src/schema/resolvers/donations.spec.js +++ b/backend/src/schema/resolvers/donations.spec.js @@ -12,6 +12,7 @@ const driver = getDriver() const updateDonationsMutation = gql` mutation($goal: Int, $progress: Int) { UpdateDonations(goal: $goal, progress: $progress) { + id goal progress createdAt @@ -22,6 +23,7 @@ const updateDonationsMutation = gql` const donationsQuery = gql` query { Donations { + id goal progress } diff --git a/webapp/graphql/Donations.js b/webapp/graphql/Donations.js index 41d7cb87e..cc2a6a783 100644 --- a/webapp/graphql/Donations.js +++ b/webapp/graphql/Donations.js @@ -3,6 +3,7 @@ import gql from 'graphql-tag' export const DonationsQuery = () => gql` query { Donations { + id goal progress } diff --git a/webapp/pages/admin/donations.vue b/webapp/pages/admin/donations.vue index 22bd43e81..9ce2eb76f 100644 --- a/webapp/pages/admin/donations.vue +++ b/webapp/pages/admin/donations.vue @@ -49,7 +49,7 @@ export default { query() { return DonationsQuery() }, - result({ data: { Donations } }) { + update({ Donations }) { const { goal, progress } = Donations[0] this.formData = { goal, From 8b5cd48d3c3e9093f089ae85000e32fbbe6f620a Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Tue, 12 Nov 2019 15:33:21 +0100 Subject: [PATCH 19/21] Translate success message, guard clause for no Donations --- webapp/components/DonationInfo/DonationInfo.vue | 5 +++-- webapp/locales/de.json | 3 ++- webapp/locales/en.json | 3 ++- webapp/locales/pt.json | 6 ++++++ webapp/pages/admin/donations.vue | 3 ++- 5 files changed, 15 insertions(+), 5 deletions(-) diff --git a/webapp/components/DonationInfo/DonationInfo.vue b/webapp/components/DonationInfo/DonationInfo.vue index 947f4bdbc..c7d42252b 100644 --- a/webapp/components/DonationInfo/DonationInfo.vue +++ b/webapp/components/DonationInfo/DonationInfo.vue @@ -17,7 +17,7 @@ export default { }, data() { return { - goal: 0, + goal: 15000, progress: 0, } }, @@ -39,7 +39,8 @@ export default { query() { return DonationsQuery() }, - result({ data: { Donations } }) { + update({ Donations }) { + if (!Donations[0]) return const { goal, progress } = Donations[0] this.goal = goal this.progress = progress diff --git a/webapp/locales/de.json b/webapp/locales/de.json index e70fd025a..a9cf7ce2c 100644 --- a/webapp/locales/de.json +++ b/webapp/locales/de.json @@ -384,7 +384,8 @@ "donations": { "name": "Spendeninfo", "goal": "Monatlich benötigte Spenden", - "progress": "Bereits gesammelte Spenden" + "progress": "Bereits gesammelte Spenden", + "successfulUpdate": "Spenden-Info erfolgreich aktualisiert!" } }, "post": { diff --git a/webapp/locales/en.json b/webapp/locales/en.json index 958025370..15ef63a95 100644 --- a/webapp/locales/en.json +++ b/webapp/locales/en.json @@ -385,7 +385,8 @@ "donations": { "name": "Donations info", "goal": "Monthly donations needed", - "progress": "Donations collected so far" + "progress": "Donations collected so far", + "successfulUpdate": "Donations info updated successfully!" } }, "post": { diff --git a/webapp/locales/pt.json b/webapp/locales/pt.json index 900b65f67..76560aba9 100644 --- a/webapp/locales/pt.json +++ b/webapp/locales/pt.json @@ -380,6 +380,12 @@ "name": "Convidar usuários", "title": "Convidar pessoas", "description": "Convites são uma maneira maravilhosa de ter seus amigos em sua rede …" + }, + "donations": { + "name": "Informações sobre Doações", + "goal": "Doações mensais necessárias", + "progress": "Doações arrecadadas até o momento", + "successfulUpdate": "Informações sobre doações atualizadas com sucesso!" } }, "post": { diff --git a/webapp/pages/admin/donations.vue b/webapp/pages/admin/donations.vue index 9ce2eb76f..7f0205be5 100644 --- a/webapp/pages/admin/donations.vue +++ b/webapp/pages/admin/donations.vue @@ -39,7 +39,7 @@ export default { }, }) .then(() => { - this.$toast.success('yay!!') + this.$toast.success(this.$t('admin.donations.successfulUpdate')) }) .catch(error => this.$toast.error(error.message)) }, @@ -50,6 +50,7 @@ export default { return DonationsQuery() }, update({ Donations }) { + if (!Donations[0]) return const { goal, progress } = Donations[0] this.formData = { goal, From 63d223e368273156ae9e173b29a54eb4f75605b0 Mon Sep 17 00:00:00 2001 From: Alina Beck Date: Tue, 12 Nov 2019 18:52:38 +0300 Subject: [PATCH 20/21] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Robert Schäfer --- webapp/components/DonationInfo/DonationInfo.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/components/DonationInfo/DonationInfo.vue b/webapp/components/DonationInfo/DonationInfo.vue index c7d42252b..10f42e880 100644 --- a/webapp/components/DonationInfo/DonationInfo.vue +++ b/webapp/components/DonationInfo/DonationInfo.vue @@ -1,7 +1,7 @@