diff --git a/client/src/components/Chapter.vue b/client/src/components/Chapter.vue index 0b3a349e..85cd8d98 100644 --- a/client/src/components/Chapter.vue +++ b/client/src/components/Chapter.vue @@ -12,7 +12,7 @@ @edit-note="editNote" @bookmark="bookmark(!chapter.bookmark)" /> -

+

{{ chapter.description }}

diff --git a/client/src/components/HeaderBar.vue b/client/src/components/HeaderBar.vue index 599b1474..32600ecf 100644 --- a/client/src/components/HeaderBar.vue +++ b/client/src/components/HeaderBar.vue @@ -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; } diff --git a/client/src/components/content-blocks/Solution.vue b/client/src/components/content-blocks/Solution.vue index c249bc96..c42c7026 100644 --- a/client/src/components/content-blocks/Solution.vue +++ b/client/src/components/content-blocks/Solution.vue @@ -13,6 +13,7 @@

diff --git a/client/src/components/modules/Module.vue b/client/src/components/modules/Module.vue index 9a55eee4..88bd9c67 100644 --- a/client/src/components/modules/Module.vue +++ b/client/src/components/modules/Module.vue @@ -20,7 +20,7 @@ @edit-note="editNote" @bookmark="bookmark(!module.bookmark)"/>

@@ -30,6 +30,8 @@ + + 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'); }, diff --git a/client/src/components/news/NewsTeaser.vue b/client/src/components/news/NewsTeaser.vue index b249fde5..2909b707 100644 --- a/client/src/components/news/NewsTeaser.vue +++ b/client/src/components/news/NewsTeaser.vue @@ -4,10 +4,10 @@ -

- Quelle {{ teaser.imageSource }}

+ Quelle {{ teaser.imageSource }} +

{{ teaser.title }}

{{ teaser.description }}

{{ teaser.displayDate }}

@@ -16,52 +16,54 @@ diff --git a/client/src/components/news/NewsTeasers.vue b/client/src/components/news/NewsTeasers.vue index 0efd518c..79c25014 100644 --- a/client/src/components/news/NewsTeasers.vue +++ b/client/src/components/news/NewsTeasers.vue @@ -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; } } diff --git a/client/src/components/objective-groups/Objective.vue b/client/src/components/objective-groups/Objective.vue index ff0a720c..cc9a625e 100644 --- a/client/src/components/objective-groups/Objective.vue +++ b/client/src/components/objective-groups/Objective.vue @@ -20,7 +20,6 @@
{{ objective.text }}
- diff --git a/client/src/components/rooms/AddRoomEntryButton.vue b/client/src/components/rooms/AddRoomEntryButton.vue index e2465a30..09f550e1 100644 --- a/client/src/components/rooms/AddRoomEntryButton.vue +++ b/client/src/components/rooms/AddRoomEntryButton.vue @@ -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 { diff --git a/client/src/components/school-class/ClassSelectionWidget.vue b/client/src/components/school-class/ClassSelectionWidget.vue index e5786d16..f4415289 100644 --- a/client/src/components/school-class/ClassSelectionWidget.vue +++ b/client/src/components/school-class/ClassSelectionWidget.vue @@ -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'; diff --git a/client/src/graphql/gql/fragments/instrumentParts.gql b/client/src/graphql/gql/fragments/instrumentParts.gql index 73c2bb1e..98538201 100644 --- a/client/src/graphql/gql/fragments/instrumentParts.gql +++ b/client/src/graphql/gql/fragments/instrumentParts.gql @@ -1,6 +1,7 @@ fragment InstrumentParts on InstrumentNode { id title + intro slug bookmarks { uuid diff --git a/client/src/helpers/survey-solutions.js b/client/src/helpers/survey-solutions.js index 835a7eeb..3e2b82ae 100644 --- a/client/src/helpers/survey-solutions.js +++ b/client/src/helpers/survey-solutions.js @@ -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) => { diff --git a/client/src/layouts/DefaultFooter.vue b/client/src/layouts/DefaultFooter.vue index eaea9cf4..555feb65 100644 --- a/client/src/layouts/DefaultFooter.vue +++ b/client/src/layouts/DefaultFooter.vue @@ -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; + } } } diff --git a/client/src/mixins/updateSelectedClass.js b/client/src/mixins/update-selected-class.js similarity index 100% rename from client/src/mixins/updateSelectedClass.js rename to client/src/mixins/update-selected-class.js diff --git a/client/src/pages/instrument.vue b/client/src/pages/instrument.vue index 041af1ec..ace59f8e 100644 --- a/client/src/pages/instrument.vue +++ b/client/src/pages/instrument.vue @@ -2,6 +2,10 @@

{{ instrument.title }}

+
+ 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], diff --git a/client/src/pages/start.vue b/client/src/pages/start.vue index c0759a01..72024c65 100644 --- a/client/src/pages/start.vue +++ b/client/src/pages/start.vue @@ -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 { diff --git a/client/src/pages/survey.vue b/client/src/pages/survey.vue index 8f2a04f0..8889bc6c 100644 --- a/client/src/pages/survey.vue +++ b/client/src/pages/survey.vue @@ -15,207 +15,206 @@ diff --git a/client/src/styles/_default-layout.scss b/client/src/styles/_default-layout.scss index 3576be7c..8817be06 100644 --- a/client/src/styles/_default-layout.scss +++ b/client/src/styles/_default-layout.scss @@ -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; + } } } diff --git a/client/src/styles/_intro.scss b/client/src/styles/_intro.scss new file mode 100644 index 00000000..9a7e8986 --- /dev/null +++ b/client/src/styles/_intro.scss @@ -0,0 +1,7 @@ +.intro { + @include lead-paragraph; + + > p { + @include lead-paragraph; + } +} diff --git a/client/src/styles/_mixins.scss b/client/src/styles/_mixins.scss index 3c86c941..f553611b 100644 --- a/client/src/styles/_mixins.scss +++ b/client/src/styles/_mixins.scss @@ -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); diff --git a/client/src/styles/_room.scss b/client/src/styles/_room.scss index abb484af..015580c4 100644 --- a/client/src/styles/_room.scss +++ b/client/src/styles/_room.scss @@ -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; diff --git a/client/src/styles/_solutions.scss b/client/src/styles/_solutions.scss index ae034f0c..84a5ed6e 100644 --- a/client/src/styles/_solutions.scss +++ b/client/src/styles/_solutions.scss @@ -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; + } } diff --git a/client/src/styles/_typography.scss b/client/src/styles/_typography.scss index c9be50cd..6e37a8ef 100644 --- a/client/src/styles/_typography.scss +++ b/client/src/styles/_typography.scss @@ -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; -} diff --git a/client/src/styles/_variables.scss b/client/src/styles/_variables.scss index 5fb05888..56a3bcc0 100644 --- a/client/src/styles/_variables.scss +++ b/client/src/styles/_variables.scss @@ -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; diff --git a/client/src/styles/main.scss b/client/src/styles/main.scss index 4b0a1d49..dcbc6d9d 100644 --- a/client/src/styles/main.scss +++ b/client/src/styles/main.scss @@ -27,3 +27,4 @@ @import "simple-list"; @import "widget-popover"; @import "toast"; +@import "intro"; diff --git a/server/basicknowledge/migrations/0007_basicknowledge_intro.py b/server/basicknowledge/migrations/0007_basicknowledge_intro.py new file mode 100644 index 00000000..cdb06dd7 --- /dev/null +++ b/server/basicknowledge/migrations/0007_basicknowledge_intro.py @@ -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=''), + ), + ] diff --git a/server/basicknowledge/models.py b/server/basicknowledge/models.py index 273c986d..abab4461 100644 --- a/server/basicknowledge/models.py +++ b/server/basicknowledge/models.py @@ -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') ] diff --git a/server/basicknowledge/queries.py b/server/basicknowledge/queries.py index 8fd40684..3481c057 100644 --- a/server/basicknowledge/queries.py +++ b/server/basicknowledge/queries.py @@ -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): diff --git a/server/objectives/admin.py b/server/objectives/admin.py index 5c70d0c0..9d62f2fb 100644 --- a/server/objectives/admin.py +++ b/server/objectives/admin.py @@ -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): diff --git a/server/objectives/migrations/0009_auto_20200928_1547.py b/server/objectives/migrations/0009_auto_20200928_1547.py new file mode 100644 index 00000000..841d0f5c --- /dev/null +++ b/server/objectives/migrations/0009_auto_20200928_1547.py @@ -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'), + ), + ] diff --git a/server/objectives/migrations/0010_auto_20200930_1323.py b/server/objectives/migrations/0010_auto_20200930_1323.py new file mode 100644 index 00000000..a37d4cee --- /dev/null +++ b/server/objectives/migrations/0010_auto_20200930_1323.py @@ -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), + ), + ] diff --git a/server/objectives/models.py b/server/objectives/models.py index 422aabdd..18d8cad8 100644 --- a/server/objectives/models.py +++ b/server/objectives/models.py @@ -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) diff --git a/server/objectives/tests/test_objective_order.py b/server/objectives/tests/test_objective_order.py new file mode 100644 index 00000000..868cce5e --- /dev/null +++ b/server/objectives/tests/test_objective_order.py @@ -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') + diff --git a/server/users/migrations/0024_auto_20200928_1547.py b/server/users/migrations/0024_auto_20200928_1547.py new file mode 100644 index 00000000..fdebac5b --- /dev/null +++ b/server/users/migrations/0024_auto_20200928_1547.py @@ -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), + ), + ]