Add form for editing project entries

This commit is contained in:
Ramon Wenger 2019-05-13 17:57:21 +02:00
parent dda9f75011
commit 175b517e75
20 changed files with 346 additions and 119 deletions

View File

@ -0,0 +1,44 @@
describe('Project Entry', () => {
beforeEach(() => {
cy.exec("python ../server/manage.py prepare_projects_for_cypress");
cy.viewport('macbook-15');
cy.startGraphQLCapture();
cy.login('rahel.cueni', 'test');
});
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.waitFor('AddProjectEntryMutation');
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.waitFor('UpdateProjectEntry');
cy.get('.project-entry__paragraph:first-of-type').contains('Defeat Thanos');
})
});

View File

@ -19,6 +19,7 @@
import NewObjectiveGroupWizard from '@/components/objective-groups/NewObjectiveGroupWizard'; import NewObjectiveGroupWizard from '@/components/objective-groups/NewObjectiveGroupWizard';
import EditObjectiveGroupWizard from '@/components/objective-groups/EditObjectiveGroupWizard'; import EditObjectiveGroupWizard from '@/components/objective-groups/EditObjectiveGroupWizard';
import NewProjectEntryWizard from '@/components/portfolio/NewProjectEntryWizard'; import NewProjectEntryWizard from '@/components/portfolio/NewProjectEntryWizard';
import EditProjectEntryWizard from '@/components/portfolio/EditProjectEntryWizard';
import FullscreenImage from '@/components/FullscreenImage'; import FullscreenImage from '@/components/FullscreenImage';
import FullscreenInfographic from '@/components/FullscreenInfographic'; import FullscreenInfographic from '@/components/FullscreenInfographic';
import FullscreenVideo from '@/components/FullscreenVideo'; import FullscreenVideo from '@/components/FullscreenVideo';
@ -41,6 +42,7 @@
NewObjectiveGroupWizard, NewObjectiveGroupWizard,
EditObjectiveGroupWizard, EditObjectiveGroupWizard,
NewProjectEntryWizard, NewProjectEntryWizard,
EditProjectEntryWizard,
FullscreenImage, FullscreenImage,
FullscreenInfographic, FullscreenInfographic,
FullscreenVideo FullscreenVideo

View File

@ -0,0 +1,59 @@
<template>
<div class="more-options">
<a @click="showMenu = !showMenu" class="more-options__more-link">
<ellipses class="more-options__ellipses"></ellipses>
</a>
<widget-popover @hide-me="showMenu = false"
class="more-options__popover"
v-if="showMenu">
<slot></slot>
</widget-popover>
</div>
</template>
<script>
import WidgetPopover from '@/components/rooms/WidgetPopover';
import Ellipses from '@/components/icons/Ellipses.vue';
export default {
components: {
WidgetPopover,
Ellipses
},
data() {
return {
showMenu: false
}
}
}
</script>
<style scoped lang="scss">
@import "@/styles/_variables.scss";
.more-options {
display: flex;
justify-content: flex-end;
&__ellipses {
width: 30px;
height: 30px;
fill: $color-darkgrey-1;
margin-top: -7px;
}
&__more-link {
background-color: rgba($color-white, 0.9);
width: 35px;
height: 15px;
border-radius: 15px;
display: flex;
justify-content: center;
}
&__popover {
width: 180px;
}
}
</style>

View File

@ -14,7 +14,7 @@
computed: { computed: {
text() { text() {
return this.value.text.replace(/<br(\/)?>/, '\n').replace(/(<([^>]+)>)/ig, '') return this.value.text ? this.value.text.replace(/<br(\/)?>/, '\n').replace(/(<([^>]+)>)/ig, '') : '';
} }
} }
} }

View File

@ -0,0 +1,58 @@
<template>
<project-entry-form :project-entry="projectEntry" @save="saveEntry" @hide="hideModal">
</project-entry-form>
</template>
<script>
import ProjectEntryForm from './ProjectEntryForm';
import {mapGetters} from 'vuex';
import PROJECT_ENTRY_QUERY from '@/graphql/gql/projectEntryQuery.gql';
import UPDATE_PROJECT_ENTRY_MUTATION from '@/graphql/gql/mutations/updateProjectEntry.gql';
export default {
components: {
ProjectEntryForm
},
data() {
return {
projectEntry: {}
}
},
apollo: {
projectEntry() {
return {
query: PROJECT_ENTRY_QUERY,
variables: {
id: this.currentProjectEntry
}
}
}
},
computed: {
...mapGetters(['currentProjectEntry'])
},
methods: {
saveEntry(entry) {
this.$apollo.mutate({
mutation: UPDATE_PROJECT_ENTRY_MUTATION,
variables: {
input: {
projectEntry: entry
}
}
}).then(() => {
this.hideModal();
});
},
hideModal() {
this.$store.dispatch('hideModal');
}
},
}
</script>

View File

@ -1,37 +1,17 @@
<template> <template>
<modal :hide-header="true"> <project-entry-form @save="save" @hide="hideModal" :project-entry="projectEntry">
<div class="project-entry-modal"> </project-entry-form>
<text-form-with-help-text title="Tätigkeit" :value="activity" @change="activity = $event"
help-text="Was? Wie? Mittel?">
</text-form-with-help-text>
<text-form-with-help-text title="Reflexion" :value="reflection" @change="reflection = $event"
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?">
</text-form-with-help-text>
<text-form-with-help-text title="Nächste Schritte" :value="nextSteps" @change="nextSteps = $event"
help-text="Wie geht es weiter? Wer macht was?">
</text-form-with-help-text>
<document-form :value="document" :index="0" @link-change-url="setDocumentUrl"></document-form>
</div>
<div slot="footer">
<a class="button button--primary" data-cy="modal-save-button" v-on:click="save">Speichern</a>
<a class="button" v-on:click="hideModal">Abbrechen</a>
</div>
</modal>
</template> </template>
<script> <script>
import Modal from '@/components/Modal'; import ProjectEntryForm from './ProjectEntryForm';
import TextFormWithHelpText from '@/components/content-forms/TextFormWithHelpText';
import NEW_PROJECT_ENTRY_MUTATION from '@/graphql/gql/mutations/addProjectEntry.gql'; import NEW_PROJECT_ENTRY_MUTATION from '@/graphql/gql/mutations/addProjectEntry.gql';
import PROJECT_QUERY from '@/graphql/gql/projectQuery.gql'; import PROJECT_QUERY from '@/graphql/gql/projectQuery.gql';
import DocumentForm from '@/components/content-forms/DocumentForm';
export default { export default {
components: { components: {
DocumentForm, ProjectEntryForm
Modal,
TextFormWithHelpText
}, },
computed: { computed: {
@ -40,30 +20,18 @@
}, },
slug() { slug() {
return this.$route.params.slug; return this.$route.params.slug;
},
document() {
return this.documentUrl > '' ? {
url: this.documentUrl
} : {};
} }
}, },
methods: { methods: {
setDocumentUrl(url) { save(entry) {
this.documentUrl = url;
},
save() {
this.$apollo.mutate({ this.$apollo.mutate({
mutation: NEW_PROJECT_ENTRY_MUTATION, mutation: NEW_PROJECT_ENTRY_MUTATION,
variables: { variables: {
input: { input: {
projectEntry: Object.assign({}, { projectEntry: Object.assign({
nextSteps: this.nextSteps,
activity: this.activity,
reflection: this.reflection,
documentUrl: this.documentUrl,
project: this.project project: this.project
}) }, entry)
} }
}, },
update: (store, {data: {addProjectEntry: {projectEntry}}}) => { update: (store, {data: {addProjectEntry: {projectEntry}}}) => {
@ -71,7 +39,7 @@
const variables = {slug: this.slug}; const variables = {slug: this.slug};
const data = store.readQuery({query, variables}); const data = store.readQuery({query, variables});
if (data.project && data.project.entries) { if (data.project && data.project.entries) {
data.project.entries.edges.unshift({ data.project.entries.edges.push({
node: projectEntry, node: projectEntry,
__typename: 'ProjectEntryNode' __typename: 'ProjectEntryNode'
}); });
@ -90,10 +58,12 @@
data() { data() {
return { return {
activity: '', projectEntry: {
reflection: '', activity: '',
nextSteps: '', reflection: '',
documentUrl: '' nextSteps: '',
documentUrl: ''
}
} }
} }
} }

View File

@ -1,7 +1,11 @@
<template> <template>
<div class="project-entry"> <div class="project-entry">
<more-options-widget class="project-entry__more" data-cy="project-entry-more">
<li class="popover-links__link"><a @click="editProjectEntry()" data-cy="edit-project-entry">Eintrag bearbeiten</a></li>
</more-options-widget>
<h3 class="project-entry__heading">Tätigkeit</h3> <h3 class="project-entry__heading">Tätigkeit</h3>
<p class="project-entry__paragraph"> <p class="project-entry__paragraph" data-cy="project-entry-activity">
{{activity}} {{activity}}
</p> </p>
<h3 class="project-entry__heading">Reflexion</h3> <h3 class="project-entry__heading">Reflexion</h3>
@ -25,12 +29,20 @@
<script> <script>
import DocumentBlock from '@/components/content-blocks/DocumentBlock'; import DocumentBlock from '@/components/content-blocks/DocumentBlock';
import MoreOptionsWidget from '@/components/MoreOptionsWidget';
export default { export default {
components: { components: {
DocumentBlock DocumentBlock,
MoreOptionsWidget
}, },
props: ['activity', 'reflection', 'nextSteps', 'documentUrl', 'created'] props: ['activity', 'reflection', 'nextSteps', 'documentUrl', 'created', 'id'],
methods: {
editProjectEntry() {
this.$store.dispatch('editProjectEntry', this.id);
}
}
} }
</script> </script>
@ -43,6 +55,7 @@
background-color: $color-white; background-color: $color-white;
border-radius: $default-border-radius; border-radius: $default-border-radius;
padding: 30px 20px; padding: 30px 20px;
position: relative;
&__heading { &__heading {
font-size: toRem(22px); font-size: toRem(22px);
@ -63,5 +76,12 @@
cursor: pointer; cursor: pointer;
@include heading-4; @include heading-4;
} }
&__more {
position: absolute;
top: 10px;
right: 10px;
}
} }
</style> </style>

View File

@ -0,0 +1,64 @@
<template>
<modal :hide-header="true">
<div class="project-entry-modal">
<text-form-with-help-text title="Tätigkeit" :value="localProjectEntry.activity"
@change="localProjectEntry.activity = $event"
data-cy="activity-input"
help-text="Was? Wie? Mittel?">
</text-form-with-help-text>
<text-form-with-help-text title="Reflexion" :value="localProjectEntry.reflection"
@change="localProjectEntry.reflection = $event"
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?">
</text-form-with-help-text>
<text-form-with-help-text title="Nächste Schritte" :value="localProjectEntry.nextSteps"
@change="localProjectEntry.nextSteps = $event"
data-cy="next-steps-input"
help-text="Wie geht es weiter? Wer macht was?">
</text-form-with-help-text>
<document-form :value="document" :index="0" @link-change-url="setDocumentUrl"></document-form>
</div>
<div slot="footer">
<a class="button button--primary" data-cy="modal-save-button" v-on:click="$emit('save', localProjectEntry)">Speichern</a>
<a class="button" v-on:click="$emit('hide')">Abbrechen</a>
</div>
</modal>
</template>
<script>
import Modal from '@/components/Modal';
import TextFormWithHelpText from '@/components/content-forms/TextFormWithHelpText';
import DocumentForm from '@/components/content-forms/DocumentForm';
export default {
props: ['project-entry'],
components: {
DocumentForm,
Modal,
TextFormWithHelpText
},
data() {
return {
localProjectEntry: Object.assign({}, {
...this.projectEntry
})
}
},
computed: {
document() {
return this.localProjectEntry.documentUrl > '' ? {
url: this.localProjectEntry.documentUrl
} : {};
}
},
methods: {
setDocumentUrl(url) {
this.localProjectEntry.documentUrl = url;
},
}
}
</script>

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="project-widget" :class="widgetClass"> <div class="project-widget" :class="widgetClass">
<router-link :to="{name: 'project', params: {slug: slug}}" tag="div" class="project-widget__content"> <router-link :to="{name: 'project', params: {slug: slug}}" tag="div" class="project-widget__content" data-cy="project-link">
<h3 class="project-widget__title">{{title}}</h3> <h3 class="project-widget__title">{{title}}</h3>
<entry-count-widget :entry-count="entriesCount"></entry-count-widget> <entry-count-widget :entry-count="entriesCount"></entry-count-widget>

View File

@ -1,17 +1,9 @@
<template> <template>
<div class="room-entry"> <div class="room-entry">
<div class="room-entry__more" v-if="myEntry"> <more-options-widget class="room-entry__more" v-if="myEntry">
<a @click="showMenu = !showMenu" class="room-entry__more-link"> <li class="popover-links__link"><a @click="deleteRoomEntry(id)">Eintrag löschen</a></li>
<ellipses class="room-entry__ellipses"></ellipses> <li class="popover-links__link"><a @click="editRoomEntry(id)">Eintrag bearbeiten</a></li>
</a> </more-options-widget>
<widget-popover @hide-me="showMenu = false"
:id="id"
class="room-entry__popover"
v-if="showMenu">
<li class="popover-links__link"><a @click="deleteRoomEntry(id)">Eintrag löschen</a></li>
<li class="popover-links__link"><a @click="editRoomEntry(id)">Eintrag bearbeiten</a></li>
</widget-popover>
</div>
<router-link :to="{name: 'article', params: { slug: slug }}" tag="div" class="room-entry__router-link"> <router-link :to="{name: 'article', params: { slug: slug }}" tag="div" class="room-entry__router-link">
<div class="room-entry__header" v-if="image"> <div class="room-entry__header" v-if="image">
<img class="room-entry__image" :src="image" :alt="title"> <img class="room-entry__image" :src="image" :alt="title">
@ -34,16 +26,14 @@
import ME_QUERY from '@/graphql/gql/meQuery.gql'; import ME_QUERY from '@/graphql/gql/meQuery.gql';
import UserWidget from '@/components/UserWidget.vue'; import UserWidget from '@/components/UserWidget.vue';
import Ellipses from '@/components/icons/Ellipses.vue'; import MoreOptionsWidget from '@/components/MoreOptionsWidget';
import WidgetPopover from '@/components/rooms/WidgetPopover';
export default { export default {
props: ['title', 'author', 'contents', 'slug', 'id'], props: ['title', 'author', 'contents', 'slug', 'id'],
components: { components: {
MoreOptionsWidget,
UserWidget, UserWidget,
Ellipses,
WidgetPopover
}, },
methods: { methods: {
@ -110,12 +100,6 @@
} }
} }
}, },
data() {
return {
showMenu: false
}
}
} }
</script> </script>
@ -157,28 +141,7 @@
position: absolute; position: absolute;
top: 10px; top: 10px;
right: 10px; right: 10px;
display: flex;
justify-content: flex-end;
} }
&__more-link {
background-color: rgba($color-white, 0.9);
width: 35px;
height: 15px;
border-radius: 15px;
display: flex;
justify-content: center;
}
&__ellipses {
width: 30px;
height: 30px;
fill: $color-darkgrey-1;
margin-top: -7px;
}
&__popover {
width: 180px;
}
} }
</style> </style>

View File

@ -47,7 +47,8 @@ const cache = new InMemoryCache({
assignment: (_, args, {getCacheKey}) => getCacheKey({__typename: 'AssignmentNode', id: args.id}), assignment: (_, args, {getCacheKey}) => getCacheKey({__typename: 'AssignmentNode', id: args.id}),
objective: (_, args, {getCacheKey}) => getCacheKey({__typename: 'ObjectiveNode', id: args.id}), objective: (_, args, {getCacheKey}) => getCacheKey({__typename: 'ObjectiveNode', id: args.id}),
objectiveGroup: (_, args, {getCacheKey}) => getCacheKey({__typename: 'ObjectiveGroupNode', id: args.id}), objectiveGroup: (_, args, {getCacheKey}) => getCacheKey({__typename: 'ObjectiveGroupNode', id: args.id}),
module: (_, args, {getCacheKey}) => getCacheKey({__typename: 'ModuleNode', id: args.id}) module: (_, args, {getCacheKey}) => getCacheKey({__typename: 'ModuleNode', id: args.id}),
projectEntry: (_, args, {getCacheKey}) => getCacheKey({__typename: 'ProjectEntryNode', id: args.id}),
} }
} }
}); });

View File

@ -4,5 +4,4 @@ fragment ProjectEntryParts on ProjectEntryNode {
reflection reflection
nextSteps nextSteps
documentUrl documentUrl
created
} }

View File

@ -3,6 +3,7 @@ mutation AddProjectEntryMutation($input: AddProjectEntryInput!) {
addProjectEntry(input: $input) { addProjectEntry(input: $input) {
projectEntry { projectEntry {
...ProjectEntryParts ...ProjectEntryParts
created
} }
errors errors
} }

View File

@ -0,0 +1,9 @@
#import "../fragments/projectEntryParts.gql"
mutation UpdateProjectEntry($input: UpdateProjectEntryInput!){
updateProjectEntry(input: $input) {
projectEntry {
...ProjectEntryParts
}
errors
}
}

View File

@ -0,0 +1,6 @@
#import "./fragments/projectEntryParts.gql"
query ProjectEntryQuery($id: ID!) {
projectEntry(id: $id) {
...ProjectEntryParts
}
}

View File

@ -7,6 +7,7 @@ query ProjectQuery($id: ID, $slug: String){
edges { edges {
node { node {
...ProjectEntryParts ...ProjectEntryParts
created
} }
} }
} }

View File

@ -13,7 +13,7 @@
</div> </div>
<div class="project__content"> <div class="project__content">
<add-project-entry v-if="isOwner" class="project__add-entry" :project="project.id"></add-project-entry> <add-project-entry v-if="isOwner" class="project__add-entry" data-cy="add-project-entry" :project="project.id"></add-project-entry>
<project-entry v-bind="entry" v-for="(entry, index) in project.entries" :key="index"></project-entry> <project-entry v-bind="entry" v-for="(entry, index) in project.entries" :key="index"></project-entry>
</div> </div>
</div> </div>

View File

@ -20,6 +20,7 @@ export default new Vuex.Store({
objectiveGroupType: '', objectiveGroupType: '',
currentObjectiveGroup: '', currentObjectiveGroup: '',
parentProject: null, parentProject: null,
currentProjectEntry: null,
imageUrl: '', imageUrl: '',
infographic: { infographic: {
id: 0, id: 0,
@ -41,6 +42,7 @@ export default new Vuex.Store({
scrollToAssignmentId: state => state.scrollToAssignmentId, scrollToAssignmentId: state => state.scrollToAssignmentId,
scrollToAssignmentReady: state => state.scrollToAssignmentReady, scrollToAssignmentReady: state => state.scrollToAssignmentReady,
scrollingToAssignment: state => state.scrollingToAssignment, scrollingToAssignment: state => state.scrollingToAssignment,
currentProjectEntry: state => state.currentProjectEntry,
}, },
actions: { actions: {
@ -61,6 +63,7 @@ export default new Vuex.Store({
commit('setObjectiveGroupType', ''); commit('setObjectiveGroupType', '');
commit('setCurrentObjectiveGroup', ''); commit('setCurrentObjectiveGroup', '');
commit('setParentProject', null); commit('setParentProject', null);
commit('setCurrentProjectEntry', null);
commit('setImageUrl', ''); commit('setImageUrl', '');
commit('setInfographic', { commit('setInfographic', {
id: 0, id: 0,
@ -110,6 +113,10 @@ export default new Vuex.Store({
commit('setParentProject', payload); commit('setParentProject', payload);
dispatch('showModal', 'new-project-entry-wizard'); dispatch('showModal', 'new-project-entry-wizard');
}, },
editProjectEntry({commit, dispatch}, payload) {
commit('setCurrentProjectEntry', payload);
dispatch('showModal', 'edit-project-entry-wizard');
},
showFullscreenImage({commit, dispatch}, payload) { showFullscreenImage({commit, dispatch}, payload) {
commit('setImageUrl', payload); commit('setImageUrl', payload);
dispatch('showModal', 'fullscreen-image'); dispatch('showModal', 'fullscreen-image');
@ -183,6 +190,9 @@ export default new Vuex.Store({
setParentProject(state, payload) { setParentProject(state, payload) {
state.parentProject = payload; state.parentProject = payload;
}, },
setCurrentProjectEntry(state, payload) {
state.currentProjectEntry = payload;
},
setImageUrl(state, payload) { setImageUrl(state, payload) {
state.imageUrl = payload; state.imageUrl = payload;
}, },

View File

@ -0,0 +1,38 @@
from django.core.management import BaseCommand
from books.models import Module
from portfolio.factories import ProjectFactory
from portfolio.models import ProjectEntry
from users.models import User
class Command(BaseCommand):
def handle(self, *args, **options):
self.stdout.write("Preparing projects")
user = User.objects.get(username='rahel.cueni')
self.stdout.write("Deleting all projects")
for project in user.projects.all():
project.delete()
self.stdout.write("Creating new project")
project = ProjectFactory(
title='Groot',
description='I am Groot',
student=user,
objectives='Be Groot\nBe awesome'
)
self.stdout.write("Creating project entries")
ProjectEntry.objects.create(
activity='Kill Thanos',
reflection='He sucks',
next_steps='Go for the head',
project=project
)
ProjectEntry.objects.create(
activity='Grow up again',
reflection='Being a teenager sucks',
next_steps='Grow',
project=project
)

View File

@ -10,27 +10,8 @@ from portfolio.schema import ProjectNode, ProjectEntryNode
from portfolio.serializers import ProjectSerializer, ProjectEntrySerializer from portfolio.serializers import ProjectSerializer, ProjectEntrySerializer
# class Mutation(relay.ClientIDMutation): def check_owner(user, project):
# class Meta: return user.id != project.student.id
# pass
#
# @classmethod
# def mutate_and_get_payload(cls, *args, **kwargs):
# data = kwargs.get(cls.meta.property)
# if data.get('id') is not None:
# project = get_object(cls.meta.serializer_class.model, data['id'])
# serializer = cls.meta.serializer_class(project, data=data)
# else:
# serializer = cls.meta.serializer_class(data=data)
# if serializer.is_valid():
# serializer.save()
# props = {
# cls.meta.property: serializer.instance,
# 'errors': None
# }
# return cls(**props)
#
# return cls(errors=['{}: {}'.format(key, value) for key, value in serializer.errors.items()])
class MutateProject(relay.ClientIDMutation): class MutateProject(relay.ClientIDMutation):
errors = graphene.List(graphene.String) errors = graphene.List(graphene.String)
@ -97,12 +78,13 @@ class MutateProjectEntry(relay.ClientIDMutation):
if data.get('project') is not None: if data.get('project') is not None:
project = get_object(Project, data.get('project')) project = get_object(Project, data.get('project'))
data['project'] = project.id data['project'] = project.id
if check_owner(info.context.user, project):
if info.context.user.id != project.student.id: return cls(project_entry=None, errors=['not allowed'])
return cls(project_entry=None, errors=['not allowed'])
if data.get('id') is not None: if data.get('id') is not None:
entity = get_object(ProjectEntry, data['id']) entity = get_object(ProjectEntry, data['id'])
if check_owner(info.context.user, entity.project):
return cls(project_entry=None, errors=['not allowed'])
serializer = ProjectEntrySerializer(entity, data=data, partial=True) serializer = ProjectEntrySerializer(entity, data=data, partial=True)
else: else:
serializer = ProjectEntrySerializer(data=data) serializer = ProjectEntrySerializer(data=data)