diff --git a/client/cypress/integration/sync-module-visibility.spec.js b/client/cypress/integration/sync-module-visibility.spec.js
new file mode 100644
index 00000000..6f778a03
--- /dev/null
+++ b/client/cypress/integration/sync-module-visibility.spec.js
@@ -0,0 +1,5 @@
+describe('Survey', () => {
+ it('needs to be implemented', () => {
+ expect(true).to.equal(false);
+ });
+});
diff --git a/client/src/components/school-class/CurrentClass.vue b/client/src/components/school-class/CurrentClass.vue
index 3b8109cd..a385418b 100644
--- a/client/src/components/school-class/CurrentClass.vue
+++ b/client/src/components/school-class/CurrentClass.vue
@@ -9,15 +9,6 @@
export default {
mixins: [me],
-
- computed: {
- currentClassName() {
- let currentClass = this.me.schoolClasses.find(schoolClass => {
- return schoolClass.id === this.me.selectedClass.id;
- });
- return currentClass ? currentClass.name : this.me.schoolClasses.length ? this.me.schoolClasses[0].name : '';
- }
- }
};
diff --git a/client/src/graphql/gql/mutations/syncModuleVisibility.gql b/client/src/graphql/gql/mutations/syncModuleVisibility.gql
new file mode 100644
index 00000000..00cd6040
--- /dev/null
+++ b/client/src/graphql/gql/mutations/syncModuleVisibility.gql
@@ -0,0 +1,5 @@
+mutation SyncModuleVisibility($input: SyncModuleVisibilityInput!) {
+ syncModuleVisibility(input: $input) {
+ success
+ }
+}
diff --git a/client/src/mixins/me.js b/client/src/mixins/me.js
index aaa043d3..e2e592c7 100644
--- a/client/src/mixins/me.js
+++ b/client/src/mixins/me.js
@@ -5,13 +5,13 @@ export default {
return {
me: {
selectedClass: {
- id: ''
+ id: '',
},
permissions: [],
schoolClasses: [],
- isTeacher: false
+ isTeacher: false,
},
- showPopover: false
+ showPopover: false,
};
},
@@ -21,8 +21,8 @@ export default {
return {
name: 'topic',
params: {
- topicSlug: this.me.lastTopic.slug
- }
+ topicSlug: this.me.lastTopic.slug,
+ },
};
}
return '/book/topic/berufliche-grundbildung';
@@ -33,15 +33,21 @@ export default {
canManageContent() {
return this.me.permissions.includes('users.can_manage_school_class_content');
},
+ currentClassName() {
+ let currentClass = this.me.schoolClasses.find(schoolClass => {
+ return schoolClass.id === this.me.selectedClass.id;
+ });
+ return currentClass ? currentClass.name : this.me.schoolClasses.length ? this.me.schoolClasses[0].name : '';
+ },
},
apollo: {
me: {
query: ME_QUERY,
- update(data) {
- return this.$getRidOfEdges(data).me;
+ update({me}) {
+ return this.$getRidOfEdges(me);
},
- fetchPolicy: 'cache-first'
+ fetchPolicy: 'cache-first',
},
},
};
diff --git a/client/src/pages/module-base.vue b/client/src/pages/module-base.vue
index f2b39298..b6c529c6 100644
--- a/client/src/pages/module-base.vue
+++ b/client/src/pages/module-base.vue
@@ -1,6 +1,6 @@
-
+
@@ -11,6 +11,12 @@
export default {
components: {
ModuleNavigation
+ },
+
+ computed: {
+ showNavigation() {
+ return !this.$route.meta.hideNavigation;
+ }
}
};
diff --git a/client/src/pages/moduleVisibility.vue b/client/src/pages/moduleVisibility.vue
index ab61135b..b0474497 100644
--- a/client/src/pages/moduleVisibility.vue
+++ b/client/src/pages/moduleVisibility.vue
@@ -12,16 +12,25 @@
Von
- für INF2019i übernehmen.
+ für {{ currentClassName }} übernehmen.
@@ -29,11 +38,48 @@
diff --git a/client/src/router/index.js b/client/src/router/index.js
index 70a678c0..57e36b6d 100644
--- a/client/src/router/index.js
+++ b/client/src/router/index.js
@@ -1,8 +1,5 @@
import Vue from 'vue';
-// import index from '@/pages/index'
import topic from '@/pages/topic';
-import moduleBase from '@/pages/module-base';
-import module from '@/pages/module';
import rooms from '@/pages/rooms';
import room from '@/pages/room';
import newRoom from '@/pages/newRoom';
@@ -10,7 +7,6 @@ import editRoom from '@/pages/editRoom';
import article from '@/pages/article';
import instrument from '@/pages/instrument';
import instrumentOverview from '@/pages/instrumentOverview';
-import submissions from '@/pages/submissions';
import p404 from '@/pages/p404';
import start from '@/pages/start';
import submission from '@/pages/studentSubmission';
@@ -41,10 +37,11 @@ import onboardingStep1 from '@/pages/onboarding/step1';
import onboardingStep2 from '@/pages/onboarding/step2';
import onboardingStep3 from '@/pages/onboarding/step3';
import settingsPage from '@/pages/moduleSettings';
+
+import moduleRoutes from './module.routes';
import portfolioRoutes from './portfolio.routes';
import store from '@/store/index';
-import moduleVisibility from '@/pages/moduleVisibility';
const ONBOARDING_STEP_1 = 'onboarding-step-1';
const ONBOARDING_STEP_2 = 'onboarding-step-2';
@@ -54,7 +51,7 @@ const routes = [
{
path: '/',
name: 'home',
- component: start
+ component: start,
},
{
path: '/login',
@@ -62,8 +59,8 @@ const routes = [
component: login,
meta: {
layout: 'public',
- public: true
- }
+ public: true,
+ },
},
{
path: '/hello',
@@ -71,8 +68,8 @@ const routes = [
component: hello,
meta: {
layout: 'public',
- public: true
- }
+ public: true,
+ },
},
{
path: '/beta-login',
@@ -80,27 +77,10 @@ const routes = [
component: betaLogin,
meta: {
layout: 'public',
- public: true
- }
- },
- {
- path: '/module/:slug',
- component: moduleBase,
- children: [
- {
- path: '',
- name: 'module',
- component: module,
- meta: {filter: true}
- },
- {
- path: 'submissions/:id',
- name: 'submissions',
- component: submissions,
- meta: {filter: true}
- }
- ]
+ public: true,
+ },
},
+ ...moduleRoutes,
{path: '/rooms', name: 'rooms', component: rooms, meta: {filter: true}},
{path: '/new-room/', name: 'new-room', component: newRoom},
{path: '/edit-room/:id', name: 'edit-room', component: editRoom, props: true},
@@ -110,13 +90,13 @@ const routes = [
name: 'moduleRoom',
component: moduleRoom,
props: true,
- meta: {layout: 'fullScreen'}
+ meta: {layout: 'fullScreen'},
},
{path: '/article/:slug', name: 'article', component: article, meta: {layout: 'simple'}},
{
path: '/instruments/',
name: 'instrument-overview',
- component: instrumentOverview
+ component: instrumentOverview,
},
{path: '/instrument/:slug', name: 'instrument', component: instrument, meta: {layout: 'simple'}},
{path: '/submission/:id', name: 'submission', component: submission, meta: {layout: 'simple'}},
@@ -134,11 +114,11 @@ const routes = [
path: 'old-classes',
name: 'old-classes',
component: oldClasses,
- meta: {isProfile: true}
+ meta: {isProfile: true},
},
{path: 'create-class', name: 'create-class', component: createClass, meta: {layout: 'simple'}},
{path: 'show-code', name: 'show-code', component: showCode, meta: {layout: 'simple'}},
- ]
+ ],
},
{path: 'join-class', name: 'join-class', component: joinClass, meta: {layout: 'public'}},
{
@@ -146,7 +126,7 @@ const routes = [
component: surveyPage,
name: 'survey',
props: true,
- meta: {layout: 'simple'}
+ meta: {layout: 'simple'},
},
{
path: '/register',
@@ -155,7 +135,7 @@ const routes = [
meta: {
public: true,
layout: 'public',
- }
+ },
},
{
path: '/check-email',
@@ -163,8 +143,8 @@ const routes = [
name: 'checkEmail',
meta: {
public: true,
- layout: 'public'
- }
+ layout: 'public',
+ },
},
{
path: '/verify-email',
@@ -172,16 +152,16 @@ const routes = [
name: 'emailVerification',
meta: {
public: true,
- layout: 'public'
- }
+ layout: 'public',
+ },
},
{
path: '/license-activation',
component: licenseActivation,
name: 'licenseActivation',
meta: {
- layout: 'public'
- }
+ layout: 'public',
+ },
},
{
path: '/forgot-password',
@@ -189,13 +169,13 @@ const routes = [
name: 'forgotPassword',
meta: {
layout: 'public',
- public: true
- }
+ public: true,
+ },
},
{
path: '/news',
component: news,
- name: 'news'
+ name: 'news',
},
{
path: '/onboarding',
@@ -207,7 +187,7 @@ const routes = [
name: 'onboarding-start',
meta: {
layout: 'blank',
- next: ONBOARDING_STEP_1
+ next: ONBOARDING_STEP_1,
},
},
{
@@ -217,7 +197,7 @@ const routes = [
meta: {
layout: 'blank',
next: ONBOARDING_STEP_2,
- illustration: 'contents'
+ illustration: 'contents',
},
},
{
@@ -227,7 +207,7 @@ const routes = [
meta: {
layout: 'blank',
next: ONBOARDING_STEP_3,
- illustration: 'rooms'
+ illustration: 'rooms',
},
},
{
@@ -237,30 +217,23 @@ const routes = [
meta: {
layout: 'blank',
next: 'home',
- illustration: 'portfolio'
+ illustration: 'portfolio',
},
},
- ]
+ ],
},
{
path: '/settings',
- component: settingsPage
- },
- {
- path: '/visibility',
- component: moduleVisibility,
- meta: {
- layout: 'simple'
- }
+ component: settingsPage,
},
{path: '/styleguide', component: styleGuidePage},
{
path: '*',
component: p404,
meta: {
- layout: 'blank'
- }
- }
+ layout: 'blank',
+ },
+ },
];
Vue.use(Router);
@@ -273,7 +246,7 @@ const router = new Router({
return savedPosition;
}
return {x: 0, y: 0};
- }
+ },
});
router.afterEach((to, from) => {
diff --git a/client/src/router/module.names.js b/client/src/router/module.names.js
new file mode 100644
index 00000000..ec35f516
--- /dev/null
+++ b/client/src/router/module.names.js
@@ -0,0 +1,3 @@
+export const SUBMISSIONS_PAGE = 'submissions';
+export const MODULE_PAGE = 'module';
+export const VISIBILITY_PAGE = 'visibility';
diff --git a/client/src/router/module.routes.js b/client/src/router/module.routes.js
new file mode 100644
index 00000000..4eef3e0b
--- /dev/null
+++ b/client/src/router/module.routes.js
@@ -0,0 +1,35 @@
+import moduleBase from '@/pages/module-base';
+import module from '@/pages/module';
+import submissions from '@/pages/submissions';
+import moduleVisibility from '@/pages/moduleVisibility';
+import {MODULE_PAGE, SUBMISSIONS_PAGE, VISIBILITY_PAGE} from '@/router/module.names';
+
+export default [
+ {
+ path: '/module/:slug',
+ component: moduleBase,
+ children: [
+ {
+ path: '',
+ name: MODULE_PAGE,
+ component: module,
+ meta: {filter: true},
+ },
+ {
+ path: 'submissions/:id',
+ name: SUBMISSIONS_PAGE,
+ component: submissions,
+ meta: {filter: true},
+ },
+ {
+ path: 'visibility',
+ name: VISIBILITY_PAGE,
+ component: moduleVisibility,
+ meta: {
+ layout: 'simple',
+ hideNavigation: true
+ },
+ },
+ ],
+}
+];
diff --git a/server/api/schema.py b/server/api/schema.py
index f5e87a87..7421800f 100644
--- a/server/api/schema.py
+++ b/server/api/schema.py
@@ -24,7 +24,6 @@ from rooms.mutations import RoomMutations
from rooms.schema import RoomsQuery, ModuleRoomsQuery
from users.schema import AllUsersQuery, UsersQuery
from users.mutations import ProfileMutations
-from registration.mutations_public import RegistrationMutations
class CustomQuery(UsersQuery, AllUsersQuery, ModuleRoomsQuery, RoomsQuery, ObjectivesQuery, BookQuery, AssignmentsQuery,
@@ -36,7 +35,7 @@ class CustomQuery(UsersQuery, AllUsersQuery, ModuleRoomsQuery, RoomsQuery, Objec
class CustomMutation(BookMutations, RoomMutations, AssignmentMutations, ObjectiveMutations, CoreMutations, PortfolioMutations,
- ProfileMutations, SurveyMutations, NoteMutations, RegistrationMutations, SpellCheckMutations,
+ ProfileMutations, SurveyMutations, NoteMutations, SpellCheckMutations,
CouponMutations, graphene.ObjectType):
if settings.DEBUG:
debug = graphene.Field(DjangoDebug, name='_debug')
diff --git a/server/books/models/module.py b/server/books/models/module.py
index 2785d3a1..093a082b 100644
--- a/server/books/models/module.py
+++ b/server/books/models/module.py
@@ -58,7 +58,7 @@ class Module(StrictHierarchyPage):
def get_child_ids(self):
return self.get_children().values_list('id', flat=True)
- def sync_from_school_class(self, school_class_pattern, school_class_to_sync):
+ def sync_from_school_class(self, school_class_template, school_class_to_sync):
# import here so we don't get a circular import error
from books.models import Chapter, ContentBlock
@@ -77,12 +77,12 @@ class Module(StrictHierarchyPage):
content_block.visible_for.remove(school_class_to_sync)
# get all content blocks with `hidden for` for class `school_class_pattern`
- for content_block in school_class_pattern.hidden_content_blocks.intersection(content_block_query):
+ for content_block in school_class_template.hidden_content_blocks.intersection(content_block_query):
# add `school_class_to_sync` to these blocks' `hidden for`
content_block.hidden_for.add(school_class_to_sync)
# get all content blocks with `visible for` for class `school_class_pattern`
- for content_block in school_class_pattern.visible_content_blocks.intersection(content_block_query):
+ for content_block in school_class_template.visible_content_blocks.intersection(content_block_query):
# add `school_class_to_sync` to these blocks' `visible for`
content_block.visible_for.add(school_class_to_sync)
diff --git a/server/books/schema/mutations/__init__.py b/server/books/schema/mutations/__init__.py
index fc85ed51..999faf9b 100644
--- a/server/books/schema/mutations/__init__.py
+++ b/server/books/schema/mutations/__init__.py
@@ -1,6 +1,7 @@
from books.schema.mutations.chapter import UpdateChapterVisibility
from books.schema.mutations.contentblock import MutateContentBlock, AddContentBlock, DeleteContentBlock
-from books.schema.mutations.module import UpdateSolutionVisibility, UpdateLastModule, UpdateLastTopic
+from books.schema.mutations.module import UpdateSolutionVisibility, UpdateLastModule, SyncModuleVisibility
+from books.schema.mutations.topic import UpdateLastTopic
class BookMutations(object):
@@ -11,3 +12,4 @@ class BookMutations(object):
update_last_module = UpdateLastModule.Field()
update_last_topic = UpdateLastTopic.Field()
update_chapter_visibility = UpdateChapterVisibility.Field()
+ sync_module_visibility = SyncModuleVisibility.Field()
diff --git a/server/books/schema/mutations/module.py b/server/books/schema/mutations/module.py
index 77b00244..d50e8b20 100644
--- a/server/books/schema/mutations/module.py
+++ b/server/books/schema/mutations/module.py
@@ -3,9 +3,10 @@ from datetime import datetime
import graphene
from graphene import relay
-from api.utils import get_errors, get_object
-from books.models import Module, Topic, RecentModule
-from books.schema.queries import ModuleNode, TopicNode
+from api.utils import get_object
+from books.models import Module, RecentModule
+from books.schema.queries import ModuleNode
+from users.models import SchoolClass
class UpdateSolutionVisibility(relay.ClientIDMutation):
@@ -75,23 +76,30 @@ class UpdateLastModule(relay.ClientIDMutation):
return cls(last_module=last_module.module)
-class UpdateLastTopic(relay.ClientIDMutation):
+class SyncModuleVisibility(relay.ClientIDMutation):
class Input:
- # todo: use slug here too
- id = graphene.ID()
+ module = graphene.String(required=True)
+ template_school_class = graphene.ID(required=True)
+ school_class = graphene.ID(required=True)
- topic = graphene.Field(TopicNode)
+ success = graphene.Boolean()
@classmethod
def mutate_and_get_payload(cls, root, info, **args):
user = info.context.user
- id = args.get('id')
+ if not user.is_teacher():
+ raise Exception('Permission denied')
- topic = get_object(Topic, id)
- if not topic:
- raise Topic.DoesNotExist
+ module_slug = args.get('module')
+ template_id = args.get('template_school_class')
+ school_class_id = args.get('school_class')
- user.last_topic = topic
- user.save()
+ module = Module.objects.get(slug=module_slug)
+ template = get_object(SchoolClass, template_id)
+ school_class = get_object(SchoolClass, school_class_id)
+ if not template.is_user_in_schoolclass(user) or not school_class.is_user_in_schoolclass(user):
+ raise Exception('Permission denied')
- return cls(topic=topic)
+ module.sync_from_school_class(template, school_class)
+
+ return cls(success=True)
diff --git a/server/books/schema/mutations/topic.py b/server/books/schema/mutations/topic.py
new file mode 100644
index 00000000..fafadae1
--- /dev/null
+++ b/server/books/schema/mutations/topic.py
@@ -0,0 +1,28 @@
+import graphene
+from graphene import relay
+
+from api.utils import get_object
+from books.models import Topic
+from books.schema.queries import TopicNode
+
+
+class UpdateLastTopic(relay.ClientIDMutation):
+ class Input:
+ # todo: use slug here too
+ id = graphene.ID()
+
+ topic = graphene.Field(TopicNode)
+
+ @classmethod
+ def mutate_and_get_payload(cls, root, info, **args):
+ user = info.context.user
+ id = args.get('id')
+
+ topic = get_object(Topic, id)
+ if not topic:
+ raise Topic.DoesNotExist
+
+ user.last_topic = topic
+ user.save()
+
+ return cls(topic=topic)
diff --git a/server/books/tests/test_copy_visibility_for_other_class.py b/server/books/tests/test_copy_visibility_for_other_class.py
index 60cff107..6fd27a22 100644
--- a/server/books/tests/test_copy_visibility_for_other_class.py
+++ b/server/books/tests/test_copy_visibility_for_other_class.py
@@ -34,6 +34,14 @@ CONTENT_BLOCK_QUERY = """
}
"""
+SYNC_MUTATION = """
+mutation SyncMutationVisibility($input: SyncModuleVisibilityInput!) {
+ syncModuleVisibility(input: $input) {
+ success
+ }
+}
+"""
+
class CopyVisibilityForClassesTestCase(TestCase):
"""
@@ -52,7 +60,7 @@ class CopyVisibilityForClassesTestCase(TestCase):
"""
def setUp(self):
- module = ModuleFactory()
+ module = ModuleFactory(slug='some-module')
chapter = Chapter(title='Some Chapter')
module.add_child(instance=chapter)
create_users()
@@ -110,6 +118,36 @@ class CopyVisibilityForClassesTestCase(TestCase):
})
return result
+ def _test_in_sync(self):
+ # the hidden block is hidden for both now
+ hidden_result = self._get_result(CONTENT_BLOCK_QUERY, self.teacher_client, self.hidden_content_block)
+ hidden_for = hidden_result.get('data').get('contentBlock').get('hiddenFor').get('edges')
+ self.assertTrue('template-class' in map(lambda x: x['node']['name'], hidden_for))
+ self.assertTrue('class-to-be-synced' in map(lambda x: x['node']['name'], hidden_for))
+
+ # the other hidden block is hidden for no one now
+ other_hidden_result = self._get_result(CONTENT_BLOCK_QUERY, self.teacher_client,
+ self.other_hidden_content_block)
+ hidden_for = other_hidden_result.get('data').get('contentBlock').get('hiddenFor').get('edges')
+ self.assertEqual(len(hidden_for), 0)
+
+ # the default block is still hidden for no one
+ default_result = self._get_result(CONTENT_BLOCK_QUERY, self.teacher_client, self.default_content_block)
+ hidden_for = default_result.get('data').get('contentBlock').get('hiddenFor').get('edges')
+ self.assertEqual(len(hidden_for), 0)
+
+ # the custom block is visible for both
+ custom_result = self._get_result(CONTENT_BLOCK_QUERY, self.teacher_client, self.custom_content_block)
+ visible_for = custom_result.get('data').get('contentBlock').get('visibleFor').get('edges')
+ self.assertTrue('template-class' in map(lambda x: x['node']['name'], visible_for))
+ self.assertTrue('class-to-be-synced' in map(lambda x: x['node']['name'], visible_for))
+
+ # the other custom block is visible for no one
+ other_custom_result = self._get_result(CONTENT_BLOCK_QUERY, self.teacher_client,
+ self.other_custom_content_block)
+ visible_for = other_custom_result.get('data').get('contentBlock').get('visibleFor').get('edges')
+ self.assertEqual(len(visible_for), 0)
+
def test_hidden_for_and_visible_for_set_correctly(self):
self.assertEqual(ContentBlock.objects.count(), 5)
@@ -142,31 +180,14 @@ class CopyVisibilityForClassesTestCase(TestCase):
def test_syncs_correctly(self):
self.module.sync_from_school_class(self.template_school_class, self.school_class_to_be_synced)
- # the hidden block is hidden for both now
- hidden_result = self._get_result(CONTENT_BLOCK_QUERY, self.teacher_client, self.hidden_content_block)
- hidden_for = hidden_result.get('data').get('contentBlock').get('hiddenFor').get('edges')
- self.assertTrue('template-class' in map(lambda x: x['node']['name'], hidden_for))
- self.assertTrue('class-to-be-synced' in map(lambda x: x['node']['name'], hidden_for))
+ self._test_in_sync()
- # the other hidden block is hidden for no one now
- other_hidden_result = self._get_result(CONTENT_BLOCK_QUERY, self.teacher_client,
- self.other_hidden_content_block)
- hidden_for = other_hidden_result.get('data').get('contentBlock').get('hiddenFor').get('edges')
- self.assertEqual(len(hidden_for), 0)
-
- # the default block is still hidden for no one
- default_result = self._get_result(CONTENT_BLOCK_QUERY, self.teacher_client, self.default_content_block)
- hidden_for = default_result.get('data').get('contentBlock').get('hiddenFor').get('edges')
- self.assertEqual(len(hidden_for), 0)
-
- # the custom block is visible for both
- custom_result = self._get_result(CONTENT_BLOCK_QUERY, self.teacher_client, self.custom_content_block)
- visible_for = custom_result.get('data').get('contentBlock').get('visibleFor').get('edges')
- self.assertTrue('template-class' in map(lambda x: x['node']['name'], visible_for))
- self.assertTrue('class-to-be-synced' in map(lambda x: x['node']['name'], visible_for))
-
- # the other custom block is visible for no one
- other_custom_result = self._get_result(CONTENT_BLOCK_QUERY, self.teacher_client,
- self.other_custom_content_block)
- visible_for = other_custom_result.get('data').get('contentBlock').get('visibleFor').get('edges')
- self.assertEqual(len(visible_for), 0)
+ def test_mutation(self):
+ self.teacher_client.execute(SYNC_MUTATION, variables={
+ 'input': {
+ 'module': self.module.slug,
+ 'templateSchoolClass': to_global_id('SchoolClassNode', self.template_school_class.pk),
+ 'schoolClass': to_global_id('SchoolClassNode', self.school_class_to_be_synced.pk)
+ }
+ })
+ self._test_in_sync()
diff --git a/server/graphql.config.json b/server/graphql.config.json
deleted file mode 100644
index 8208188c..00000000
--- a/server/graphql.config.json
+++ /dev/null
@@ -1,25 +0,0 @@
-{
- "schema": {
- "request": {
- "url": "http://localhost:8000/graphql",
- "method": "POST",
- "postIntrospectionQuery": true,
- "options": {
- "headers": {
- "user-agent": "JS GraphQL"
- }
- }
- }
- },
- "endpoints": [
- {
- "name": "Default (http://localhost:8000/graphql",
- "url": "http://localhost:8000/graphql",
- "options": {
- "headers": {
- "user-agent": "JS GraphQL"
- }
- }
- }
- ]
-}
\ No newline at end of file