diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2bec6cebb..5dd385ad8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -213,7 +213,7 @@ jobs: report_name: Coverage Backend type: lcov result_path: ./coverage/lcov.info - min_coverage: 58 + min_coverage: 57 token: ${{ github.token }} ############################################################################## diff --git a/backend/README.md b/backend/README.md index d7031106e..6d837856c 100644 --- a/backend/README.md +++ b/backend/README.md @@ -174,7 +174,6 @@ $ yarn run db:migrate up **Beware**: We have no multiple database setup at the moment. We clean the database after each test, running the tests will wipe out all your data! - {% tabs %} {% tab title="Docker" %} diff --git a/backend/src/db/factories.js b/backend/src/db/factories.js index bedf592ed..64ee2009c 100644 --- a/backend/src/db/factories.js +++ b/backend/src/db/factories.js @@ -197,6 +197,7 @@ Factory.define('comment') Factory.define('donations') .attr('id', uuid) + .attr('showDonations', true) .attr('goal', 15000) .attr('progress', 0) .after((buildObject, options) => { diff --git a/backend/src/db/migrations/20210506150512-add-donations-node.js b/backend/src/db/migrations/20210506150512-add-donations-node.js new file mode 100644 index 000000000..699c451cf --- /dev/null +++ b/backend/src/db/migrations/20210506150512-add-donations-node.js @@ -0,0 +1,66 @@ +import { getDriver } from '../../db/neo4j' +import { v4 as uuid } from 'uuid' + +export const description = + 'This migration adds a Donations node with default settings to the database.' + +export async function up(next) { + const driver = getDriver() + const session = driver.session() + const transaction = session.beginTransaction() + + try { + // Implement your migration here. + const donationId = uuid() + await transaction.run( + ` + MERGE (donationInfo:Donations) + SET donationInfo.id = $donationId + SET donationInfo.createdAt = toString(datetime()) + SET donationInfo.updatedAt = donationInfo.createdAt + SET donationInfo.showDonations = false + SET donationInfo.goal = 15000 + SET donationInfo.progress = 1200 + RETURN donationInfo {.*} + `, + { donationId }, + ) + await transaction.commit() + next() + } catch (error) { + // eslint-disable-next-line no-console + console.log(error) + await transaction.rollback() + // eslint-disable-next-line no-console + console.log('rolled back') + throw new Error(error) + } finally { + session.close() + } +} + +export async function down(next) { + const driver = getDriver() + const session = driver.session() + const transaction = session.beginTransaction() + + try { + // Implement your migration here. + await transaction.run(` + MATCH (donationInfo:Donations) + DETACH DELETE donationInfo + RETURN donationInfo + `) + await transaction.commit() + next() + } catch (error) { + // eslint-disable-next-line no-console + console.log(error) + await transaction.rollback() + // eslint-disable-next-line no-console + console.log('rolled back') + throw new Error(error) + } finally { + session.close() + } +} diff --git a/backend/src/models/Donations.js b/backend/src/models/Donations.js index 1409c85d4..742bfb569 100644 --- a/backend/src/models/Donations.js +++ b/backend/src/models/Donations.js @@ -2,9 +2,15 @@ import { v4 as uuid } from 'uuid' export default { id: { type: 'string', primary: true, default: uuid }, - goal: { type: 'number' }, - progress: { type: 'number' }, - createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, + showDonations: { type: 'boolean', required: true }, + goal: { type: 'number', required: true }, + progress: { type: 'number', required: true }, + createdAt: { + type: 'string', + isoDate: true, + required: true, + default: () => new Date().toISOString(), + }, updatedAt: { type: 'string', isoDate: true, diff --git a/backend/src/schema/index.js b/backend/src/schema/index.js index 274697238..612487147 100644 --- a/backend/src/schema/index.js +++ b/backend/src/schema/index.js @@ -20,7 +20,6 @@ export default makeAugmentedSchema({ 'FILED', 'REVIEWED', 'Report', - 'Donations', ], }, mutation: false, diff --git a/backend/src/schema/resolvers/donations.js b/backend/src/schema/resolvers/donations.js index 15a1db812..d077e7bed 100644 --- a/backend/src/schema/resolvers/donations.js +++ b/backend/src/schema/resolvers/donations.js @@ -1,4 +1,32 @@ export default { + Query: { + Donations: async (_parent, _params, context, _resolveInfo) => { + const { driver } = context + let donations + const session = driver.session() + const writeTxResultPromise = session.writeTransaction(async (txc) => { + const donationsTransactionResponse = await txc.run( + ` + MATCH (donations:Donations) + WITH donations LIMIT 1 + RETURN donations + `, + {}, + ) + return donationsTransactionResponse.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 + }, + }, Mutation: { UpdateDonations: async (_parent, params, context, _resolveInfo) => { const { driver } = context diff --git a/backend/src/schema/resolvers/donations.spec.js b/backend/src/schema/resolvers/donations.spec.js index 32c502c29..982150d00 100644 --- a/backend/src/schema/resolvers/donations.spec.js +++ b/backend/src/schema/resolvers/donations.spec.js @@ -9,9 +9,10 @@ const instance = getNeode() const driver = getDriver() const updateDonationsMutation = gql` - mutation ($goal: Int, $progress: Int) { - UpdateDonations(goal: $goal, progress: $progress) { + mutation ($showDonations: Boolean, $goal: Int, $progress: Int) { + UpdateDonations(showDonations: $showDonations, goal: $goal, progress: $progress) { id + showDonations goal progress createdAt @@ -23,6 +24,7 @@ const donationsQuery = gql` query { Donations { id + showDonations goal progress } @@ -73,20 +75,21 @@ describe('donations', () => { errors: [{ message: 'Not Authorised!' }], }) }) + }) - describe('authenticated', () => { - beforeEach(async () => { - currentUser = await Factory.build('user', { - id: 'normal-user', - role: 'user', - }) - authenticatedUser = await currentUser.toJson() + describe('authenticated', () => { + beforeEach(async () => { + currentUser = await Factory.build('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 }] }, - }) + it('returns the current Donations info', async () => { + await expect(query({ query: donationsQuery, variables })).resolves.toMatchObject({ + data: { Donations: { showDonations: true, goal: 15000, progress: 0 } }, + errors: undefined, }) }) }) @@ -94,7 +97,7 @@ describe('donations', () => { describe('update donations', () => { beforeEach(() => { - variables = { goal: 20000, progress: 3000 } + variables = { showDonations: false, goal: 20000, progress: 3000 } }) describe('unauthenticated', () => { @@ -106,75 +109,75 @@ describe('donations', () => { errors: [{ message: 'Not Authorised!' }], }) }) + }) - describe('authenticated', () => { - describe('as a normal user', () => { - beforeEach(async () => { - currentUser = await Factory.build('user', { - id: 'normal-user', - role: 'user', - }) - authenticatedUser = await currentUser.toJson() + describe('authenticated', () => { + describe('as a normal user', () => { + beforeEach(async () => { + currentUser = await Factory.build('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!' }], - }) + 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.build('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.build('user', { + id: 'admin', + role: 'admin', + }) + authenticatedUser = await currentUser.toJson() + }) + + it('updates Donations info', async () => { + await expect( + mutate({ mutation: updateDonationsMutation, variables }), + ).resolves.toMatchObject({ + data: { UpdateDonations: { showDonations: false, goal: 20000, progress: 3000 } }, + errors: undefined, }) }) - describe('as a moderator', () => { - beforeEach(async () => { - currentUser = await Factory.build('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.build('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) - }) + 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 index 39cfe9b71..10fb8597c 100644 --- a/backend/src/schema/types/type/Donations.gql +++ b/backend/src/schema/types/type/Donations.gql @@ -1,15 +1,16 @@ type Donations { id: ID! + createdAt: String! + updatedAt: String! + showDonations: Boolean! goal: Int! progress: Int! - createdAt: String - updatedAt: String } type Query { - Donations: [Donations] + Donations: Donations } type Mutation { - UpdateDonations(goal: Int, progress: Int): Donations + UpdateDonations(showDonations: Boolean, goal: Int, progress: Int): Donations } \ No newline at end of file diff --git a/cypress/features.md b/cypress/features.md index 1c9bbff01..23fa3f43e 100644 --- a/cypress/features.md +++ b/cypress/features.md @@ -1,6 +1,6 @@ # Network Specification -Human Connection is a nonprofit social, action and knowledge network that connects information to action and promotes positive local and global change in all areas of life. +ocelot.social is free and open-source social network code that connects information to action and promotes positive local and global change in all areas of life. * **Social**: Interact with other people not just by commenting their posts, but by providing **Pro & Contra** arguments, give a **Versus** or ask them by integrated **Chat** or **Let's Talk** * **Knowledge**: Read articles about interesting topics and find related posts in the **More Info** tab or by **Filtering** based on **Categories** and **Tagging** or by using the **Fulltext Search**. @@ -84,6 +84,7 @@ The following features will be implemented. This gets done in three steps: * Upvote comments of others ### Notifications + [Cucumber features](https://github.com/Ocelot-Social-Community/Ocelot-Social/tree/master/cypress/integration/notifications) * User @-mentionings diff --git a/cypress/integration/Admin.DonationInfo.feature b/cypress/integration/Admin.DonationInfo.feature new file mode 100644 index 000000000..4aa869760 --- /dev/null +++ b/cypress/integration/Admin.DonationInfo.feature @@ -0,0 +1,31 @@ +Feature: Admin sets donations info settings + As an admin + I want to switch the donation info on and off and like to change to donations goal and progress + In order to manage the funds + + Background: + Given the following "users" are in the database: + | slug | email | password | id | name | role | termsAndConditionsAgreedVersion | + | user | user@example.org | abcd | user | User-Chad | user | 0.0.4 | + | admin | admin@example.org | 1234 | admin | Admin-Man | admin | 0.0.4 | + Given the following "posts" are in the database: + | id | title | pinned | createdAt | + | p1 | Some other post | | 2020-01-21 | + | p2 | Houston we have a problem | x | 2020-01-20 | + | p3 | Yet another post | | 2020-01-19 | + Given the following "donations" are in the database: + | id | showDonations | goal | progress | + | d1 | x | 15000.0 | 7000.0 | + + Scenario: The donation info is visible on the index page by default + When I am logged in as "user" + And I navigate to page "/" + Then the donation info is "visible" + And the donation info contains goal "15,000" and progress "7,000" + + Scenario: Admin changes the donation info to be invisible + When I am logged in as "admin" + And I navigate to page "/admin/donations" + Then I click the checkbox show donations progress bar and save + And I navigate to page "/" + Then the donation info is "invisible" diff --git a/cypress/integration/Admin.DonationInfo/I_click_the_checkbox_show_donations_progress_bar_and_save.js b/cypress/integration/Admin.DonationInfo/I_click_the_checkbox_show_donations_progress_bar_and_save.js new file mode 100644 index 000000000..b4289dd5e --- /dev/null +++ b/cypress/integration/Admin.DonationInfo/I_click_the_checkbox_show_donations_progress_bar_and_save.js @@ -0,0 +1,6 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then("I click the checkbox show donations progress bar and save", () => { + cy.get("#showDonations").click() + cy.get(".donations-info-button").click() +}) \ No newline at end of file diff --git a/cypress/integration/Admin.DonationInfo/the_donation_info_contains_goal_{string}_and_progress_{string}.js b/cypress/integration/Admin.DonationInfo/the_donation_info_contains_goal_{string}_and_progress_{string}.js new file mode 100644 index 000000000..cd2473800 --- /dev/null +++ b/cypress/integration/Admin.DonationInfo/the_donation_info_contains_goal_{string}_and_progress_{string}.js @@ -0,0 +1,8 @@ +import { When } from "cypress-cucumber-preprocessor/steps"; + +When("the donation info contains goal {string} and progress {string}", (goal, progress) => { + cy.get('.top-info-bar') + .should('contain', goal) + cy.get('.top-info-bar') + .should('contain', progress) +}); \ No newline at end of file diff --git a/cypress/integration/Admin.DonationInfo/the_donation_info_is_{string}.js b/cypress/integration/Admin.DonationInfo/the_donation_info_is_{string}.js new file mode 100644 index 000000000..d259e6520 --- /dev/null +++ b/cypress/integration/Admin.DonationInfo/the_donation_info_is_{string}.js @@ -0,0 +1,6 @@ +import { Then } from "cypress-cucumber-preprocessor/steps"; + +Then("the donation info is {string}", (visibility) => { + cy.get('.top-info-bar') + .should(visibility === 'visible' ? 'exist' : 'not.exist') +}) \ No newline at end of file diff --git a/cypress/integration/Admin.PinPost/I_open_the_content_menu_of_post_{string}.js b/cypress/integration/Admin.PinPost/I_open_the_content_menu_of_post_{string}.js index 78e9ab1ea..a7be22495 100644 --- a/cypress/integration/Admin.PinPost/I_open_the_content_menu_of_post_{string}.js +++ b/cypress/integration/Admin.PinPost/I_open_the_content_menu_of_post_{string}.js @@ -1,6 +1,6 @@ import { When } from "cypress-cucumber-preprocessor/steps"; -When("I open the content menu of post {string}", (title)=> { +When("I open the content menu of post {string}", (title) => { cy.contains('.post-teaser', title) .find('.content-menu .base-button') .click() diff --git a/cypress/integration/common/the_following_{string}_are_in_the_database.js b/cypress/integration/common/the_following_{string}_are_in_the_database.js index 1d17ec686..e03a705f4 100644 --- a/cypress/integration/common/the_following_{string}_are_in_the_database.js +++ b/cypress/integration/common/the_following_{string}_are_in_the_database.js @@ -18,12 +18,12 @@ Given("the following {string} are in the database:", (table,data) => { case "comments": data.hashes().forEach( entry => { cy.factory() - .build("comment",entry,entry); + .build("comment", entry, entry); }) break case "users": data.hashes().forEach( entry => { - cy.factory().build("user",entry,entry); + cy.factory().build("user", entry, entry); }); break case "tags": @@ -31,5 +31,10 @@ Given("the following {string} are in the database:", (table,data) => { cy.factory().build("tag", entry, entry) }); break + case "donations": + data.hashes().forEach( entry => { + cy.factory().build("donations", entry, entry) + }); + break } }) \ No newline at end of file diff --git a/cypress/parallel-features.sh b/cypress/parallel-features.sh index a234b1d0e..c7d4d4878 100755 --- a/cypress/parallel-features.sh +++ b/cypress/parallel-features.sh @@ -4,16 +4,23 @@ function join_by { local IFS="$1"; shift; echo "$*"; } # Arguments: -CUR_JOB=$1 +CUR_JOB=$(expr $1 - 1) MAX_JOBS=$2 # Features FEATURE_LIST=( $(find cypress/integration/ -maxdepth 1 -name "*.feature") ) # Calculation -MAX_FEATURES=$(find cypress/integration/ -maxdepth 1 -name "*.feature" -printf '.' | wc -m) -FEATURES_PER_JOB=$(expr $(expr ${MAX_FEATURES} + ${MAX_JOBS} - 1) / ${MAX_JOBS} ) -FEATURES_SKIP=$(expr $(expr ${CUR_JOB} - 1 ) \* ${FEATURES_PER_JOB} ) +MAX_FEATURES=$(find cypress/integration/ -maxdepth 1 -name "*.feature" -print | wc -l) +# adds overhead features to the first jobs +if [[ $CUR_JOB -lt $(expr ${MAX_FEATURES} % ${MAX_JOBS}) ]] +then + FEATURES_PER_JOB=$(expr ${MAX_FEATURES} / ${MAX_JOBS} + 1) + FEATURES_SKIP=$(expr $(expr ${MAX_FEATURES} / ${MAX_JOBS} + 1) \* ${CUR_JOB}) +else + FEATURES_PER_JOB=$(expr ${MAX_FEATURES} / ${MAX_JOBS}) + FEATURES_SKIP=$(expr $(expr ${MAX_FEATURES} / ${MAX_JOBS} + 1) \* $(expr ${MAX_FEATURES} % ${MAX_JOBS}) + $(expr $(expr ${MAX_FEATURES} / ${MAX_JOBS}) \* $(expr ${CUR_JOB} - ${MAX_FEATURES} % ${MAX_JOBS}))) +fi # Comma separated list echo $(join_by , ${FEATURE_LIST[@]:${FEATURES_SKIP}:${FEATURES_PER_JOB}}) \ No newline at end of file diff --git a/webapp/components/CommentForm/CommentForm.vue b/webapp/components/CommentForm/CommentForm.vue index 4bdb95f90..5f6a2420d 100644 --- a/webapp/components/CommentForm/CommentForm.vue +++ b/webapp/components/CommentForm/CommentForm.vue @@ -23,7 +23,7 @@ diff --git a/webapp/components/Dropdown.vue b/webapp/components/Dropdown.vue index 6a5d07083..c01b12bd9 100644 --- a/webapp/components/Dropdown.vue +++ b/webapp/components/Dropdown.vue @@ -33,6 +33,7 @@ export default { data() { return { isPopoverOpen: false, + developerNoAutoClosing: false, // stops automatic closing of menu for developer purposes: default is 'false' } }, computed: { @@ -113,6 +114,7 @@ export default { } }, popoveMouseLeave() { + if (this.developerNoAutoClosing) return if (this.disabled) { return } diff --git a/webapp/components/FilterMenu/FilterMenu.spec.js b/webapp/components/FilterMenu/FilterMenu.spec.js index 18358b67f..9e7ebfec0 100644 --- a/webapp/components/FilterMenu/FilterMenu.spec.js +++ b/webapp/components/FilterMenu/FilterMenu.spec.js @@ -12,6 +12,7 @@ describe('FilterMenu.vue', () => { const getters = { 'posts/isActive': () => false, + 'posts/orderBy': () => 'createdAt_desc', } const stubs = { diff --git a/webapp/components/FilterMenu/FilterMenu.vue b/webapp/components/FilterMenu/FilterMenu.vue index 94d950689..9e211ccf9 100644 --- a/webapp/components/FilterMenu/FilterMenu.vue +++ b/webapp/components/FilterMenu/FilterMenu.vue @@ -15,6 +15,10 @@