diff --git a/backend/src/graphql/posts.js b/backend/src/graphql/posts.js index f1b62a286..beff6f028 100644 --- a/backend/src/graphql/posts.js +++ b/backend/src/graphql/posts.js @@ -70,6 +70,7 @@ export const filterPosts = () => { id title content + eventStart } } ` diff --git a/backend/src/schema/resolvers/filter-posts.spec.js b/backend/src/schema/resolvers/filter-posts.spec.js new file mode 100644 index 000000000..0b96e001f --- /dev/null +++ b/backend/src/schema/resolvers/filter-posts.spec.js @@ -0,0 +1,230 @@ +import { createTestClient } from 'apollo-server-testing' +import Factory, { cleanDatabase } from '../../db/factories' +import { getNeode, getDriver } from '../../db/neo4j' +import createServer from '../../server' +import CONFIG from '../../config' +import { filterPosts, createPostMutation } from '../../graphql/posts' + +CONFIG.CATEGORIES_ACTIVE = false + +const driver = getDriver() +const neode = getNeode() + +let query +let mutate +let authenticatedUser +let user + +beforeAll(async () => { + await cleanDatabase() + + const { server } = createServer({ + context: () => { + return { + driver, + neode, + user: authenticatedUser, + } + }, + }) + query = createTestClient(server).query + mutate = createTestClient(server).mutate +}) + +afterAll(async () => { + await cleanDatabase() + driver.close() +}) + +describe('Filter Posts', () => { + const now = new Date() + + beforeAll(async () => { + user = await Factory.build('user', { + id: 'user', + name: 'User', + about: 'I am a user.', + }) + authenticatedUser = await user.toJson() + await mutate({ + mutation: createPostMutation(), + variables: { + id: 'a1', + title: 'I am an article', + content: 'I am an article written by user.', + }, + }) + await mutate({ + mutation: createPostMutation(), + variables: { + id: 'a2', + title: 'I am anonther article', + content: 'I am another article written by user.', + }, + }) + await mutate({ + mutation: createPostMutation(), + variables: { + id: 'e1', + title: 'Illegaler Kindergeburtstag', + content: 'Elli wird fünf. Wir feiern ihren Geburtstag.', + postType: 'Event', + eventInput: { + eventStart: new Date(now.getFullYear(), now.getMonth() + 1).toISOString(), + eventVenue: 'Garten der Familie Maier', + }, + }, + }) + await mutate({ + mutation: createPostMutation(), + variables: { + id: 'e2', + title: 'Räuber-Treffen', + content: 'Planung der nächsten Räuberereien', + postType: 'Event', + eventInput: { + eventStart: new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1).toISOString(), + eventVenue: 'Wirtshaus im Spessart', + }, + }, + }) + }) + + describe('no filters set', () => { + it('finds all posts', async () => { + const { + data: { Post: result }, + } = await query({ query: filterPosts() }) + expect(result).toHaveLength(4) + expect(result).toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: 'a1' }), + expect.objectContaining({ id: 'a2' }), + expect.objectContaining({ id: 'e1' }), + expect.objectContaining({ id: 'e2' }), + ]), + ) + }) + }) + + describe('post type filter set to ["Article"]', () => { + it('finds the articles', async () => { + const { + data: { Post: result }, + } = await query({ query: filterPosts(), variables: { filter: { postType_in: ['Article'] } } }) + expect(result).toHaveLength(2) + expect(result).toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: 'a1' }), + expect.objectContaining({ id: 'a2' }), + ]), + ) + }) + }) + + describe('post type filter set to ["Event"]', () => { + it('finds the articles', async () => { + const { + data: { Post: result }, + } = await query({ query: filterPosts(), variables: { filter: { postType_in: ['Event'] } } }) + expect(result).toHaveLength(2) + expect(result).toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: 'e1' }), + expect.objectContaining({ id: 'e2' }), + ]), + ) + }) + }) + + describe('post type filter set to ["Article", "Event"]', () => { + it('finds all posts', async () => { + const { + data: { Post: result }, + } = await query({ + query: filterPosts(), + variables: { filter: { postType_in: ['Article', 'Event'] } }, + }) + expect(result).toHaveLength(4) + expect(result).toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: 'a1' }), + expect.objectContaining({ id: 'a2' }), + expect.objectContaining({ id: 'e1' }), + expect.objectContaining({ id: 'e2' }), + ]), + ) + }) + }) + + describe('order events by event start descending', () => { + it('finds the events orderd accordingly', async () => { + const { + data: { Post: result }, + } = await query({ + query: filterPosts(), + variables: { filter: { postType_in: ['Event'] }, orderBy: ['eventStart_desc'] }, + }) + expect(result).toHaveLength(2) + expect(result).toEqual([ + expect.objectContaining({ + id: 'e1', + eventStart: new Date(now.getFullYear(), now.getMonth() + 1).toISOString(), + }), + expect.objectContaining({ + id: 'e2', + eventStart: new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1).toISOString(), + }), + ]) + }) + }) + + describe('order events by event start ascending', () => { + it('finds the events orderd accordingly', async () => { + const { + data: { Post: result }, + } = await query({ + query: filterPosts(), + variables: { filter: { postType_in: ['Event'] }, orderBy: ['eventStart_asc'] }, + }) + expect(result).toHaveLength(2) + expect(result).toEqual([ + expect.objectContaining({ + id: 'e2', + eventStart: new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1).toISOString(), + }), + expect.objectContaining({ + id: 'e1', + eventStart: new Date(now.getFullYear(), now.getMonth() + 1).toISOString(), + }), + ]) + }) + }) + + describe('filter events by event start date', () => { + it('finds only events after given date', async () => { + const { + data: { Post: result }, + } = await query({ + query: filterPosts(), + variables: { + filter: { + postType_in: ['Event'], + eventStart_gte: new Date( + now.getFullYear(), + now.getMonth(), + now.getDate() + 2, + ).toISOString(), + }, + }, + }) + expect(result).toHaveLength(1) + expect(result).toEqual([ + expect.objectContaining({ + id: 'e1', + eventStart: new Date(now.getFullYear(), now.getMonth() + 1).toISOString(), + }), + ]) + }) + }) +}) diff --git a/backend/src/schema/resolvers/posts.js b/backend/src/schema/resolvers/posts.js index 42a755e8f..b6f3457ae 100644 --- a/backend/src/schema/resolvers/posts.js +++ b/backend/src/schema/resolvers/posts.js @@ -334,6 +334,7 @@ export default { ` MATCH (user:User {id: $userId}) WHERE user.role = 'admin' MATCH (post:Post {id: $params.id}) + WHERE NOT((post)-[:IN]->(:Group)) MERGE (user)-[pinned:PINNED {createdAt: toString(datetime())}]->(post) SET post.pinned = true RETURN post, pinned.createdAt as pinnedAt @@ -346,10 +347,12 @@ export default { })) }) const [transactionResult] = await writeTxResultPromise - const { pinnedPost, pinnedAt } = transactionResult - pinnedPostWithNestedAttributes = { - ...pinnedPost, - pinnedAt, + if (transactionResult) { + const { pinnedPost, pinnedAt } = transactionResult + pinnedPostWithNestedAttributes = { + ...pinnedPost, + pinnedAt, + } } } finally { session.close() diff --git a/backend/src/schema/resolvers/postsInGroups.spec.js b/backend/src/schema/resolvers/postsInGroups.spec.js index 86a278207..ba9041090 100644 --- a/backend/src/schema/resolvers/postsInGroups.spec.js +++ b/backend/src/schema/resolvers/postsInGroups.spec.js @@ -818,11 +818,13 @@ describe('Posts in Groups', () => { id: 'post-to-public-group', title: 'A post to a public group', content: 'I am posting into a public group as a member of the group', + eventStart: null, }, { id: 'post-without-group', title: 'A post without a group', content: 'I am a user who does not belong to a group yet.', + eventStart: null, }, ]), }, @@ -846,11 +848,13 @@ describe('Posts in Groups', () => { id: 'post-to-public-group', title: 'A post to a public group', content: 'I am posting into a public group as a member of the group', + eventStart: null, }, { id: 'post-without-group', title: 'A post without a group', content: 'I am a user who does not belong to a group yet.', + eventStart: null, }, ]), }, @@ -874,11 +878,13 @@ describe('Posts in Groups', () => { id: 'post-to-public-group', title: 'A post to a public group', content: 'I am posting into a public group as a member of the group', + eventStart: null, }, { id: 'post-without-group', title: 'A post without a group', content: 'I am a user who does not belong to a group yet.', + eventStart: null, }, ]), }, @@ -902,11 +908,13 @@ describe('Posts in Groups', () => { id: 'post-to-public-group', title: 'A post to a public group', content: 'I am posting into a public group as a member of the group', + eventStart: null, }, { id: 'post-without-group', title: 'A post without a group', content: 'I am a user who does not belong to a group yet.', + eventStart: null, }, ]), }, @@ -930,21 +938,25 @@ describe('Posts in Groups', () => { id: 'post-to-public-group', title: 'A post to a public group', content: 'I am posting into a public group as a member of the group', + eventStart: null, }, { id: 'post-without-group', title: 'A post without a group', content: 'I am a user who does not belong to a group yet.', + eventStart: null, }, { id: 'post-to-closed-group', title: 'A post to a closed group', content: 'I am posting into a closed group as a member of the group', + eventStart: null, }, { id: 'post-to-hidden-group', title: 'A post to a hidden group', content: 'I am posting into a hidden group as a member of the group', + eventStart: null, }, ]), }, @@ -1319,16 +1331,19 @@ describe('Posts in Groups', () => { id: 'post-to-public-group', title: 'A post to a public group', content: 'I am posting into a public group as a member of the group', + eventStart: null, }, { id: 'post-without-group', title: 'A post without a group', content: 'I am a user who does not belong to a group yet.', + eventStart: null, }, { id: 'post-to-closed-group', title: 'A post to a closed group', content: 'I am posting into a closed group as a member of the group', + eventStart: null, }, ]), }, @@ -1361,21 +1376,25 @@ describe('Posts in Groups', () => { id: 'post-to-public-group', title: 'A post to a public group', content: 'I am posting into a public group as a member of the group', + eventStart: null, }, { id: 'post-without-group', title: 'A post without a group', content: 'I am a user who does not belong to a group yet.', + eventStart: null, }, { id: 'post-to-closed-group', title: 'A post to a closed group', content: 'I am posting into a closed group as a member of the group', + eventStart: null, }, { id: 'post-to-hidden-group', title: 'A post to a hidden group', content: 'I am posting into a hidden group as a member of the group', + eventStart: null, }, ]), }, @@ -1410,16 +1429,19 @@ describe('Posts in Groups', () => { id: 'post-to-public-group', title: 'A post to a public group', content: 'I am posting into a public group as a member of the group', + eventStart: null, }, { id: 'post-without-group', title: 'A post without a group', content: 'I am a user who does not belong to a group yet.', + eventStart: null, }, { id: 'post-to-hidden-group', title: 'A post to a hidden group', content: 'I am posting into a hidden group as a member of the group', + eventStart: null, }, ]), }, @@ -1452,11 +1474,13 @@ describe('Posts in Groups', () => { id: 'post-to-public-group', title: 'A post to a public group', content: 'I am posting into a public group as a member of the group', + eventStart: null, }, { id: 'post-without-group', title: 'A post without a group', content: 'I am a user who does not belong to a group yet.', + eventStart: null, }, ]), }, @@ -1489,21 +1513,25 @@ describe('Posts in Groups', () => { id: 'post-to-public-group', title: 'A post to a public group', content: 'I am posting into a public group as a member of the group', + eventStart: null, }, { id: 'post-without-group', title: 'A post without a group', content: 'I am a user who does not belong to a group yet.', + eventStart: null, }, { id: 'post-to-closed-group', title: 'A post to a closed group', content: 'I am posting into a closed group as a member of the group', + eventStart: null, }, { id: 'post-to-hidden-group', title: 'A post to a hidden group', content: 'I am posting into a hidden group as a member of the group', + eventStart: null, }, ]), }, @@ -1534,21 +1562,25 @@ describe('Posts in Groups', () => { id: 'post-to-public-group', title: 'A post to a public group', content: 'I am posting into a public group as a member of the group', + eventStart: null, }, { id: 'post-without-group', title: 'A post without a group', content: 'I am a user who does not belong to a group yet.', + eventStart: null, }, { id: 'post-to-closed-group', title: 'A post to a closed group', content: 'I am posting into a closed group as a member of the group', + eventStart: null, }, { id: 'post-to-hidden-group', title: 'A post to a hidden group', content: 'I am posting into a hidden group as a member of the group', + eventStart: null, }, ]), }, @@ -1579,21 +1611,25 @@ describe('Posts in Groups', () => { id: 'post-to-public-group', title: 'A post to a public group', content: 'I am posting into a public group as a member of the group', + eventStart: null, }, { id: 'post-without-group', title: 'A post without a group', content: 'I am a user who does not belong to a group yet.', + eventStart: null, }, { id: 'post-to-closed-group', title: 'A post to a closed group', content: 'I am posting into a closed group as a member of the group', + eventStart: null, }, { id: 'post-to-hidden-group', title: 'A post to a hidden group', content: 'I am posting into a hidden group as a member of the group', + eventStart: null, }, ]), }, @@ -1628,21 +1664,25 @@ describe('Posts in Groups', () => { id: 'post-to-public-group', title: 'A post to a public group', content: 'I am posting into a public group as a member of the group', + eventStart: null, }, { id: 'post-without-group', title: 'A post without a group', content: 'I am a user who does not belong to a group yet.', + eventStart: null, }, { id: 'post-to-closed-group', title: 'A post to a closed group', content: 'I am posting into a closed group as a member of the group', + eventStart: null, }, { id: 'post-to-hidden-group', title: 'A post to a hidden group', content: 'I am posting into a hidden group as a member of the group', + eventStart: null, }, ]), }, @@ -1675,21 +1715,25 @@ describe('Posts in Groups', () => { id: 'post-to-public-group', title: 'A post to a public group', content: 'I am posting into a public group as a member of the group', + eventStart: null, }, { id: 'post-without-group', title: 'A post without a group', content: 'I am a user who does not belong to a group yet.', + eventStart: null, }, { id: 'post-to-closed-group', title: 'A post to a closed group', content: 'I am posting into a closed group as a member of the group', + eventStart: null, }, { id: 'post-to-hidden-group', title: 'A post to a hidden group', content: 'I am posting into a hidden group as a member of the group', + eventStart: null, }, ]), }, @@ -1739,11 +1783,13 @@ describe('Posts in Groups', () => { id: 'post-to-closed-group', title: 'A post to a closed group', content: 'I am posting into a closed group as a member of the group', + eventStart: null, }, { id: 'post-to-hidden-group', title: 'A post to a hidden group', content: 'I am posting into a hidden group as a member of the group', + eventStart: null, }, ]), }, diff --git a/backend/src/schema/resolvers/searches.js b/backend/src/schema/resolvers/searches.js index 3fdf22da3..a67abe90d 100644 --- a/backend/src/schema/resolvers/searches.js +++ b/backend/src/schema/resolvers/searches.js @@ -223,8 +223,7 @@ export default { }, searchResults: async (_parent, args, context, _resolveInfo) => { const { query, limit } = args - let userId = null - if (context.user) userId = context.user.id + const userId = context.user?.id || null const searchType = query.replace(/^([!@#&]?).*$/, '$1') const searchString = query.replace(/^([!@#&])/, '') diff --git a/backend/src/schema/resolvers/searches/queryString.js b/backend/src/schema/resolvers/searches/queryString.js index 5ef84cdce..8f415c5e6 100644 --- a/backend/src/schema/resolvers/searches/queryString.js +++ b/backend/src/schema/resolvers/searches/queryString.js @@ -33,7 +33,7 @@ const matchSomeWordsExactly = (str, boost = 2) => { const matchBeginningOfWords = (str) => { return str .split(' ') - .filter((s) => s.length > 3) + .filter((s) => s.length >= 2) .map((s) => s + '*') .join(' ') } diff --git a/backend/src/schema/resolvers/searches/queryString.spec.js b/backend/src/schema/resolvers/searches/queryString.spec.js index 23a746be1..fe3c91d3c 100644 --- a/backend/src/schema/resolvers/searches/queryString.spec.js +++ b/backend/src/schema/resolvers/searches/queryString.spec.js @@ -37,7 +37,7 @@ describe('queryString', () => { describe('globbing for longer words', () => { it('globs words with more than three characters', () => { - expect(queryString('a couple of words')).toContain('couple* words*') + expect(queryString('a couple of words')).toContain('couple* of* words*') }) }) }) diff --git a/backend/src/schema/types/type/Post.gql b/backend/src/schema/types/type/Post.gql index c35ee7054..8fac449d1 100644 --- a/backend/src/schema/types/type/Post.gql +++ b/backend/src/schema/types/type/Post.gql @@ -83,6 +83,8 @@ input _PostFilter { emotions_every: _PostEMOTEDFilter group: _GroupFilter postsInMyGroups: Boolean + postType_in: [PostType] + eventStart_gte: String } enum _PostOrdering { @@ -104,6 +106,8 @@ enum _PostOrdering { language_desc pinned_asc pinned_desc + eventStart_asc + eventStart_desc } diff --git a/webapp/components/ContentMenu/ContentMenu.vue b/webapp/components/ContentMenu/ContentMenu.vue index 60bcccf43..d723a9667 100644 --- a/webapp/components/ContentMenu/ContentMenu.vue +++ b/webapp/components/ContentMenu/ContentMenu.vue @@ -80,7 +80,7 @@ export default { }) } - if (this.isAdmin) { + if (this.isAdmin && !this.resource.group) { if (!this.resource.pinnedBy) { routes.push({ label: this.$t(`post.menu.pin`), diff --git a/webapp/components/ContributionForm/ContributionForm.vue b/webapp/components/ContributionForm/ContributionForm.vue index f2c56883b..d1184153a 100644 --- a/webapp/components/ContributionForm/ContributionForm.vue +++ b/webapp/components/ContributionForm/ContributionForm.vue @@ -218,7 +218,6 @@ export default { aspectRatio: imageAspectRatio = null, type: imageType = null, } = image || {} - return { categoriesActive: this.$env.CATEGORIES_ACTIVE, links, diff --git a/webapp/components/generic/SearchableInput/SearchableInput.spec.js b/webapp/components/generic/SearchableInput/SearchableInput.spec.js index cda9150b8..223ee6186 100644 --- a/webapp/components/generic/SearchableInput/SearchableInput.spec.js +++ b/webapp/components/generic/SearchableInput/SearchableInput.spec.js @@ -63,13 +63,6 @@ describe('SearchableInput.vue', () => { expect(select.element.value).toBe('abcd') }) - it('calls onDelete when the delete key is pressed', () => { - const spy = jest.spyOn(wrapper.vm, 'onDelete') - select.trigger('input') - select.trigger('keyup.delete') - expect(spy).toHaveBeenCalledTimes(1) - }) - describe('navigating to resource', () => { beforeEach(() => { propsData = { options: searchResults } diff --git a/webapp/components/generic/SearchableInput/SearchableInput.vue b/webapp/components/generic/SearchableInput/SearchableInput.vue index 2149732c5..b0e813b45 100644 --- a/webapp/components/generic/SearchableInput/SearchableInput.vue +++ b/webapp/components/generic/SearchableInput/SearchableInput.vue @@ -1,9 +1,10 @@