Merged in feature/share-portfolio (pull request #13)

Feature/share portfolio

Approved-by: Ramon Wenger <ramon.wenger@iterativ.ch>
This commit is contained in:
Christian Cueni 2019-04-10 07:51:19 +00:00
commit afe29f3f0c
25 changed files with 530 additions and 57 deletions

View File

@ -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>

View File

@ -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>

View File

@ -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);
} }
} }
} }

View File

@ -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">

View File

@ -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}});
} }
} }
} }

View File

@ -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>

View File

@ -5,4 +5,11 @@ fragment ProjectParts on ProjectNode {
description description
slug slug
objectives objectives
final
student {
firstName
lastName
id
}
entriesCount
} }

View File

@ -0,0 +1,7 @@
mutation DeleteProject($input: DeleteProjectInput!) {
deleteProject(input: $input) {
success
errors
}
}

View File

@ -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>

View File

@ -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
}
}
}
})
} }
} }
} }

View File

@ -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',

View File

@ -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'

View File

@ -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

View File

@ -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)

View File

@ -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,
),
]

View File

@ -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

View File

@ -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()

View File

@ -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)

View File

@ -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',)

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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()

View File

@ -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'))

View File

@ -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')