diff --git a/client/src/components/WidgetFooter.vue b/client/src/components/WidgetFooter.vue
index 9e1f1099..52c232b5 100644
--- a/client/src/components/WidgetFooter.vue
+++ b/client/src/components/WidgetFooter.vue
@@ -1,14 +1,13 @@
@@ -17,7 +16,6 @@
import WidgetPopover from '@/components/rooms/WidgetPopover';
export default {
- props: ['on-delete', 'on-edit', 'id', 'entity'],
components: {
Ellipses,
@@ -28,6 +26,12 @@
return {
showMenu: false
}
+ },
+
+ methods: {
+ toggleMenu: function () {
+ this.showMenu = !this.showMenu
+ }
}
}
diff --git a/client/src/components/portfolio/EditProject.vue b/client/src/components/portfolio/EditProject.vue
new file mode 100644
index 00000000..343c7a53
--- /dev/null
+++ b/client/src/components/portfolio/EditProject.vue
@@ -0,0 +1,41 @@
+
+
+
+
+
diff --git a/client/src/components/portfolio/ProjectWidget.vue b/client/src/components/portfolio/ProjectWidget.vue
index de355da8..9f6ff647 100644
--- a/client/src/components/portfolio/ProjectWidget.vue
+++ b/client/src/components/portfolio/ProjectWidget.vue
@@ -3,13 +3,17 @@
-
-
-
+
+
-
+
@@ -19,7 +23,7 @@
import WidgetFooter from '@/components/WidgetFooter';
export default {
- props: ['title', 'appearance', 'slug'],
+ props: ['title', 'appearance', 'slug', 'id', 'final', 'student', 'entriesCount', 'userId'],
components: {
WidgetFooter,
@@ -28,8 +32,27 @@
},
computed: {
- widgetClass() {
+ widgetClass () {
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);
}
}
}
diff --git a/client/src/components/rooms/RoomEntry.vue b/client/src/components/rooms/RoomEntry.vue
index a7e3ebe1..f591459e 100644
--- a/client/src/components/rooms/RoomEntry.vue
+++ b/client/src/components/rooms/RoomEntry.vue
@@ -4,13 +4,13 @@
-
+ v-if="showMenu">
+
Raum löschen
+ Raum bearbeiten
+
@@ -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}});
}
}
}
diff --git a/client/src/components/rooms/WidgetPopover.vue b/client/src/components/rooms/WidgetPopover.vue
index 68f0963c..5df364e0 100644
--- a/client/src/components/rooms/WidgetPopover.vue
+++ b/client/src/components/rooms/WidgetPopover.vue
@@ -1,14 +1,13 @@
-
-
{{entity}} löschen
-
{{entity}} bearbeiten
+
diff --git a/client/src/pages/portfolio.vue b/client/src/pages/portfolio.vue
index 00c90076..f7a15708 100644
--- a/client/src/pages/portfolio.vue
+++ b/client/src/pages/portfolio.vue
@@ -5,11 +5,15 @@
-
@@ -17,7 +21,10 @@
import ProjectWidget from '@/components/portfolio/ProjectWidget';
import AddProject from '@/components/portfolio/AddProject';
+ import ME_QUERY from '@/graphql/gql/meQuery.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 {
components: {
@@ -32,11 +39,65 @@
return this.$getRidOfEdges(data).projects
}
},
+ me: {
+ query: ME_QUERY
+ }
},
- data() {
+ data () {
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
+ }
+ }
+ }
+ })
}
}
}
diff --git a/client/src/router/index.js b/client/src/router/index.js
index 3256ddf3..e70eedc9 100644
--- a/client/src/router/index.js
+++ b/client/src/router/index.js
@@ -18,6 +18,7 @@ import start from '@/pages/start'
import submission from '@/pages/studentSubmission'
import portfolio from '@/pages/portfolio'
import project from '@/pages/project'
+import editProject from '@/pages/editProject'
import newProject from '@/pages/newProject'
import store from '@/store/index';
@@ -60,6 +61,7 @@ const routes = [
{path: '/portfolio', name: 'portfolio', component: portfolio},
{path: '/portfolio/:slug', name: 'project', component: project, props: true},
{path: '/new-project/', name: 'new-project', component: newProject},
+ {path: '/edit-project/:id', name: 'edit-project', component: editProject, props: true},
{
path: '/book',
name: 'book',
diff --git a/server/core/settings_test.py b/server/core/settings_test.py
index 1ad6e870..a480a60b 100644
--- a/server/core/settings_test.py
+++ b/server/core/settings_test.py
@@ -10,3 +10,7 @@ class DisableMigrations(object):
MIGRATION_MODULES = DisableMigrations()
+
+# Email Settings
+SENDGRID_API_KEY = ""
+EMAIL_BACKEND = 'django.core.mail.backends.dummy.EmailBackend'
diff --git a/server/portfolio/factories.py b/server/portfolio/factories.py
new file mode 100644
index 00000000..4432b5fc
--- /dev/null
+++ b/server/portfolio/factories.py
@@ -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
+
diff --git a/server/portfolio/inputs.py b/server/portfolio/inputs.py
index 584b8018..ecfda7a3 100644
--- a/server/portfolio/inputs.py
+++ b/server/portfolio/inputs.py
@@ -15,6 +15,7 @@ class AddProjectArgument(ProjectInput):
class UpdateProjectArgument(ProjectInput):
id = graphene.ID(required=True)
+ final = graphene.Boolean()
class ProjectEntryInput(InputObjectType):
@@ -29,3 +30,4 @@ class AddProjectEntryArgument(ProjectEntryInput):
class UpdateProjectEntryArgument(ProjectEntryInput):
id = graphene.ID(required=True)
+
diff --git a/server/portfolio/migrations/0003_auto_20190325_1452.py b/server/portfolio/migrations/0003_auto_20190325_1452.py
new file mode 100644
index 00000000..a04c9f33
--- /dev/null
+++ b/server/portfolio/migrations/0003_auto_20190325_1452.py
@@ -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,
+ ),
+ ]
diff --git a/server/portfolio/models.py b/server/portfolio/models.py
index 32ca421b..9a601463 100644
--- a/server/portfolio/models.py
+++ b/server/portfolio/models.py
@@ -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
diff --git a/server/portfolio/mutations.py b/server/portfolio/mutations.py
index bbb6ebc6..8e6dee3f 100644
--- a/server/portfolio/mutations.py
+++ b/server/portfolio/mutations.py
@@ -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)
@@ -41,8 +41,10 @@ class MutateProject(relay.ClientIDMutation):
# serializer_class = ProjectSerializer
@classmethod
- def mutate_and_get_payload(cls, *args, **kwargs):
+ def mutate_and_get_payload(cls, root, info, **kwargs):
data = kwargs.get('project')
+ data['student'] = info.context.user.id
+
if data.get('id') is not None:
entity = get_object(Project, data['id'])
serializer = ProjectSerializer(entity, data=data)
@@ -64,6 +66,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 +117,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()
+
diff --git a/server/portfolio/schema.py b/server/portfolio/schema.py
index 2edf9f91..780ff14b 100644
--- a/server/portfolio/schema.py
+++ b/server/portfolio/schema.py
@@ -5,10 +5,12 @@ 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):
pk = graphene.Int()
+ entries_count = graphene.Int()
class Meta:
model = Project
@@ -18,6 +20,9 @@ class ProjectNode(DjangoObjectType):
def resolve_pk(self, *args, **kwargs):
return self.id
+ def resolve_entries_count(self, *args, **kwargs):
+ return self.entries.count()
+
class ProjectEntryNode(DjangoObjectType):
class Meta:
@@ -30,7 +35,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)
diff --git a/server/portfolio/serializers.py b/server/portfolio/serializers.py
index ed186aa8..dc19d3b3 100644
--- a/server/portfolio/serializers.py
+++ b/server/portfolio/serializers.py
@@ -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', 'final',)
read_only_fields = ('id', 'slug',)
diff --git a/server/portfolio/tests/__init__.py b/server/portfolio/tests/__init__.py
index e69de29b..336672d4 100644
--- a/server/portfolio/tests/__init__.py
+++ b/server/portfolio/tests/__init__.py
@@ -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
+from django.conf import settings
diff --git a/server/portfolio/tests/test_project_mutations.py b/server/portfolio/tests/test_project_mutations.py
index 30e9e215..ddcd3638 100644
--- a/server/portfolio/tests/test_project_mutations.py
+++ b/server/portfolio/tests/test_project_mutations.py
@@ -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 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):
def test_add_project(self):
@@ -28,3 +85,4 @@ class ProjectMutationsTestCase(DefaultUserTestCase):
})
self.assertIsNone(result.get('errors'))
self.assertEqual(Project.objects.count(), 1)
+
diff --git a/server/portfolio/tests/test_project_query.py b/server/portfolio/tests/test_project_query.py
new file mode 100644
index 00000000..4ff1d7a4
--- /dev/null
+++ b/server/portfolio/tests/test_project_query.py
@@ -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)
diff --git a/server/rooms/schema.py b/server/rooms/schema.py
index 74f6a217..556c98b4 100644
--- a/server/rooms/schema.py
+++ b/server/rooms/schema.py
@@ -11,6 +11,7 @@ from users.schema import UserNode
logger = logging.getLogger(__name__)
+
class RoomEntryNode(DjangoObjectType):
pk = graphene.Int()
author = UserNode()
diff --git a/server/rooms/tests/test_room_delete_edit_permissions.py b/server/rooms/tests/test_room_delete_edit_permissions.py
index 9ef3833d..b0ea2bd5 100644
--- a/server/rooms/tests/test_room_delete_edit_permissions.py
+++ b/server/rooms/tests/test_room_delete_edit_permissions.py
@@ -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'))
diff --git a/server/users/services.py b/server/users/services.py
index 60b796fc..b075cc2d 100644
--- a/server/users/services.py
+++ b/server/users/services.py
@@ -34,9 +34,6 @@ def create_users(data=None):
name='second_class'
)
-
-
-
else:
for school_class in data:
first, last = school_class.get('teacher')