Merge branch 'develop'

This commit is contained in:
Ramon Wenger 2020-10-08 10:10:20 +02:00
commit ffd87e57bf
34 changed files with 499 additions and 245 deletions

View File

@ -12,7 +12,7 @@
@edit-note="editNote"
@bookmark="bookmark(!chapter.bookmark)"
/>
<p class="chapter__description">
<p class="chapter__description intro">
{{ chapter.description }}
</p>

View File

@ -64,7 +64,7 @@
grid-template-columns: 1fr 1fr 1fr;
@include desktop {
grid-template-columns: 50px 1fr 200px;
grid-template-columns: 50px 1fr auto;
grid-template-rows: 50px;
grid-auto-rows: auto;
}

View File

@ -13,6 +13,7 @@
<p
class="solution__text solution-text fade"
data-cy="solution-text"
v-if="visible"
v-html="value.text"/>
</transition>

View File

@ -20,7 +20,7 @@
@edit-note="editNote"
@bookmark="bookmark(!module.bookmark)"/>
<div
class="module__intro"
class="module__intro intro"
v-html="module.intro"/>
</div>
@ -30,6 +30,8 @@
<objective-groups :groups="societyObjectiveGroups"/>
<objective-groups :groups="interdisciplinaryObjectiveGroups"/>
<chapter
:chapter="chapter"
:index="index"
@ -86,6 +88,11 @@
.filter(group => group.title === 'SOCIETY')
.sort(withoutOwnerFirst) : [];
},
interdisciplinaryObjectiveGroups() {
return this.module.objectiveGroups ? this.module.objectiveGroups
.filter(group => group.title === 'INTERDISCIPLINARY')
.sort(withoutOwnerFirst) : [];
},
isStudent() {
return !this.me.permissions.includes('users.can_manage_school_class_content');
},

View File

@ -4,10 +4,10 @@
<img
:src="teaser.imageUrl"
class="news-teaser__image">
<p class="news-teaser__image-source">
<a
:href="teaser.imageSource"
class="tiny-text">Quelle {{ teaser.imageSource }}</a></p>
<a
:href="teaser.imageSource"
class="news-teaser__image-source">Quelle {{ teaser.imageSource }}</a>
<h4 class="news-teaser__title">{{ teaser.title }}</h4>
<p class="news-teaser__description">{{ teaser.description }}</p>
<p class="news-teaser__date">{{ teaser.displayDate }}</p>
@ -16,52 +16,54 @@
</template>
<script>
export default {
props: {
teaser: {
type: Object,
default: () => {
return {};
}
export default {
props: {
teaser: {
type: Object,
default: () => {
return {};
}
}
};
}
};
</script>
<style scoped lang="scss">
@import "@/styles/_variables.scss";
@import "@/styles/_functions.scss";
@import "@/styles/_mixins.scss";
@import "@/styles/_variables.scss";
@import "@/styles/_functions.scss";
@import "@/styles/_mixins.scss";
.news-teaser {
position: relative;
padding-bottom: $large-spacing;
.news-teaser {
position: relative;
&__image {
display: block;
max-width: 100%;
height: auto;
&__image {
display: block;
max-width: 100%;
height: auto;
@include desktop {
max-width: $news_width;
}
}
&__image-source {
line-height: 25px;
}
&__description {
margin-bottom: $large-spacing;
}
&__date {
font-family: $sans-serif-font-family;
font-weight: $font-weight-regular;
color: $color-silver-dark;
position: absolute;
bottom: 0;
left: 0;
@include desktop {
max-width: $news-width;
}
}
&__image-source {
line-height: 25px;
@include tiny-text;
margin-bottom: $medium-spacing;
display: block;
}
&__title {
margin-bottom: $small-spacing;
}
&__description {
margin-bottom: $small-spacing;
}
&__date {
@include regular-text;
color: $color-silver-dark;
}
}
</style>

View File

@ -35,14 +35,15 @@
display: grid;
}
margin-bottom: $large-spacing;
grid-gap: 40px;
grid-gap: $large-spacing;
@include desktop {
grid-column-gap: 40px;
grid-template-columns: repeat(auto-fit, minmax(320px, $news_width));
grid-column-gap: $large-spacing;
grid-row-gap: $section-spacing;
grid-template-columns: repeat(auto-fit, minmax(320px, $news-width));
grid-auto-rows: minmax(400px, auto);
grid-template-rows: auto auto;
-ms-grid-columns: $news_width $news_width;
-ms-grid-columns: $news-width $news-width;
}
}
</style>

View File

@ -20,7 +20,6 @@
<div>
{{ objective.text }}
</div>
</li>
</template>

View File

@ -37,8 +37,9 @@
margin-bottom: 25px;
justify-content: center;
align-items: center;
break-inside: avoid;
break-inside: avoid-page;
cursor: pointer;
overflow: hidden;
display: none;
@include desktop {

View File

@ -50,7 +50,7 @@
import CurrentClass from '@/components/school-class/CurrentClass';
import AddIcon from '@/components/icons/AddIcon';
import updateSelectedClassMixin from '@/mixins/updateSelectedClass';
import updateSelectedClassMixin from '@/mixins/update-selected-class';
import sidebarMixin from '@/mixins/sidebar';
import meMixin from '@/mixins/me';

View File

@ -1,6 +1,7 @@
fragment InstrumentParts on InstrumentNode {
id
title
intro
slug
bookmarks {
uuid

View File

@ -1,5 +1,19 @@
const extractAnswerFromQuestion = (previous, question) => {
return [...previous, {title: question.title, answer: question.correctAnswer}];
let answer = question.correctAnswer;
if (question.getType() === 'matrix') {
const correctAnswer = question.correctAnswer;
const questionRows = question.getRows();
const keys = questionRows.map(question => {
const text = question.value;
if (/[,.!?]/.test(text.slice(-1))) {
return text.slice(0, -1);
}
return text;
}); // get the keys as they appear in the question, without punctuation at the end
answer = keys.map(key => `${key}: ${correctAnswer[key]}`); // return an array, it gets converted to a string further up
}
return [...previous, {title: question.title, answer, type: question.getType()}];
};
export const extractSurveySolutions = (prev, element) => {

View File

@ -79,16 +79,33 @@
max-width: $footer-width;
padding: 2*$large-spacing 0;
display: flex;
justify-content: space-between;
flex-direction: column;
@include desktop {
flex-direction: row;
justify-content: space-between;
}
}
&__who-are-we {
width: 330px;
width: 100%;
margin-bottom: $large-spacing;
@include desktop {
width: 330px;
margin-bottom: 0;
}
}
&__logo-hep {
width: 147px;
width: auto;
height: 35px;
margin-bottom: $large-spacing;
@include desktop {
width: 147px;
margin-bottom: 0;
}
}
&__logo-ehb {
@ -100,11 +117,22 @@
width: 100%;
max-width: $footer-width;
padding: $large-spacing 0;
display: flex;
flex-direction: column;
@include desktop {
flex-direction: row;
}
}
&__link {
@include aside-with-cheese;
margin-right: $large-spacing;
margin-bottom: $small-spacing;
@include desktop {
margin-bottom: 0;
}
}
}

View File

@ -2,6 +2,10 @@
<div class="instrument">
<h1 class="instrument__title">{{ instrument.title }}</h1>
<div
class="instrument__intro intro"
v-html="instrument.intro"/>
<content-component
:key="component.id"
:component="component"

View File

@ -18,7 +18,7 @@
<script>
import OLD_CLASSES_QUERY from '@/graphql/gql/oldClasses.gql';
import updateSelectedClassMixin from '@/mixins/updateSelectedClass';
import updateSelectedClassMixin from '@/mixins/update-selected-class';
export default {
mixins: [updateSelectedClassMixin],

View File

@ -160,7 +160,7 @@
}
&__modules {
margin-bottom: $large-spacing;
margin-bottom: $section-spacing;
}
&__modules-list {
@ -201,6 +201,10 @@
-ms-grid-column: 3;
}
}
&__news {
margin-bottom: $section-spacing;
}
}
.news {

View File

@ -15,207 +15,206 @@
</template>
<script>
import * as SurveyVue from 'survey-vue';
import {css} from '@/survey.config';
import * as SurveyVue from 'survey-vue';
import {css} from '@/survey.config';
import SURVEY_QUERY from '@/graphql/gql/surveyQuery.gql';
import MODULE_QUERY from '@/graphql/gql/moduleByIdQuery.gql';
import UPDATE_ANSWER from '@/graphql/gql/mutations/updateAnswer.gql';
import Solution from '@/components/content-blocks/Solution';
import SURVEY_QUERY from '@/graphql/gql/surveyQuery.gql';
import MODULE_QUERY from '@/graphql/gql/moduleByIdQuery.gql';
import UPDATE_ANSWER from '@/graphql/gql/mutations/updateAnswer.gql';
import Solution from '@/components/content-blocks/Solution';
import {extractSurveySolutions} from '@/helpers/survey-solutions';
import {isTeacher} from '@/helpers/is-teacher';
import {extractSurveySolutions} from '@/helpers/survey-solutions';
import {isTeacher} from '@/helpers/is-teacher';
import {meQuery} from '@/graphql/queries';
import {meQuery} from '@/graphql/queries';
const Survey = SurveyVue.Survey;
const Survey = SurveyVue.Survey;
export default {
props: ['id'],
export default {
props: ['id'],
components: {
Solution,
Survey
components: {
Solution,
Survey
},
data() {
return {
survey: this.initSurvey(),
title: '',
module: {},
completed: false,
me: {
permissions: []
},
};
},
computed: {
surveyComplete() {
return this.survey && this.survey.isCompleted;
},
data() {
showSolution() {
return (module.solutionsEnabled || isTeacher) && !this.survey.isCompleted;
},
solution() {
// todo: should this be done inside of Solution.vue?
return {
survey: this.initSurvey(),
title: '',
module: {},
completed: false,
me: {
permissions: []
},
};
},
computed: {
surveyComplete() {
return this.survey && this.survey.isCompleted;
},
showSolution() {
return (module.solutionsEnabled || isTeacher) && !this.survey.isCompleted;
},
solution() {
return {
text: this.answers.reduce((previous, answer) => {
if (!answer.answer) {
return previous;
}
let answerText;
if (typeof answer.answer === 'object') {
// this means the answer comes from a matrix, where the keys are the labels and the values are the respective answers
let answerObject = answer.answer;
let keysAndValues = [];
for (let prop of Object.keys(answerObject)) {
keysAndValues.push(`${prop}: ${answerObject[prop]}`);
}
answerText = keysAndValues.join(', ');
} else {
answerText = answer.answer;
}
text: this.answers.reduce((previous, answer) => {
if (!answer.answer) {
return previous;
}
if (answer.type === 'matrix') {
// wrap all the answers inside li tags and convert to a single string
const answerText = answer.answer.map(a => `<li class="solution-text__list-item">${a}</li>`).join('');
return `
${previous}
<h2 class="solution-text__heading">${answer.title}</h2>
<ul class="solution-text__answer solution-text__list">${answerText}</ul>
`;
} else {
return `
${previous}
<h2 class="solution-text__heading">${answer.title}</h2>
<p class="solution-text__answer">${answerText}</p>
<p class="solution-text__answer">${answer.answer}</p>
`;
}, '')
};
},
answers() {
return this.survey.currentPage && this.survey.currentPage.elements
? this.survey.currentPage.elements.reduce(extractSurveySolutions, [])
: [];
},
isTeacher() {
return isTeacher(this);
}
}
}, '')
};
},
answers() {
return this.survey.currentPage && this.survey.currentPage.elements
? this.survey.currentPage.elements.reduce(extractSurveySolutions, [])
: [];
},
isTeacher() {
return isTeacher(this);
}
},
methods: {
initSurvey(data, answers) {
let survey = new SurveyVue.Model(data);
const flatAnswers = {};
for (let k in answers) {
flatAnswers[k] = answers[k].answer;
methods: {
initSurvey(data, answers) {
let survey = new SurveyVue.Model(data);
const flatAnswers = {};
for (let k in answers) {
flatAnswers[k] = answers[k].answer;
}
survey.data = flatAnswers;
const saveSurvey = (sender, options) => {
// sender.clear(false);
//
// sender.mode = 'display';
this.completed = true;
const data = {};
for (let k in survey.data) {
if (survey.data.hasOwnProperty(k)) {
let question = sender.getQuestionByName(k);
data[k] = {
answer: survey.data[k],
correct: question && question.correctAnswer ? question.correctAnswer : ''
};
}
}
survey.data = flatAnswers;
const saveSurvey = (sender, options) => {
// sender.clear(false);
//
// sender.mode = 'display';
this.completed = true;
const data = {};
for (let k in survey.data) {
if (survey.data.hasOwnProperty(k)) {
let question = sender.getQuestionByName(k);
data[k] = {
answer: survey.data[k],
correct: question && question.correctAnswer ? question.correctAnswer : ''
};
this.$apollo.mutate({
mutation: UPDATE_ANSWER,
variables: {
input: {
answer: {
surveyId: this.id,
data: JSON.stringify(data)
}
}
},
// fixme: make the update work instead of refetching
update: (store, {data: {updateAnswer: {answer}}}) => {
const query = SURVEY_QUERY;
const variables = {id: this.id};
const queryData = store.readQuery({query, variables});
if (queryData.survey) {
queryData.survey.answer = answer;
store.writeQuery({query, variables, data: queryData});
}
}
});
};
this.$apollo.mutate({
mutation: UPDATE_ANSWER,
survey.onComplete.add(saveSurvey);
survey.onCurrentPageChanged.add(saveSurvey);
survey.css = css;
survey.locale = 'de';
survey.showProgressBar = 'bottom';
survey.pageNextText = 'Speichern & Weiter';
return survey;
},
reopen() {
this.completed = false;
let data = this.survey.data; // save the data
this.survey.clear();
this.survey.data = data; // reapply it
}
},
apollo: {
survey: {
query: SURVEY_QUERY,
variables() {
return {
id: this.id
};
},
manual: true,
result({data, loading, networkStatus}) {
if (!loading) {
let json = JSON.parse(data.survey.data);
json.showTitle = false;
let answer = {};
if (data.survey.answer && data.survey.answer.data) {
answer = JSON.parse(data.survey.answer.data);
}
if (!this.completed) {
this.survey = this.initSurvey(json, answer);
}
this.title = json.title;
const module = data.survey.module;
this.$apollo.addSmartQuery('module', {
query: MODULE_QUERY,
variables: {
input: {
answer: {
surveyId: this.id,
data: JSON.stringify(data)
}
}
},
// fixme: make the update work instead of refetching
update: (store, {data: {updateAnswer: {answer}}}) => {
const query = SURVEY_QUERY;
const variables = {id: this.id};
const queryData = store.readQuery({query, variables});
if (queryData.survey) {
queryData.survey.answer = answer;
store.writeQuery({query, variables, data: queryData});
}
id: module.id
}
});
};
survey.onComplete.add(saveSurvey);
survey.onCurrentPageChanged.add(saveSurvey);
survey.css = css;
survey.locale = 'de';
survey.showProgressBar = 'bottom';
survey.pageNextText = 'Speichern & Weiter';
return survey;
}
},
reopen() {
this.completed = false;
let data = this.survey.data; // save the data
this.survey.clear();
this.survey.data = data; // reapply it
}
},
apollo: {
survey: {
query: SURVEY_QUERY,
variables() {
return {
id: this.id
};
},
manual: true,
result({data, loading, networkStatus}) {
if (!loading) {
let json = JSON.parse(data.survey.data);
json.showTitle = false;
let answer = {};
if (data.survey.answer && data.survey.answer.data) {
answer = JSON.parse(data.survey.answer.data);
}
if (!this.completed) {
this.survey = this.initSurvey(json, answer);
}
this.title = json.title;
const module = data.survey.module;
this.$apollo.addSmartQuery('module', {
query: MODULE_QUERY,
variables: {
id: module.id
}
});
}
},
},
me: meQuery
}
};
me: meQuery
}
};
</script>
<style scoped lang="scss">
@import "@/styles/_variables.scss";
@import "@/styles/_mixins.scss";
@import "@/styles/_variables.scss";
@import "@/styles/_mixins.scss";
.survey-page {
max-width: 800px;
display: grid;
grid-template-rows: auto 1fr;
grid-auto-rows: auto;
grid-row-gap: $large-spacing;
justify-self: center;
padding: 100px 0;
width: 100%;
.survey-page {
max-width: 800px;
display: grid;
grid-template-rows: auto 1fr;
grid-auto-rows: auto;
grid-row-gap: $large-spacing;
justify-self: center;
padding: 100px 0;
width: 100%;
&__title {
@include meta-title;
margin: 0;
}
&__title {
@include meta-title;
margin: 0;
}
}
</style>

View File

@ -15,7 +15,7 @@
@supports (display: grid) {
display: grid;
}
grid-template-rows: auto 1fr auto;
grid-template-rows: auto 1fr max-content;
grid-template-areas: "h" "c" "f";
min-height: 100vh;
grid-auto-rows: 1fr;
@ -46,9 +46,27 @@
grid-area: h;
}
&__content {
padding: 0 $small-spacing;
@include desktop {
padding: 0;
}
}
&__footer {
grid-area: f;
// we usually set the margin to the bottom and the right, but here we want the footer to always have
// this margin, and we don't want to set it on every content element. And we don't want to set it on
// the content element, for if there's no footer.
margin-top: 3*$large-spacing;
margin-bottom: -3*$large-spacing;
padding: 0 $small-spacing;
@include desktop {
padding: 0;
}
}
}

View File

@ -0,0 +1,7 @@
.intro {
@include lead-paragraph;
> p {
@include lead-paragraph;
}
}

View File

@ -37,6 +37,7 @@
}
&--blue {
background-color: $color-accent-2;
& .widget-footer {
background-color: $color-accent-2-dark;
}
@ -44,12 +45,14 @@
}
&--red {
background-color: $color-accent-3;
& .widget-footer {
background-color: $color-accent-3-dark;
}
}
&--green {
background-color: $color-accent-4;
& .widget-footer {
background-color: $color-accent-4-dark;
}
@ -118,6 +121,13 @@
font-size: toRem(14px);
}
@mixin tiny-text {
font-size: toRem(11px);
font-family: $sans-serif-font-family;
font-weight: $font-weight-regular;
color: $color-silver-dark;
}
@mixin aside-text {
@include regular-text;
font-size: toRem(14px);

View File

@ -4,8 +4,8 @@
.room {
display: grid;
grid-template-rows: auto 1fr;
margin-bottom: -50px;
grid-template-rows: max-content 1fr;
margin-bottom: -90px;
&__header {
padding: 30px;

View File

@ -8,4 +8,14 @@
@include regular-text;
margin-bottom: $medium-spacing;
}
&__list {
list-style: disc;
padding-left: $medium-spacing;
}
&__list-item {
@include inline-title;
margin-bottom: $medium-spacing;
}
}

View File

@ -82,9 +82,3 @@ input, textarea, select, button {
color: $color-brand-dark;
}
.tiny-text {
font-size: toRem(11px);
font-family: $sans-serif-font-family;
font-weight: $font-weight-regular;
color: $color-silver-dark;
}

View File

@ -65,6 +65,7 @@ $default-padding: 30px;
$small-spacing: 10px;
$medium-spacing: 20px;
$large-spacing: 30px;
$section-spacing: 60px;
$font-weight-bold: 700;
$font-weight-semibold: 600;
@ -77,4 +78,4 @@ $default-heading-line-height: 1.2;
$popover-default-bottom: -110px;
$footer-width: 800px;
$news_width: 550px;
$news-width: 550px;

View File

@ -27,3 +27,4 @@
@import "simple-list";
@import "widget-popover";
@import "toast";
@import "intro";

View File

@ -0,0 +1,19 @@
# Generated by Django 2.2.12 on 2020-09-29 07:54
from django.db import migrations
import wagtail.core.fields
class Migration(migrations.Migration):
dependencies = [
('basicknowledge', '0006_auto_20200520_0954'),
]
operations = [
migrations.AddField(
model_name='basicknowledge',
name='intro',
field=wagtail.core.fields.RichTextField(blank=True, default=''),
),
]

View File

@ -1,16 +1,18 @@
from django.db import models
from wagtail.admin.edit_handlers import FieldPanel, StreamFieldPanel
from wagtail.core.fields import StreamField
from wagtail.core.fields import StreamField, RichTextField
from wagtail.images.blocks import ImageChooserBlock
from books.blocks import LinkBlock, VideoBlock, DocumentBlock, SectionTitleBlock, InfogramBlock, \
GeniallyBlock, InstrumentTextBlock, SubtitleBlock, ThinglinkBlock
GeniallyBlock, InstrumentTextBlock, SubtitleBlock, ThinglinkBlock, DEFAULT_RICH_TEXT_FEATURES
from core.wagtail_utils import StrictHierarchyPage
class BasicKnowledge(StrictHierarchyPage):
parent_page_types = ['books.book']
intro = RichTextField(features=DEFAULT_RICH_TEXT_FEATURES, default='', blank=True)
contents = StreamField([
('text_block', InstrumentTextBlock()),
('image_block', ImageChooserBlock()),
@ -42,6 +44,7 @@ class BasicKnowledge(StrictHierarchyPage):
content_panels = [
FieldPanel('title', classname="full title"),
FieldPanel('type'),
FieldPanel('intro'),
StreamFieldPanel('contents')
]

View File

@ -17,7 +17,7 @@ class InstrumentNode(DjangoObjectType):
filter_fields = ['slug', 'type']
interfaces = (relay.Node,)
only_fields = [
'slug', 'title', 'type', 'contents',
'slug', 'title', 'intro', 'type', 'contents',
]
def resolve_bookmarks(self, info, **kwargs):

View File

@ -12,7 +12,7 @@ class ObjectiveGroupAdmin(admin.ModelAdmin):
@admin.register(Objective)
class ObjectiveAdmin(admin.ModelAdmin):
list_display = ('text', 'get_topic', 'group', 'owner')
list_display = ('text', 'get_topic', 'group', 'order', 'owner')
list_filter = ('group', 'owner')
def get_topic(self, obj):

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.14 on 2020-09-28 15:47
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('objectives', '0008_auto_20190821_1252'),
]
operations = [
migrations.AlterField(
model_name='objectivegroup',
name='title',
field=models.CharField(blank=True, choices=[('language_communication', 'Sprache & Kommunikation'), ('society', 'Gesellschaft'), ('interdisciplinary', 'Überfachliche Lernziele')], default='language_communication', max_length=255, verbose_name='title'),
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 2.2.14 on 2020-09-30 13:23
from django.db import migrations, models
import django.db.models.expressions
class Migration(migrations.Migration):
dependencies = [
('objectives', '0009_auto_20200928_1547'),
]
operations = [
migrations.AlterModelOptions(
name='objective',
options={'ordering': [django.db.models.expressions.OrderBy(django.db.models.expressions.F('owner'), nulls_first=True), django.db.models.expressions.OrderBy(django.db.models.expressions.F('order'), nulls_last=True)], 'verbose_name': 'Lernziel', 'verbose_name_plural': 'Lernziele'},
),
migrations.AddField(
model_name='objective',
name='order',
field=models.IntegerField(null=True),
),
]

View File

@ -1,5 +1,6 @@
from django.contrib.auth import get_user_model
from django.db import models
from django.db.models import F
from books.models import Module
from users.models import SchoolClass
@ -12,10 +13,12 @@ class ObjectiveGroup(models.Model):
LANGUAGE_COMMUNICATION = 'language_communication'
SOCIETY = 'society'
INTERDISCIPLINARY = 'interdisciplinary'
TITLE_CHOICES = (
(LANGUAGE_COMMUNICATION, 'Sprache & Kommunikation'),
(SOCIETY, 'Gesellschaft'),
(INTERDISCIPLINARY, 'Überfachliche Lernziele'),
)
title = models.CharField('title', blank=True, null=False, max_length=255, choices=TITLE_CHOICES, default=LANGUAGE_COMMUNICATION)
@ -34,6 +37,7 @@ class Objective(models.Model):
class Meta:
verbose_name = 'Lernziel'
verbose_name_plural = 'Lernziele'
ordering = [F('owner').asc(nulls_first=True), F('order').asc(nulls_last=True)]
text = models.CharField('text', blank=True, null=False, max_length=255)
group = models.ForeignKey(ObjectiveGroup, blank=False, null=False, on_delete=models.CASCADE,
@ -41,6 +45,7 @@ class Objective(models.Model):
owner = models.ForeignKey(get_user_model(), blank=True, null=True, on_delete=models.CASCADE)
hidden_for = models.ManyToManyField(SchoolClass, related_name='hidden_objectives', blank=True)
visible_for = models.ManyToManyField(SchoolClass, related_name='visible_objectives', blank=True)
order = models.IntegerField(null=True, blank=True)
def __str__(self):
return 'Objective {}-{}'.format(self.id, self.text)

View File

@ -0,0 +1,66 @@
from django.test import TestCase, RequestFactory
from graphene.test import Client
from graphql_relay import to_global_id
from api.schema import schema
from api.utils import get_object, get_graphql_mutation
from books.models import ContentBlock, Chapter
from books.factories import ModuleFactory
from core.factories import UserFactory
from core.management.commands import create_teacher
from notes.factories import ChapterBookmarkFactory, ModuleBookmarkFactory
from objectives.factories import ObjectiveGroupFactory
from objectives.models import Objective
from users.models import User
from users.services import create_users
class ObjectiveOrderTestCase(TestCase):
def setUp(self):
create_users()
self.user = user = User.objects.get(username='teacher')
self.objective_group = ObjectiveGroupFactory(owner=None)
request = RequestFactory().get('/')
request.user = user
self.client = Client(schema=schema, context_value=request)
Objective.objects.create(owner=None, text='first', group=self.objective_group, order=0)
Objective.objects.create(owner=None, text='second', group=self.objective_group, order=1)
Objective.objects.create(owner=None, text='third', group=self.objective_group)
Objective.objects.create(owner=user, text='fourth', group=self.objective_group)
def test_objective_order(self):
query = """
query ObjectiveGroupQuery($id: ID!) {
objectiveGroup(id: $id) {
objectives {
edges {
node {
id
text
}
}
}
}
}
"""
result = self.client.execute(query, variables={
'id': to_global_id('ObjectiveGroupNode', self.objective_group.pk)
})
self.assertIsNone(result.get('errors'))
objective_nodes = result.get('data').get('objectiveGroup').get('objectives').get('edges')
objective1, objective2, objective3, objective4 = [node['node'] for node in objective_nodes]
self.assertEqual(objective1.get('text'), 'first')
self.assertEqual(objective2.get('text'), 'second')
self.assertEqual(objective3.get('text'), 'third')
self.assertEqual(objective4.get('text'), 'fourth')

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.14 on 2020-09-28 15:47
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0023_user_onboarding_visited'),
]
operations = [
migrations.AlterField(
model_name='license',
name='isbn',
field=models.CharField(default='978-3-0355-1397-4', max_length=50),
),
]