Merge branch 'master' of https://github.com/Ocelot-Social-Community/Ocelot-Social into allow-only-supported-file-formats

This commit is contained in:
Wolfgang Huß 2020-11-02 12:41:15 +01:00
commit 0095a94863
24 changed files with 406 additions and 35 deletions

View File

@ -45,10 +45,12 @@ script:
- docker-compose down - docker-compose down
- docker-compose -f docker-compose.yml up -d - docker-compose -f docker-compose.yml up -d
- wait-on http://localhost:7474 - wait-on http://localhost:7474
- yarn run cypress:run --record # disable for last deploy, because of flakiness!
- yarn run cucumber # - yarn run cypress:run --record
# - yarn run cucumber
# Coverage # Coverage
- yarn run codecov # disable this uneffective thing for last deploy, because of easyness!
# - yarn run codecov
after_success: after_success:
- wget https://raw.githubusercontent.com/DiscordHooks/travis-ci-discord-webhook/master/send.sh - wget https://raw.githubusercontent.com/DiscordHooks/travis-ci-discord-webhook/master/send.sh

View File

@ -4,6 +4,36 @@ All notable changes to this project will be documented in this file. Dates are d
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
#### [v0.6.3](https://github.com/Human-Connection/Human-Connection/compare/v0.6.0...v0.6.3)
> 16 October 2020
- feat: Export User Data Update [`#3954`](https://github.com/Human-Connection/Human-Connection/pull/3954)
- chore: Upgrade to v0.6.2 [`#3947`](https://github.com/Human-Connection/Human-Connection/pull/3947)
#### [v0.6.2](https://github.com/Human-Connection/Human-Connection/compare/v0.6.1...v0.6.2)
> 15 October 2020
- build: 🍰 Disable Codecov for last deploy [`#3946`](https://github.com/Human-Connection/Human-Connection/pull/3946)
- feat: Export User Data [`#3899`](https://github.com/Human-Connection/Human-Connection/pull/3899)
- build: 💥 Disable full stack tests, Fix deployment to develop, tryout [`#3937`](https://github.com/Human-Connection/Human-Connection/pull/3937)
- build: 💥 Disable full stack tests [`#3935`](https://github.com/Human-Connection/Human-Connection/pull/3935)
- fix: 🍰 Sign Up Page On Safari [`#3882`](https://github.com/Human-Connection/Human-Connection/pull/3882)
- build: Add semantic PR config [`#3884`](https://github.com/Human-Connection/Human-Connection/pull/3884)
- feat: 🍰 Admin - Remove User Profile [`#3140`](https://github.com/Human-Connection/Human-Connection/pull/3140)
- fix: 🍰 Comment Counters Are Now Equal [`#3769`](https://github.com/Human-Connection/Human-Connection/pull/3769)
- feat: 🍰 Redesign Data Privacy Warning Box [`#3780`](https://github.com/Human-Connection/Human-Connection/pull/3780)
- fix: 🍰 Checkboxes Not Missing Anymore On Delete User Account Page [`#3506`](https://github.com/Human-Connection/Human-Connection/pull/3506)
- feat: 🍰 Increase Margin Of Header And Ruler For Better Legibility [`#3774`](https://github.com/Human-Connection/Human-Connection/pull/3774)
- chore: 💬 Rename stale.yml to stale-disabled.yml [`#3662`](https://github.com/Human-Connection/Human-Connection/pull/3662)
- build(deps): [security] bump apollo-server-core from 2.12.0 to 2.15.0 in /backend [`#3650`](https://github.com/Human-Connection/Human-Connection/pull/3650)
- fix: Corrected Code-of-Conduct Mail Link [`#3609`](https://github.com/Human-Connection/Human-Connection/pull/3609)
- feat: 🍰 Hero image height on post page is now set without having to wait for… [`#3583`](https://github.com/Human-Connection/Human-Connection/pull/3583)
- feat: 🍰 Alphabetically sorting tags using compute functions on index and more… [`#3589`](https://github.com/Human-Connection/Human-Connection/pull/3589)
- fix: Fixed webapp unit test command. [`#3584`](https://github.com/Human-Connection/Human-Connection/pull/3584)
- chore: Upgrade to v0.6.1 [`#3525`](https://github.com/Human-Connection/Human-Connection/pull/3525)
#### [v0.6.1](https://github.com/Human-Connection/Human-Connection/compare/v0.6.0...v0.6.1) #### [v0.6.1](https://github.com/Human-Connection/Human-Connection/compare/v0.6.0...v0.6.1)
> 4 May 2020 > 4 May 2020

View File

@ -1,6 +1,6 @@
{ {
"name": "human-connection-backend", "name": "human-connection-backend",
"version": "0.6.1", "version": "0.6.3",
"description": "GraphQL Backend for Human Connection", "description": "GraphQL Backend for Human Connection",
"main": "src/index.js", "main": "src/index.js",
"scripts": { "scripts": {

View File

@ -105,6 +105,7 @@ export default shield(
blockedUsers: isAuthenticated, blockedUsers: isAuthenticated,
notifications: isAuthenticated, notifications: isAuthenticated,
Donations: isAuthenticated, Donations: isAuthenticated,
userData: isAuthenticated,
}, },
Mutation: { Mutation: {
'*': deny, '*': deny,

View File

@ -0,0 +1,61 @@
export default {
Query: {
userData: async (object, args, context, resolveInfo) => {
const id = context.user.id
const cypher = `
MATCH (user:User { id: $id })
WITH user
OPTIONAL MATCH (posts:Post)
WHERE (user)-[:WROTE]->(posts)
AND posts.deleted = FALSE
AND posts.disabled = FALSE
RETURN { user: properties(user),
posts: collect(
posts {
.*,
author: [
(posts)<-[:WROTE]-(author:User) |
author {
.*
}
][0],
comments: [
(posts)<-[:COMMENTS]-(comment:Comment)
WHERE comment.disabled = FALSE
AND comment.deleted = FALSE |
comment {
.*,
author: [ (comment)<-[:WROTE]-(commentator:User) |
commentator { .name, .slug, .id } ][0]
}
],
categories: [ (posts)-[:CATEGORIZED]->(category:Category) |
category { .name, .id } ]
})
} AS result`
const session = context.driver.session()
const resultPromise = session.readTransaction(async (transaction) => {
const transactionResponse = transaction.run(cypher, {
id,
})
return transactionResponse
})
try {
const result = await resultPromise
const userData = result.records[0].get('result')
userData.posts.sort(byCreationDate)
userData.posts.forEach((post) => post.comments.sort(byCreationDate))
return userData
} finally {
session.close()
}
},
},
}
const byCreationDate = (a, b) => {
if (a.createdAt < b.createdAt) return -1
if (a.createdAt > b.createdAt) return 1
return 0
}

View File

@ -0,0 +1,143 @@
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 query, authenticatedUser
const driver = getDriver()
const neode = getNeode()
beforeAll(async () => {
await cleanDatabase()
const user = await Factory.build('user', {
id: 'a-user',
name: 'John Doe',
slug: 'john-doe',
})
await Factory.build('user', {
id: 'o-user',
name: 'Unauthenticated User',
slug: 'unauthenticated-user',
})
authenticatedUser = await user.toJson()
const { server } = createServer({
context: () => {
return {
driver,
neode,
user: authenticatedUser,
}
},
})
query = createTestClient(server).query
})
afterAll(async () => {
await cleanDatabase()
})
const userDataQuery = gql`
query($id: ID!) {
userData(id: $id) {
user {
id
name
slug
}
posts {
id
title
content
comments {
content
author {
slug
}
}
}
}
}
`
describe('resolvers/userData', () => {
let variables = { id: 'a-user' }
describe('given one authenticated user who did not write anything so far', () => {
it("returns the user's data and no posts", async () => {
await expect(query({ query: userDataQuery, variables })).resolves.toMatchObject({
data: {
userData: {
user: {
id: 'a-user',
name: 'John Doe',
slug: 'john-doe',
},
posts: [],
},
},
})
})
describe('the user writes a post', () => {
beforeAll(async () => {
await Factory.build(
'post',
{
id: 'a-post',
title: 'A post',
content: 'A post',
},
{ authorId: 'a-user' },
)
})
it("returns the user's data and the post", async () => {
await expect(query({ query: userDataQuery, variables })).resolves.toMatchObject({
data: {
userData: {
user: {
id: 'a-user',
name: 'John Doe',
slug: 'john-doe',
},
posts: [
{
id: 'a-post',
title: 'A post',
content: 'A post',
},
],
},
},
})
})
})
})
describe('try to request data of another user', () => {
variables = { id: 'o-user' }
it('returns the data of the authenticated user', async () => {
await expect(query({ query: userDataQuery, variables })).resolves.toMatchObject({
data: {
userData: {
user: {
id: 'a-user',
name: 'John Doe',
slug: 'john-doe',
},
posts: expect.arrayContaining([
{
id: 'a-post',
title: 'A post',
content: 'A post',
comments: [],
},
]),
},
},
})
})
})
})

View File

@ -0,0 +1,10 @@
type UserData {
user: User!
posts: [Post]
}
type Query {
userData(
id: ID
): UserData
}

View File

@ -1,6 +1,6 @@
{ {
"name": "human-connection", "name": "human-connection",
"version": "0.6.1", "version": "0.6.3",
"description": "Fullstack and API tests with cypress and cucumber for Human Connection", "description": "Fullstack and API tests with cypress and cucumber for Human Connection",
"author": "Human Connection gGmbh", "author": "Human Connection gGmbh",
"license": "MIT", "license": "MIT",

View File

@ -0,0 +1,5 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<title>download</title>
<path d="M15 4h2v16.563l5.281-5.281 1.438 1.438-7 7-0.719 0.688-0.719-0.688-7-7 1.438-1.438 5.281 5.281v-16.563zM7 26h18v2h-18v-2z"></path>
</svg>

After

Width:  |  Height:  |  Size: 302 B

View File

@ -279,7 +279,7 @@ $size-avatar-large: 114px;
$size-image-max-height: 2000px; $size-image-max-height: 2000px;
$size-image-cropper-max-height: 600px; $size-image-cropper-max-height: 600px;
$size-image-cropper-min-height: 400px; $size-image-cropper-min-height: 400px;
$size-image-uploader-min-height: 200px; $size-image-uploader-min-height: 250px;
/** /**
* @tokens Size Icons * @tokens Size Icons

View File

@ -20,6 +20,9 @@
:title="$t('actions.delete')" :title="$t('actions.delete')"
@click.stop="deleteImage" @click.stop="deleteImage"
/> />
<div v-if="!hasImage" class="supported-formats">
{{ $t('contribution.teaserImage.supportedFormats') }}
</div>
</vue-dropzone> </vue-dropzone>
<div v-show="showCropper" class="crop-overlay"> <div v-show="showCropper" class="crop-overlay">
<img id="cropping-image" /> <img id="cropping-image" />
@ -209,6 +212,11 @@ export default {
right: $space-small; right: $space-small;
z-index: $z-index-surface; z-index: $z-index-surface;
} }
> .supported-formats {
margin-top: 150px;
font-weight: bold;
}
} }
} }
</style> </style>

View File

@ -292,3 +292,32 @@ export const currentUserCountQuery = () => gql`
} }
} }
` `
export const userDataQuery = (i18n) => {
return gql`
${userFragment}
${postFragment}
${commentFragment}
query($id: ID!) {
userData(id: $id) {
user {
...user
}
posts {
...post
categories {
id
name
}
comments {
author {
id
slug
}
...comment
}
}
}
}
`
}

View File

@ -258,7 +258,8 @@
"newPost": "Erstelle einen neuen Beitrag", "newPost": "Erstelle einen neuen Beitrag",
"success": "Gespeichert!", "success": "Gespeichert!",
"teaserImage": { "teaserImage": {
"cropperConfirm": "Bestätigen" "cropperConfirm": "Bestätigen",
"supportedFormats": "Füge ein Bild im Dateiformat JPG, PNG oder GIF ein"
}, },
"title": "Titel" "title": "Titel"
}, },
@ -648,6 +649,8 @@
"success": "Konto erfolgreich gelöscht!" "success": "Konto erfolgreich gelöscht!"
}, },
"download": { "download": {
"description": "Klicke auf den Knopf oben, um den Inhalt deiner Beiträge und Kommentare herunterzuladen. Um die Bilder der Beiträge herunterzuladen, musst du auf den jeweiligen Link unten klicken.",
"json": "als JSON",
"name": "Daten herunterladen" "name": "Daten herunterladen"
}, },
"email": { "email": {

View File

@ -258,7 +258,8 @@
"newPost": "Create a new Post", "newPost": "Create a new Post",
"success": "Saved!", "success": "Saved!",
"teaserImage": { "teaserImage": {
"cropperConfirm": "Confirm" "cropperConfirm": "Confirm",
"supportedFormats": "Insert a picture of file format JPG , PNG or GIF"
}, },
"title": "Title" "title": "Title"
}, },
@ -648,6 +649,8 @@
"success": "Account successfully deleted!" "success": "Account successfully deleted!"
}, },
"download": { "download": {
"description": "Click on the button above to download the content of your posts and comments. To download the images of your posts, you have to click on the corresponding link below.",
"json": "as JSON",
"name": "Download Data" "name": "Download Data"
}, },
"email": { "email": {

View File

@ -256,7 +256,8 @@
"newPost": "Crear una nueva contribución", "newPost": "Crear una nueva contribución",
"success": "¡Guardado!", "success": "¡Guardado!",
"teaserImage": { "teaserImage": {
"cropperConfirm": "Confirmar" "cropperConfirm": "Confirmar",
"supportedFormats": "Insertar una imagen de formato de archivo JPG, PNG o GIF"
}, },
"title": "Título" "title": "Título"
}, },

View File

@ -256,7 +256,8 @@
"newPost": "Créer un nouveau Post", "newPost": "Créer un nouveau Post",
"success": "Enregistré!", "success": "Enregistré!",
"teaserImage": { "teaserImage": {
"cropperConfirm": "Confirmer" "cropperConfirm": "Confirmer",
"supportedFormats": "Insérer une image au format de fichier JPG, PNG ou GIF"
}, },
"title": "Titre" "title": "Titre"
}, },

View File

@ -261,7 +261,8 @@
"newPost": "", "newPost": "",
"success": "", "success": "",
"teaserImage": { "teaserImage": {
"cropperConfirm": "Confermare" "cropperConfirm": "Confermare",
"supportedFormats": "Inserisci un'immagine in formato file JPG, PNG o GIF"
}, },
"title": "" "title": ""
}, },

View File

@ -68,7 +68,8 @@
"delete": "Bijdrage verwijderen", "delete": "Bijdrage verwijderen",
"edit": "Bijdrage bewerken", "edit": "Bijdrage bewerken",
"teaserImage": { "teaserImage": {
"cropperConfirm": "Bevestigen" "cropperConfirm": "Bevestigen",
"supportedFormats": "Voeg een afbeelding in met het bestandsformaat JPG, PNG of GIF"
} }
}, },
"disable": { "disable": {

View File

@ -119,7 +119,8 @@
"newPost": "Utwórz nowy wpis", "newPost": "Utwórz nowy wpis",
"success": "Zapisano!", "success": "Zapisano!",
"teaserImage": { "teaserImage": {
"cropperConfirm": "Potwierdzać" "cropperConfirm": "Potwierdzać",
"supportedFormats": "Wstaw zdjęcie w formacie pliku JPG, PNG lub GIF"
} }
}, },
"delete": { "delete": {

View File

@ -252,7 +252,8 @@
"newPost": "Criar uma nova publicação", "newPost": "Criar uma nova publicação",
"success": "Salvo!", "success": "Salvo!",
"teaserImage": { "teaserImage": {
"cropperConfirm": "Confirmar" "cropperConfirm": "Confirmar",
"supportedFormats": "Insira uma imagem do formato JPG, PNG ou GIF"
}, },
"title": "Título" "title": "Título"
}, },

View File

@ -256,7 +256,8 @@
"newPost": "Создать пост", "newPost": "Создать пост",
"success": "Сохранено!", "success": "Сохранено!",
"teaserImage": { "teaserImage": {
"cropperConfirm": "Подтвердить" "cropperConfirm": "Подтвердить",
"supportedFormats": "Вставьте изображение файла формата JPG, PNG или GIF"
}, },
"title": "Заголовок" "title": "Заголовок"
}, },

View File

@ -1,6 +1,6 @@
{ {
"name": "human-connection-webapp", "name": "human-connection-webapp",
"version": "0.6.1", "version": "0.6.3",
"description": "Human Connection Frontend", "description": "Human Connection Frontend",
"authors": [ "authors": [
"Grzegorz Leoniec (appinteractive)", "Grzegorz Leoniec (appinteractive)",

View File

@ -51,6 +51,10 @@ export default {
name: this.$t('settings.embeds.name'), name: this.$t('settings.embeds.name'),
path: `/settings/embeds`, path: `/settings/embeds`,
}, },
{
name: this.$t('settings.download.name'),
path: `/settings/data-download`,
},
{ {
name: this.$t('settings.deleteUserAccount.name'), name: this.$t('settings.deleteUserAccount.name'),
path: `/settings/delete-account`, path: `/settings/delete-account`,
@ -61,12 +65,6 @@ export default {
path: `/settings/invites` path: `/settings/invites`
}, */ }, */
// TODO implement // TODO implement
/* {
name: this.$t('settings.download.name'),
path: `/settings/data-download`
}, */
// TODO implement
// TODO implement
/* { /* {
name: this.$t('settings.organizations.name'), name: this.$t('settings.organizations.name'),
path: `/settings/my-organizations` path: `/settings/my-organizations`

View File

@ -1,16 +1,87 @@
<template> <template>
<base-card> <base-card>
<h2 class="title">{{ $t('settings.download.name') }}</h2> <h2 class="title">{{ $t('settings.download.name') }}</h2>
<hc-empty icon="tasks" message="Coming Soon…" /> <base-button
@click="onClick(jsonData)"
icon="download"
secondary
filled
:disabled="loading"
:loading="loading"
>
{{ $t('settings.download.json') }}
</base-button>
<ds-space margin="large" />
<ds-text>{{ $t('settings.download.description') }}</ds-text>
<ds-space margin="large" />
<base-card v-for="image in imageList" :key="image.key">
<a :href="image.url" target="_blank" rel="noopener noreferrer">{{ image.title }}</a>
<ds-space margin="xxx-small" />
</base-card>
</base-card> </base-card>
</template> </template>
<script> <script>
import HcEmpty from '~/components/Empty/Empty' import { mapGetters } from 'vuex'
import { userDataQuery } from '~/graphql/User'
import BaseButton from '~/components/_new/generic/BaseButton/BaseButton.vue'
import isEmpty from 'lodash/isEmpty'
export default { export default {
components: { components: {
HcEmpty, BaseButton,
},
data() {
return {
userData: {},
loading: true,
imageList: [],
}
},
computed: {
...mapGetters({
user: 'auth/user',
}),
jsonData() {
return { data: JSON.stringify(this.userData, null, 2), type: 'json' }
},
},
methods: {
onClick(method) {
var fileURL = window.URL.createObjectURL(new Blob([method.data]))
var fileLink = document.createElement('a')
fileLink.href = fileURL
fileLink.setAttribute('download', 'userData.' + method.type)
document.body.appendChild(fileLink)
fileLink.click()
},
},
apollo: {
queryUserData: {
query() {
return userDataQuery()
},
variables() {
return { id: this.user.id }
},
update({ userData }) {
this.userData = userData
this.loading = false
if (isEmpty(this.userData)) return null
const userId = this.userData.user.id
if (isEmpty(userId)) return null
this.imageList = this.userData.posts
.filter((post) => post.author.id === userId && post.image)
.map((post) => {
const obj = {}
obj.key = post.id
obj.url = post.image.url
obj.title = post.title
return obj
})
},
fetchPolicy: 'cache-and-network',
},
}, },
} }
</script> </script>