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: {
'@': resolve('src'),
styles: resolve('src/styles'),
gql: resolve('src/graphql/gql')
},
},
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 {
@include default-link;
color: $color-brand;
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 "../../fragments/assignmentParts.gql"
#import "../../fragments/objectiveGroupParts.gql"
#import "../../fragments/objectiveParts.gql"
#import "../../fragments/moduleParts.gql"
#import "../../fragments/contentBlockInterfaceParts.gql"
#import "../../fragments/contentBlockParts.gql"
query ModuleDetailsQuery($slug: String!) {
module(slug: $slug) {
#import "gql/fragments/chapterParts.gql"
#import "gql/fragments/assignmentParts.gql"
#import "gql/fragments/objectiveGroupParts.gql"
#import "gql/fragments/objectiveParts.gql"
#import "gql/fragments/moduleParts.gql"
#import "gql/fragments/contentBlockInterfaceParts.gql"
#import "gql/fragments/contentBlockParts.gql"
query ModuleDetailsQuery($slug: String, $id: ID) {
module(slug: $slug, id: $id) {
...ModuleParts
assignments {
edges {
@ -16,22 +16,12 @@ query ModuleDetailsQuery($slug: String!) {
}
}
objectiveGroups {
edges {
node {
...ObjectiveGroupParts
objectives {
edges {
node {
...ObjectiveParts
}
}
}
}
}
}
chapters {
edges {
node {
...ChapterParts
contentBlocks {
...ContentBlockInterfaceParts
@ -40,5 +30,3 @@ query ModuleDetailsQuery($slug: String!) {
}
}
}
}
}

View File

@ -1,7 +1,7 @@
import ADD_NOTE_MUTATION from '@/graphql/gql/mutations/addNote.gql';
import CONTENT_BLOCK_QUERY from '@/graphql/gql/queries/contentBlockQuery.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';
const getBlockType = id => atob(id).split(':')[0];

View File

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

View File

@ -1,11 +1,23 @@
<template>
<module :module="snapshot"/>
<div class="snapshot">
<header class="snapshot__header">
<snapshot-header
:snapshot="snapshot"
/>
</header>
<module
:module="snapshot"
class="snapshot__module"/>
</div>
</template>
<script>
import SNAPSHOT_DETAIL_QUERY from '@/graphql/gql/queries/snapshots/detail.gql';
import Module from '@/components/modules/Module';
import Checkbox from '@/components/ui/Checkbox';
import SnapshotHeader from '@/components/modules/SnapshotHeader';
export default {
props: {
@ -16,6 +28,8 @@
},
components: {
SnapshotHeader,
Checkbox,
Module,
},
@ -37,4 +51,21 @@
<style scoped lang="scss">
@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>

View File

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

View File

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

View File

@ -11,6 +11,7 @@ import joinTeam from '@/pages/me/joinTeam';
import createTeam from '@/pages/me/createTeam';
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 [
{
@ -32,23 +33,23 @@ export default [
alias: 'create-class',
name: 'create-class',
component: createClass,
meta: {layout: 'simple'},
meta: {layout: LAYOUT_SIMPLE},
},
{
path: 'class/code',
alias: 'show-code',
name: SHOW_SCHOOL_CLASS_CODE,
component: showSchoolClassCode,
meta: {layout: 'simple'},
meta: {layout: LAYOUT_SIMPLE},
},
{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/create', name: CREATE_TEAM, component: createTeam, 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: LAYOUT_SIMPLE}},
{
path: 'team/code',
name: SHOW_TEAM_CODE,
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 submissions from '@/pages/submissions';
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 snapshots from '@/pages/snapshot/snapshots';
import snapshot from '@/pages/snapshot/snapshot';
import {LAYOUT_SIMPLE} from '@/router/core.constants';
export default [
{
@ -40,7 +48,7 @@ export default [
name: VISIBILITY_PAGE,
component: moduleVisibility,
meta: {
layout: 'simple',
layout: LAYOUT_SIMPLE,
hideNavigation: true,
},
},
@ -56,8 +64,13 @@ export default [
path: 'snapshot/:id',
component: snapshot,
name: SNAPSHOT_DETAIL,
props: true
}
props: true,
meta: {
layout: LAYOUT_SIMPLE,
hideNavigation: true,
fullWidth: true
},
},
],
},
];

View File

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

View File

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

View File

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

View File

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

View File

@ -6,16 +6,15 @@ from api.schema import schema
from books.factories import ModuleFactory, ChapterFactory, ContentBlockFactory
from books.models import Snapshot, ChapterSnapshot
from core.tests.base_test import SkillboxTestCase
from users.factories import SchoolClassFactory
from users.models import User, SchoolClass
MODULE_QUERY = """
query ModulesQuery($slug: String!) {
module(slug: $slug) {
query ModulesQuery($slug: String, $id: ID) {
module(slug: $slug, id: $id) {
id
title
chapters {
edges {
node {
id
contentBlocks {
id
@ -30,8 +29,6 @@ query ModulesQuery($slug: String!) {
}
}
}
}
}
"""
CREATE_SNAPSHOT_MUTATION = """
@ -226,3 +223,43 @@ class CreateSnapshotTestCase(SkillboxTestCase):
self.assertEqual(second['title'], 'hidden')
self.assertEqual(second['hidden'], True)
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
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):
pk = graphene.Int()
display_title = graphene.String()
objectives = graphene.List(ObjectiveNode)
class Meta:
model = ObjectiveGroup
@ -37,26 +58,6 @@ class ObjectiveGroupNode(DjangoObjectType, HiddenForMixin):
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):
objective_group = relay.Node.Field(ObjectiveGroupNode)
objective_groups = DjangoFilterConnectionField(ObjectiveGroupNode)