Add snapshot module header

Also refactor some queries and other code
This commit is contained in:
Ramon Wenger 2021-05-06 23:13:57 +02:00
parent 046b741458
commit 3d78761e20
20 changed files with 409 additions and 139 deletions

View File

@ -42,6 +42,7 @@ module.exports = {
alias: { alias: {
'@': resolve('src'), '@': resolve('src'),
styles: resolve('src/styles'), styles: resolve('src/styles'),
gql: resolve('src/graphql/gql')
}, },
}, },
module: { module: {

View File

@ -0,0 +1,176 @@
<template>
<div class="snapshot-header">
<h1>Snapshot {{ id }}</h1>
<div class="snapshot-header__meta">
{{ created }} {{ creator }}
</div>
<section class="snapshot-header__section">
<h2 class="snapshot-header__subtitle">
In diesem Snapshot sind {{ changesCount }} Anpassungen gespeichert:
</h2>
<ul class="snapshot-header__list">
<li class="snapshot-header__list-item">{{ hiddenObjectives }} Lernziele wurden ausgeblendet
</li>
<li class="snapshot-header__list-item">{{ newObjectives }} Lernziele wurde erfasst</li>
<li class="snapshot-header__list-item">{{ hiddenContentBlocks }} Inhaltsblöcke wurden
ausgeblendet
</li>
<li class="snapshot-header__list-item">{{ newContentBlocks }} Inhaltsblock wurde erfasst</li>
</ul>
</section>
<section class="snapshot-header__section">
<h2 class="snapshot-header__subtitle">
Willst du diesen Snapshot anwenden?
</h2>
<div>
<checkbox
:checked="agreement"
label="Ich will die Anpassungen aus diesem Snapshot in das Modul kopieren."
@input="agreement = $event"/>
</div>
</section>
<section class="snapshot-header__buttons snapshot-header__section">
<button
:disabled="!agreement"
:class="{'button--disabled-alt': !agreement}"
class="button button--primary"
@click="apply">Snapshot anwenden
</button>
<button
class="button button--secondary"
@click="back">Abbrechen
</button>
</section>
</div>
</template>
<script>
import dateformat from '@/helpers/date-format';
import Checkbox from '@/components/ui/Checkbox';
import me from '@/mixins/me';
import APPLY_SNAPSHOT_MUTATION from 'gql/mutations/snapshots/applySnapshot.gql';
import {MODULE_PAGE} from '@/router/module.names';
const _getChange = (snapshot, index) => {
try {
return snapshot.changes[index];
} catch (e) {
return 0;
}
};
export default {
props: {
snapshot: {
type: Object,
default: () => ({}),
},
},
mixins: [me],
components: {
Checkbox,
},
data: () => ({
agreement: false,
}),
computed: {
created() {
return dateformat(this.snapshot.created);
},
creator() {
const {firstName, lastName} = this.snapshot.creator || {};
return `${firstName} ${lastName}`;
},
hiddenObjectives() {
return _getChange(this.snapshot, 'hiddenObjectives');
},
newObjectives() {
return _getChange(this.snapshot, 'newObjectives');
},
hiddenContentBlocks() {
return _getChange(this.snapshot, 'hiddenContentBlocks');
},
newContentBlocks() {
return _getChange(this.snapshot, 'newContentBlocks');
},
changesCount() {
return this.hiddenObjectives + this.newObjectives + this.hiddenContentBlocks + this.newContentBlocks;
},
id() {
try {
return atob(this.snapshot.id).split(':')[1];
} catch (e) {
return '';
}
},
},
methods: {
apply() {
this.$apollo.mutate({
mutation: APPLY_SNAPSHOT_MUTATION,
variables: {
input: {
snapshot: this.snapshot.id,
selectedClass: this.me.selectedClass.id,
},
},
}).then(({data: {applySnapshot: {module: {slug}}}}) => {
this.$router.push({
name: MODULE_PAGE,
params: {
slug: slug,
},
});
});
},
back() {
this.$router.go(-1);
},
},
};
</script>
<style scoped lang="scss">
@import '~styles/helpers';
.snapshot-header {
&__subtitle {
@include heading-3;
margin-bottom: $small-spacing;
}
&__meta {
@include regular-text;
margin-bottom: $large-spacing;
}
&__list {
padding-left: $small-spacing;
}
&__list-item {
@include regular-text;
line-height: 1.5;
list-style-type: '';
padding-left: $small-spacing;
}
&__section {
margin-bottom: $large-spacing;
}
&__buttons {
}
}
</style>

View File

@ -59,6 +59,7 @@
&__link { &__link {
@include default-link; @include default-link;
color: $color-brand;
margin-left: auto; margin-left: auto;
} }
} }

View File

@ -1,24 +0,0 @@
#import "../fragments/moduleParts.gql"
query ModuleQuery($id: ID!) {
module(id: $id) {
...ModuleParts
chapters {
edges {
node {
id
contentBlocks {
edges {
node {
id
slug
title
type
contents
}
}
}
}
}
}
}
}

View File

@ -0,0 +1,16 @@
#import "../fragments/moduleParts.gql"
query ModuleQuery($id: ID, $slug: String) {
module(id: $id, slug:$slug) {
...ModuleParts
chapters {
id
contentBlocks {
id
slug
title
type
contents
}
}
}
}

View File

@ -1,12 +1,12 @@
#import "../../fragments/chapterParts.gql" #import "gql/fragments/chapterParts.gql"
#import "../../fragments/assignmentParts.gql" #import "gql/fragments/assignmentParts.gql"
#import "../../fragments/objectiveGroupParts.gql" #import "gql/fragments/objectiveGroupParts.gql"
#import "../../fragments/objectiveParts.gql" #import "gql/fragments/objectiveParts.gql"
#import "../../fragments/moduleParts.gql" #import "gql/fragments/moduleParts.gql"
#import "../../fragments/contentBlockInterfaceParts.gql" #import "gql/fragments/contentBlockInterfaceParts.gql"
#import "../../fragments/contentBlockParts.gql" #import "gql/fragments/contentBlockParts.gql"
query ModuleDetailsQuery($slug: String!) { query ModuleDetailsQuery($slug: String, $id: ID) {
module(slug: $slug) { module(slug: $slug, id: $id) {
...ModuleParts ...ModuleParts
assignments { assignments {
edges { edges {
@ -16,28 +16,16 @@ query ModuleDetailsQuery($slug: String!) {
} }
} }
objectiveGroups { objectiveGroups {
edges { ...ObjectiveGroupParts
node { objectives {
...ObjectiveGroupParts ...ObjectiveParts
objectives {
edges {
node {
...ObjectiveParts
}
}
}
}
} }
} }
chapters { chapters {
edges { ...ChapterParts
node { contentBlocks {
...ChapterParts ...ContentBlockInterfaceParts
contentBlocks { ...ContentBlockParts
...ContentBlockInterfaceParts
...ContentBlockParts
}
}
} }
} }
} }

View File

@ -1,7 +1,7 @@
import ADD_NOTE_MUTATION from '@/graphql/gql/mutations/addNote.gql'; import ADD_NOTE_MUTATION from '@/graphql/gql/mutations/addNote.gql';
import CONTENT_BLOCK_QUERY from '@/graphql/gql/queries/contentBlockQuery.gql'; import CONTENT_BLOCK_QUERY from '@/graphql/gql/queries/contentBlockQuery.gql';
import CHAPTER_QUERY from '@/graphql/gql/queries/chapterQuery.gql'; import CHAPTER_QUERY from '@/graphql/gql/queries/chapterQuery.gql';
import MODULE_QUERY from '@/graphql/gql/queries/moduleByIdQuery.gql'; import MODULE_QUERY from '@/graphql/gql/queries/modules/moduleDetailsQuery.gql';
import INSTRUMENT_FRAGMENT from '@/graphql/gql/fragments/instrumentParts.gql'; import INSTRUMENT_FRAGMENT from '@/graphql/gql/fragments/instrumentParts.gql';
const getBlockType = id => atob(id).split(':')[0]; const getBlockType = id => atob(id).split(':')[0];

View File

@ -45,9 +45,3 @@
} }
}; };
</script> </script>
<style lang="scss" scoped>
@import "@/styles/_variables.scss";
@import "@/styles/_mixins.scss";
@import "@/styles/_default-layout.scss";
</style>

View File

@ -1,11 +1,13 @@
<template> <template>
<div class="skillbox layout layout--simple"> <div
:class="{'layout--full-width': $route.meta.fullWidth}"
class="skillbox layout layout--simple">
<div <div
class="close-button" class="close-button"
@click="back"> @click="back">
<cross class="close-button__icon"/> <cross class="close-button__icon"/>
</div> </div>
<router-view/> <router-view class="layout__content" />
<simple-footer <simple-footer
class="layout__footer" class="layout__footer"
v-if="enableFooter"/> v-if="enableFooter"/>
@ -36,8 +38,7 @@
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "@/styles/_variables.scss"; @import "~styles/helpers";
@import "@/styles/_mixins.scss";
.layout { .layout {
&--simple { &--simple {
@ -60,6 +61,15 @@
} }
} }
$parent: &;
&--full-width {
#{$parent}__content {
grid-column: 1 / span 3;
grid-row: 1 / span 2;
}
}
&__footer { &__footer {
grid-column: 1 / span 3; grid-column: 1 / span 3;
} }
@ -74,7 +84,9 @@
@include desktop { @include desktop {
grid-column: 3; grid-column: 3;
grid-row: 1;
-ms-grid-column: 3; -ms-grid-column: 3;
-ms-grid-row: 1;
margin-right: $medium-spacing; margin-right: $medium-spacing;
margin-top: $medium-spacing; margin-top: $medium-spacing;
} }

View File

@ -1,11 +1,23 @@
<template> <template>
<module :module="snapshot"/> <div class="snapshot">
<header class="snapshot__header">
<snapshot-header
:snapshot="snapshot"
/>
</header>
<module
:module="snapshot"
class="snapshot__module"/>
</div>
</template> </template>
<script> <script>
import SNAPSHOT_DETAIL_QUERY from '@/graphql/gql/queries/snapshots/detail.gql'; import SNAPSHOT_DETAIL_QUERY from '@/graphql/gql/queries/snapshots/detail.gql';
import Module from '@/components/modules/Module'; import Module from '@/components/modules/Module';
import Checkbox from '@/components/ui/Checkbox';
import SnapshotHeader from '@/components/modules/SnapshotHeader';
export default { export default {
props: { props: {
@ -16,6 +28,8 @@
}, },
components: { components: {
SnapshotHeader,
Checkbox,
Module, Module,
}, },
@ -37,4 +51,21 @@
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/helpers'; @import '~styles/helpers';
.snapshot {
width: 100%;
display: grid;
grid-template-columns: 1fr 800px 1fr;
grid-template-rows: auto auto;
&__header {
background-color: $color-brand-light;
grid-column: 1 / span 3;
display: flex;
justify-content: center;
}
&__module {
grid-column: 2;
}
}
</style> </style>

View File

@ -17,9 +17,9 @@
<script> <script>
import * as SurveyVue from 'survey-vue'; import * as SurveyVue from 'survey-vue';
import {css} from '@/survey.config'; import {css} from '@/survey.config';
import gql from 'graphql-tag';
import SURVEY_QUERY from '@/graphql/gql/queries/surveyQuery.gql'; import SURVEY_QUERY from '@/graphql/gql/queries/surveyQuery.gql';
import MODULE_QUERY from '@/graphql/gql/queries/moduleByIdQuery.gql';
import UPDATE_ANSWER from '@/graphql/gql/mutations/updateAnswer.gql'; import UPDATE_ANSWER from '@/graphql/gql/mutations/updateAnswer.gql';
import Solution from '@/components/content-blocks/Solution'; import Solution from '@/components/content-blocks/Solution';
@ -30,12 +30,20 @@
const Survey = SurveyVue.Survey; const Survey = SurveyVue.Survey;
const MODULE_QUERY = gql`
query Module($id: ID) {
module(id: $id) {
solutionsEnabled
}
}
`;
export default { export default {
props: ['id'], props: ['id'],
components: { components: {
Solution, Solution,
Survey Survey,
}, },
data() { data() {
@ -45,9 +53,9 @@
module: {}, module: {},
completed: false, completed: false,
me: { me: {
permissions: [] permissions: [],
}, },
saveDisabled: false saveDisabled: false,
}; };
}, },
@ -80,7 +88,7 @@
<p class="solution-text__answer">${answer.answer}</p> <p class="solution-text__answer">${answer.answer}</p>
`; `;
} }
}, '') }, ''),
}; };
}, },
answers() { answers() {
@ -90,7 +98,7 @@
}, },
isTeacher() { isTeacher() {
return isTeacher(this); return isTeacher(this);
} },
}, },
methods: { methods: {
@ -116,21 +124,21 @@
let question = sender.getQuestionByName(k); let question = sender.getQuestionByName(k);
data[k] = { data[k] = {
answer: survey.data[k], answer: survey.data[k],
correct: question && question.correctAnswer ? question.correctAnswer : '' correct: question && question.correctAnswer ? question.correctAnswer : '',
}; };
} }
} }
const answer = { const answer = {
surveyId: this.id, surveyId: this.id,
data: JSON.stringify(data) data: JSON.stringify(data),
}; };
this.$apollo.mutate({ this.$apollo.mutate({
mutation: UPDATE_ANSWER, mutation: UPDATE_ANSWER,
variables: { variables: {
input: { input: {
answer answer,
} },
}, },
update: (store, {data: {updateAnswer: {answer}}}) => { update: (store, {data: {updateAnswer: {answer}}}) => {
const query = SURVEY_QUERY; const query = SURVEY_QUERY;
@ -140,7 +148,7 @@
queryData.survey.answer = answer; queryData.survey.answer = answer;
store.writeQuery({query, variables, data: queryData}); store.writeQuery({query, variables, data: queryData});
} }
} },
}); });
}; };
@ -160,7 +168,7 @@
this.survey.clear(); this.survey.clear();
this.survey.data = data; // reapply it this.survey.data = data; // reapply it
this.saveDisabled = false; this.saveDisabled = false;
} },
}, },
apollo: { apollo: {
@ -168,7 +176,7 @@
query: SURVEY_QUERY, query: SURVEY_QUERY,
variables() { variables() {
return { return {
id: this.id id: this.id,
}; };
}, },
manual: true, manual: true,
@ -190,14 +198,14 @@
this.$apollo.addSmartQuery('module', { this.$apollo.addSmartQuery('module', {
query: MODULE_QUERY, query: MODULE_QUERY,
variables: { variables: {
id: module.id id: module.id,
} },
}); });
} }
}, },
}, },
me: meQuery me: meQuery,
} },
}; };
</script> </script>

View File

@ -24,6 +24,7 @@ import authRoutes from './auth.routes';
import roomRoutes from './room.routes'; import roomRoutes from './room.routes';
import store from '@/store/index'; import store from '@/store/index';
import {LAYOUT_SIMPLE} from '@/router/core.constants';
const routes = [ const routes = [
{ {
@ -34,24 +35,25 @@ const routes = [
...moduleRoutes, ...moduleRoutes,
...authRoutes, ...authRoutes,
...roomRoutes, ...roomRoutes,
{path: '/article/:slug', name: 'article', component: article, meta: {layout: 'simple'}}, ...onboardingRoutes,
...portfolioRoutes,
...meRoutes,
{path: '/article/:slug', name: 'article', component: article, meta: {layout: LAYOUT_SIMPLE}},
{ {
path: '/instruments/', path: '/instruments/',
name: 'instrument-overview', name: 'instrument-overview',
component: instrumentOverview, component: instrumentOverview,
}, },
{path: '/instrument/:slug', name: 'instrument', component: instrument, meta: {layout: 'simple'}}, {path: '/instrument/:slug', name: 'instrument', component: instrument, meta: {layout: LAYOUT_SIMPLE}},
{path: '/submission/:id', name: 'submission', component: submission, meta: {layout: 'simple'}}, {path: '/submission/:id', name: 'submission', component: submission, meta: {layout: LAYOUT_SIMPLE}},
...portfolioRoutes,
{path: '/topic/:topicSlug', name: 'topic', component: topic, alias: '/book/topic/:topicSlug'}, {path: '/topic/:topicSlug', name: 'topic', component: topic, alias: '/book/topic/:topicSlug'},
...meRoutes, {path: '/join-class', name: 'join-class', component: joinClass, meta: {layout: LAYOUT_SIMPLE}},
{path: '/join-class', name: 'join-class', component: joinClass, meta: {layout: 'simple'}},
{ {
path: '/survey/:id', path: '/survey/:id',
component: surveyPage, component: surveyPage,
name: 'survey', name: 'survey',
props: true, props: true,
meta: {layout: 'simple'}, meta: {layout: LAYOUT_SIMPLE},
}, },
{ {
path: '/check-email', path: '/check-email',
@ -93,7 +95,6 @@ const routes = [
component: news, component: news,
name: 'news', name: 'news',
}, },
...onboardingRoutes,
{path: '/styleguide', component: styleGuidePage}, {path: '/styleguide', component: styleGuidePage},
{ {
path: '*', path: '*',

View File

@ -11,6 +11,7 @@ import joinTeam from '@/pages/me/joinTeam';
import createTeam from '@/pages/me/createTeam'; import createTeam from '@/pages/me/createTeam';
import {CREATE_TEAM, JOIN_TEAM, MY_TEAM, SHOW_SCHOOL_CLASS_CODE, SHOW_TEAM_CODE} from './me.names'; import {CREATE_TEAM, JOIN_TEAM, MY_TEAM, SHOW_SCHOOL_CLASS_CODE, SHOW_TEAM_CODE} from './me.names';
import {LAYOUT_SIMPLE} from '@/router/core.constants';
export default [ export default [
{ {
@ -32,23 +33,23 @@ export default [
alias: 'create-class', alias: 'create-class',
name: 'create-class', name: 'create-class',
component: createClass, component: createClass,
meta: {layout: 'simple'}, meta: {layout: LAYOUT_SIMPLE},
}, },
{ {
path: 'class/code', path: 'class/code',
alias: 'show-code', alias: 'show-code',
name: SHOW_SCHOOL_CLASS_CODE, name: SHOW_SCHOOL_CLASS_CODE,
component: showSchoolClassCode, component: showSchoolClassCode,
meta: {layout: 'simple'}, meta: {layout: LAYOUT_SIMPLE},
}, },
{path: 'team', name: MY_TEAM, component: myTeam, meta: {isProfile: true}}, {path: 'team', name: MY_TEAM, component: myTeam, meta: {isProfile: true}},
{path: 'team/join', name: JOIN_TEAM, component: joinTeam, meta: {isProfile: true, layout: 'simple'}}, {path: 'team/join', name: JOIN_TEAM, component: joinTeam, meta: {isProfile: true, layout: LAYOUT_SIMPLE}},
{path: 'team/create', name: CREATE_TEAM, component: createTeam, meta: {isProfile: true, layout: 'simple'}}, {path: 'team/create', name: CREATE_TEAM, component: createTeam, meta: {isProfile: true, layout: LAYOUT_SIMPLE}},
{ {
path: 'team/code', path: 'team/code',
name: SHOW_TEAM_CODE, name: SHOW_TEAM_CODE,
component: showTeamCode, component: showTeamCode,
meta: {layout: 'simple'}, meta: {layout: LAYOUT_SIMPLE},
}, },
], ],
}, },

View File

@ -2,10 +2,18 @@ import moduleBase from '@/pages/module/module-base';
import module from '@/pages/module/module'; import module from '@/pages/module/module';
import submissions from '@/pages/submissions'; import submissions from '@/pages/submissions';
import moduleVisibility from '@/pages/module/moduleVisibility'; import moduleVisibility from '@/pages/module/moduleVisibility';
import {MODULE_PAGE, MODULE_SETTINGS_PAGE, SUBMISSIONS_PAGE, VISIBILITY_PAGE, SNAPSHOT_LIST, SNAPSHOT_DETAIL} from '@/router/module.names'; import {
MODULE_PAGE,
MODULE_SETTINGS_PAGE,
SNAPSHOT_DETAIL,
SNAPSHOT_LIST,
SUBMISSIONS_PAGE,
VISIBILITY_PAGE,
} from '@/router/module.names';
import settingsPage from '@/pages/module/moduleSettings'; import settingsPage from '@/pages/module/moduleSettings';
import snapshots from '@/pages/snapshot/snapshots'; import snapshots from '@/pages/snapshot/snapshots';
import snapshot from '@/pages/snapshot/snapshot'; import snapshot from '@/pages/snapshot/snapshot';
import {LAYOUT_SIMPLE} from '@/router/core.constants';
export default [ export default [
{ {
@ -40,7 +48,7 @@ export default [
name: VISIBILITY_PAGE, name: VISIBILITY_PAGE,
component: moduleVisibility, component: moduleVisibility,
meta: { meta: {
layout: 'simple', layout: LAYOUT_SIMPLE,
hideNavigation: true, hideNavigation: true,
}, },
}, },
@ -56,8 +64,13 @@ export default [
path: 'snapshot/:id', path: 'snapshot/:id',
component: snapshot, component: snapshot,
name: SNAPSHOT_DETAIL, name: SNAPSHOT_DETAIL,
props: true props: true,
} meta: {
layout: LAYOUT_SIMPLE,
hideNavigation: true,
fullWidth: true
},
},
], ],
}, },
]; ];

View File

@ -12,9 +12,17 @@
&--white-bg { &--white-bg {
background-color: $color-white; background-color: $color-white;
} }
@mixin disabled {
cursor: default;
}
&--disabled { &--disabled {
@include disabled;
background-color: $color-silver-light; background-color: $color-silver-light;
} }
&--disabled-alt {
@include disabled;
opacity: 0.3;
}
&--big { &--big {
padding: 15px; padding: 15px;
} }

View File

@ -73,6 +73,7 @@ $icon-size: 20px;
&__icon { &__icon {
width: $icon-size; width: $icon-size;
height: $icon-size; height: $icon-size;
overflow: hidden;
display: flex; display: flex;
border: 2px solid $color-silver-dark; border: 2px solid $color-silver-dark;
justify-content: center; justify-content: center;

View File

@ -10,6 +10,7 @@ from books.schema.interfaces.module import ModuleInterface
from books.schema.nodes.chapter import ChapterNode from books.schema.nodes.chapter import ChapterNode
from notes.models import ModuleBookmark, ContentBlockBookmark, ChapterBookmark from notes.models import ModuleBookmark, ContentBlockBookmark, ChapterBookmark
from notes.schema import ModuleBookmarkNode, ContentBlockBookmarkNode, ChapterBookmarkNode from notes.schema import ModuleBookmarkNode, ContentBlockBookmarkNode, ChapterBookmarkNode
from objectives.schema import ObjectiveGroupNode
from surveys.models import Answer from surveys.models import Answer
from surveys.schema import AnswerNode from surveys.schema import AnswerNode
@ -26,7 +27,7 @@ class ModuleNode(DjangoObjectType):
} }
interfaces = (ModuleInterface,) interfaces = (ModuleInterface,)
chapters = DjangoFilterConnectionField(ChapterNode) chapters = graphene.List(ChapterNode)
solutions_enabled = graphene.Boolean() solutions_enabled = graphene.Boolean()
bookmark = graphene.Field(ModuleBookmarkNode) bookmark = graphene.Field(ModuleBookmarkNode)
my_submissions = DjangoFilterConnectionField(StudentSubmissionNode) my_submissions = DjangoFilterConnectionField(StudentSubmissionNode)
@ -34,6 +35,7 @@ class ModuleNode(DjangoObjectType):
my_content_bookmarks = DjangoFilterConnectionField(ContentBlockBookmarkNode) my_content_bookmarks = DjangoFilterConnectionField(ContentBlockBookmarkNode)
my_chapter_bookmarks = DjangoFilterConnectionField(ChapterBookmarkNode) my_chapter_bookmarks = DjangoFilterConnectionField(ChapterBookmarkNode)
snapshots = graphene.List('books.schema.nodes.SnapshotNode') snapshots = graphene.List('books.schema.nodes.SnapshotNode')
objective_groups = graphene.List(ObjectiveGroupNode)
def resolve_chapters(self, info, **kwargs): def resolve_chapters(self, info, **kwargs):
return Chapter.get_by_parent(self) return Chapter.get_by_parent(self)

View File

@ -44,14 +44,17 @@ class BookQuery(object):
slug = kwargs.get('slug') slug = kwargs.get('slug')
id = kwargs.get('id') id = kwargs.get('id')
module = None module = None
try:
if id is not None:
module = get_object(Module, id)
if id is not None: elif slug is not None:
module = get_object(Module, id) module = Module.objects.get(slug=slug)
elif slug is not None: return module
module = Module.objects.get(slug=slug)
return module except Module.DoesNotExist:
return None
def resolve_topic(self, info, **kwargs): def resolve_topic(self, info, **kwargs):
slug = kwargs.get('slug') slug = kwargs.get('slug')

View File

@ -6,28 +6,25 @@ from api.schema import schema
from books.factories import ModuleFactory, ChapterFactory, ContentBlockFactory from books.factories import ModuleFactory, ChapterFactory, ContentBlockFactory
from books.models import Snapshot, ChapterSnapshot from books.models import Snapshot, ChapterSnapshot
from core.tests.base_test import SkillboxTestCase from core.tests.base_test import SkillboxTestCase
from users.factories import SchoolClassFactory
from users.models import User, SchoolClass from users.models import User, SchoolClass
MODULE_QUERY = """ MODULE_QUERY = """
query ModulesQuery($slug: String!) { query ModulesQuery($slug: String, $id: ID) {
module(slug: $slug) { module(slug: $slug, id: $id) {
id id
title title
chapters { chapters {
edges { id
node { contentBlocks {
id id
contentBlocks { title
id visibleFor {
title name
visibleFor {
name
}
hiddenFor {
name
}
}
} }
hiddenFor {
name
}
} }
} }
} }
@ -226,3 +223,43 @@ class CreateSnapshotTestCase(SkillboxTestCase):
self.assertEqual(second['title'], 'hidden') self.assertEqual(second['title'], 'hidden')
self.assertEqual(second['hidden'], True) self.assertEqual(second['hidden'], True)
self.assertEqual(third['title'], 'custom') self.assertEqual(third['title'], 'custom')
def test_not_too_much_user_creator_info(self):
self.assertTrue(False)
def test_apply_initial_snapshot(self):
teacher2 = User.objects.get(username='teacher2')
teacher2_client = self.get_client(user=teacher2)
third_class = SchoolClassFactory(
users=[teacher2],
name='third_class'
)
# make a neutral snapshot, nothing new, nothing hidden
result = teacher2_client.execute(CREATE_SNAPSHOT_MUTATION, variables={
'input': {
'module': self.slug,
'selectedClass': to_global_id('SchoolClassNode', third_class.pk),
}
})
self.assertIsNone(result.get('errors'))
snapshot_id = result['data']['createSnapshot']['snapshot']['id']
result = self.client.execute(APPLY_SNAPSHOT_MUTATION, variables={
'input': {
'snapshot': snapshot_id,
'selectedClass': to_global_id('SchoolClassNode', self.skillbox_class.pk),
}
})
self.assertIsNone(result.get('errors'))
result = self.client.execute(MODULE_QUERY, variables={
'slug': self.module.slug
})
self.assertIsNone(result.get('errors'))
module = result['data']['module']
chapter1, chapter2 = module['chapters']
cb1, cb2, cb3 = chapter1['contentBlocks']
self.assertTrue(self.skillbox_class.name not in [sc['name'] for sc in cb1['hiddenFor']])
self.assertTrue(self.skillbox_class.name not in [sc['name'] for sc in cb2['hiddenFor']])
self.assertTrue(self.skillbox_class.name not in [sc['name'] for sc in cb3['visibleFor']])

View File

@ -8,9 +8,30 @@ from core.mixins import HiddenAndVisibleForMixin, HiddenForMixin
from objectives.models import ObjectiveGroup, Objective from objectives.models import ObjectiveGroup, Objective
class ObjectiveNode(DjangoObjectType, HiddenAndVisibleForMixin):
pk = graphene.Int()
user_created = graphene.Boolean()
mine = graphene.Boolean()
class Meta:
model = Objective
filter_fields = ['text']
interfaces = (relay.Node,)
def resolve_objective_progress(self, info, **kwargs):
return self.objective_progress.filter(user=info.context.user)
def resolve_user_created(self, info, **kwargs):
return self.owner is not None
def resolve_mine(self, info, **kwargs):
return self.owner is not None and self.owner.pk == info.context.user.pk
class ObjectiveGroupNode(DjangoObjectType, HiddenForMixin): class ObjectiveGroupNode(DjangoObjectType, HiddenForMixin):
pk = graphene.Int() pk = graphene.Int()
display_title = graphene.String() display_title = graphene.String()
objectives = graphene.List(ObjectiveNode)
class Meta: class Meta:
model = ObjectiveGroup model = ObjectiveGroup
@ -37,26 +58,6 @@ class ObjectiveGroupNode(DjangoObjectType, HiddenForMixin):
return self.objectives.filter(objectives_from_publisher | objectives_from_teacher) return self.objectives.filter(objectives_from_publisher | objectives_from_teacher)
class ObjectiveNode(DjangoObjectType, HiddenAndVisibleForMixin):
pk = graphene.Int()
user_created = graphene.Boolean()
mine = graphene.Boolean()
class Meta:
model = Objective
filter_fields = ['text']
interfaces = (relay.Node,)
def resolve_objective_progress(self, info, **kwargs):
return self.objective_progress.filter(user=info.context.user)
def resolve_user_created(self, info, **kwargs):
return self.owner is not None
def resolve_mine(self, info, **kwargs):
return self.owner is not None and self.owner.pk == info.context.user.pk
class ObjectivesQuery(object): class ObjectivesQuery(object):
objective_group = relay.Node.Field(ObjectiveGroupNode) objective_group = relay.Node.Field(ObjectiveGroupNode)
objective_groups = DjangoFilterConnectionField(ObjectiveGroupNode) objective_groups = DjangoFilterConnectionField(ObjectiveGroupNode)