Merge branch 'feature/new-portfolio-layout' into develop

This commit is contained in:
Ramon Wenger 2021-10-07 20:21:20 +02:00
commit 91a1dd0bdb
71 changed files with 10102 additions and 9756 deletions

View File

@ -1,4 +1,4 @@
FROM python:3.8.7
FROM python:3.8.10
ENV PYTHONUNBUFFERED 1
RUN pip install pipenv

View File

@ -60,6 +60,14 @@ module.exports = {
},
},
},
{
test: /\.tsx?$/,
loader: 'ts-loader',
options: {
appendTsSuffixTo: [/\.vue$/]
},
exclude: /node_modules/
},
{
test: /\.js$/,
loader: 'babel-loader',

View File

@ -1,5 +1,5 @@
const selectedClass = {
id: 'selectedClassId',
id: btoa('SchoolClassNode:selectedClassId'),
name: 'Moordale',
readOnly: false,
code: 'XXXX',

View File

@ -1,57 +0,0 @@
const operations = {
ProjectsQuery: {
projects: {
edges: [
{
node: {
id: 'UHJvamVjdE5vZGU6NjY=',
title: 'Some random title',
appearance: 'blue',
description: 'This description rocks',
slug: 'some-random-title',
objectives: 'Git gud',
final: false,
student: {
firstName: 'Rachel',
lastName: 'Green',
id: 'VXNlck5vZGU6NQ==',
avatarUrl: '',
},
entriesCount: 0,
},
},
],
},
},
MeQuery: {
me: {
}
},
AddProject: variables => ({
addProject: {
project: Object.assign({}, variables.input.project),
errors: null,
__typename: 'AddProjectPayload',
},
}),
};
describe('New project', () => {
before(() => {
cy.setup();
});
it('creates a new project and displays it', () => {
cy.mockGraphqlOps({
operations
});
cy.visit('/portfolio');
cy.get('[data-cy=add-project-button]').click();
cy.get('[data-cy=page-form-input-titel]').type('Some random title');
cy.get('[data-cy=page-form-input-beschreibung]').type('This description rocks');
cy.get('[data-cy=page-form-input-ziele]').type('Git gud');
cy.get('[data-cy=save-project-button]').click();
cy.get('.project-widget:first').contains('random');
});
});

View File

@ -0,0 +1,56 @@
import {getMinimalMe} from '../../../support/helpers';
describe('New project', () => {
const MeQuery = getMinimalMe({isTeacher: false});
const schoolClass = MeQuery.me.selectedClass;
const operations = {
ProjectsQuery: {
projects: [
{
id: 'UHJvamVjdE5vZGU6NjY=',
title: 'Some random title',
appearance: 'blue',
description: 'This description rocks',
slug: 'some-random-title',
objectives: 'Git gud',
final: false,
schoolClass,
student: {
firstName: 'Rachel',
lastName: 'Green',
id: 'VXNlck5vZGU6NQ==',
avatarUrl: '',
},
entriesCount: 0,
},
],
},
MeQuery,
AddProject: variables => ({
addProject: {
project: Object.assign({}, variables.input.project, {schoolClass}),
errors: null,
__typename: 'AddProjectPayload',
},
}),
};
before(() => {
cy.setup();
});
it('creates a new project and displays it', () => {
cy.mockGraphqlOps({
operations,
});
cy.visit('/portfolio');
cy.get('[data-cy=add-project-button]').click();
cy.get('[data-cy=page-form-input-titel]').type('Some random title');
cy.get('[data-cy=page-form-input-beschreibung]').should('exist').should('be.empty');
cy.get('[data-cy=page-form-input-ziele]').should('not.exist');
cy.get('[data-cy=save-project-button]').click();
cy.getByDataCy('project').contains('random');
});
});

View File

@ -0,0 +1,152 @@
import {PROJECT_ENTRY_TEMPLATE} from '../../../../src/consts/strings.consts';
describe('Project Page', () => {
const created = '2021-06-01T11:49:00+00:00';
const createdLater = '2021-06-01T12:49:00+00:00';
const operations = {
MeQuery: {
me: {
id: 'VXNlck5vZGU6NQ==',
permissions: [],
},
},
ProjectsQuery: {
projects: [
{
id: 'UHJvamVjdE5vZGU6MzM=',
title: 'Groot',
appearance: 'red',
description: 'I am Groot',
slug: 'groot',
objectives: 'Be Groot\nBe awesome',
final: false,
student: {
firstName: 'Rachel',
lastName: 'Green',
id: 'VXNlck5vZGU6NQ==',
avatarUrl: ''
},
entriesCount: 2,
},
],
},
ProjectQuery: {
project: {
id: 'UHJvamVjdE5vZGU6MzY=',
title: 'Groot',
appearance: 'yellow',
description: 'I am Groot',
slug: 'groot',
objectives: 'Be Groot\nBe awesome',
final: false,
student: {
firstName: 'Rachel',
lastName: 'Green',
id: 'VXNlck5vZGU6NQ==',
avatarUrl: '',
},
entriesCount: 1,
entries: [
{
id: 'UHJvamVjdEVudHJ5Tm9kZTo2NQ==',
description: 'Aktivität:\nKill Thanos\n\n\nReflexion:\nHe sucks\n\n\nNächste Schritte:\nGo for the head',
documentUrl: '',
created,
},
],
},
},
AddProjectEntry: variables => ({
addProjectEntry: {
projectEntry: Object.assign({}, variables.input.projectEntry, {
created: createdLater
}),
errors: null
},
}),
UpdateProjectEntry: variables => ({
updateProjectEntry: {
projectEntry: variables.input.projectEntry,
errors: null,
__typename: 'UpdateProjectEntryPayload',
},
}),
DeleteProjectEntry: {
deleteProjectEntry: {
success: true,
},
},
};
beforeEach(() => {
cy.setup();
cy.task('getSchema').then(schema => {
cy.mockGraphqlOps({
operations,
});
});
});
it('has the correct layout', () => {
cy.visit('/portfolio/groot');
cy.getByDataCy('project-entry').eq(0).within(() => {
cy.getByDataCy('project-entry-date').should('contain', '1. Juni 2021');
});
});
describe('Project Entry', () => {
it('should create a new project entry', () => {
cy.visit('/portfolio');
cy.get('[data-cy=project-link]:first-of-type').click();
cy.get('[data-cy=add-project-entry]:first-of-type').click();
cy.getByDataCy('activity-input').should('not.exist');
cy.getByDataCy('reflection-input').should('not.exist');
cy.getByDataCy('next-steps-input').should('not.exist');
cy.getByDataCy('modal-title').should('contain', 'Beitrag erfassen');
cy.getByDataCy('project-entry-textarea').should('exist');
cy.getByDataCy('use-template-button').should('exist').click();
cy.getByDataCy('upload-document-button').should('exist');
cy.getByDataCy('modal-save-button').click();
cy.get('.project-entry:last-of-type').within(() => {
cy.get('.project-entry__paragraph:first-of-type').contains('Schwierigkeiten');
});
});
it('should edit first entry', () => {
cy.visit('/portfolio/groot');
cy.get('.project-entry__paragraph:first-of-type').contains('Kill Thanos');
cy.get('.project-entry:first-of-type').within(() => {
cy.get('[data-cy=project-entry-more]').click();
cy.get('[data-cy=edit-project-entry]').click();
});
cy.getByDataCy('activity-input').should('not.exist');
cy.getByDataCy('project-entry-textarea').clear().type('Defeat Thanos');
cy.get('[data-cy=modal-save-button]').click();
cy.get('.project-entry__paragraph:first-of-type').contains('Defeat Thanos');
});
it('should delete the last entry', () => {
cy.visit('/portfolio/groot');
cy.get('.project-entry').should('have.length', 1);
cy.get('.project-entry:last-of-type').within(() => {
cy.get('[data-cy=project-entry-more]').click();
cy.get('[data-cy=delete-project-entry]').click();
});
cy.get('.project-entry').should('have.length', 0);
});
it('should use the template', () => {
cy.visit('/portfolio/groot');
cy.get('[data-cy=add-project-entry]:first-of-type').click();
cy.getByDataCy('use-template-button').click();
cy.getByDataCy('project-entry-textarea').should('have.value', PROJECT_ENTRY_TEMPLATE);
});
});
});

File diff suppressed because one or more lines are too long

View File

@ -1,151 +0,0 @@
const operations = {
MeQuery: {
me: {
id: 'VXNlck5vZGU6NQ==',
permissions: [],
},
},
ProjectsQuery: {
projects: {
edges: [{
node: {
id: 'UHJvamVjdE5vZGU6MzM=',
title: 'Groot',
appearance: 'red',
'description': 'I am Groot',
'slug': 'groot',
'objectives': 'Be Groot\nBe awesome',
'final': false,
'student': {
'firstName': 'Rachel',
'lastName': 'Green',
'id': 'VXNlck5vZGU6NQ==',
'avatarUrl': '',
'__typename': 'UserNode',
},
'entriesCount': 2,
'__typename': 'ProjectNode',
},
'__typename': 'ProjectNodeEdge',
}],
'__typename': 'ProjectNodeConnection',
},
},
ProjectQuery: {
'project': {
'id': 'UHJvamVjdE5vZGU6MzY=',
'title': 'Groot',
'appearance': 'yellow',
'description': 'I am Groot',
'slug': 'groot',
'objectives': 'Be Groot\nBe awesome',
'final': false,
'student': {
'firstName': 'Rachel',
'lastName': 'Green',
'id': 'VXNlck5vZGU6NQ==',
'avatarUrl': '',
'__typename': 'UserNode',
},
'entriesCount': 1,
'__typename': 'ProjectNode',
'entries': {
'edges': [{
'node': {
'id': 'UHJvamVjdEVudHJ5Tm9kZTo2NQ==',
'activity': 'Kill Thanos',
'reflection': 'He sucks',
'nextSteps': 'Go for the head',
'documentUrl': '',
'__typename': 'ProjectEntryNode',
'created': '2020-01-20T15:20:31.262510+00:00',
},
'__typename': 'ProjectEntryNodeEdge',
}],
'__typename': 'ProjectEntryNodeConnection',
},
},
},
AddProjectEntry: variables => ({
addProjectEntry: {
projectEntry: Object.assign({}, variables.input.projectEntry, {
created: '2020-01-20T15:26:58.722773+00:00',
}),
errors: null,
__typename: 'AddProjectEntryPayload',
},
}),
UpdateProjectEntry: variables => ({
updateProjectEntry: {
projectEntry: variables.input.projectEntry,
errors: null,
__typename: 'UpdateProjectEntryPayload',
},
}),
DeleteProjectEntry: {
deleteProjectEntry: {
success: true,
__typename: 'DeleteProjectEntryPayload',
},
},
};
describe('Project Entry', () => {
beforeEach(() => {
cy.setup();
cy.task('getSchema').then(schema => {
cy.mockGraphqlOps({
operations,
});
});
});
it('should create a new project entry', () => {
cy.visit('/portfolio');
cy.get('[data-cy=project-link]:first-of-type').click();
cy.get('[data-cy=add-project-entry]:first-of-type').click();
cy.get('[data-cy=activity-input]').within(() => {
cy.get('[data-cy=text-form-input]').type('Join the Guardians');
});
cy.get('[data-cy=reflection-input]').within(() => {
cy.get('[data-cy=text-form-input]').type('They are cool!');
});
cy.get('[data-cy=next-steps-input]').within(() => {
cy.get('[data-cy=text-form-input]').type('Stay with Rocket\nMeet Quill');
});
cy.get('[data-cy=modal-save-button]').click();
cy.get('.project-entry:last-of-type').within(() => {
cy.get('.project-entry__paragraph:first-of-type').contains('Join the Guardians');
});
});
it('should edit first entry', () => {
cy.visit('/portfolio/groot');
cy.get('.project-entry__paragraph:first-of-type').contains('Kill Thanos');
cy.get('.project-entry:first-of-type').within(() => {
cy.get('[data-cy=project-entry-more]').click();
cy.get('[data-cy=edit-project-entry]').click();
});
cy.get('[data-cy=activity-input]').within(() => {
cy.get('[data-cy=text-form-input]').clear().type('Defeat Thanos');
});
cy.get('[data-cy=modal-save-button]').click();
cy.get('.project-entry__paragraph:first-of-type').contains('Defeat Thanos');
});
it('should delete the last entry', () => {
cy.visit('/portfolio/groot');
cy.get('.project-entry').should('have.length', 1);
cy.get('.project-entry:last-of-type').within(() => {
cy.get('[data-cy=project-entry-more]').click();
cy.get('[data-cy=delete-project-entry]').click();
});
cy.get('.project-entry').should('have.length', 0);
});
});

View File

@ -3,20 +3,16 @@ import {getMinimalMe} from '../../../support/helpers';
const getOperations = ({readOnly = false, classReadOnly = false}) => ({
MeQuery: getMinimalMe({readOnly, classReadOnly}),
ProjectsQuery: {
projects: {
edges: [
{
node: {
id: 'projectId',
final: false,
student: {
id: btoa('PrivateUserNode:1'),
},
entriesCount: 3,
},
projects: [
{
id: 'projectId',
final: false,
student: {
id: btoa('PrivateUserNode:1'),
},
],
},
entriesCount: 3,
},
],
},
});
@ -29,24 +25,29 @@ describe('Read Only Portfolio', () => {
cy.mockGraphqlOps({operations: getOperations({readOnly: false})});
cy.visit('/portfolio');
cy.getByDataCy('project-list').should('exist');
cy.getByDataCy('add-project-button').should('exist');
cy.getByDataCy('project-widget').should('have.length', 1);
cy.getByDataCy('project-widget-actions').should('exist');
cy.getByDataCy('project').should('have.length', 1);
cy.getByDataCy('project-actions').should('exist');
});
it('Can not create and edit project when license invalid', () => {
cy.mockGraphqlOps({operations: getOperations({readOnly: true})});
cy.visit('/portfolio');
cy.getByDataCy('project-list').should('exist');
cy.getByDataCy('add-project-button').should('not.exist');
cy.getByDataCy('project-widget').should('have.length', 1);
cy.getByDataCy('project-widget-actions').should('not.exist');
cy.getByDataCy('project').should('have.length', 1);
cy.getByDataCy('project-actions').should('not.exist');
});
it('Can not create and edit project when class inactive', () => {
cy.mockGraphqlOps({operations: getOperations({readOnly: false, classReadOnly: true})});
cy.visit('/portfolio');
cy.getByDataCy('project-list').should('exist');
cy.getByDataCy('add-project-button').should('not.exist');
cy.getByDataCy('project-widget').should('have.length', 1);
cy.getByDataCy('project-widget-actions').should('not.exist');
cy.getByDataCy('project').should('have.length', 1);
cy.getByDataCy('project-actions').should('not.exist');
});
});

View File

@ -9,14 +9,10 @@ const getOperations = ({readOnly = false, classReadOnly = false}) => ({
student: {
id: btoa('PrivateUserNode:1'),
},
entriesCount: 3,
entries: {
edges: [
{
node: {},
},
],
},
entriesCount: 1,
entries: [
{}
]
},
},
});

16999
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -20,7 +20,8 @@
"cypress:frontend:test": "cypress run --config-file cypress.frontend.json",
"install:cypress": "cypress install",
"test:unit": "jest",
"cypress:parallel": "CYPRESS_API_URL=\"https://iterativ-cypress-director.herokuapp.com/\" cy2 run --parallel --record --key somekey --config-file cypress.frontend.json --ci-build-id some-id"
"cypress:parallel": "CYPRESS_API_URL=\"https://iterativ-cypress-director.herokuapp.com/\" cy2 run --parallel --record --key somekey --config-file cypress.frontend.json --ci-build-id some-id",
"cypress:parallel:run": "cy2 run --parallel --record --config-file cypress.frontend.json --ci-build-id "
},
"dependencies": {
"@babel/core": "^7.5.4",
@ -127,7 +128,8 @@
"jest-transform-graphql": "^2.1.0",
"jest-transform-stub": "^2.0.0",
"jest-watch-typeahead": "^0.3.1",
"typescript": "^4.2.3",
"ts-loader": "^8.3.0",
"typescript": "^4.4.3",
"vue-jest": "^3.0.4"
}
}

View File

@ -48,8 +48,7 @@
</script>
<style scoped lang="scss">
@import "@/styles/_variables.scss";
@import "@/styles/_mixins.scss";
@import "~styles/helpers";
.add-widget {
display: none;

View File

@ -11,6 +11,7 @@
import ChevronLeft from '@/components/icons/ChevronLeft';
import {MODULE_PAGE} from '@/router/module.names';
import {ROOMS_PAGE} from '@/router/room.names';
import {PROJECTS_PAGE} from '@/router/portfolio.names';
export default {
props: {
@ -39,6 +40,8 @@
return {name: 'topic', params: {topicSlug: this.slug}};
case 'module':
return {name: MODULE_PAGE};
case 'portfolio':
return {name: PROJECTS_PAGE};
default:
return {name: ROOMS_PAGE};
}

View File

@ -42,9 +42,7 @@
</script>
<style scoped lang="scss">
@import '@/styles/_variables.scss';
@import '@/styles/_functions.scss';
@import '@/styles/_mixins.scss';
@import "~styles/helpers";
.document-block {
display: grid;

View File

@ -26,20 +26,10 @@
@click="$emit('spellcheck')"
>{{ spellcheckText }}
</button>
<div v-if="userInput.document">
<document-block
:value="{url: userInput.document}"
show-trash-icon
@trash="changeDocumentUrl('')"
/>
</div>
<simple-file-upload
:value="userInput.document"
class="submission-form-container__document"
<file-upload
:document="userInput.document"
v-if="allowsDocuments"
@link-change-url="changeDocumentUrl"
/>
@change-document-url="changeDocumentUrl"/>
<slot/>
</div>
@ -55,8 +45,8 @@
<script>
import SubmissionInput from '@/components/content-blocks/assignment/SubmissionInput';
import FinalSubmission from '@/components/content-blocks/assignment/FinalSubmission';
import SimpleFileUpload from '@/components/SimpleFileUpload';
import DocumentBlock from '@/components/content-blocks/DocumentBlock';
import FileUpload from '@/components/ui/file-upload/FileUpload';
export default {
props: {
@ -68,24 +58,24 @@
document: String,
readOnly: {
type: Boolean,
default: false
default: false,
},
spellcheck: {
type: Boolean,
default: false
default: false,
},
spellcheckLoading: {
type: Boolean,
default: false
default: false,
},
sharedMsg: String
sharedMsg: String,
},
components: {
FileUpload,
SubmissionInput,
FinalSubmission,
SimpleFileUpload,
DocumentBlock
DocumentBlock,
},
computed: {
@ -107,7 +97,7 @@
} else {
return 'Wird geprüft...';
}
}
},
},
methods: {
@ -119,7 +109,7 @@
},
changeDocumentUrl(documentUrl) {
this.$emit('changeDocumentUrl', documentUrl);
}
},
},
};

View File

@ -11,7 +11,15 @@
<script>
export default {
props: ['value', 'index'],
props: {
value: {
type: Object,
default: null,
validator(value) {
return value.hasOwnProperty('text');
}
}
},
computed: {
text() {
@ -22,7 +30,7 @@
</script>
<style scoped lang="scss">
@import "@/styles/_variables.scss";
@import "~styles/helpers";
.text-form {
&__input {

View File

@ -1,8 +1,7 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 100 100"
id="shape">
viewBox="0 0 100 100">
<path
d="M22.64,5.3a8.38,8.38,0,0,0-8.37,8.37V86.33a8.38,8.38,0,0,0,8.37,8.37H77.36a8.38,8.38,0,0,0,8.37-8.37v-57L61.39,5.3ZM79.17,88.08a2.74,2.74,0,0,1-2.11.94H22.34a3,3,0,0,1-3-3V13.37a3,3,0,0,1,3-3H54V28.32a7.78,7.78,0,0,0,7.77,7.77H79.75V85.73A2.85,2.85,0,0,1,79.17,88.08ZM62.11,31a2.34,2.34,0,0,1-2.39-2.39V11.15L79.58,31Z"/>
<path

View File

@ -0,0 +1,37 @@
<template>
<svg
viewBox="0 0 51 51"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<rect
width="51"
height="51"
fill="white"/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M12.185 4C9.87483 4.00276 8.00276 5.87483 8 8.185V44.515C8.00276 46.8252 9.87483 48.6972 12.185 48.7H39.545C41.8552 48.6972 43.7272 46.8252 43.73 44.515V16.015L31.56 4H12.185ZM40.45 45.39C40.1848 45.6945 39.7987 45.8665 39.395 45.86H12.035C11.2066 45.86 10.535 45.1884 10.535 44.36V8.035C10.535 7.20657 11.2066 6.535 12.035 6.535H27.865V15.51C27.8678 17.6545 29.6055 19.3922 31.75 19.395H40.74V44.215C40.8227 44.6303 40.7164 45.0609 40.45 45.39ZM31.0674 16.5076C31.293 16.7332 31.601 16.8569 31.92 16.85H40.655L30.725 6.925V15.655C30.7181 15.974 30.8418 16.282 31.0674 16.5076Z"
fill="#333333"/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M15 26.25C15 25.5596 15.5596 25 16.25 25H35.75C36.4404 25 37 25.5596 37 26.25C37 26.9404 36.4404 27.5 35.75 27.5H16.25C15.5596 27.5 15 26.9404 15 26.25Z"
fill="#333333"/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M15 33.25C15 32.5596 15.5596 32 16.25 32H35.75C36.4404 32 37 32.5596 37 33.25C37 33.9404 36.4404 34.5 35.75 34.5H16.25C15.5596 34.5 15 33.9404 15 33.25Z"
fill="#333333"/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M15 40.25C15 39.5596 15.5596 39 16.25 39H35.75C36.4404 39 37 39.5596 37 40.25C37 40.9404 36.4404 41.5 35.75 41.5H16.25C15.5596 41.5 15 40.9404 15 40.25Z"
fill="#333333"/>
</svg>
</template>
<style scoped lang="scss">
@import '~styles/helpers';
</style>

View File

@ -1,6 +1,6 @@
<template>
<svg
viewBox="16 16 23 23"
viewBox="15 15 20 20"
xmlns="http://www.w3.org/2000/svg">
<path
d="M33.855 23.7749H26.215V16.1349C26.215 15.4584 25.6666 14.9099 24.99 14.9099C24.3135 14.9099 23.765 15.4584 23.765 16.1349V23.7749H16.125C15.4485 23.7749 14.9 24.3234 14.9 24.9999C14.9 25.6765 15.4485 26.2249 16.125 26.2249H23.765V33.8649C23.765 34.5415 24.3135 35.0899 24.99 35.0899C25.6666 35.0899 26.215 34.5415 26.215 33.8649V26.2249H33.855C34.5316 26.2249 35.08 25.6765 35.08 24.9999C35.08 24.3234 34.5316 23.7749 33.855 23.7749Z"

View File

@ -1,234 +1,201 @@
<template>
<svg
width="181"
height="240"
viewBox="0 0 181 240"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M114.005 101.371H118.068V237.6H114.005V101.371Z"
fill="white"/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M155.858 101.371H159.92V237.6H155.858V101.371Z"
fill="white"/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M15.8368 159.598H19.8997V240H15.8368V159.598Z"
fill="white"/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M64.921 157.197H68.9839V237.6H64.921V157.197Z"
fill="white"/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M1.19991 159.597C1.19991 158.961 1.45301 158.35 1.90351 157.9C2.35401 157.45 2.96502 157.197 3.60213 157.197H76.4732C77.1103 157.197 77.7214 157.45 78.1719 157.9C78.6224 158.35 78.8755 158.961 78.8755 159.597V159.598C78.8755 159.913 78.8134 160.225 78.6927 160.516C78.5719 160.808 78.395 161.072 78.1719 161.295C77.9488 161.518 77.684 161.695 77.3925 161.815C77.1011 161.936 76.7887 161.998 76.4732 161.998H3.60213C3.28666 161.998 2.97428 161.936 2.68282 161.815C2.39136 161.695 2.12654 161.518 1.90347 161.295C1.6804 161.072 1.50345 160.807 1.38273 160.516C1.26201 160.225 1.1999 159.913 1.19991 159.598V159.597V159.597Z"
fill="white"/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M92.9249 98.9703C92.9249 98.6551 92.9871 98.343 93.1078 98.0518C93.2285 97.7606 93.4054 97.496 93.6285 97.2731C93.8516 97.0502 94.1164 96.8734 94.4079 96.7528C94.6993 96.6321 95.0117 96.5701 95.3272 96.5701H177.255C177.57 96.5701 177.883 96.6321 178.174 96.7528C178.466 96.8734 178.731 97.0502 178.954 97.2731C179.177 97.496 179.354 97.7606 179.474 98.0518C179.595 98.343 179.657 98.6551 179.657 98.9703V98.9706C179.657 99.2859 179.595 99.598 179.474 99.8892C179.354 100.18 179.177 100.445 178.954 100.668C178.731 100.891 178.466 101.068 178.174 101.188C177.883 101.309 177.57 101.371 177.255 101.371H95.3272C94.6901 101.371 94.079 101.118 93.6285 100.668C93.178 100.218 92.9249 99.6072 92.9249 98.9706V98.9703V98.9703Z"
fill="white"/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M95.2142 94.9064C95.2142 94.6881 95.2573 94.4718 95.3409 94.2701C95.4246 94.0684 95.5472 93.8851 95.7017 93.7307C95.8563 93.5763 96.0398 93.4538 96.2418 93.3703C96.4437 93.2867 96.6602 93.2437 96.8788 93.2437H136.291V96.5692H96.8788C96.4373 96.5692 96.0139 96.394 95.7018 96.0822C95.3896 95.7703 95.2142 95.3474 95.2142 94.9064V94.9064Z"
fill="#FFB800"/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M151.757 58.4911C152.159 58.673 152.473 59.007 152.628 59.4197C152.784 59.8324 152.769 60.29 152.587 60.6919C149.417 67.6931 136.34 96.57 136.34 96.57L133.307 95.1989L138.533 83.6576C138.533 83.6576 147.048 64.8554 149.554 59.3204C149.736 58.9186 150.071 58.6055 150.484 58.45C150.897 58.2944 151.355 58.3091 151.757 58.4908L151.757 58.4911L151.757 58.4911Z"
fill="#FFB800"/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M98.7205 81.8774C99.7849 82.4474 100.963 82.7731 102.169 82.8308C103.376 82.8886 104.58 82.6768 105.694 82.2111C105.746 82.1896 105.679 83.8883 105.681 83.916C105.751 85.0618 106.227 87.0481 107.629 87.2388C108.998 87.4247 109.847 84.5403 110.032 83.7182C110.111 83.3649 110.177 83.0105 110.233 82.6526C110.31 82.1632 110.333 81.7945 110.341 81.8826C110.42 82.7114 110.455 83.5361 110.605 84.3582C110.687 84.8653 110.823 85.3623 111.01 85.8409C111.125 86.1301 111.277 86.4032 111.463 86.6532C112.972 88.6409 114.046 85.5721 114.226 84.3522C114.265 84.0896 114.195 82.8152 114.364 82.6738C114.554 82.5143 114.566 83.1269 114.674 83.3504C114.854 83.7299 115.053 84.0996 115.272 84.458C115.823 85.3477 117.744 87.5934 119.046 86.8781C120.109 86.2942 119.096 82.9414 118.888 82.1243C117.872 78.1243 116.685 72.5217 113.221 69.8203C108.723 66.3113 100.75 68.3095 95.814 69.4932C87.1165 71.5792 66.722 80.4336 66.722 80.4336L72.3178 95.3635C72.3178 95.3635 94.6785 84.0005 98.7205 81.8774V81.8774Z"
fill="#FFA861"/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M71.5765 78.1558L79.0002 96.3235L50.7942 107.83C49.6002 108.318 48.3219 108.565 47.0321 108.559C45.7424 108.553 44.4665 108.293 43.2774 107.794C42.0883 107.295 41.0092 106.566 40.1018 105.651C39.1944 104.735 38.4764 103.649 37.9888 102.456C37.5013 101.263 37.2538 99.9858 37.2604 98.6971C37.267 97.4084 37.5276 96.1336 38.0274 94.9455C38.5271 93.7575 39.2562 92.6795 40.173 91.773C41.0898 90.8665 42.1763 90.1494 43.3704 89.6625L71.5765 78.1558Z"
fill="#074062"/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M27.3945 73.5401C26.509 71.0929 26.6328 68.3944 27.7386 66.0383C28.8443 63.6822 30.8415 61.8616 33.2907 60.9768C35.74 60.0921 38.4406 60.2158 40.7986 61.3206C43.1566 62.4255 44.9787 64.4211 45.8642 66.8683L56.2227 95.4968C57.1066 97.9436 56.9818 100.641 55.8758 102.996C54.7698 105.351 52.7731 107.171 50.3248 108.055C47.8764 108.94 45.1767 108.816 42.8193 107.713C40.4618 106.609 38.6395 104.615 37.753 102.169L27.3945 73.5401V73.5401Z"
fill="#4FA399"/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M126.272 190.871L137.462 177.762L150.294 191.529L157.881 199.669L149.255 208.351L126.272 190.871Z"
fill="#013E62"/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M112.782 145.13C106.979 138.633 84.2473 108.64 68.368 134.169C61.3579 145.439 97.2139 171.669 97.2139 171.669L134.317 199.484L146.999 184.38C146.999 184.38 119.053 152.15 112.782 145.13V145.13Z"
fill="#C64946"/>
<path
d="M103.911 135.348C104.712 135.736 105.446 136.249 106.085 136.868L107.193 138.56C107.209 138.641 107.223 138.723 107.239 138.804C106.215 137.689 105.101 136.52 103.911 135.348Z"
fill="#BE3D3A"/>
<path
d="M90.7722 149.336C91.7376 144.215 97.1561 132.205 103.911 135.348C105.101 136.52 106.215 137.689 107.239 138.804C109.274 149.292 110.577 160.274 109.831 170.947C109.761 171.941 109.236 177.543 108.508 180.136L97.2136 171.668C97.2136 171.668 95.8072 170.639 93.6104 168.905C92.6516 166.77 91.8753 164.557 91.2897 162.29C90.1809 158.061 90.0043 153.641 90.7722 149.336V149.336Z"
fill="#BE3D3A"/>
<path
d="M157.881 199.67C157.881 199.67 158.916 199.061 160.675 198.027C160.79 200.039 160.958 201.97 161.193 203.887C161.207 204 161.262 204.105 161.347 204.181C161.433 204.256 161.543 204.298 161.658 204.298C161.677 204.298 161.696 204.297 161.715 204.295C161.839 204.28 161.951 204.216 162.028 204.118C162.104 204.02 162.139 203.896 162.124 203.773C161.872 201.724 161.698 199.658 161.585 197.492C162.355 197.04 163.222 196.53 164.178 195.968C164.215 196.21 164.254 196.452 164.291 196.694C164.524 198.219 164.766 199.796 165.108 201.334C165.135 201.455 165.209 201.561 165.314 201.627C165.418 201.693 165.545 201.716 165.666 201.689C165.787 201.662 165.893 201.588 165.96 201.484C166.026 201.379 166.049 201.253 166.022 201.131C165.687 199.624 165.449 198.063 165.218 196.552C165.162 196.187 165.104 195.822 165.047 195.458C167.866 193.801 171.306 191.779 174.982 189.618C175.828 189.121 176.828 188.952 177.791 189.145C178.754 189.338 179.612 189.878 180.201 190.663C180.79 191.447 181.069 192.421 180.985 193.399C180.901 194.376 180.46 195.288 179.745 195.961C171.189 204.018 162.673 212.037 158.03 216.408C157.62 216.795 157.124 217.078 156.583 217.236C156.042 217.393 155.471 217.42 154.917 217.315C154.364 217.209 153.843 216.974 153.398 216.628C152.953 216.283 152.596 215.837 152.357 215.327C150.868 212.148 149.128 208.435 149.128 208.435L157.881 199.67Z"
fill="#EDA11F"/>
<path
d="M161.715 204.295C161.696 204.297 161.677 204.298 161.658 204.298C161.543 204.298 161.433 204.256 161.347 204.18C161.262 204.105 161.207 204 161.193 203.887C160.958 201.97 160.79 200.039 160.675 198.027C160.96 197.86 161.263 197.681 161.585 197.492C161.698 199.658 161.872 201.724 162.123 203.773C162.139 203.896 162.104 204.02 162.028 204.118C161.951 204.216 161.839 204.28 161.715 204.295V204.295Z"
fill="white"/>
<path
d="M166.022 201.131C166.036 201.192 166.037 201.254 166.027 201.314C166.016 201.375 165.994 201.433 165.961 201.485C165.928 201.537 165.885 201.582 165.834 201.617C165.784 201.653 165.727 201.678 165.667 201.691C165.606 201.704 165.544 201.706 165.484 201.695C165.423 201.684 165.365 201.662 165.313 201.628C165.261 201.595 165.216 201.552 165.181 201.502C165.146 201.451 165.121 201.394 165.107 201.334C164.765 199.796 164.524 198.219 164.291 196.694C164.254 196.452 164.215 196.21 164.178 195.968C164.459 195.803 164.751 195.631 165.047 195.458C165.104 195.822 165.162 196.187 165.218 196.552C165.449 198.062 165.687 199.624 166.022 201.131Z"
fill="white"/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M71.838 197.409L88.8162 200.426L80.7604 226.061H68.3677L69.7345 214.777L71.838 197.409Z"
fill="#013E62"/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M52.457 119.81L14.7322 111.389L6.93797 109.772C5.67452 112.304 4.37842 116.478 3.12935 119.018C-6.4276 138.458 7.74628 157.678 20.9219 157.197L20.9916 157.197H73.4078L68.8088 203.946L89.3047 208.76C89.3047 208.76 101.761 166.287 104.194 157.197C106.402 148.953 110.697 123.893 94.944 122.399C79.1905 120.905 52.457 119.81 52.457 119.81V119.81Z"
fill="#EF624D"/>
<path
d="M65.7664 233.212C66.9663 229.914 68.3681 226.061 68.3681 226.061H80.7606C80.7606 226.061 82.8909 226.615 86.2979 227.502C84.8747 229.358 83.6146 231.334 82.5318 233.407C82.5033 233.462 82.4858 233.521 82.4803 233.583C82.4748 233.644 82.4814 233.706 82.4998 233.764C82.5182 233.823 82.548 233.877 82.5874 233.925C82.6269 233.972 82.6752 234.011 82.7298 234.039C82.8399 234.097 82.9684 234.108 83.0869 234.071C83.2055 234.034 83.3045 233.951 83.3621 233.841C84.4799 231.698 85.792 229.662 87.2819 227.758C88.1568 227.986 89.099 228.231 90.1011 228.492C89.3174 229.78 88.5651 231.088 87.8342 232.364C87.4917 232.961 87.1477 233.558 86.8024 234.156C86.74 234.263 86.7229 234.391 86.7549 234.511C86.7868 234.631 86.8651 234.734 86.9727 234.796C87.0803 234.858 87.2082 234.875 87.3284 234.844C87.4485 234.812 87.5511 234.733 87.6135 234.626C87.9608 234.028 88.3055 233.428 88.6477 232.829C89.4211 231.479 90.2191 230.093 91.0498 228.739C93.728 229.436 96.7647 230.226 99.9602 231.058C100.91 231.305 101.736 231.893 102.279 232.711C102.823 233.528 103.046 234.516 102.906 235.487C102.766 236.458 102.274 237.344 101.522 237.974C100.77 238.605 99.8118 238.937 98.8305 238.907C85.0615 238.483 72.5392 238.098 72.5392 238.098C72.5392 238.098 70.865 238.046 69.0067 237.989C68.4433 237.972 67.8921 237.821 67.3982 237.549C66.9044 237.278 66.482 236.893 66.1658 236.427C65.8495 235.961 65.6484 235.426 65.579 234.868C65.5096 234.309 65.5738 233.741 65.7664 233.212H65.7664Z"
fill="#FFB800"/>
<path
d="M87.6135 234.626C87.5511 234.733 87.4486 234.812 87.3284 234.844C87.2082 234.875 87.0803 234.858 86.9727 234.796C86.8651 234.734 86.7868 234.631 86.7549 234.511C86.7229 234.391 86.74 234.263 86.8024 234.156C87.1491 233.559 87.493 232.962 87.8342 232.363C88.5651 231.088 89.3174 229.78 90.1011 228.492C90.4115 228.572 90.7288 228.655 91.0498 228.739C90.2191 230.093 89.4211 231.479 88.6477 232.829C88.3042 233.428 87.9595 234.027 87.6135 234.626Z"
fill="white"/>
<path
d="M82.9466 234.093C82.866 234.093 82.7868 234.072 82.7167 234.032C82.6466 233.993 82.5878 233.936 82.5462 233.867C82.5045 233.798 82.4812 233.719 82.4787 233.639C82.4762 233.558 82.4944 233.479 82.5317 233.407C83.6145 231.334 84.8745 229.358 86.2978 227.502C86.615 227.584 86.943 227.67 87.2817 227.758C85.7919 229.662 84.4798 231.698 83.362 233.841C83.3224 233.917 83.2627 233.981 83.1895 234.025C83.1162 234.069 83.0322 234.093 82.9466 234.093V234.093Z"
fill="white"/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M43.3768 46.0062C29.2864 51.8131 11.2146 96.1537 6.93786 109.772C17.302 118.605 38.727 122.988 52.4569 119.81C51.997 115.951 53.2534 77.9939 53.3666 71.1581C53.433 67.1618 54.1158 64.4296 54.012 60.4266"
fill="#004E7C"/>
<path
d="M33.5552 74.8441L52.1852 88.178C52.4362 89.0366 52.6547 89.9065 52.8474 90.7846C52.5113 103.401 52.1857 117.534 52.4569 119.81C44.0004 118.602 42.5996 113.941 33.5552 74.8441Z"
fill="#074062"/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M48.3128 31.966C48.4289 36.9591 45.9808 41.8802 43.3766 46.0062C46.6918 51.6965 49.6933 54.9627 54.0117 60.4266C54.9356 55.2214 56.4239 50.1321 58.4504 45.2488"
fill="#FFB56E"/>
<path
d="M53.0197 46.1663C52.2301 44.4102 51.6113 42.5823 51.1717 40.7079L50.5539 38.5259C51.2269 37.9597 52.1675 37.9814 53.1102 38.2517L58.4504 45.2488C57.6208 47.2143 56.9003 49.2191 56.2652 51.2624C55.7264 50.6315 55.2475 49.875 54.8124 49.2855C54.1023 48.3145 53.5012 47.2685 53.0197 46.1663V46.1663Z"
fill="#FFA861"/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M74.8668 21.4207C75.409 24.2634 75.526 27.1708 75.214 30.0478C75.0485 31.631 74.5001 33.2528 74.5192 34.8404C74.5354 36.1904 74.9291 37.509 75.1546 38.8323C75.2352 39.3053 75.5045 39.989 75.3311 40.4502C74.8583 41.7076 73.0323 41.1874 72.1661 40.7211C71.019 42.9066 69.266 44.7157 67.1169 45.9318C59.086 49.9589 50.2549 41.9785 48.331 34.4943C47.6386 31.8457 47.724 29.0538 48.5771 26.4523C49.4302 23.8509 51.0149 21.5498 53.1418 19.8242C55.1065 18.2113 57.5014 17.3111 59.8793 16.4926C65.2839 14.6317 71.0211 14.5359 73.967 20.0225L74.8668 21.4207Z"
fill="#FFB56E"/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M75.2016 25.914C72.6194 28.0969 67.3001 29.186 62.9355 28.8449C61.3444 28.7206 56.9269 26.583 55.8604 27.2158C54.405 28.0793 53.9833 30.0909 53.719 31.6175C53.0583 35.4333 53.8125 39.3896 53.7272 43.2406C53.641 47.1382 49.5202 46.899 46.888 45.5825C45.6318 44.9706 44.4454 44.2248 43.3496 43.3581C41.7454 42.0663 40.3928 40.4569 39.0512 38.9016C37.0579 36.5909 35.3731 34.197 34.9529 31.1015C33.3422 19.2387 43.8583 13.7828 51.8589 9.6278C56.8102 7.05637 63.9819 7.10256 69.4904 9.65362C75.9835 12.6609 79.5322 22.253 75.2016 25.914H75.2016Z"
fill="#333333"/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M51.0956 1.65645e-09C49.8701 -2.83179e-05 48.672 0.363068 47.653 1.04337C46.634 1.72367 45.8398 2.69061 45.3708 3.82193C44.9018 4.95325 44.7791 6.19813 45.0182 7.39914C45.2573 8.60015 45.8474 9.70335 46.714 10.5692C47.5806 11.4351 48.6847 12.0248 49.8866 12.2637C51.0886 12.5026 52.3345 12.3799 53.4667 11.9113C54.599 11.4427 55.5667 10.6491 56.2475 9.63093C56.9284 8.61276 57.2918 7.41571 57.2917 6.19117C57.2909 4.54943 56.6378 2.97517 55.476 1.81429C54.3142 0.653403 52.7386 0.000848909 51.0956 1.65645e-09Z"
fill="#333333"/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M58.3123 27.1419C56.8595 24.9343 54.1796 24.1276 52.3316 25.3415C50.4835 26.5557 50.1625 29.3336 51.615 31.5415C53.0678 33.749 55.7477 34.5558 57.5957 33.3418C59.4437 32.1279 59.7648 29.3497 58.3123 27.1419V27.1419Z"
fill="#FFB56E"/>
<path
d="M53.2882 37.7759C53.6568 37.7754 54.023 37.7166 54.3731 37.6017C55.1441 37.3764 55.8108 36.8867 56.2561 36.2187C56.7014 35.5506 56.8967 34.7471 56.8075 33.9495C56.7429 33.0192 56.3668 32.1377 55.74 31.4468C55.1132 30.756 54.272 30.2959 53.3518 30.1406C53.2291 30.1212 53.1037 30.1513 53.0032 30.2242C52.9027 30.2972 52.8353 30.407 52.8157 30.5296C52.7962 30.6521 52.8262 30.7774 52.899 30.878C52.9718 30.9785 53.0816 31.0461 53.2043 31.0658C53.9167 31.1881 54.5673 31.5463 55.0513 32.0828C55.5354 32.6192 55.8248 33.3028 55.8731 34.0234C55.9447 34.6113 55.8035 35.2054 55.4751 35.6983C55.1466 36.1913 54.6526 36.5506 54.0821 36.7114C52.7203 37.1556 51.5836 36.3752 51.0241 35.5718C50.4742 34.7823 50.1387 33.4743 51.0019 32.3869C51.0402 32.3386 51.0686 32.2834 51.0854 32.2242C51.1023 32.1651 51.1073 32.1032 51.1003 32.0421C51.0932 31.981 51.0742 31.9218 51.0442 31.8681C51.0143 31.8143 50.9741 31.767 50.9258 31.7288C50.8776 31.6906 50.8223 31.6622 50.7631 31.6454C50.7039 31.6285 50.6419 31.6234 50.5808 31.6305C50.5196 31.6376 50.4604 31.6566 50.4067 31.6865C50.3529 31.7164 50.3055 31.7566 50.2672 31.8048C49.2938 33.0312 49.2888 34.72 50.2547 36.1066C50.5862 36.6105 51.0358 37.0259 51.5645 37.3168C52.0931 37.6077 52.6848 37.7653 53.2882 37.7759V37.7759Z"
fill="white"/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M105.468 86.5124C106.532 87.0826 107.711 87.4084 108.917 87.4662C110.123 87.5239 111.327 87.3121 112.441 86.8462C112.493 86.8251 112.427 88.5237 112.429 88.5514C112.498 89.6972 112.974 91.6833 114.376 91.874C115.745 92.0602 116.594 89.1757 116.779 88.3534C116.859 88.0003 116.924 87.6459 116.981 87.2881C117.058 86.7987 117.08 86.4298 117.089 86.5181C117.167 87.3468 117.202 88.1715 117.352 88.9935C117.435 89.5007 117.571 89.9977 117.758 90.4763C117.873 90.7655 118.025 91.0386 118.21 91.2887C119.72 93.2762 120.793 90.2076 120.974 88.9874C121.013 88.725 120.943 87.4505 121.111 87.3092C121.301 87.1493 121.314 87.7623 121.421 87.9858C121.601 88.3653 121.801 88.735 122.019 89.0935C122.571 89.983 124.491 92.2286 125.794 91.5131C126.857 90.9294 125.843 87.5769 125.636 86.7598C124.619 82.7598 123.432 77.157 119.969 74.4556C115.471 70.9465 107.497 72.9446 102.561 74.1287C93.864 76.2145 73.4695 85.069 73.4695 85.069L79.0656 99.9989C79.0656 99.9989 101.426 88.636 105.468 86.5124V86.5124Z"
fill="#FFB56E"/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M34.1419 78.1757C33.2581 75.7289 33.3828 73.0314 34.4888 70.6765C35.5948 68.3215 37.5915 66.5017 40.0399 65.6173C42.4882 64.7328 45.1879 64.856 47.5454 65.9599C49.9029 67.0637 51.7252 69.0577 52.6117 71.5036L62.9702 100.132C63.8557 102.58 63.7319 105.278 62.6261 107.634C61.5204 109.99 59.5232 111.811 57.0739 112.696C54.6247 113.58 51.924 113.457 49.566 112.352C47.2081 111.247 45.3859 109.251 44.5005 106.804L34.1419 78.1757V78.1757Z"
fill="#004E7C"/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M78.3243 82.791L85.7478 100.959L57.5418 112.466C55.1312 113.447 52.4295 113.431 50.0303 112.423C47.6312 111.414 45.7309 109.495 44.7471 107.087C43.7632 104.68 43.7763 101.98 44.7834 99.5819C45.7905 97.1838 47.7093 95.2833 50.118 94.298L78.3243 82.791V82.791Z"
fill="#004E7C"/>
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 1400 1000">
<defs>
<linearGradient
x1="698.62638"
y1="914.96361"
x2="700.24867"
y2="661.88735"
gradientUnits="userSpaceOnUse"
id="linear-gradient">
<stop
offset="0"
stop-color="#fff"/>
<stop
offset="1"
stop-color="#e6f4f9"/>
</linearGradient>
</defs>
<g id="hintergrund">
<path
class="cls-1"
d="M790.58048,193.00863C668.12186,163.85091,544.06485,204.9364,487.632,322.51394c-18.8724,39.32132-27.20646,84.87125-54.41879,119.85436C388.8747,499.36968,310.57637,508.31035,273.24433,572.437c-18.87234,32.41847-26.26685,70.57793-23.81744,107.81165C258.17366,813.202,396.18153,880.93469,510.86738,904.36176c57.8334,11.8138,120.62074,12.00015,179.52571,15.07359,15.71577.82041,31.49658,1.69709,47.29807,2.4723,116.12768,5.69747,233.37033,5.90715,334.19485-62.20929,22.95567-15.509,47.53642-32.99747,61.01751-57.89225,57.95657-107.02874-37.35371-181.9064-86.2334-264.78937C986.76151,435.43276,1004.73463,292.0828,885.71433,232.19c-28.474-14.32891-95.13385-39.18137-95.13385-39.18137Z"/>
</g>
<g
data-name="Layer 3"
id="Layer_3">
<polygon
class="cls-2"
points="598.832 466.839 585.223 466.839 585.223 923.515 598.832 923.515 598.832 466.839 598.832 466.839"/>
<polygon
class="cls-2"
points="458.646 466.839 445.037 466.839 445.037 923.515 458.646 923.515 458.646 466.839 458.646 466.839"/>
<polygon
class="cls-2"
points="927.653 662.03 914.044 662.03 914.044 931.561 927.653 931.561 927.653 662.03 927.653 662.03"/>
<polygon
class="cls-2"
points="763.242 653.984 749.633 653.984 749.633 923.515 763.242 923.515 763.242 653.984 763.242 653.984"/>
<path
class="cls-2"
d="M976.68117,662.0302a8.04672,8.04672,0,0,0-8.04642-8.04666H724.54887a8.04682,8.04682,0,0,0-8.04655,8.04666v.00073a8.04641,8.04641,0,0,0,8.04655,8.04666H968.63475a8.04631,8.04631,0,0,0,8.04642-8.04666v-.00073Z"/>
<path
class="cls-2"
d="M669.44163,458.7913a8.04641,8.04641,0,0,0-8.04649-8.04643H386.97286a8.04636,8.04636,0,0,0-8.04646,8.04643v.001a8.04638,8.04638,0,0,0,8.04646,8.04643H661.39514a8.04668,8.04668,0,0,0,8.04649-8.04643v-.001Z"/>
<path
class="cls-3"
d="M661.77331,445.16931a5.57476,5.57476,0,0,0-5.57544-5.57508H524.184v11.15064H656.19787a5.57558,5.57558,0,0,0,5.57544-5.57556Z"/>
<path
class="cls-3"
d="M472.38007,323.09415a5.57621,5.57621,0,0,0-2.78129,7.37749c10.62,23.46987,54.42274,120.27323,54.42274,120.27323l10.16-4.59645L516.67522,407.459S488.154,344.42882,479.7588,325.87423a5.57634,5.57634,0,0,0-7.37777-2.781l-.001.001Z"/>
<path
class="cls-4"
d="M650.0296,401.491a27.20991,27.20991,0,0,1-23.35656,1.11857c-.17432-.07189.04725,5.62269.04136,5.71526-.23245,3.8412-1.82694,10.49974-6.52287,11.1391-4.58665.62325-7.42992-9.046-8.0495-11.802-.26594-1.18446-.48553-2.37252-.67458-3.57213-.25909-1.64083-.333-2.87674-.36152-2.58147-.26395,2.7784-.38117,5.5431-.88245,8.2989a24.49793,24.49793,0,0,1-1.35915,4.97059,12.04093,12.04093,0,0,1-1.51477,2.7231c-5.05631,6.66311-8.65218-3.62431-9.25788-7.71365-.13-.88029.10538-5.15261-.45986-5.62654-.63623-.53452-.67771,1.51893-1.03917,2.26817a39.8136,39.8136,0,0,1-2.00319,3.713c-1.84666,2.98254-8.279,10.51056-12.642,8.11279-3.56125-1.95727-.16639-13.19688.52989-15.93584,3.40472-13.40919,7.38074-32.19076,18.98073-41.2466,15.06673-11.76283,41.77384-5.06461,58.30708-1.09646,29.1329,6.99278,97.44548,36.67516,97.44548,36.67516L738.46721,446.7s-74.89846-38.09165-88.43761-45.209Z"/>
<path
class="cls-5"
d="M740.94914,389.01551l-24.86635,60.90317,94.478,38.574a32.8921,32.8921,0,1,0,24.86635-60.90341l-94.478-38.57376Z"/>
<path
class="cls-5"
d="M888.94081,373.54254a32.89212,32.89212,0,0,0-61.86551-22.36572L792.37881,447.147a32.89232,32.89232,0,0,0,61.86557,22.36669l34.69643-95.97116Z"/>
<polygon
class="cls-6"
points="557.746 766.867 520.263 722.922 477.281 769.074 451.868 796.362 480.762 825.467 557.746 766.867 557.746 766.867"/>
<path
class="cls-7"
d="M602.92846,613.53042c19.43962-21.7807,95.58012-122.32258,148.76874-36.74416,23.48064,37.7793-96.621,125.70908-96.621,125.70908L530.79554,795.74,488.31946,745.1089s93.60537-108.04657,114.609-131.57848Z"/>
<path
class="cls-8"
d="M632.64188,580.73872a27.99108,27.99108,0,0,0-7.27979,5.09662l-3.71093,5.671c-.05323.27221-.10279.54663-.15577.819C624.9241,588.58717,628.65677,584.66761,632.64188,580.73872Z"/>
<path
class="cls-9"
d="M751.69705,576.78626c-37.59472-60.48852-86.65454-27.99249-119.05517,3.95246,22.62695-10.53681,40.77636,29.726,44.01,46.89239a101.25512,101.25512,0,0,1-1.7334,43.42633A139.16647,139.16647,0,0,1,667.1453,693.232C696.98441,669.66035,770.533,607.09229,751.69705,576.78626Z"/>
<path
class="cls-8"
d="M676.65189,627.63111c-3.23365-17.16644-21.38306-57.4292-44.01-46.89239-3.98511,3.92889-7.71778,7.84845-11.14649,11.58667-6.81714,35.15851-11.18164,71.97125-8.68164,107.75079.23242,3.33173,1.99146,22.11371,4.43116,30.8031l37.83129-28.384s4.7107-3.45068,12.0691-9.26331a139.16647,139.16647,0,0,0,7.77319-22.17456A101.25512,101.25512,0,0,0,676.65189,627.63111Z"/>
<path
class="cls-10"
d="M451.868,796.36231s-3.467-2.03955-9.35986-5.50585c-.38306,6.74377-.94653,13.21826-1.73389,19.644a1.57086,1.57086,0,0,1-1.55664,1.3789,1.63045,1.63045,0,0,1-.19336-.01171,1.57036,1.57036,0,0,1-1.36719-1.75c.84253-6.86707,1.426-13.79248,1.80286-21.0542-2.57727-1.51612-5.48376-3.22583-8.68408-5.1084-.12512.81079-.25549,1.62012-.37952,2.43115-.78125,5.11328-1.58886,10.40137-2.73437,15.55664a1.56947,1.56947,0,0,1-3.06446-.67969c1.12207-5.05371,1.92188-10.28808,2.69532-15.35156.187-1.22388.381-2.446.57226-3.66876-9.44311-5.55475-20.96582-12.33276-33.27807-19.57514A13.37562,13.37562,0,0,0,378.63187,783.931c28.66015,27.00855,57.1853,53.89026,72.73657,68.54419a11.93871,11.93871,0,0,0,19.00147-3.62622c4.98852-10.65466,10.81591-23.10169,10.81591-23.10169Z"/>
<path
class="cls-11"
d="M439.0242,811.86769a1.63045,1.63045,0,0,0,.19336.01171,1.57086,1.57086,0,0,0,1.55664-1.3789c.78736-6.42578,1.35083-12.90027,1.73389-19.644q-1.43041-.84156-3.04822-1.793c-.37683,7.26172-.96033,14.18713-1.80286,21.0542A1.57036,1.57036,0,0,0,439.0242,811.86769Z"/>
<path
class="cls-11"
d="M424.59744,801.26319a1.56947,1.56947,0,1,0,3.06446.67969c1.14551-5.15527,1.95312-10.44336,2.73437-15.55664.124-.811.2544-1.62036.37952-2.43115-.94348-.55506-1.91968-1.12921-2.91077-1.71222-.19128,1.22272-.38525,2.44488-.57226,3.66876C426.51932,790.97511,425.71951,796.20948,424.59744,801.26319Z"/>
<polygon
class="cls-6"
points="740.073 788.785 683.204 798.898 710.187 884.833 751.697 884.833 747.119 847.008 740.073 788.785 740.073 788.785"/>
<path
class="cls-12"
d="M804.99223,528.65028l126.36166-28.2279,26.10737-5.42168c4.232,8.48886,8.57338,22.4809,12.75721,30.99717,32.01163,65.168-15.46474,129.59909-59.5974,127.9847l-.23342.001H734.81627L750.2209,810.6994l-68.6524,16.13734S639.84689,684.45388,631.695,653.98354c-7.39355-27.63784-21.78269-111.64443,30.98442-116.65253,52.76717-5.00906,142.31284-8.68073,142.31284-8.68073Z"/>
<path
class="cls-13"
d="M760.41141,908.80543c-4.01929-11.05615-8.71436-23.9729-8.71436-23.9729H710.18729s-7.1355,1.85864-18.54737,4.8313a136.08924,136.08924,0,0,1,12.61475,19.79663,1.56944,1.56944,0,1,1-2.78125,1.45508A132.57079,132.57079,0,0,0,688.344,890.52235c-2.93067.76343-6.08643,1.58557-9.44312,2.45984,2.625,4.31726,5.145,8.70447,7.593,12.97925q1.72119,3.00439,3.45605,6.00781a1.57045,1.57045,0,0,1-2.71679,1.57617q-1.74464-3.00879-3.46387-6.02441c-2.59082-4.5232-5.26367-9.16993-8.04614-13.71106-8.97071,2.33679-19.14258,4.9862-29.84595,7.77429a13.37594,13.37594,0,0,0,3.78393,26.31347c46.12012-1.42126,88.06446-2.71435,88.06446-2.71435s5.60791-.17249,11.83227-.3645a11.93939,11.93939,0,0,0,10.85352-16.01343Z"/>
<path
class="cls-11"
d="M687.23319,913.54542a1.57045,1.57045,0,0,0,2.71679-1.57617q-1.74169-3-3.45605-6.00781c-2.448-4.27478-4.968-8.662-7.593-12.97925-1.03955.27075-2.10254.5476-3.17773.82776,2.78247,4.54113,5.45532,9.18786,8.04614,13.71106Q685.49491,910.53418,687.23319,913.54542Z"/>
<path
class="cls-11"
d="M702.865,911.75831a1.571,1.571,0,0,0,1.38965-2.29785,136.08924,136.08924,0,0,0-12.61475-19.79663q-1.59375.41528-3.29589.85852a132.57079,132.57079,0,0,1,13.12939,20.39319A1.56853,1.56853,0,0,0,702.865,911.75831Z"/>
<path
class="cls-14"
d="M835.40642,281.24137C882.60314,300.70751,943.13608,449.3493,957.46126,495.0007c-34.71525,29.61049-106.47985,44.30585-152.469,33.64958,1.54038-12.93671-2.668-140.1777-3.04723-163.09309-.22254-13.39645-2.50946-22.55568-2.16183-35.97473"/>
<path
class="cls-15"
d="M868.30448,377.91365l-62.40235,44.699c-.84057,2.87817-1.57251,5.79425-2.218,8.738,1.12574,42.29352,2.21656,89.67065,1.30811,97.29956C833.31766,524.60322,838.00955,508.978,868.30448,377.91365Z"/>
<path
class="cls-16"
d="M818.87222,234.17468c-.389,16.73822,7.811,33.235,16.5342,47.06669-11.10459,19.07516-21.1583,30.02455-35.62325,48.34109a244.13134,244.13134,0,0,0-14.86782-50.88024"/>
<path
class="cls-17"
d="M803.10623,281.778a100.72064,100.72064,0,0,0,6.18994-18.29821l2.06934-7.31452c-2.25439-1.89813-5.40478-1.82531-8.5625-.91913l-17.88769,23.45606c2.7788,6.589,5.19238,13.30969,7.31958,20.15936,1.80493-2.115,3.40893-4.65112,4.86645-6.62707A55.06214,55.06214,0,0,0,803.10623,281.778Z"/>
<path
class="cls-16"
d="M729.92828,198.82416a98.05578,98.05578,0,0,0-1.16312,28.9204c.55448,5.30721,2.39128,10.74379,2.32726,16.06616-.05416,4.52552-1.37291,8.94571-2.12829,13.38178-.26984,1.58553-1.17207,3.8775-.591,5.4236,1.58366,4.21533,7.69982,2.47134,10.60127.90818A43.05429,43.05429,0,0,0,755.88687,280.992c26.90013,13.49984,56.48015-13.25242,62.92433-38.34148a47.79058,47.79058,0,0,0-16.11377-49.17833c-6.581-5.407-14.60279-8.42466-22.56764-11.16844-18.10314-6.23824-37.32017-6.55948-47.18777,11.83328Z"/>
<path
class="cls-6"
d="M728.80748,213.88693c8.64924,7.31786,26.46678,10.96862,41.0864,9.82527,5.32922-.4167,20.12617-7.58259,23.6983-5.4611,4.87519,2.89454,6.28753,9.638,7.17293,14.75547,2.213,12.79171-.31319,26.05423-.02759,38.964.2886,13.06559,14.09177,12.26393,22.90848,7.85046a64.97755,64.97755,0,0,0,11.852-7.4566c5.37358-4.33051,9.904-9.72573,14.398-14.93965,6.67659-7.74587,12.31989-15.7709,13.72731-26.14777,5.39528-39.76735-29.82918-58.057-56.62784-71.98576-16.58452-8.62014-40.60682-8.46529-59.05765.08656-21.74921,10.08112-33.63575,42.23653-19.13041,54.50912Z"/>
<path
class="cls-6"
d="M809.55225,127.01633a20.75495,20.75495,0,1,1-20.75447,20.75447,20.76521,20.76521,0,0,1,20.75447-20.75447Z"/>
<path
class="cls-16"
d="M785.37924,218.003c4.86629-7.40058,13.84254-10.10468,20.03257-6.03554,6.19,4.07058,7.26545,13.38274,2.40018,20.78428-4.86636,7.4001-13.84261,10.10468-20.03258,6.03506-6.19-4.06938-7.2655-13.3825-2.40017-20.7838Z"/>
<path
class="cls-11"
d="M802.20682,253.65089a11.68174,11.68174,0,0,1-3.63379-.584,11.43407,11.43407,0,0,1-8.1543-12.24316,13.92838,13.92838,0,0,1,11.5752-12.76855,1.57034,1.57034,0,0,1,.49414,3.10156,10.79156,10.79156,0,0,0-8.93945,9.915,8.31845,8.31845,0,0,0,5.999,9.01074c4.56152,1.48926,8.36914-1.12695,10.24316-3.82031,1.8418-2.64649,2.96582-7.03125.07422-10.67676a1.57029,1.57029,0,1,1,2.46094-1.95117c3.26074,4.11133,3.27734,9.77246.042,14.4209A12.42172,12.42172,0,0,1,802.20682,253.65089Z"/>
<path
class="cls-16"
d="M627.42842,417.02917a27.20058,27.20058,0,0,1-23.3565,1.11882c-.17427-.07069.04635,5.62365.04136,5.71646-.23246,3.841-1.827,10.49878-6.52294,11.13814-4.58653.62421-7.42991-9.0455-8.04943-11.80178-.26594-1.184-.48553-2.3718-.67561-3.57117-.259-1.64083-.33194-2.87723-.36049-2.58147-.264,2.77816-.38118,5.54262-.88239,8.29842a24.52342,24.52342,0,0,1-1.35915,4.97059,12.06235,12.06235,0,0,1-1.51478,2.72334c-5.05636,6.66263-8.65224-3.62407-9.25787-7.71461-.13009-.87957.10435-5.15189-.46-5.62558-.63623-.536-.67861,1.51869-1.03911,2.26817a40.26336,40.26336,0,0,1-2.00325,3.71328c-1.84666,2.98182-8.27894,10.5096-12.64288,8.1111-3.5604-1.95678-.16555-13.19518.52984-15.93439,3.40567-13.40919,7.38068-32.191,18.98066-41.24684,15.06776-11.76355,41.77487-5.06533,58.30811-1.096,29.13289,6.9923,97.44542,36.67492,97.44542,36.67492l-18.7443,50.04876s-74.89744-38.09117-88.43671-45.21018Z"/>
<path
class="cls-14"
d="M866.33975,389.08236a32.89232,32.89232,0,0,0-61.86557-22.36669l-34.6965,95.97092a32.89212,32.89212,0,0,0,61.86551,22.36572l34.69656-95.96995Z"/>
<path
class="cls-14"
d="M718.34711,404.55389,693.48167,465.458l94.478,38.574A32.8921,32.8921,0,0,0,812.826,443.12861l-94.47887-38.57472Z"/>
<path
class="cls-18"
d="M1075.85037,928.90772h-.002l-717.04687-.68945a1.56445,1.56445,0,0,1,.00195-3.1289h.002l717.04688.68945a1.56445,1.56445,0,0,1-.002,3.1289Z"/>
</g>
</svg>
</template>
<style scoped lang="scss">
.cls-1 {
fill: none;
clip-rule: evenodd;
}
.cls-11, .cls-12, .cls-14, .cls-15, .cls-17, .cls-2, .cls-3, .cls-5, .cls-6, .cls-7, .cls-8, .cls-9 {
.cls-1, .cls-12, .cls-14, .cls-16, .cls-2, .cls-3, .cls-4, .cls-5, .cls-6, .cls-7 {
fill-rule: evenodd;
}
.cls-2 {
.cls-1 {
fill: url(#linear-gradient);
}
.cls-13, .cls-3 {
.cls-11, .cls-2 {
fill: #fff;
}
.cls-4, .cls-8 {
fill: #013e62;
}
.cls-5 {
.cls-13, .cls-3 {
fill: #ffb800;
}
.cls-6 {
.cls-17, .cls-4 {
fill: #ffa861;
}
.cls-7 {
.cls-15, .cls-5 {
fill: #4fa399;
}
.cls-9 {
.cls-18, .cls-6 {
fill: #013e62;
}
.cls-7 {
fill: #c64946;
}
.cls-10 {
clip-path: url(#clip-path);
}
.cls-11 {
.cls-8 {
fill: #be3d3a;
}
.cls-12 {
.cls-9 {
fill: none;
}
.cls-10 {
fill: #eda11f;
}
.cls-14 {
.cls-12 {
fill: #ef624d;
}
.cls-15 {
.cls-14 {
fill: #60bea2;
}
.cls-16 {
clip-path: url(#clip-path-2);
}
.cls-17 {
fill: #ffb56e;
}
.cls-18 {
clip-path: url(#clip-path-3);
}
</style>

File diff suppressed because one or more lines are too long

View File

@ -24,8 +24,7 @@
</script>
<style scoped lang="scss">
@import "@/styles/_variables.scss";
@import "@/styles/_mixins.scss";
@import "~styles/helpers";
.page-form {
width: 710px;
@ -41,6 +40,8 @@
}
grid-template-rows: 1fr 55px;
@include widget-shadow;
margin: $large-spacing 0;
&__page {

View File

@ -1,15 +0,0 @@
<template>
<add-widget
route="/new-project"
data-cy="add-project-button"/>
</template>
<script>
import AddWidget from '@/components/AddWidget';
export default {
components: {
AddWidget
}
};
</script>

View File

@ -1,18 +1,17 @@
<template>
<add-widget
:reverse="true"
@click="addProjectEntry"/>
<a
class="add-project-entry"
@click="addProjectEntry">
<plus-icon class="add-project-entry__icon" />
<span class="add-project-entry__text">Beitrag erfassen</span>
</a>
</template>
<script>
import AddWidget from '@/components/AddWidget';
import PlusIcon from '@/components/icons/PlusIcon';
export default {
props: ['project'],
components: {
AddWidget
},
components: {PlusIcon},
methods: {
addProjectEntry() {
@ -21,3 +20,28 @@
}
};
</script>
<style lang="scss" scoped>
@import "~styles/helpers";
.add-project-entry {
display: flex;
align-items: center;
justify-content: center;
border: 2px solid $color-brand;
border-radius: $default-border-radius;
cursor: pointer;
&__icon {
width: 20px;
height: 20px;
fill: $color-brand;
margin-right: $small-spacing;
}
&__text {
@include navigation-link;
color: $color-brand;
}
}
</style>

View File

@ -0,0 +1,26 @@
<template>
<router-link
:to="createProjectRoute"
class="button button--primary"
data-cy="add-project-button">Projekt erstellen
</router-link>
</template>
<script>
import {NEW_PROJECT_PAGE} from '@/router/portfolio.names';
export default {
data() {
return {
createProjectRoute: {
name: NEW_PROJECT_PAGE,
},
};
},
};
</script>
<style scoped lang="scss">
@import '~styles/helpers';
</style>

View File

@ -27,7 +27,6 @@
id: project.id,
title: project.title,
description: project.description,
appearance: project.appearance,
objectives: project.objectives
}
}

View File

@ -19,9 +19,7 @@
data() {
return {
projectEntry: {
activity: '',
reflection: '',
nextSteps: '',
description: '',
documentUrl: ''
}
};
@ -52,10 +50,7 @@
const variables = {slug: this.slug};
const data = store.readQuery({query, variables});
if (data.project && data.project.entries) {
data.project.entries.edges.push({
node: projectEntry,
__typename: 'ProjectEntryNode'
});
data.project.entries.push(projectEntry);
store.writeQuery({query, variables, data});
}
}

View File

@ -3,7 +3,7 @@
<avatar
:avatar-url="owner.avatarUrl"
class="owner-widget__avatar" />
<span>
<span data-cy="owner-name">
{{ name }}
</span>
</div>

View File

@ -0,0 +1,52 @@
<template>
<div class="portfolio-onboarding">
<h1
class="portfolio-onboarding__heading"
data-cy="page-title">Portfolio</h1>
<portfolio-illustration
data-cy="portfolio-onboarding-illustration"
class="portfolio-onboarding__illustration" />
<h2
class="portfolio-onboarding__subheading"
data-cy="portfolio-onboarding-subtitle">Woran denken Sie gerade?</h2>
<p
class="portfolio-onboarding__text"
data-cy="portfolio-onboarding-text">
Hier können Sie Projekte erstellen, um Ihre Gedanken festzuhalten oder Ihre Arbeit zu dokumentieren.
</p>
<create-project-button />
</div>
</template>
<script>
import PortfolioIllustration from '@/components/illustrations/PortfolioIllustration';
import CreateProjectButton from '@/components/portfolio/CreateProjectButton';
export default {
components: {CreateProjectButton, PortfolioIllustration},
};
</script>
<style scoped lang="scss">
@import '~styles/helpers';
.portfolio-onboarding {
@include onboarding-page;
&__heading {
@include heading-1;
}
&__subheading {
@include heading-2;
}
&__illustration {
@include onboarding-illustration;
}
&__text {
@include onboarding-text;
}
}
</style>

View File

@ -16,21 +16,13 @@
</li>
</more-options-widget>
<h3 class="project-entry__heading">Tätigkeit</h3>
<h3
class="project-entry__heading"
data-cy="project-entry-date">{{ created | datetime }}</h3>
<p
class="project-entry__paragraph"
data-cy="project-entry-activity">
{{ activity }}
</p>
<h3 class="project-entry__heading">Reflexion</h3>
<p class="project-entry__paragraph">
{{ reflection }}
</p>
<h3 class="project-entry__heading">
Nächste Schritte
</h3>
<p class="project-entry__paragraph">
{{ nextSteps }}
{{ description }}
</p>
<p
class="project-entry__paragraph"
@ -51,7 +43,7 @@
import PROJECT_QUERY from '@/graphql/gql/queries/projectQuery.gql';
export default {
props: ['activity', 'reflection', 'nextSteps', 'documentUrl', 'created', 'id', 'readOnly'],
props: ['description', 'documentUrl', 'created', 'id', 'readOnly'],
components: {
DocumentBlock,
MoreOptionsWidget
@ -78,7 +70,7 @@
};
const data = store.readQuery({query, variables});
if (data) {
data.project.entries.edges.splice(data.project.entries.edges.findIndex(edge => edge.node.id === projectEntry.id), 1);
data.project.entries.splice(data.project.entries.findIndex(entry => entry.id === projectEntry.id), 1);
store.writeQuery({query, variables, data});
}
}
@ -105,6 +97,7 @@
&__paragraph {
margin-bottom: 30px;
white-space: pre-line;
}
&__date {

View File

@ -1,28 +1,34 @@
<template>
<modal :hide-header="true">
<modal :hide-header="false">
<h2
class="project-entry-modal__heading"
data-cy="modal-title"
slot="header">
Beitrag erfassen
</h2>
<div class="project-entry-modal">
<text-form-with-help-text
:value="localProjectEntry.activity"
title="Tätigkeit"
data-cy="activity-input"
help-text="Was? Wie? Mittel?"
@change="localProjectEntry.activity = $event"/>
<text-form-with-help-text
:value="localProjectEntry.reflection"
title="Reflexion"
data-cy="reflection-input"
help-text="Nachdenken über die eigene Tätigkeit und das eigene Handeln. Was ging gut? Was hatte ich für Schwierigkeiten? Was habe ich gelernt?"
@change="localProjectEntry.reflection = $event"/>
<text-form-with-help-text
:value="localProjectEntry.nextSteps"
title="Nächste Schritte"
data-cy="next-steps-input"
help-text="Wie geht es weiter? Wer macht was?"
@change="localProjectEntry.nextSteps = $event"/>
<document-form
:value="document"
:index="0"
@link-change-url="setDocumentUrl"/>
<div class="project-entry-modal__form-field">
<textarea
:value="localProjectEntry.description"
class="project-entry-modal__textarea"
data-cy="project-entry-textarea"
@input="localProjectEntry.description = $event.target.value"
/>
<div class="project-entry-modal__buttons">
<button-with-icon-and-text
icon="document-with-lines-icon"
data-cy="use-template-button"
text="Vorlage nutzen"
@click.native="useTemplate" />
<file-upload
:with-text="true"
:document="localProjectEntry.documentUrl"
data-cy="upload-document-button"
@change-document-url="setDocumentUrl" />
</div>
</div>
</div>
<div slot="footer">
<a
@ -40,36 +46,98 @@
import Modal from '@/components/Modal';
import TextFormWithHelpText from '@/components/content-forms/TextFormWithHelpText';
import DocumentForm from '@/components/content-forms/DocumentForm';
import TextForm from '@/components/content-forms/TextForm';
import DocumentIcon from '@/components/icons/DocumentIcon';
import NoteIcon from '@/components/icons/NoteIcon';
import ButtonWithIconAndText from '@/components/ui/ButtonWithIconAndText';
import SimpleFileUpload from '@/components/ui/file-upload/SimpleFileUpload';
import FileUpload from '@/components/ui/file-upload/FileUpload';
import {PROJECT_ENTRY_TEMPLATE} from '@/consts/strings.consts';
export default {
props: ['project-entry'],
props: {
projectEntry: {
type: Object,
default: null,
},
},
components: {
FileUpload,
SimpleFileUpload,
ButtonWithIconAndText,
NoteIcon,
DocumentIcon,
TextForm,
DocumentForm,
Modal,
TextFormWithHelpText
TextFormWithHelpText,
},
data() {
return {
localProjectEntry: Object.assign({}, {
...this.projectEntry
})
...this.projectEntry,
}),
};
},
computed: {
document() {
return this.localProjectEntry.documentUrl > '' ? {
url: this.localProjectEntry.documentUrl
} : {};
}
},
methods: {
setDocumentUrl(url) {
this.localProjectEntry.documentUrl = url;
},
}
useTemplate() {
this.localProjectEntry.description = `${this.localProjectEntry.description}${PROJECT_ENTRY_TEMPLATE}`;
},
},
};
</script>
<style lang="scss" scoped>
@import "~styles/helpers";
.project-entry-modal {
display: flex;
flex-direction: column;
&__form-field {
@include inputstyle;
padding: 0;
display: flex;
flex-direction: column;
grid-template-rows: auto 1rem;
grid-template-columns: 1fr 1fr;
width: 100%;
}
&__textarea {
@include auto-grow;
border: 0;
min-height: 400px;
padding: $medium-spacing;
}
&__buttons {
display: grid;
grid-template-columns: 1fr 1fr;
padding: $medium-spacing;
}
&__button {
@include regular-text;
&--template {
}
&--document {
}
}
&__heading {
@include heading-3;
margin-bottom: 0;
}
}
</style>

View File

@ -9,14 +9,6 @@
v-model="localProject.description"
label="Beschreibung"
type="textarea"/>
<page-form-input
v-model="localProject.objectives"
label="Ziele"
type="textarea"/>
<color-chooser
:selected-color="localProject.appearance"
@input="updateColor"
/>
<template slot="footer">
<button
:class="{'button--disabled': !formValid}"
@ -56,23 +48,16 @@
computed: {
formValid() {
return this.localProject.title && this.localProject.description && this.localProject.objectives;
}
},
created() {
this.$store.dispatch('setSpecialContainerClass', this.localProject.appearance);
},
beforeDestroy() {
this.$store.dispatch('setSpecialContainerClass', '');
},
methods: {
updateColor(newColor) {
this.localProject.appearance = newColor;
this.$store.dispatch('setSpecialContainerClass', newColor);
return this.localProject.title;
}
},
};
</script>
<style scoped lang="scss">
@import "~styles/helpers";
.project-form {
@include widget-shadow;
}
</style>

View File

@ -0,0 +1,40 @@
<template>
<ul
class="project-list"
data-cy="project-list">
<project-list-item
:key="project.id"
:project="project"
data-cy="project"
class="project-list__item"
v-for="project in projects"/>
</ul>
</template>
<script>
import OwnerWidget from '@/components/portfolio/OwnerWidget';
import EntryCountWidget from '@/components/rooms/EntryCountWidget';
import MoreActions from '@/components/rooms/MoreActions';
import ProjectActions from '@/components/portfolio/ProjectActions';
import ProjectListItem from '@/components/portfolio/ProjectListItem';
export default {
props: {
projects: {
type: Array,
default: () => ([]),
},
},
components: {ProjectListItem, MoreActions, EntryCountWidget, OwnerWidget, ProjectActions},
};
</script>
<style scoped lang="scss">
@import '~styles/helpers';
.project-list {
border-top: 1px solid $color-silver;
}
</style>

View File

@ -0,0 +1,88 @@
<template>
<li
class="project">
<router-link
:to="{name: 'project', params: {slug: project.slug}}"
tag="div"
class="project__link"
data-cy="project-link">
<span
class="project__title"
data-cy="project-title">{{ project.title }}</span>
<owner-widget
:owner="project.student"
class="project__owner"/>
<entry-count-widget
:verbose="false"
:entry-count="project.entriesCount"/>
</router-link>
<project-actions
:id="project.id"
:final="project.final"
data-cy="project-actions"
class="project__actions"
v-if="!isReadOnly && isOwner"/>
</li>
</template>
<script>
import OwnerWidget from '@/components/portfolio/OwnerWidget';
import ProjectActions from '@/components/portfolio/ProjectActions';
import EntryCountWidget from '@/components/rooms/EntryCountWidget';
import me from '@/mixins/me';
export default {
props: {
project: {
type: Object,
default: () => ({}),
}
},
mixins: [me],
components: {
EntryCountWidget,
OwnerWidget,
ProjectActions,
},
computed: {
isOwner() {
return this.project.student.id === this.me.id;
},
},
};
</script>
<style scoped lang="scss">
@import "~styles/helpers";
.project {
display: flex;
justify-content: flex-start;
align-items: center;
position: relative;
padding: $small-spacing 0;
border-bottom: 1px solid $color-silver;
&__link {
flex: 80%;
display: flex;
align-items: center;
}
&__title {
flex: 50%;
@include heading-4;
}
&__owner {
flex: 40%;
}
}
</style>

View File

@ -1,7 +1,7 @@
<template>
<div class="entry-count-widget">
<cards/>
<span>{{ entryCount }} {{ entryCount === 1 ? 'Beitrag' : 'Beiträge' }}</span>
<span data-cy="entry-count">{{ entryCount }} <template v-if="verbose">{{ entryCount === 1 ? 'Beitrag' : 'Beiträge' }}</template></span>
</div>
</template>
@ -9,11 +9,19 @@
import Cards from '@/components/icons/Cards.vue';
export default {
props: ['entryCount'],
props: {
entryCount: {
type: Number,
},
verbose: {
type: Boolean,
default: true,
},
},
components: {
Cards
}
Cards,
},
};
</script>

View File

@ -47,31 +47,18 @@
@import '~styles/helpers';
.rooms-onboarding {
display: flex;
width: 100vw;
max-width: 800px;
flex-direction: column;
justify-self: center;
margin: 0 auto;
grid-column: 1 / -1;
align-items: center;
@include onboarding-page;
&__heading {
margin-bottom: $large-spacing;
}
&__illustration {
width: 400px;
height: 320px;
flex-shrink: 0;
margin-bottom: $large-spacing;
@include onboarding-illustration;
}
&__text {
@include regular-text;
text-align: center;
max-width: 500px;
margin-bottom: $large-spacing;
@include onboarding-text;
}
}
</style>

View File

@ -0,0 +1,51 @@
<template>
<a class="button-with-icon-and-text">
<component
:is="icon"
class="button-with-icon-and-text__icon"/>
<span class="button-with-icon-and-text__text">{{ text }}</span>
</a>
</template>
<script>
import DocumentIcon from '@/components/icons/DocumentIcon';
import DocumentWithLinesIcon from '@/components/icons/DocumentWithLinesIcon';
export default {
props: {
icon: {
type: String,
default: ''
},
text: {
type: String,
default: ''
}
},
components: {
DocumentIcon,
DocumentWithLinesIcon
}
};
</script>
<style scoped lang="scss">
@import '~styles/helpers';
.button-with-icon-and-text {
display: flex;
align-items: center;
cursor: pointer;
&__icon {
width: 24px;
height: 24px;
margin-right: $small-spacing;
}
&__text {
@include small-text;
}
}
</style>

View File

@ -0,0 +1,45 @@
<template>
<div class="file-upload">
<template v-if="document">
<document-block
:value="{url: document}"
show-trash-icon
@trash="$emit('change-document-url', '')"
/>
</template>
<template v-else>
<simple-file-upload
:with-text="withText"
:value="document"
@link-change-url="$emit('change-document-url', $event)"
/>
</template>
</div>
</template>
<script>
import DocumentBlock from '@/components/content-blocks/DocumentBlock';
import SimpleFileUpload from '@/components/ui/file-upload/SimpleFileUpload';
export default {
props: {
document: {
type: String,
default: '',
},
withText: {
type: Boolean,
default: false
}
},
components: {SimpleFileUpload, DocumentBlock},
};
</script>
<style scoped lang="scss">
@import '~styles/helpers';
</style>

View File

@ -0,0 +1,71 @@
<template>
<div class="simple-file-upload">
<component
:is="button"
@click.native="clickUploadCare" />
<simple-file-upload-hidden-input @link-change-url="$emit('link-change-url', $event)" />
</div>
</template>
<script>
import DocumentIcon from '@/components/icons/DocumentIcon';
import SimpleFileUploadHiddenInput from '@/components/ui/file-upload/SimpleFileUploadHiddenInput';
import SimpleFileUploadIcon from '@/components/ui/file-upload/SimpleFileUploadIcon';
import SimpleFileUploadIconAndText from '@/components/ui/file-upload/SimpleFileUploadIconAndText';
export default {
props: {
value: {
type: String,
default: ''
},
withText: {
type: Boolean,
default: false
}
},
components: {
SimpleFileUploadHiddenInput,
DocumentIcon,
SimpleFileUploadIcon,
SimpleFileUploadIconAndText
},
computed: {
button() {
return this.withText ? 'simple-file-upload-icon-and-text' : 'simple-file-upload-icon';
}
},
methods: {
clickUploadCare() {
// workaround for styling the uploadcare widget
let button = this.$el.querySelector('.uploadcare--widget__button');
button.click();
}
},
};
</script>
<style scoped lang="scss">
@import "~styles/_helpers";
.simple-file-upload {
height: 25px;
overflow: hidden;
cursor: pointer;
&__link {
display: inline-block;
overflow: hidden;
width: 25px;
height: 25px;
}
}
/deep/ .uploadcare--widget {
display: none;
}
</style>

View File

@ -0,0 +1,27 @@
<template>
<input
type="hidden"
class="simple-file-upload-hidden-input"
role="uploadcare-uploader"
name="file-upload"
data-system-dialog
ref="uploadcare-filedialog">
</template>
<script>
import uploadcareWidget from 'uploadcare-widget';
export default {
mounted() {
let widget = uploadcareWidget.Widget('[role=uploadcare-uploader]');
widget.onChange(result => {
result.done(fileInfo => {
let urlWithFilename = fileInfo.cdnUrl + fileInfo.name;
this.$emit('link-change-url', urlWithFilename);
});
});
},
};
</script>

View File

@ -0,0 +1,24 @@
<template>
<a class="simple-file-upload-icon">
<document-icon class="simple-file-upload-icon__icon"/>
</a>
</template>
<script>
import DocumentIcon from '@/components/icons/DocumentIcon';
export default {
components: {DocumentIcon},
};
</script>
<style scoped lang="scss">
@import '~styles/helpers';
.simple-file-upload-icon {
&__icon {
width: 25px;
fill: $color-silver-dark;
}
}
</style>

View File

@ -0,0 +1,13 @@
<template>
<button-with-icon-and-text
icon="document-icon"
text="Dokument hochladen"
/>
</template>
<script>
import ButtonWithIconAndText from '@/components/ui/ButtonWithIconAndText';
export default {
components: {ButtonWithIconAndText}
};
</script>

View File

@ -1,41 +1,27 @@
<template>
<div class="simple-file-upload">
<a
class="simple-file-upload__link"
<button-with-icon-and-text
icon="document-icon"
text="Dokument hochladen"
v-if="!value"
@click="clickUploadCare">
<document-icon class="simple-file-upload__icon"/>
</a>
<input
type="hidden"
role="uploadcare-uploader"
name="file-upload"
data-system-dialog
ref="uploadcare-filedialog">
@click.native="clickUploadCare"/>
<simple-file-upload-hidden-input @link-change-url="$emit('link-change-url', $event)"/>
</div>
</template>
<script>
import uploadcareWidget from 'uploadcare-widget';
import DocumentIcon from '@/components/icons/DocumentIcon';
import SimpleFileUploadHiddenInput from '@/components/ui/file-upload/SimpleFileUploadHiddenInput';
import ButtonWithIconAndText from '@/components/ui/ButtonWithIconAndText';
export default {
props: ['value'],
components: {
DocumentIcon
},
mounted() {
let widget = uploadcareWidget.Widget('[role=uploadcare-uploader]');
widget.onChange(result => {
result.done(fileInfo => {
let urlWithFilename = fileInfo.cdnUrl + fileInfo.name;
this.$emit('link-change-url', urlWithFilename);
});
});
ButtonWithIconAndText,
SimpleFileUploadHiddenInput,
DocumentIcon,
},
methods: {
@ -43,13 +29,13 @@
// workaround for styling the uploadcare widget
let button = this.$el.querySelector('.uploadcare--widget__button');
button.click();
}
},
},
};
</script>
<style scoped lang="scss">
@import "@/styles/_variables.scss";
@import "~styles/_helpers";
.simple-file-upload {
width: 25px;

View File

@ -0,0 +1,10 @@
export const PROJECT_ENTRY_TEMPLATE = `Tätigkeit:
Was? Wie? Welche Mittel?
Reflexion:
Was ging gut? Was hatte ich für Schwierigkeiten? Was habe ich gelernt?
Nächste Schritte:
Wie geht es weiter? Wer macht was?`;

View File

@ -10,6 +10,6 @@ export const dateFilter = value => {
export const dateTimeFilter = value => {
if (value) {
return moment(String(value)).format('DD. MMMM YYYY, hh:mm');
return moment(String(value)).format('DD. MMMM YYYY, HH:mm');
}
};

View File

@ -1,7 +1,5 @@
fragment ProjectEntryParts on ProjectEntryNode {
id
activity
reflection
nextSteps
description
documentUrl
}

View File

@ -1,10 +1,6 @@
#import "../fragments/projectParts.gql"
query ProjectsQuery {
projects {
edges {
node {
...ProjectParts
}
}
...ProjectParts
}
}

View File

@ -4,12 +4,8 @@ query ProjectQuery($id: ID, $slug: String){
project(slug: $slug, id: $id) {
...ProjectParts
entries {
edges {
node {
...ProjectEntryParts
created
}
}
...ProjectEntryParts
created
}
}

View File

@ -34,6 +34,9 @@ export default {
canManageContent() {
return this.me.isTeacher;
},
isReadOnly() {
return this.me.readOnly || this.me.selectedClass.readOnly;
},
currentClassName() {
let currentClass = this.me.schoolClasses.find(schoolClass => {
return schoolClass.id === this.me.selectedClass.id;

View File

@ -11,8 +11,6 @@
import ADD_PROJECT_MUTATION from '@/graphql/gql/mutations/addProject.gql';
import PROJECTS_QUERY from '@/graphql/gql/queries/allProjects.gql';
const defaultAppearance = 'blue';
export default {
components: {
ProjectForm
@ -24,7 +22,6 @@
title: '',
description: '',
objectives: '',
appearance: defaultAppearance
};
}
},

View File

@ -1,52 +1,52 @@
<template>
<div class="portfolio__page">
<div class="portfolio">
<add-project
class="portfolio__add-project"
v-if="!me.readOnly && !me.selectedClass.readOnly"/>
<template v-if="projects.length > 0">
<div class="portfolio">
<h1 data-cy="page-title">Portfolio</h1>
<project-widget
v-bind="project"
:user-id="userId"
:key="project.id"
:read-only="me.readOnly || me.selectedClass.readOnly"
class="portfolio__project"
v-for="project in projects"
/>
</div>
<create-project-button
class="portfolio__create-button"
v-if="!isReadOnly" />
<project-list
:projects="projects" />
</div>
</template>
<portfolio-onboarding v-else/>
</div>
</template>
<script>
import ProjectWidget from '@/components/portfolio/ProjectWidget';
import AddProject from '@/components/portfolio/AddProject';
import ME_QUERY from '@/graphql/gql/queries/meQuery.gql';
import PROJECTS_QUERY from '@/graphql/gql/queries/allProjects.gql';
import PortfolioOnboarding from '@/components/portfolio/PortfolioOnboarding';
import CreateProjectButton from '@/components/portfolio/CreateProjectButton';
import ProjectList from '@/components/portfolio/ProjectList';
import me from '@/mixins/me';
export default {
mixins: [me],
components: {
ProjectWidget,
AddProject
ProjectList,
CreateProjectButton,
PortfolioOnboarding,
ProjectWidget
},
apollo: {
projects: {
query: PROJECTS_QUERY,
update(data) {
return this.$getRidOfEdges(data).projects;
},
pollInterval: 5000,
},
me: {
query: ME_QUERY
}
},
data() {
return {
projects: [],
me: {}
};
},
@ -67,15 +67,21 @@
@supports (display: grid) {
display: grid;
grid-template-columns: minmax(min-content, 840px);
grid-template-rows: auto;
}
grid-row-gap: 30px;
grid-auto-rows: 225px;
grid-auto-rows: auto;
max-width: 840px;
width: 100vw;
/*justify-self: center;*/
box-sizing: border-box;
padding: $large-spacing $medium-spacing;
&__create-button {
display: inline-flex;
width: 150px;
}
&__page {
display: flex;
flex-direction: column;

View File

@ -3,6 +3,18 @@
:class="specialContainerClass"
class="project">
<div class="project__header">
<back-link
class="project__back"
title="Zurück zur Übersicht"
type="portfolio"/>
<a class="link">Mit Lehrperson teilen</a>
<project-actions
:id="project.id"
class="project__actions"
v-if="!me.readOnly && !me.selectedClass.readOnly"/>
<h1
class="project__title"
data-cy="project-title">{{ project.title }}</h1>
@ -10,19 +22,7 @@
{{ project.description }}
</p>
<h2 class="project__objectives-title">Ziele</h2>
<ul class="project__objectives">
<li
:key="index"
class="project__objective"
v-for="(objective, index) in objectives">{{ objective }}
</li>
</ul>
<div class="project__meta">
<project-actions
:id="project.id"
v-if="!me.readOnly && !me.selectedClass.readOnly"/>
<owner-widget :owner="project.student"/>
<entry-count-widget :entry-count="projectEntryCount"/>
</div>
@ -52,11 +52,13 @@
import ME_QUERY from '@/graphql/gql/queries/meQuery.gql';
import PROJECT_QUERY from '@/graphql/gql/queries/projectQuery.gql';
import BackLink from '@/components/BackLink';
export default {
props: ['slug'],
components: {
BackLink,
AddProjectEntry,
ProjectEntry,
ProjectActions,
@ -101,11 +103,6 @@
slug: this.slug
};
},
update(data) {
const project = this.$getRidOfEdges(data).project || {};
this.$store.dispatch('setSpecialContainerClass', project.appearance);
return project;
},
pollInterval: 5000,
},
me: {
@ -113,26 +110,39 @@
}
},
created() {
},
beforeDestroy() {
this.$store.dispatch('setSpecialContainerClass', '');
},
};
</script>
<style scoped lang="scss">
@import "@/styles/_variables.scss";
@import "@/styles/_functions.scss";
@import "@/styles/_mixins.scss";
@import "~styles/helpers";
.project {
@include skillbox-colors;
&__header {
padding: $large-spacing;
border-bottom: 1px solid $color-silver;
display: grid;
grid-template-areas:
"b a"
"t t"
"d d"
"m m"
}
&__back {
grid-area: b;
}
&__actions {
grid-area: a;
justify-self: end;
}
&__title {
grid-area: t;
}
&__description {
grid-area: d;
}
&__add-entry {
@ -163,6 +173,8 @@
}
&__meta {
grid-area: m;
display: flex;
flex-direction: column;
@include desktop {
@ -182,7 +194,6 @@
}
&__content {
background-color: rgba($color-charcoal-dark, 0.18);
display: flex;
flex-direction: column;
/*max-width: 840px;*/

View File

@ -0,0 +1,2 @@
export const NEW_PROJECT_PAGE = 'new-project';
export const PROJECTS_PAGE = 'portfolio';

View File

@ -2,11 +2,12 @@ import portfolio from '@/pages/portfolio/portfolio';
import project from '@/pages/portfolio/project';
import newProject from '@/pages/portfolio/newProject';
import editProject from '@/pages/portfolio/editProject';
import {NEW_PROJECT_PAGE, PROJECTS_PAGE} from '@/router/portfolio.names';
const portfolioRoutes = [
{path: '/portfolio', name: 'portfolio', component: portfolio},
{path: '/portfolio', name: PROJECTS_PAGE, component: portfolio, meta: {hideFooter: true}},
{path: '/portfolio/:slug', name: 'project', component: project, props: true},
{path: '/new-project/', name: 'new-project', component: newProject},
{path: '/new-project/', name: NEW_PROJECT_PAGE, component: newProject},
{path: '/edit-project/:id', name: 'edit-project', component: editProject, props: true},
];

View File

@ -1,13 +1,3 @@
@mixin inputstyle {
display: flex;
padding: 16px;
box-shadow: inset 0 1px 4px 0 rgba(0, 0, 0, 0.15);
border-radius: 4px;
box-sizing: border-box;
border: 1px solid $color-silver-light;
max-width: 100%;
background-color: $color-white;
}
$icon-size: 20px;
@ -39,9 +29,7 @@ $icon-size: 20px;
}
.skillbox-auto-grow {
overflow: hidden;
resize: none;
outline: 0;
@include auto-grow;
}
.base-input-container {

View File

@ -78,6 +78,7 @@
@content
}
}
@mixin light-border($border-position) {
border-#{$border-position}: 1px solid $color-silver;
}
@ -188,3 +189,45 @@
border: 0;
min-height: 110px;
}
@mixin onboarding-illustration {
width: 400px;
height: 320px;
flex-shrink: 0;
margin-bottom: $large-spacing;
}
@mixin onboarding-page {
display: flex;
width: 100vw;
max-width: 800px;
flex-direction: column;
justify-self: center;
margin: 0 auto;
grid-column: 1 / -1;
align-items: center;
}
@mixin onboarding-text {
@include regular-text;
text-align: center;
max-width: 500px;
margin-bottom: $large-spacing;
}
@mixin inputstyle {
display: flex;
padding: $medium-spacing;
box-shadow: inset 0 1px 4px 0 rgba(0, 0, 0, 0.15);
border-radius: 4px;
box-sizing: border-box;
border: 1px solid $color-silver-light;
max-width: 100%;
background-color: $color-white;
}
@mixin auto-grow {
overflow: hidden;
resize: none;
outline: 0;
}

View File

@ -1,10 +1,17 @@
{
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"checkJs": false,
"resolveJsonModule": true,
"lib": ["es2017", "dom"],
"target": "ES5",
"module": "es2015",
"strict": true,
"moduleResolution": "node",
"baseUrl": "src",
"paths": {
"@/*": ["./*"]
},
"types": [
"cypress"
]

View File

@ -2,9 +2,15 @@ from django.test import TestCase, RequestFactory
from graphene.test import Client
from api.schema import schema
from core.tests.helpers import GQLResult
from users.models import SchoolClass, User
from users.services import create_users
class GQLClient(Client):
def get_result(self, *args, **kwargs):
return GQLResult(self.execute(*args, **kwargs))
class SkillboxTestCase(TestCase):
def createDefault(self) -> None:
@ -22,4 +28,6 @@ class SkillboxTestCase(TestCase):
if user is None:
user = self.teacher
request.user = user
return Client(schema=schema, context_value=request)
return GQLClient(schema=schema, context_value=request)

View File

@ -4,6 +4,7 @@ import factory
from core.factories import fake
from portfolio.models import Project, ProjectEntry
from users.models import User
class ProjectFactory(factory.django.DjangoModelFactory):
@ -14,4 +15,12 @@ class ProjectFactory(factory.django.DjangoModelFactory):
title = factory.LazyAttribute(lambda x: fake.sentence(nb_words=random.randint(4, 8)))
appearance = factory.LazyAttribute(lambda x: random.choice(['red', 'green', 'yellow']))
final = False
student = factory.Iterator(User.objects.all())
class ProjectEntryFactory(factory.django.DjangoModelFactory):
class Meta:
model = ProjectEntry
project = factory.SubFactory(ProjectFactory)

View File

@ -19,9 +19,7 @@ class UpdateProjectArgument(ProjectInput):
class ProjectEntryInput(InputObjectType):
activity = graphene.String()
reflection = graphene.String()
next_steps = graphene.String()
description = graphene.String()
document_url = graphene.String()

View File

@ -0,0 +1,6 @@
def migrate_project_entries(ProjectEntry):
template = 'Tätigkeit:\n{}\n\n\nReflexion:\n{}\n\n\nNächste Schritte:\n{}'
for pe in ProjectEntry.objects.all():
pe.description = template.format(pe.activity, pe.reflection, pe.next_steps)
pe.save()

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.22 on 2021-09-28 12:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('portfolio', '0006_auto_20210810_1348'),
]
operations = [
migrations.AddField(
model_name='projectentry',
name='description',
field=models.TextField(blank=True),
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 2.2.22 on 2021-09-28 12:27
from django.db import migrations
from portfolio.migration_helpers import migrate_project_entries
def migrate_project_entries_migration(apps, schema_editor):
ProjectEntry = apps.get_model('portfolio', 'ProjectEntry')
migrate_project_entries(ProjectEntry)
class Migration(migrations.Migration):
dependencies = [
('portfolio', '0007_projectentry_description'),
]
operations = [
migrations.RunPython(migrate_project_entries_migration)
]

View File

@ -1,9 +1,20 @@
from django.contrib.auth import get_user_model
from django.db import models
from django_extensions.db.models import TitleSlugDescriptionModel
from graphql_relay import to_global_id
from users.models import User
class Project(TitleSlugDescriptionModel):
class GraphqlNodeMixin:
def default_node_name(self):
return f'{self.__class__.__name__}Node'
@property
def graphql_id(self):
return to_global_id(self.default_node_name(), self.id)
class Project(TitleSlugDescriptionModel, GraphqlNodeMixin):
objectives = models.TextField(blank=True)
appearance = models.CharField(blank=True, null=False, max_length=255)
student = models.ForeignKey(get_user_model(), on_delete=models.CASCADE, related_name='projects')
@ -13,14 +24,19 @@ class Project(TitleSlugDescriptionModel):
def __str__(self):
return self.title
def is_viewable_by(self, user: User):
return user.id == self.student.id or (
self.final and self.student.get_teacher().id == user.id
)
class ProjectEntry(models.Model):
activity = models.TextField(blank=True)
reflection = models.TextField(blank=True)
next_steps = models.TextField(blank=True)
document_url = models.TextField(blank=True)
description = models.TextField(blank=True)
created = models.DateTimeField(auto_now_add=True)
project = models.ForeignKey(Project, related_name='entries', on_delete=models.CASCADE)
def __str__(self):
return self.activity
return self.description

View File

@ -2,16 +2,23 @@ import graphene
from django.db.models import Q
from graphene import relay
from graphene_django import DjangoObjectType
from graphene_django.filter import DjangoFilterConnectionField
from api.utils import get_by_id_or_slug
from portfolio.models import Project, ProjectEntry
from users.models import Role, UserRole
from users.models import Role, UserRole, User
class ProjectEntryNode(DjangoObjectType):
class Meta:
model = ProjectEntry
interfaces = (relay.Node,)
fields = ('description', 'document_url', 'project', 'created')
class ProjectNode(DjangoObjectType):
pk = graphene.Int()
entries_count = graphene.Int()
entries = graphene.List(ProjectEntryNode)
class Meta:
model = Project
@ -24,16 +31,14 @@ class ProjectNode(DjangoObjectType):
def resolve_entries_count(self, *args, **kwargs):
return self.entries.count()
class ProjectEntryNode(DjangoObjectType):
class Meta:
model = ProjectEntry
interfaces = (relay.Node,)
@staticmethod
def resolve_entries(root: Project, info, **kwargs):
return root.entries.all()
class PortfolioQuery(object):
project = graphene.Field(ProjectNode, id=graphene.ID(), slug=graphene.String())
projects = DjangoFilterConnectionField(ProjectNode)
projects = graphene.List(ProjectNode)
def resolve_projects(self, info, **kwargs):
user = info.context.user
@ -47,4 +52,9 @@ class PortfolioQuery(object):
return Project.objects.filter(student=user)
def resolve_project(self, info, **kwargs):
return get_by_id_or_slug(Project, **kwargs)
user = info.context.user # type: User
project = get_by_id_or_slug(Project, **kwargs) #type: Project
if project.is_viewable_by(user):
return project
return None

View File

@ -13,5 +13,5 @@ class ProjectSerializer(serializers.ModelSerializer):
class ProjectEntrySerializer(serializers.ModelSerializer):
class Meta:
model = ProjectEntry
fields = ('id', 'activity', 'reflection', 'next_steps', 'document_url', 'created', 'project')
read_only_fields = ('id', 'created',)
fields = ('id', 'document_url', 'description', 'created', 'project')
read_only_fields = ('id', 'created', 'activity', 'reflection', 'next_steps',)

View File

@ -0,0 +1,35 @@
from api.test_utils import DefaultUserTestCase
from core.tests.base_test import SkillboxTestCase
from portfolio.factories import ProjectEntryFactory
from portfolio.migration_helpers import migrate_project_entries
from portfolio.models import ProjectEntry
ACTIVITY='Kill Thanos'
REFLECTION='He sucks'
NEXT_STEPS='Go for the head'
class ProjectEntryMigrationTestCase(SkillboxTestCase):
def setUp(self):
self.createDefault()
self.project_entry = ProjectEntryFactory(
activity=ACTIVITY,
reflection=REFLECTION,
next_steps=NEXT_STEPS
)
def test_migration(self):
self.assertEqual(self.project_entry.description, '')
self.assertEqual(self.project_entry.activity, ACTIVITY)
self.assertEqual(self.project_entry.reflection, REFLECTION)
self.assertEqual(self.project_entry.next_steps, NEXT_STEPS)
migrate_project_entries(ProjectEntry)
project_entry = ProjectEntry.objects.get(id=self.project_entry.id)
self.assertEqual(project_entry.activity, ACTIVITY)
self.assertEqual(project_entry.reflection, REFLECTION)
self.assertEqual(project_entry.next_steps, NEXT_STEPS)
self.assertEqual(
project_entry.description,
f'Tätigkeit:\n{ACTIVITY}\n\n\nReflexion:\n{REFLECTION}\n\n\nNächste Schritte:\n{NEXT_STEPS}'
)

View File

@ -25,9 +25,7 @@ class ProjectMutationsTestCase(DefaultUserTestCase):
addProjectEntry(input: $input) {
projectEntry {
id
activity
reflection
nextSteps
description
created
}
errors
@ -39,9 +37,7 @@ class ProjectMutationsTestCase(DefaultUserTestCase):
'input': {
'projectEntry': {
'project': to_global_id('ProjectNode', self.project1.id),
'activity': 'testactivity',
'nextSteps': 'teststep',
'reflection': 'testreflection'
'description': 'testdescription'
}
}
}
@ -53,7 +49,7 @@ class ProjectMutationsTestCase(DefaultUserTestCase):
self.assertIsNone(result.get('errors'))
self.assertEqual(ProjectEntry.objects.count(), 1)
project_entry = ProjectEntry.objects.first()
self.assertEqual(project_entry.activity, 'testactivity')
self.assertEqual(project_entry.description, 'testdescription')
def test_should_not_be_able_to_add_entry_as_other_person(self):
client = create_client(self.student2)

View File

@ -1,37 +1,39 @@
from django.test import TestCase, RequestFactory
from graphene.test import Client
from graphql_relay import to_global_id
from api.schema import schema
from core.tests.base_test import SkillboxTestCase
from portfolio.factories import ProjectFactory
from portfolio.models import Project
from rooms.models import Room
from users.factories import SchoolClassFactory
from users.models import User, SchoolClass
from users.services import create_users
from users.models import User
project_query = """
query ProjectQuery($id: ID!) {
project(id: $id) {
id
}
}
"""
class ProjectQuery(TestCase):
class ProjectQueryTestCaswe(SkillboxTestCase):
def _test_direct_project_access(self, user: User, should_have_access: bool):
result = self.get_client(user).get_result(project_query, variables={
'id': self.project1.graphql_id
})
self.assertIsNone(result.errors)
if should_have_access:
self.assertEqual(result.data.get('project').get('id'), self.project1.graphql_id)
else:
self.assertIsNone(result.data.get('project'))
def setUp(self):
create_users()
self.teacher = User.objects.get(username='teacher')
self.teacher2 = User.objects.get(username='teacher2')
self.student = User.objects.get(username='student1')
self.student2 = User.objects.get(username='student2')
school_class1 = SchoolClassFactory(users=[self.teacher, self.student])
self.createDefault()
school_class1 = SchoolClassFactory(users=[self.teacher, self.student1])
school_class2 = SchoolClassFactory(users=[self.teacher2, self.student2])
self.project1 = ProjectFactory(student=self.student)
self.project1 = ProjectFactory(student=self.student1)
self.query = '''
query ProjectsQuery {
projects {
edges {
node {
...ProjectParts
__typename
}
__typename
}
__typename
...ProjectParts
}
}
@ -49,72 +51,65 @@ class ProjectQuery(TestCase):
def test_should_see_own_projects(self):
self.assertEqual(Project.objects.count(), 1)
request = RequestFactory().get('/')
request.user = self.student
self.client = Client(schema=schema, context_value=request)
result = self.client.execute(self.query)
result = self.get_client(self.student1).execute(self.query)
self.assertIsNone(result.get('errors'))
self.assertEqual(result.get('data').get('projects').get('edges')[0].get('node').get('title'), self.project1.title)
self.assertEqual(result.get('data').get('projects')[0].get('title'), self.project1.title)
def test_should_not_see_other_projects(self):
self.assertEqual(Project.objects.count(), 1)
request = RequestFactory().get('/')
request.user = self.student2
self.client = Client(schema=schema, context_value=request)
result = self.client.execute(self.query)
result = self.get_client(self.student2).execute(self.query)
self.assertIsNone(result.get('errors'))
self.assertEqual(len(result.get('data').get('projects').get('edges')), 0)
self.assertEqual(len(result.get('data').get('projects')), 0)
def test_teacher_should_not_see_unfinished_projects(self):
request = RequestFactory().get('/')
request.user = self.teacher
self.client = Client(schema=schema, context_value=request)
result = self.client.execute(self.query)
result = self.get_client().execute(self.query)
self.assertIsNone(result.get('errors'))
self.assertEqual(len(result.get('data').get('projects').get('edges')), 0)
def test_teacher_should_only_see_finished_projects(self):
self.project1.final = True
self.assertEqual(Project.objects.count(), 1)
request = RequestFactory().get('/')
request.user = self.teacher
self.client = Client(schema=schema, context_value=request)
result = self.client.execute(self.query)
self.assertIsNone(result.get('errors'))
self.assertEqual(result.get('data').get('projects').get('edges')[0].get('node').get('title'),
self.project1.title)
self.assertEqual(len(result.get('data').get('projects')), 0)
def test_teacher_should_only_see_finished_projects(self):
self.project1.final = True
self.project1.save()
self.assertEqual(Project.objects.count(), 1)
request = RequestFactory().get('/')
request.user = self.teacher
self.client = Client(schema=schema, context_value=request)
result = self.client.execute(self.query)
result = self.get_client().execute(self.query)
self.assertIsNone(result.get('errors'))
self.assertEqual(result.get('data').get('projects').get('edges')[0].get('node').get('title'),
self.assertEqual(result.get('data').get('projects')[0].get('title'),
self.project1.title)
def test_other_teacher_should_not_see_projects(self):
self.project1.final = True
self.project1.save()
self.assertEqual(Project.objects.count(), 1)
request = RequestFactory().get('/')
request.user = self.teacher2
self.client = Client(schema=schema, context_value=request)
result = self.client.execute(self.query)
result = self.get_client(self.teacher2).execute(self.query)
self.assertIsNone(result.get('errors'))
self.assertEqual(len(result.get('data').get('projects').get('edges')), 0)
self.assertEqual(len(result.get('data').get('projects')), 0)
def test_direct_project_access(self):
# student can access own project directly
self._test_direct_project_access(self.student1, True)
# teacher can't access project, as it's not final
self._test_direct_project_access(self.teacher, False)
self._test_direct_project_access(self.teacher2, False)
# non-owner can't access project
self._test_direct_project_access(self.student2, False)
def test_direct_final_project_access(self):
self.project1.final = True
self.project1.save()
# student can access own project directly
self._test_direct_project_access(self.student1, True)
# teacher of student can access project, as it's final
self._test_direct_project_access(self.teacher, True)
# other teacher can't access project, as it's not final
self._test_direct_project_access(self.teacher2, False)
# non-owner can't access project
self._test_direct_project_access(self.student2, False)

View File

@ -69,9 +69,7 @@ input AddProjectArgument {
}
input AddProjectEntryArgument {
activity: String
reflection: String
nextSteps: String
description: String
documentUrl: String
project: ID!
}
@ -461,7 +459,7 @@ type CustomQuery {
survey(id: ID): SurveyNode
surveys(offset: Int, before: String, after: String, first: Int, last: Int): SurveyNodeConnection
project(id: ID, slug: String): ProjectNode
projects(offset: Int, before: String, after: String, first: Int, last: Int, slug: String, appearance: String): ProjectNodeConnection
projects: [ProjectNode]
instrument(slug: String, id: ID): InstrumentNode
instruments(offset: Int, before: String, after: String, first: Int, last: Int, slug: String, type: String): InstrumentNodeConnection
studentSubmission(id: ID!): StudentSubmissionNode
@ -857,23 +855,11 @@ type PrivateUserNodeEdge {
}
type ProjectEntryNode implements Node {
id: ID!
activity: String!
reflection: String!
nextSteps: String!
documentUrl: String!
description: String!
created: DateTime!
project: ProjectNode!
}
type ProjectEntryNodeConnection {
pageInfo: PageInfo!
edges: [ProjectEntryNodeEdge]!
}
type ProjectEntryNodeEdge {
node: ProjectEntryNode
cursor: String!
id: ID!
}
type ProjectNode implements Node {
@ -886,21 +872,11 @@ type ProjectNode implements Node {
student: PrivateUserNode!
final: Boolean!
schoolClass: SchoolClassNode
entries(offset: Int, before: String, after: String, first: Int, last: Int): ProjectEntryNodeConnection!
entries: [ProjectEntryNode]
pk: Int
entriesCount: Int
}
type ProjectNodeConnection {
pageInfo: PageInfo!
edges: [ProjectNodeEdge]!
}
type ProjectNodeEdge {
node: ProjectNode
cursor: String!
}
type PublicUserNode implements Node {
firstName: String!
lastName: String!
@ -1370,9 +1346,7 @@ input UpdateProjectArgument {
}
input UpdateProjectEntryArgument {
activity: String
reflection: String
nextSteps: String
description: String
documentUrl: String
id: ID!
}