Merge pull request #3926 from Ocelot-Social-Community/improve-data-export

feat: Improve Data Export
This commit is contained in:
Wolfgang Huß 2020-10-26 12:57:17 +01:00 committed by GitHub
commit 7f52046b9a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 379 additions and 25 deletions

View File

@ -45,10 +45,12 @@ script:
- docker-compose down
- docker-compose -f docker-compose.yml up -d
- wait-on http://localhost:7474
- yarn run cypress:run --record
- yarn run cucumber
# disable for last deploy, because of flakiness!
# - yarn run cypress:run --record
# - yarn run cucumber
# Coverage
- yarn run codecov
# disable this uneffective thing for last deploy, because of easyness!
# - yarn run codecov
after_success:
- 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).
#### [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)
> 4 May 2020

View File

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

View File

@ -105,6 +105,7 @@ export default shield(
blockedUsers: isAuthenticated,
notifications: isAuthenticated,
Donations: isAuthenticated,
userData: isAuthenticated,
},
Mutation: {
'*': 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",
"version": "0.6.1",
"version": "0.6.3",
"description": "Fullstack and API tests with cypress and cucumber for Human Connection",
"author": "Human Connection gGmbh",
"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

@ -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

@ -648,6 +648,8 @@
"success": "Konto erfolgreich gelöscht!"
},
"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"
},
"email": {

View File

@ -648,6 +648,8 @@
"success": "Account successfully deleted!"
},
"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"
},
"email": {

View File

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

View File

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

View File

@ -1,16 +1,87 @@
<template>
<base-card>
<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>
</template>
<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 {
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>