mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2025-12-13 07:46:06 +00:00
Merge branch 'master' of https://github.com/Ocelot-Social-Community/Ocelot-Social into 2144-Add_Search_Results_Page
This commit is contained in:
commit
7ffd8c59cb
@ -69,6 +69,7 @@
|
||||
"helmet": "~3.22.0",
|
||||
"ioredis": "^4.16.1",
|
||||
"jsonwebtoken": "~8.5.1",
|
||||
"languagedetect": "^2.0.0",
|
||||
"linkifyjs": "~2.1.8",
|
||||
"lodash": "~4.17.14",
|
||||
"merge-graphql-schemas": "^1.7.7",
|
||||
|
||||
@ -14,6 +14,7 @@ import notifications from './notifications/notificationsMiddleware'
|
||||
import hashtags from './hashtags/hashtagsMiddleware'
|
||||
import email from './email/emailMiddleware'
|
||||
import sentry from './sentryMiddleware'
|
||||
import languages from './languages/languages'
|
||||
|
||||
export default (schema) => {
|
||||
const middlewares = {
|
||||
@ -30,6 +31,7 @@ export default (schema) => {
|
||||
softDelete,
|
||||
includedFields,
|
||||
orderBy,
|
||||
languages,
|
||||
}
|
||||
|
||||
let order = [
|
||||
@ -39,6 +41,7 @@ export default (schema) => {
|
||||
// 'activityPub', disabled temporarily
|
||||
'validation',
|
||||
'sluggify',
|
||||
'languages',
|
||||
'excerpt',
|
||||
'email',
|
||||
'notifications',
|
||||
|
||||
28
backend/src/middleware/languages/languages.js
Normal file
28
backend/src/middleware/languages/languages.js
Normal file
@ -0,0 +1,28 @@
|
||||
import LanguageDetect from 'languagedetect'
|
||||
import sanitizeHtml from 'sanitize-html'
|
||||
|
||||
const removeHtmlTags = (input) => {
|
||||
return sanitizeHtml(input, {
|
||||
allowedTags: [],
|
||||
allowedAttributes: {},
|
||||
})
|
||||
}
|
||||
|
||||
const setPostLanguage = (text) => {
|
||||
const lngDetector = new LanguageDetect()
|
||||
lngDetector.setLanguageType('iso2')
|
||||
return lngDetector.detect(removeHtmlTags(text), 1)[0][0]
|
||||
}
|
||||
|
||||
export default {
|
||||
Mutation: {
|
||||
CreatePost: async (resolve, root, args, context, info) => {
|
||||
args.language = await setPostLanguage(args.content)
|
||||
return resolve(root, args, context, info)
|
||||
},
|
||||
UpdatePost: async (resolve, root, args, context, info) => {
|
||||
args.language = await setPostLanguage(args.content)
|
||||
return resolve(root, args, context, info)
|
||||
},
|
||||
},
|
||||
}
|
||||
132
backend/src/middleware/languages/languages.spec.js
Normal file
132
backend/src/middleware/languages/languages.spec.js
Normal file
@ -0,0 +1,132 @@
|
||||
import Factory, { cleanDatabase } from '../../db/factories'
|
||||
import { gql } from '../../helpers/jest'
|
||||
import { getNeode, getDriver } from '../../db/neo4j'
|
||||
import createServer from '../../server'
|
||||
import { createTestClient } from 'apollo-server-testing'
|
||||
|
||||
let mutate
|
||||
let authenticatedUser
|
||||
let variables
|
||||
|
||||
const driver = getDriver()
|
||||
const neode = getNeode()
|
||||
|
||||
beforeAll(async () => {
|
||||
const { server } = createServer({
|
||||
context: () => {
|
||||
return {
|
||||
driver,
|
||||
neode,
|
||||
user: authenticatedUser,
|
||||
}
|
||||
},
|
||||
})
|
||||
mutate = createTestClient(server).mutate
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDatabase()
|
||||
})
|
||||
|
||||
const createPostMutation = gql`
|
||||
mutation($title: String!, $content: String!, $categoryIds: [ID]) {
|
||||
CreatePost(title: $title, content: $content, categoryIds: $categoryIds) {
|
||||
language
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
describe('languagesMiddleware', () => {
|
||||
variables = {
|
||||
title: 'Test post languages',
|
||||
categoryIds: ['cat9'],
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
await cleanDatabase()
|
||||
const user = await Factory.build('user')
|
||||
authenticatedUser = await user.toJson()
|
||||
await Factory.build('category', {
|
||||
id: 'cat9',
|
||||
name: 'Democracy & Politics',
|
||||
icon: 'university',
|
||||
})
|
||||
})
|
||||
|
||||
it('detects German', async () => {
|
||||
variables = {
|
||||
...variables,
|
||||
content: 'Jeder sollte vor seiner eigenen Tür kehren.',
|
||||
}
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createPostMutation,
|
||||
variables,
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
CreatePost: {
|
||||
language: 'de',
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('detects English', async () => {
|
||||
variables = {
|
||||
...variables,
|
||||
content: 'A journey of a thousand miles begins with a single step.',
|
||||
}
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createPostMutation,
|
||||
variables,
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
CreatePost: {
|
||||
language: 'en',
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('detects Spanish', async () => {
|
||||
variables = {
|
||||
...variables,
|
||||
content: 'A caballo regalado, no le mires el diente.',
|
||||
}
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createPostMutation,
|
||||
variables,
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
CreatePost: {
|
||||
language: 'es',
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('detects German in between lots of html tags', async () => {
|
||||
variables = {
|
||||
...variables,
|
||||
content:
|
||||
'<strong>Jeder</strong> <strike>sollte</strike> <strong>vor</strong> <span>seiner</span> eigenen <blockquote>Tür</blockquote> kehren.',
|
||||
}
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createPostMutation,
|
||||
variables,
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
CreatePost: {
|
||||
language: 'de',
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -2,6 +2,7 @@ import slugify from 'slug'
|
||||
export default async function uniqueSlug(string, isUnique) {
|
||||
const slug = slugify(string || 'anonymous', {
|
||||
lower: true,
|
||||
multicharmap: { Ä: 'AE', ä: 'ae', Ö: 'OE', ö: 'oe', Ü: 'UE', ü: 'ue', ß: 'ss' },
|
||||
})
|
||||
if (await isUnique(slug)) return slug
|
||||
|
||||
|
||||
@ -18,4 +18,16 @@ describe('uniqueSlug', () => {
|
||||
const isUnique = jest.fn().mockResolvedValue(true)
|
||||
expect(uniqueSlug(string, isUnique)).resolves.toEqual('anonymous')
|
||||
})
|
||||
|
||||
it('Converts umlaut to a two letter equivalent', async () => {
|
||||
const umlaut = 'ÄÖÜäöüß'
|
||||
const isUnique = jest.fn().mockResolvedValue(true)
|
||||
await expect(uniqueSlug(umlaut, isUnique)).resolves.toEqual('aeoeueaeoeuess')
|
||||
})
|
||||
|
||||
it('Removes Spanish enya and diacritics', async () => {
|
||||
const diacritics = 'áàéèíìóòúùñçÁÀÉÈÍÌÓÒÚÙÑÇ'
|
||||
const isUnique = jest.fn().mockResolvedValue(true)
|
||||
await expect(uniqueSlug(diacritics, isUnique)).resolves.toEqual('aaeeiioouuncaaeeiioouunc')
|
||||
})
|
||||
})
|
||||
|
||||
@ -317,19 +317,6 @@ describe('CreatePost', () => {
|
||||
expected,
|
||||
)
|
||||
})
|
||||
|
||||
describe('language', () => {
|
||||
beforeEach(() => {
|
||||
variables = { ...variables, language: 'es' }
|
||||
})
|
||||
|
||||
it('allows a user to set the language of the post', async () => {
|
||||
const expected = { data: { CreatePost: { language: 'es' } } }
|
||||
await expect(mutate({ mutation: createPostMutation, variables })).resolves.toMatchObject(
|
||||
expected,
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -6302,6 +6302,11 @@ knuth-shuffle-seeded@^1.0.6:
|
||||
dependencies:
|
||||
seed-random "~2.2.0"
|
||||
|
||||
languagedetect@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/languagedetect/-/languagedetect-2.0.0.tgz#4b8fa2b7593b2a3a02fb1100891041c53238936c"
|
||||
integrity sha512-AZb/liiQ+6ZoTj4f1J0aE6OkzhCo8fyH+tuSaPfSo8YHCWLFJrdSixhtO2TYdIkjcDQNaR4RmGaV2A5FJklDMQ==
|
||||
|
||||
latest-version@^3.0.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-3.1.0.tgz#a205383fea322b33b5ae3b18abee0dc2f356ee15"
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { config, mount } from '@vue/test-utils'
|
||||
import ContributionForm from './ContributionForm.vue'
|
||||
|
||||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
import PostMutations from '~/graphql/PostMutations.js'
|
||||
|
||||
@ -17,15 +16,7 @@ config.stubs['nuxt-link'] = '<span><slot /></span>'
|
||||
config.stubs['v-popover'] = '<span><slot /></span>'
|
||||
|
||||
describe('ContributionForm.vue', () => {
|
||||
let wrapper,
|
||||
postTitleInput,
|
||||
expectedParams,
|
||||
cancelBtn,
|
||||
mocks,
|
||||
propsData,
|
||||
categoryIds,
|
||||
englishLanguage,
|
||||
deutschLanguage
|
||||
let wrapper, postTitleInput, expectedParams, cancelBtn, mocks, propsData
|
||||
const postTitle = 'this is a title for a post'
|
||||
const postTitleTooShort = 'xx'
|
||||
let postTitleTooLong = ''
|
||||
@ -52,8 +43,6 @@ describe('ContributionForm.vue', () => {
|
||||
slug: 'this-is-a-title-for-a-post',
|
||||
content: postContent,
|
||||
contentExcerpt: postContent,
|
||||
language: 'en',
|
||||
categoryIds,
|
||||
},
|
||||
},
|
||||
}),
|
||||
@ -109,10 +98,6 @@ describe('ContributionForm.vue', () => {
|
||||
postTitleInput = wrapper.find('.ds-input')
|
||||
postTitleInput.setValue(postTitle)
|
||||
await wrapper.vm.updateEditorContent(postContent)
|
||||
englishLanguage = wrapper
|
||||
.findAll('li')
|
||||
.filter((language) => language.text() === 'English')
|
||||
englishLanguage.trigger('click')
|
||||
})
|
||||
|
||||
it('title cannot be empty', async () => {
|
||||
@ -147,7 +132,6 @@ describe('ContributionForm.vue', () => {
|
||||
variables: {
|
||||
title: postTitle,
|
||||
content: postContent,
|
||||
language: 'en',
|
||||
id: null,
|
||||
image: null,
|
||||
},
|
||||
@ -155,29 +139,13 @@ describe('ContributionForm.vue', () => {
|
||||
postTitleInput = wrapper.find('.ds-input')
|
||||
postTitleInput.setValue(postTitle)
|
||||
await wrapper.vm.updateEditorContent(postContent)
|
||||
englishLanguage = wrapper
|
||||
.findAll('li')
|
||||
.filter((language) => language.text() === 'English')
|
||||
englishLanguage.trigger('click')
|
||||
await Vue.nextTick()
|
||||
await Vue.nextTick()
|
||||
})
|
||||
|
||||
it('creates a post with valid title, content, and at least one category', async () => {
|
||||
it('creates a post with valid title and content', async () => {
|
||||
await wrapper.find('form').trigger('submit')
|
||||
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
|
||||
})
|
||||
|
||||
it('supports changing the language', async () => {
|
||||
expectedParams.variables.language = 'de'
|
||||
deutschLanguage = wrapper
|
||||
.findAll('li')
|
||||
.filter((language) => language.text() === 'Deutsch')
|
||||
deutschLanguage.trigger('click')
|
||||
wrapper.find('form').trigger('submit')
|
||||
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
|
||||
})
|
||||
|
||||
it('supports adding a teaser image', async () => {
|
||||
expectedParams.variables.image = {
|
||||
aspectRatio: null,
|
||||
@ -235,13 +203,6 @@ describe('ContributionForm.vue', () => {
|
||||
postTitleInput = wrapper.find('.ds-input')
|
||||
postTitleInput.setValue(postTitle)
|
||||
await wrapper.vm.updateEditorContent(postContent)
|
||||
categoryIds = ['cat12']
|
||||
englishLanguage = wrapper
|
||||
.findAll('li')
|
||||
.filter((language) => language.text() === 'English')
|
||||
englishLanguage.trigger('click')
|
||||
await Vue.nextTick()
|
||||
await Vue.nextTick()
|
||||
})
|
||||
|
||||
it('shows an error toaster when apollo mutation rejects', async () => {
|
||||
@ -260,14 +221,7 @@ describe('ContributionForm.vue', () => {
|
||||
slug: 'dies-ist-ein-post',
|
||||
title: 'dies ist ein Post',
|
||||
content: 'auf Deutsch geschrieben',
|
||||
language: 'de',
|
||||
image,
|
||||
categories: [
|
||||
{
|
||||
id: 'cat12',
|
||||
name: 'Democracy & Politics',
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
wrapper = Wrapper()
|
||||
@ -290,8 +244,6 @@ describe('ContributionForm.vue', () => {
|
||||
slug: 'this-is-a-title-for-a-post',
|
||||
content: postContent,
|
||||
contentExcerpt: postContent,
|
||||
language: 'en',
|
||||
categoryIds,
|
||||
},
|
||||
},
|
||||
})
|
||||
@ -301,7 +253,6 @@ describe('ContributionForm.vue', () => {
|
||||
variables: {
|
||||
title: propsData.contribution.title,
|
||||
content: propsData.contribution.content,
|
||||
language: propsData.contribution.language,
|
||||
id: propsData.contribution.id,
|
||||
image: {
|
||||
sensitive: false,
|
||||
|
||||
@ -50,17 +50,6 @@
|
||||
{{ contentLength }}
|
||||
<base-icon v-if="errors && errors.content" name="warning" />
|
||||
</ds-chip>
|
||||
<ds-select
|
||||
model="language"
|
||||
icon="globe"
|
||||
class="select-field"
|
||||
:options="languageOptions"
|
||||
:placeholder="$t('contribution.languageSelectText')"
|
||||
:label="$t('contribution.languageSelectLabel')"
|
||||
/>
|
||||
<ds-chip v-if="errors && errors.language" size="base" color="danger">
|
||||
<base-icon name="warning" />
|
||||
</ds-chip>
|
||||
<div class="buttons">
|
||||
<base-button data-test="cancel-button" :disabled="loading" @click="$router.back()" danger>
|
||||
{{ $t('actions.cancel') }}
|
||||
@ -76,10 +65,8 @@
|
||||
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
import orderBy from 'lodash/orderBy'
|
||||
import { mapGetters } from 'vuex'
|
||||
import HcEditor from '~/components/Editor/Editor'
|
||||
import locales from '~/locales'
|
||||
import PostMutations from '~/graphql/PostMutations.js'
|
||||
import ImageUploader from '~/components/ImageUploader/ImageUploader'
|
||||
import links from '~/constants/links.js'
|
||||
@ -96,11 +83,7 @@ export default {
|
||||
},
|
||||
},
|
||||
data() {
|
||||
const { title, content, image, language } = this.contribution
|
||||
|
||||
const languageOptions = orderBy(locales, 'name').map((locale) => {
|
||||
return { label: locale.name, value: locale.code }
|
||||
})
|
||||
const { title, content, image } = this.contribution
|
||||
const { sensitive: imageBlurred = false, aspectRatio: imageAspectRatio = null } = image || {}
|
||||
|
||||
return {
|
||||
@ -111,15 +94,12 @@ export default {
|
||||
image: image || null,
|
||||
imageAspectRatio,
|
||||
imageBlurred,
|
||||
language: languageOptions.find((option) => option.value === language) || null,
|
||||
},
|
||||
formSchema: {
|
||||
title: { required: true, min: 3, max: 100 },
|
||||
content: { required: true },
|
||||
language: { required: true },
|
||||
imageBlurred: { required: false },
|
||||
},
|
||||
languageOptions,
|
||||
loading: false,
|
||||
users: [],
|
||||
hashtags: [],
|
||||
@ -155,7 +135,6 @@ export default {
|
||||
title,
|
||||
content,
|
||||
id: this.contribution.id || null,
|
||||
language: this.formData.language.value,
|
||||
image,
|
||||
},
|
||||
})
|
||||
|
||||
@ -3,20 +3,8 @@ import gql from 'graphql-tag'
|
||||
export default () => {
|
||||
return {
|
||||
CreatePost: gql`
|
||||
mutation(
|
||||
$title: String!
|
||||
$content: String!
|
||||
$language: String
|
||||
$categoryIds: [ID]
|
||||
$image: ImageInput
|
||||
) {
|
||||
CreatePost(
|
||||
title: $title
|
||||
content: $content
|
||||
language: $language
|
||||
categoryIds: $categoryIds
|
||||
image: $image
|
||||
) {
|
||||
mutation($title: String!, $content: String!, $categoryIds: [ID], $image: ImageInput) {
|
||||
CreatePost(title: $title, content: $content, categoryIds: $categoryIds, image: $image) {
|
||||
title
|
||||
slug
|
||||
content
|
||||
@ -34,7 +22,6 @@ export default () => {
|
||||
$id: ID!
|
||||
$title: String!
|
||||
$content: String!
|
||||
$language: String
|
||||
$image: ImageInput
|
||||
$categoryIds: [ID]
|
||||
) {
|
||||
@ -42,7 +29,6 @@ export default () => {
|
||||
id: $id
|
||||
title: $title
|
||||
content: $content
|
||||
language: $language
|
||||
image: $image
|
||||
categoryIds: $categoryIds
|
||||
) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user