Merged in feature/share-portfolio (pull request #13)
Feature/share portfolio Approved-by: Ramon Wenger <ramon.wenger@iterativ.ch>
This commit is contained in:
commit
afe29f3f0c
|
|
@ -1,14 +1,13 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="widget-footer">
|
<div class="widget-footer">
|
||||||
<a @click="showMenu = !showMenu" class="widget-footer__more-link">
|
<a @click="toggleMenu"
|
||||||
|
class="widget-footer__more-link">
|
||||||
<ellipses></ellipses>
|
<ellipses></ellipses>
|
||||||
</a>
|
</a>
|
||||||
<widget-popover :entity="entity"
|
<widget-popover v-if="showMenu"
|
||||||
@delete="onDelete"
|
@hide-me="showMenu = false">
|
||||||
@hide-me="showMenu = false"
|
<slot :hide="toggleMenu"></slot>
|
||||||
@edit="onEdit"
|
</widget-popover>
|
||||||
:id="id"
|
|
||||||
v-if="showMenu"></widget-popover>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -17,7 +16,6 @@
|
||||||
import WidgetPopover from '@/components/rooms/WidgetPopover';
|
import WidgetPopover from '@/components/rooms/WidgetPopover';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ['on-delete', 'on-edit', 'id', 'entity'],
|
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
Ellipses,
|
Ellipses,
|
||||||
|
|
@ -28,6 +26,12 @@
|
||||||
return {
|
return {
|
||||||
showMenu: false
|
showMenu: false
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
toggleMenu: function () {
|
||||||
|
this.showMenu = !this.showMenu
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
<template>
|
||||||
|
<project-form
|
||||||
|
:project="project"
|
||||||
|
@save="updateProject"
|
||||||
|
></project-form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ProjectForm from '@/components/portfolio/ProjectForm';
|
||||||
|
|
||||||
|
import UPDATE_PROJECT_MUTATION from '@/graphql/gql/mutations/updateProject.gql';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: ['project'],
|
||||||
|
|
||||||
|
components: {
|
||||||
|
ProjectForm
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
updateProject(project) {
|
||||||
|
this.$apollo.mutate({
|
||||||
|
mutation: UPDATE_PROJECT_MUTATION,
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
project: {
|
||||||
|
id: project.id,
|
||||||
|
title: project.title,
|
||||||
|
description: project.description,
|
||||||
|
appearance: project.appearance,
|
||||||
|
objectives: project.objectives
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).then(() => {
|
||||||
|
this.$router.push('/portfolio');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
@ -3,13 +3,17 @@
|
||||||
<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">
|
||||||
<h3 class="project-widget__title">{{title}}</h3>
|
<h3 class="project-widget__title">{{title}}</h3>
|
||||||
|
|
||||||
<entry-count-widget entry-count="4"></entry-count-widget>
|
<entry-count-widget :entry-count="entriesCount"></entry-count-widget>
|
||||||
<owner-widget name="Hans Muster"></owner-widget>
|
<owner-widget :name="owner"></owner-widget>
|
||||||
|
|
||||||
</router-link>
|
</router-link>
|
||||||
<widget-footer class="project-widget__footer"
|
<widget-footer v-if="isOwner" class="project-widget__footer">
|
||||||
entity="Eintrag"
|
<template slot-scope="scope">
|
||||||
></widget-footer>
|
<li class="popover-links__link"><a @click="$emit('delete', id)">Projekt löschen</a></li>
|
||||||
|
<li class="popover-links__link"><a @click="$emit('edit', id)">Projekt bearbeiten</a></li>
|
||||||
|
<li v-if="!final" class="popover-links__link"><a @click="share(scope)">Projekt teilen</a></li>
|
||||||
|
<li v-if="final" class="popover-links__link"><a @click="unshare(scope)">Projekt nicht mehr teilen</a></li>
|
||||||
|
</template>
|
||||||
|
</widget-footer>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -19,7 +23,7 @@
|
||||||
import WidgetFooter from '@/components/WidgetFooter';
|
import WidgetFooter from '@/components/WidgetFooter';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ['title', 'appearance', 'slug'],
|
props: ['title', 'appearance', 'slug', 'id', 'final', 'student', 'entriesCount', 'userId'],
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
WidgetFooter,
|
WidgetFooter,
|
||||||
|
|
@ -28,8 +32,27 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
widgetClass() {
|
widgetClass () {
|
||||||
return `project-widget--${this.appearance}`;
|
return `project-widget--${this.appearance}`;
|
||||||
|
},
|
||||||
|
isOwner () {
|
||||||
|
return this.student.id === this.userId;
|
||||||
|
},
|
||||||
|
owner () {
|
||||||
|
return `${this.student.firstName} ${this.student.lastName}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
share: function (scope) {
|
||||||
|
this.updateShare(scope, true);
|
||||||
|
},
|
||||||
|
unshare: function (scope) {
|
||||||
|
this.updateShare(scope, false);
|
||||||
|
},
|
||||||
|
updateShare: function (scope, state) {
|
||||||
|
scope.hide();
|
||||||
|
this.$emit('updateShare', this.id, state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,13 @@
|
||||||
<a @click="showMenu = !showMenu" class="room-entry__more-link">
|
<a @click="showMenu = !showMenu" class="room-entry__more-link">
|
||||||
<ellipses class="room-entry__ellipses"></ellipses>
|
<ellipses class="room-entry__ellipses"></ellipses>
|
||||||
</a>
|
</a>
|
||||||
<widget-popover entity="Eintrag"
|
<widget-popover @hide-me="showMenu = false"
|
||||||
@delete="deleteRoomEntry"
|
|
||||||
@edit="editRoomEntry"
|
|
||||||
@hide-me="showMenu = false"
|
|
||||||
:id="id"
|
:id="id"
|
||||||
class="room-entry__popover"
|
class="room-entry__popover"
|
||||||
v-if="showMenu"></widget-popover>
|
v-if="showMenu">
|
||||||
|
<li class="popover-links__link"><a @click="deleteRoomEntry(id)">Raum löschen</a></li>
|
||||||
|
<li class="popover-links__link"><a @click="editRoomEntry(id)">Raum bearbeiten</a></li>
|
||||||
|
</widget-popover>
|
||||||
</div>
|
</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">
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,9 @@
|
||||||
<room-group-widget v-bind="schoolClass"></room-group-widget>
|
<room-group-widget v-bind="schoolClass"></room-group-widget>
|
||||||
<entry-count-widget :entryCount="entryCount"></entry-count-widget>
|
<entry-count-widget :entryCount="entryCount"></entry-count-widget>
|
||||||
</router-link>
|
</router-link>
|
||||||
<widget-footer
|
<widget-footer v-if="canEditRoom">
|
||||||
v-if="canEditRoom"
|
<li class="popover-links__link"><a @click="deleteRoom()">Raum löschen</a></li>
|
||||||
:on-delete="deleteRoom"
|
<li class="popover-links__link"><a @click="editRoom()">Raum bearbeiten</a></li>
|
||||||
:on-edit="editRoom"
|
|
||||||
:id="id"
|
|
||||||
entity="Raum"
|
|
||||||
>
|
|
||||||
</widget-footer>
|
</widget-footer>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -58,27 +54,28 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
deleteRoom(id) {
|
deleteRoom() {
|
||||||
|
const theId = this.id
|
||||||
this.$apollo.mutate({
|
this.$apollo.mutate({
|
||||||
mutation: DELETE_ROOM_MUTATION,
|
mutation: DELETE_ROOM_MUTATION,
|
||||||
variables: {
|
variables: {
|
||||||
input: {
|
input: {
|
||||||
id
|
id: theId
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
update(store, {data: {deleteRoom: {success}}}) {
|
update(store, {data: {deleteRoom: {success}}}) {
|
||||||
if (success) {
|
if (success) {
|
||||||
const data = store.readQuery({query: ROOMS_QUERY});
|
const data = store.readQuery({query: ROOMS_QUERY});
|
||||||
if (data) {
|
if (data) {
|
||||||
data.rooms.edges.splice(data.rooms.edges.findIndex(edge => edge.node.id === id), 1);
|
data.rooms.edges.splice(data.rooms.edges.findIndex(edge => edge.node.id === theId), 1);
|
||||||
store.writeQuery({query: ROOMS_QUERY, data});
|
store.writeQuery({query: ROOMS_QUERY, data});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
editRoom(id) {
|
editRoom() {
|
||||||
this.$router.push({name: 'edit-room', params: {id: id}});
|
this.$router.push({name: 'edit-room', params: {id: this.id}});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,13 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="room-popover" v-click-outside="hidePopover">
|
<div class="widget-popover" v-click-outside="hidePopover">
|
||||||
<a class="room-popover__link" @click="$emit('delete', id)">{{entity}} löschen</a>
|
<ul class="widget-popover__links popover-links">
|
||||||
<a class="room-popover__link" @click="$emit('edit', id)">{{entity}} bearbeiten</a>
|
<slot></slot>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: ['id', 'entity'],
|
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
hidePopover() {
|
hidePopover() {
|
||||||
this.$emit('hide-me');
|
this.$emit('hide-me');
|
||||||
|
|
@ -21,7 +20,7 @@
|
||||||
@import "@/styles/_variables.scss";
|
@import "@/styles/_variables.scss";
|
||||||
@import "@/styles/_mixins.scss";
|
@import "@/styles/_mixins.scss";
|
||||||
|
|
||||||
.room-popover {
|
.widget-popover {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: -110px;
|
bottom: -110px;
|
||||||
|
|
@ -31,14 +30,23 @@
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
@include widget-shadow;
|
@include widget-shadow;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popover-links {
|
||||||
|
|
||||||
|
list-style: none;
|
||||||
|
display: grid;
|
||||||
|
|
||||||
&__link {
|
&__link {
|
||||||
color: $color-grey;
|
& > a {
|
||||||
font-family: $sans-serif-font-family;
|
display: inline-block;
|
||||||
font-size: toRem(14px);
|
color: $color-grey;
|
||||||
line-height: 1.5;
|
font-family: $sans-serif-font-family;
|
||||||
padding: 5px 0;
|
font-size: toRem(14px);
|
||||||
cursor: pointer;
|
line-height: 1.5;
|
||||||
|
padding: 5px 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -5,4 +5,11 @@ fragment ProjectParts on ProjectNode {
|
||||||
description
|
description
|
||||||
slug
|
slug
|
||||||
objectives
|
objectives
|
||||||
|
final
|
||||||
|
student {
|
||||||
|
firstName
|
||||||
|
lastName
|
||||||
|
id
|
||||||
|
}
|
||||||
|
entriesCount
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
mutation DeleteProject($input: DeleteProjectInput!) {
|
||||||
|
deleteProject(input: $input) {
|
||||||
|
success
|
||||||
|
errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
<template>
|
||||||
|
<div class="edit-project-page">
|
||||||
|
<edit-project :project="project" v-if="this.project.id"></edit-project>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// todo: refactor this, we don't need 2 components, remove editRoom or EditRoom component
|
||||||
|
import EditProject from '@/components/portfolio/EditProject';
|
||||||
|
|
||||||
|
import PROJECT_QUERY from '@/graphql/gql/projectQuery.gql';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: ['id'],
|
||||||
|
|
||||||
|
components: {
|
||||||
|
EditProject
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
project: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
apollo: {
|
||||||
|
project: {
|
||||||
|
query: PROJECT_QUERY,
|
||||||
|
variables() {
|
||||||
|
return {
|
||||||
|
id: this.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
@ -5,11 +5,15 @@
|
||||||
|
|
||||||
<project-widget
|
<project-widget
|
||||||
v-for="project in projects"
|
v-for="project in projects"
|
||||||
v-bind="project" :key="project.id"
|
v-bind="project"
|
||||||
|
:userId="userId"
|
||||||
|
@delete="deleteProject"
|
||||||
|
@updateShare="updateShareState"
|
||||||
|
@edit="editProject"
|
||||||
|
:key="project.id"
|
||||||
class="portfolio__project"
|
class="portfolio__project"
|
||||||
></project-widget>
|
></project-widget>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -17,7 +21,10 @@
|
||||||
import ProjectWidget from '@/components/portfolio/ProjectWidget';
|
import ProjectWidget from '@/components/portfolio/ProjectWidget';
|
||||||
import AddProject from '@/components/portfolio/AddProject';
|
import AddProject from '@/components/portfolio/AddProject';
|
||||||
|
|
||||||
|
import ME_QUERY from '@/graphql/gql/meQuery.gql';
|
||||||
import PROJECTS_QUERY from '@/graphql/gql/allProjects.gql';
|
import PROJECTS_QUERY from '@/graphql/gql/allProjects.gql';
|
||||||
|
import DELETE_PROJECT_MUTATION from '@/graphql/gql/mutations/deleteProject.gql';
|
||||||
|
import UPDATE_PROJECT_MUTATION from '@/graphql/gql/mutations/updateProject.gql';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
|
@ -32,11 +39,65 @@
|
||||||
return this.$getRidOfEdges(data).projects
|
return this.$getRidOfEdges(data).projects
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
me: {
|
||||||
|
query: ME_QUERY
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data () {
|
||||||
return {
|
return {
|
||||||
projects: []
|
projects: [],
|
||||||
|
me: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
userId () {
|
||||||
|
return this.me.id;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
deleteProject(id) {
|
||||||
|
this.$apollo.mutate({
|
||||||
|
mutation: DELETE_PROJECT_MUTATION,
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
},
|
||||||
|
update(store, {data: {deleteProject: {success}}}) {
|
||||||
|
if (success) {
|
||||||
|
const data = store.readQuery({query: PROJECTS_QUERY});
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
data.projects.edges.splice(data.projects.edges.findIndex(edge => edge.node.id === id), 1);
|
||||||
|
store.writeQuery({query: PROJECTS_QUERY, data});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
editProject(id) {
|
||||||
|
this.$router.push({name: 'edit-project', params: { id }});
|
||||||
|
},
|
||||||
|
updateShareState(id, state) {
|
||||||
|
const project = this.projects.filter(project => project.id === id)[0];
|
||||||
|
this.$apollo.mutate({
|
||||||
|
mutation: UPDATE_PROJECT_MUTATION,
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
project: {
|
||||||
|
id: project.id,
|
||||||
|
title: project.title,
|
||||||
|
description: project.description,
|
||||||
|
appearance: project.appearance,
|
||||||
|
objectives: project.objectives,
|
||||||
|
final: state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import start from '@/pages/start'
|
||||||
import submission from '@/pages/studentSubmission'
|
import submission from '@/pages/studentSubmission'
|
||||||
import portfolio from '@/pages/portfolio'
|
import portfolio from '@/pages/portfolio'
|
||||||
import project from '@/pages/project'
|
import project from '@/pages/project'
|
||||||
|
import editProject from '@/pages/editProject'
|
||||||
import newProject from '@/pages/newProject'
|
import newProject from '@/pages/newProject'
|
||||||
|
|
||||||
import store from '@/store/index';
|
import store from '@/store/index';
|
||||||
|
|
@ -60,6 +61,7 @@ const routes = [
|
||||||
{path: '/portfolio', name: 'portfolio', component: portfolio},
|
{path: '/portfolio', name: 'portfolio', component: portfolio},
|
||||||
{path: '/portfolio/:slug', name: 'project', component: project, props: true},
|
{path: '/portfolio/:slug', name: 'project', component: project, props: true},
|
||||||
{path: '/new-project/', name: 'new-project', component: newProject},
|
{path: '/new-project/', name: 'new-project', component: newProject},
|
||||||
|
{path: '/edit-project/:id', name: 'edit-project', component: editProject, props: true},
|
||||||
{
|
{
|
||||||
path: '/book',
|
path: '/book',
|
||||||
name: 'book',
|
name: 'book',
|
||||||
|
|
|
||||||
|
|
@ -10,3 +10,7 @@ class DisableMigrations(object):
|
||||||
|
|
||||||
|
|
||||||
MIGRATION_MODULES = DisableMigrations()
|
MIGRATION_MODULES = DisableMigrations()
|
||||||
|
|
||||||
|
# Email Settings
|
||||||
|
SENDGRID_API_KEY = ""
|
||||||
|
EMAIL_BACKEND = 'django.core.mail.backends.dummy.EmailBackend'
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
import random
|
||||||
|
|
||||||
|
import factory
|
||||||
|
|
||||||
|
from core.factories import fake
|
||||||
|
from portfolio.models import Project, ProjectEntry
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectFactory(factory.django.DjangoModelFactory):
|
||||||
|
class Meta:
|
||||||
|
model = Project
|
||||||
|
|
||||||
|
objectives = factory.LazyAttribute(lambda x: fake.sentence(nb_words=random.randint(4, 8)))
|
||||||
|
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
|
||||||
|
|
||||||
|
|
@ -15,6 +15,7 @@ class AddProjectArgument(ProjectInput):
|
||||||
|
|
||||||
class UpdateProjectArgument(ProjectInput):
|
class UpdateProjectArgument(ProjectInput):
|
||||||
id = graphene.ID(required=True)
|
id = graphene.ID(required=True)
|
||||||
|
final = graphene.Boolean()
|
||||||
|
|
||||||
|
|
||||||
class ProjectEntryInput(InputObjectType):
|
class ProjectEntryInput(InputObjectType):
|
||||||
|
|
@ -29,3 +30,4 @@ class AddProjectEntryArgument(ProjectEntryInput):
|
||||||
|
|
||||||
class UpdateProjectEntryArgument(ProjectEntryInput):
|
class UpdateProjectEntryArgument(ProjectEntryInput):
|
||||||
id = graphene.ID(required=True)
|
id = graphene.ID(required=True)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
# Generated by Django 2.0.6 on 2019-03-25 14:52
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('portfolio', '0002_projectentry'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='project',
|
||||||
|
name='final',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='project',
|
||||||
|
name='student',
|
||||||
|
field=models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, related_name='projects', to=settings.AUTH_USER_MODEL),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django_extensions.db.models import TitleSlugDescriptionModel
|
from django_extensions.db.models import TitleSlugDescriptionModel
|
||||||
|
|
||||||
|
|
@ -5,6 +6,8 @@ from django_extensions.db.models import TitleSlugDescriptionModel
|
||||||
class Project(TitleSlugDescriptionModel):
|
class Project(TitleSlugDescriptionModel):
|
||||||
objectives = models.TextField(blank=True)
|
objectives = models.TextField(blank=True)
|
||||||
appearance = models.CharField(blank=True, null=False, max_length=255)
|
appearance = models.CharField(blank=True, null=False, max_length=255)
|
||||||
|
student = models.ForeignKey(get_user_model(), on_delete=models.CASCADE, related_name='projects')
|
||||||
|
final = models.BooleanField(default=False)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.title
|
return self.title
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import graphene
|
import graphene
|
||||||
from graphene import relay
|
from graphene import relay
|
||||||
|
from rest_framework.exceptions import PermissionDenied
|
||||||
|
|
||||||
from api.utils import get_object
|
from api.utils import get_object
|
||||||
from portfolio.inputs import AddProjectArgument, UpdateProjectArgument, AddProjectEntryArgument, \
|
from portfolio.inputs import AddProjectArgument, UpdateProjectArgument, AddProjectEntryArgument, \
|
||||||
|
|
@ -31,7 +32,6 @@ from portfolio.serializers import ProjectSerializer, ProjectEntrySerializer
|
||||||
#
|
#
|
||||||
# return cls(errors=['{}: {}'.format(key, value) for key, value in serializer.errors.items()])
|
# 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)
|
||||||
project = graphene.Field(ProjectNode)
|
project = graphene.Field(ProjectNode)
|
||||||
|
|
@ -41,8 +41,10 @@ class MutateProject(relay.ClientIDMutation):
|
||||||
# serializer_class = ProjectSerializer
|
# serializer_class = ProjectSerializer
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def mutate_and_get_payload(cls, *args, **kwargs):
|
def mutate_and_get_payload(cls, root, info, **kwargs):
|
||||||
data = kwargs.get('project')
|
data = kwargs.get('project')
|
||||||
|
data['student'] = info.context.user.id
|
||||||
|
|
||||||
if data.get('id') is not None:
|
if data.get('id') is not None:
|
||||||
entity = get_object(Project, data['id'])
|
entity = get_object(Project, data['id'])
|
||||||
serializer = ProjectSerializer(entity, data=data)
|
serializer = ProjectSerializer(entity, data=data)
|
||||||
|
|
@ -64,6 +66,19 @@ class AddProject(MutateProject):
|
||||||
project = graphene.Argument(
|
project = graphene.Argument(
|
||||||
AddProjectArgument) # NB: can't be named AddProjectInput, otherwise graphene complains
|
AddProjectArgument) # NB: can't be named AddProjectInput, otherwise graphene complains
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def mutate_and_get_payload(cls, root, info, **kwargs):
|
||||||
|
|
||||||
|
data = kwargs.get('project')
|
||||||
|
data['student'] = info.context.user.id
|
||||||
|
|
||||||
|
serializer = ProjectSerializer(data=data)
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save()
|
||||||
|
return cls(project=serializer.instance)
|
||||||
|
|
||||||
|
return cls(room=None, errors=['{}: {}'.format(key, value) for key, value in serializer.errors.items()])
|
||||||
|
|
||||||
|
|
||||||
class UpdateProject(MutateProject):
|
class UpdateProject(MutateProject):
|
||||||
class Input:
|
class Input:
|
||||||
|
|
@ -102,8 +117,29 @@ class UpdateProjectEntry(MutateProjectEntry):
|
||||||
project_entry = graphene.Argument(UpdateProjectEntryArgument)
|
project_entry = graphene.Argument(UpdateProjectEntryArgument)
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteProject(relay.ClientIDMutation):
|
||||||
|
class Input:
|
||||||
|
id = graphene.ID(required=True)
|
||||||
|
|
||||||
|
success = graphene.Boolean()
|
||||||
|
errors = graphene.List(graphene.String)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def mutate_and_get_payload(cls, root, info, **kwargs):
|
||||||
|
id = kwargs.get('id')
|
||||||
|
project = get_object(Project, id)
|
||||||
|
user = info.context.user
|
||||||
|
|
||||||
|
if project.student != user:
|
||||||
|
raise PermissionDenied('Permission denied: Incorrect project')
|
||||||
|
project.delete()
|
||||||
|
return cls(success=True)
|
||||||
|
|
||||||
|
|
||||||
class PortfolioMutations:
|
class PortfolioMutations:
|
||||||
add_project = AddProject.Field()
|
add_project = AddProject.Field()
|
||||||
update_project = UpdateProject.Field()
|
update_project = UpdateProject.Field()
|
||||||
|
delete_project = DeleteProject.Field()
|
||||||
add_project_entry = AddProjectEntry.Field()
|
add_project_entry = AddProjectEntry.Field()
|
||||||
update_project_entry = UpdateProjectEntry.Field()
|
update_project_entry = UpdateProjectEntry.Field()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,12 @@ from graphene_django.filter import DjangoFilterConnectionField
|
||||||
|
|
||||||
from api.utils import get_by_id_or_slug
|
from api.utils import get_by_id_or_slug
|
||||||
from portfolio.models import Project, ProjectEntry
|
from portfolio.models import Project, ProjectEntry
|
||||||
|
from users.models import UserRole, Role
|
||||||
|
|
||||||
|
|
||||||
class ProjectNode(DjangoObjectType):
|
class ProjectNode(DjangoObjectType):
|
||||||
pk = graphene.Int()
|
pk = graphene.Int()
|
||||||
|
entries_count = graphene.Int()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Project
|
model = Project
|
||||||
|
|
@ -18,6 +20,9 @@ class ProjectNode(DjangoObjectType):
|
||||||
def resolve_pk(self, *args, **kwargs):
|
def resolve_pk(self, *args, **kwargs):
|
||||||
return self.id
|
return self.id
|
||||||
|
|
||||||
|
def resolve_entries_count(self, *args, **kwargs):
|
||||||
|
return self.entries.count()
|
||||||
|
|
||||||
|
|
||||||
class ProjectEntryNode(DjangoObjectType):
|
class ProjectEntryNode(DjangoObjectType):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
@ -30,7 +35,14 @@ class PortfolioQuery(object):
|
||||||
projects = DjangoFilterConnectionField(ProjectNode)
|
projects = DjangoFilterConnectionField(ProjectNode)
|
||||||
|
|
||||||
def resolve_projects(self, info, **kwargs):
|
def resolve_projects(self, info, **kwargs):
|
||||||
return Project.objects.all().order_by('-pk')
|
user = info.context.user
|
||||||
|
if user.is_superuser:
|
||||||
|
return Project.objects.all().order_by('-pk')
|
||||||
|
|
||||||
|
if UserRole.get_role_for_user(user).role == Role.objects.get_default_teacher_role():
|
||||||
|
return Project.objects.filter(student__school_classes__in=user.school_classes.all(), final=True)
|
||||||
|
|
||||||
|
return Project.objects.filter(student=user)
|
||||||
|
|
||||||
def resolve_project(self, info, **kwargs):
|
def resolve_project(self, info, **kwargs):
|
||||||
return get_by_id_or_slug(Project, **kwargs)
|
return get_by_id_or_slug(Project, **kwargs)
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ from portfolio.models import Project, ProjectEntry
|
||||||
class ProjectSerializer(serializers.ModelSerializer):
|
class ProjectSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Project
|
model = Project
|
||||||
fields = ('id', 'title', 'description', 'objectives', 'slug', 'appearance',)
|
fields = ('id', 'title', 'description', 'objectives', 'slug', 'appearance', 'student', 'final',)
|
||||||
read_only_fields = ('id', 'slug',)
|
read_only_fields = ('id', 'slug',)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# ITerativ GmbH
|
||||||
|
# http://www.iterativ.ch/
|
||||||
|
#
|
||||||
|
# Copyright (c) 2019 ITerativ GmbH. All rights reserved.
|
||||||
|
#
|
||||||
|
# Created on 26.03.19
|
||||||
|
# @author: chrigu <christian.cueni@iterativ.ch>
|
||||||
|
from django.conf import settings
|
||||||
|
|
@ -1,6 +1,63 @@
|
||||||
|
from django.test import TestCase, RequestFactory
|
||||||
|
from graphene.test import Client
|
||||||
|
from graphql_relay import to_global_id
|
||||||
|
|
||||||
|
from api.schema import schema
|
||||||
|
from portfolio.factories import ProjectFactory
|
||||||
|
from users.factories import SchoolClassFactory
|
||||||
|
from users.models import User
|
||||||
|
from users.services import create_users
|
||||||
from api.test_utils import create_client, DefaultUserTestCase
|
from api.test_utils import create_client, DefaultUserTestCase
|
||||||
from portfolio.models import Project
|
from portfolio.models import Project
|
||||||
|
|
||||||
|
class ProjectQuery(TestCase):
|
||||||
|
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])
|
||||||
|
school_class2 = SchoolClassFactory(users=[self.teacher2, self.student2])
|
||||||
|
self.project1 = ProjectFactory(student=self.student)
|
||||||
|
|
||||||
|
self.mutation = '''
|
||||||
|
mutation DeleteProject($input: DeleteProjectInput!) {
|
||||||
|
deleteProject(input: $input) {
|
||||||
|
success
|
||||||
|
errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
|
||||||
|
self.variables = {
|
||||||
|
'input': {
|
||||||
|
'id': to_global_id('ProjectNode', self.project1.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_should_be_able_to_delete_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.mutation, variables=self.variables)
|
||||||
|
|
||||||
|
self.assertIsNone(result.get('errors'))
|
||||||
|
self.assertEqual(Project.objects.count(), 0)
|
||||||
|
|
||||||
|
def test_should_not_be_able_to_delete_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.mutation, variables=self.variables)
|
||||||
|
self.assertEqual(result.get('errors')[0]['message'], 'Permission denied: Incorrect project')
|
||||||
|
|
||||||
|
|
||||||
class ProjectMutationsTestCase(DefaultUserTestCase):
|
class ProjectMutationsTestCase(DefaultUserTestCase):
|
||||||
def test_add_project(self):
|
def test_add_project(self):
|
||||||
|
|
@ -28,3 +85,4 @@ class ProjectMutationsTestCase(DefaultUserTestCase):
|
||||||
})
|
})
|
||||||
self.assertIsNone(result.get('errors'))
|
self.assertIsNone(result.get('errors'))
|
||||||
self.assertEqual(Project.objects.count(), 1)
|
self.assertEqual(Project.objects.count(), 1)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,120 @@
|
||||||
|
from django.test import TestCase, RequestFactory
|
||||||
|
from graphene.test import Client
|
||||||
|
from graphql_relay import to_global_id
|
||||||
|
|
||||||
|
from api.schema import schema
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectQuery(TestCase):
|
||||||
|
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])
|
||||||
|
school_class2 = SchoolClassFactory(users=[self.teacher2, self.student2])
|
||||||
|
self.project1 = ProjectFactory(student=self.student)
|
||||||
|
self.query = '''
|
||||||
|
query ProjectsQuery {
|
||||||
|
projects {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
...ProjectParts
|
||||||
|
__typename
|
||||||
|
}
|
||||||
|
__typename
|
||||||
|
}
|
||||||
|
__typename
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment ProjectParts on ProjectNode {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
appearance
|
||||||
|
description
|
||||||
|
slug
|
||||||
|
objectives
|
||||||
|
__typename
|
||||||
|
}
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
self.assertIsNone(result.get('errors'))
|
||||||
|
self.assertEqual(result.get('data').get('projects').get('edges')[0].get('node').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)
|
||||||
|
|
||||||
|
self.assertIsNone(result.get('errors'))
|
||||||
|
self.assertEqual(len(result.get('data').get('projects').get('edges')), 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)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
self.assertIsNone(result.get('errors'))
|
||||||
|
self.assertEqual(result.get('data').get('projects').get('edges')[0].get('node').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)
|
||||||
|
|
||||||
|
self.assertIsNone(result.get('errors'))
|
||||||
|
self.assertEqual(len(result.get('data').get('projects').get('edges')), 0)
|
||||||
|
|
@ -11,6 +11,7 @@ from users.schema import UserNode
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class RoomEntryNode(DjangoObjectType):
|
class RoomEntryNode(DjangoObjectType):
|
||||||
pk = graphene.Int()
|
pk = graphene.Int()
|
||||||
author = UserNode()
|
author = UserNode()
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,6 @@ class RoomDeleteEditPermissionsTestcase(TestCase):
|
||||||
request.user = self.teacher
|
request.user = self.teacher
|
||||||
self.client = Client(schema=schema, context_value=request)
|
self.client = Client(schema=schema, context_value=request)
|
||||||
|
|
||||||
|
|
||||||
result = self.client.execute(self.mutation, variables=self.variables)
|
result = self.client.execute(self.mutation, variables=self.variables)
|
||||||
|
|
||||||
self.assertIsNone(result.get('errors'))
|
self.assertIsNone(result.get('errors'))
|
||||||
|
|
|
||||||
|
|
@ -34,9 +34,6 @@ def create_users(data=None):
|
||||||
name='second_class'
|
name='second_class'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
for school_class in data:
|
for school_class in data:
|
||||||
first, last = school_class.get('teacher')
|
first, last = school_class.get('teacher')
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue