refactor widget footer, add possibility to delete project
This commit is contained in:
parent
43278550f7
commit
d9f07c1adb
|
|
@ -3,12 +3,10 @@
|
|||
<a @click="showMenu = !showMenu" class="widget-footer__more-link">
|
||||
<ellipses></ellipses>
|
||||
</a>
|
||||
<widget-popover :entity="entity"
|
||||
@delete="onDelete"
|
||||
@hide-me="showMenu = false"
|
||||
@edit="onEdit"
|
||||
:id="id"
|
||||
v-if="showMenu"></widget-popover>
|
||||
<widget-popover v-if="showMenu"
|
||||
@hide-me="showMenu = false">
|
||||
<slot></slot>
|
||||
</widget-popover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -17,7 +15,6 @@
|
|||
import WidgetPopover from '@/components/rooms/WidgetPopover';
|
||||
|
||||
export default {
|
||||
props: ['on-delete', 'on-edit', 'id', 'entity'],
|
||||
|
||||
components: {
|
||||
Ellipses,
|
||||
|
|
|
|||
|
|
@ -7,19 +7,25 @@
|
|||
<owner-widget name="Hans Muster"></owner-widget>
|
||||
|
||||
</router-link>
|
||||
<widget-footer
|
||||
entity="Eintrag"
|
||||
></widget-footer>
|
||||
<widget-footer>
|
||||
<li class="popover-links__link"><a @click="deleteProject()">Projekt löschen</a></li>
|
||||
<li class="popover-links__link"><a @click="editProject()">Projekt bearbeiten</a></li>
|
||||
<li class="popover-links__link"><a @click="shareProject()">Projekt teilen</a></li>
|
||||
</widget-footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import DELETE_PROJECT_MUTATION from '@/graphql/gql/mutations/deleteProject.gql';
|
||||
import PROJECTS_QUERY from '@/graphql/gql/allProjects.gql';
|
||||
|
||||
import OwnerWidget from '@/components/portfolio/OwnerWidget';
|
||||
import EntryCountWidget from '@/components/rooms/EntryCountWidget';
|
||||
import WidgetFooter from '@/components/WidgetFooter';
|
||||
|
||||
export default {
|
||||
props: ['title', 'appearance', 'slug'],
|
||||
props: ['title', 'appearance', 'slug', 'id'],
|
||||
|
||||
components: {
|
||||
WidgetFooter,
|
||||
|
|
@ -31,6 +37,35 @@
|
|||
widgetClass() {
|
||||
return `project-widget--${this.appearance}`;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
deleteProject() {
|
||||
const theId = this.id
|
||||
this.$apollo.mutate({
|
||||
mutation: DELETE_PROJECT_MUTATION,
|
||||
variables: {
|
||||
input: {
|
||||
id: theId
|
||||
}
|
||||
},
|
||||
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 === theId), 1);
|
||||
store.writeQuery({query: PROJECTS_QUERY, data});
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
editProject() {
|
||||
this.$router.push({name: 'edit-room', params: {id: this.id}});
|
||||
},
|
||||
shareProject() {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -5,13 +5,9 @@
|
|||
<room-group-widget v-bind="schoolClass"></room-group-widget>
|
||||
<entry-count-widget :entryCount="entryCount"></entry-count-widget>
|
||||
</router-link>
|
||||
<widget-footer
|
||||
v-if="canEditRoom"
|
||||
:on-delete="deleteRoom"
|
||||
:on-edit="editRoom"
|
||||
:id="id"
|
||||
entity="Raum"
|
||||
>
|
||||
<widget-footer v-if="canEditRoom">
|
||||
<li class="popover-links__link"><a @click="deleteRoom()">Raum löschen</a></li>
|
||||
<li class="popover-links__link"><a @click="editRoom()">Raum bearbeiten</a></li>
|
||||
</widget-footer>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -58,27 +54,28 @@
|
|||
},
|
||||
|
||||
methods: {
|
||||
deleteRoom(id) {
|
||||
deleteRoom() {
|
||||
const theId = this.id
|
||||
this.$apollo.mutate({
|
||||
mutation: DELETE_ROOM_MUTATION,
|
||||
variables: {
|
||||
input: {
|
||||
id
|
||||
id: theId
|
||||
}
|
||||
},
|
||||
update(store, {data: {deleteRoom: {success}}}) {
|
||||
if (success) {
|
||||
const data = store.readQuery({query: ROOMS_QUERY});
|
||||
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});
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
editRoom(id) {
|
||||
this.$router.push({name: 'edit-room', params: {id: id}});
|
||||
editRoom() {
|
||||
this.$router.push({name: 'edit-room', params: {id: this.id}});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
<template>
|
||||
<div class="room-popover" v-click-outside="hidePopover">
|
||||
<a class="room-popover__link" @click="$emit('delete', id)">{{entity}} löschen</a>
|
||||
<a class="room-popover__link" @click="$emit('edit', id)">{{entity}} bearbeiten</a>
|
||||
<div class="widget-popover" v-click-outside="hidePopover">
|
||||
<ul class="widget-popover__links popover-links">
|
||||
<slot></slot>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ['id', 'entity'],
|
||||
|
||||
methods: {
|
||||
hidePopover() {
|
||||
this.$emit('hide-me');
|
||||
|
|
@ -21,23 +20,31 @@
|
|||
@import "@/styles/_variables.scss";
|
||||
@import "@/styles/_mixins.scss";
|
||||
|
||||
.room-popover {
|
||||
.widget-popover {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: -110px;
|
||||
display: grid;
|
||||
background-color: $color-white;
|
||||
padding: 20px;
|
||||
z-index: 10;
|
||||
@include widget-shadow;
|
||||
}
|
||||
|
||||
.popover-links {
|
||||
|
||||
list-style: none;
|
||||
display: grid;
|
||||
|
||||
&__link {
|
||||
color: $color-grey;
|
||||
font-family: $sans-serif-font-family;
|
||||
font-size: toRem(14px);
|
||||
line-height: 1.5;
|
||||
padding: 5px 0;
|
||||
cursor: pointer;
|
||||
& > a {
|
||||
display: inline-block;
|
||||
color: $color-grey;
|
||||
font-family: $sans-serif-font-family;
|
||||
font-size: toRem(14px);
|
||||
line-height: 1.5;
|
||||
padding: 5px 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
mutation DeleteProject($input: DeleteProjectInput!) {
|
||||
deleteProject(input: $input) {
|
||||
success
|
||||
errors
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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_extensions.db.models import TitleSlugDescriptionModel
|
||||
|
||||
|
|
@ -5,6 +6,8 @@ from django_extensions.db.models import TitleSlugDescriptionModel
|
|||
class Project(TitleSlugDescriptionModel):
|
||||
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')
|
||||
final = models.BooleanField(default=False)
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import graphene
|
||||
from graphene import relay
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
|
||||
from api.utils import get_object
|
||||
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()])
|
||||
|
||||
|
||||
class MutateProject(relay.ClientIDMutation):
|
||||
errors = graphene.List(graphene.String)
|
||||
project = graphene.Field(ProjectNode)
|
||||
|
|
@ -64,6 +64,19 @@ class AddProject(MutateProject):
|
|||
project = graphene.Argument(
|
||||
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 Input:
|
||||
|
|
@ -102,8 +115,29 @@ class UpdateProjectEntry(MutateProjectEntry):
|
|||
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:
|
||||
add_project = AddProject.Field()
|
||||
update_project = UpdateProject.Field()
|
||||
delete_project = DeleteProject.Field()
|
||||
add_project_entry = AddProjectEntry.Field()
|
||||
update_project_entry = UpdateProjectEntry.Field()
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ 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 UserRole, Role
|
||||
|
||||
|
||||
class ProjectNode(DjangoObjectType):
|
||||
|
|
@ -30,7 +31,14 @@ class PortfolioQuery(object):
|
|||
projects = DjangoFilterConnectionField(ProjectNode)
|
||||
|
||||
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):
|
||||
return get_by_id_or_slug(Project, **kwargs)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ from portfolio.models import Project, ProjectEntry
|
|||
class ProjectSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Project
|
||||
fields = ('id', 'title', 'description', 'objectives', 'slug', 'appearance',)
|
||||
fields = ('id', 'title', 'description', 'objectives', 'slug', 'appearance', 'student',)
|
||||
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
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
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.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')
|
||||
|
||||
|
||||
|
|
@ -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)
|
||||
|
|
@ -37,7 +37,6 @@ class RoomDeleteEditPermissionsTestcase(TestCase):
|
|||
request.user = self.teacher
|
||||
self.client = Client(schema=schema, context_value=request)
|
||||
|
||||
|
||||
result = self.client.execute(self.mutation, variables=self.variables)
|
||||
|
||||
self.assertIsNone(result.get('errors'))
|
||||
|
|
|
|||
|
|
@ -34,9 +34,6 @@ def create_users(data=None):
|
|||
name='second_class'
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
else:
|
||||
for school_class in data:
|
||||
first, last = school_class.get('teacher')
|
||||
|
|
|
|||
Loading…
Reference in New Issue