Merge branch 'develop'

This commit is contained in:
Christian Cueni 2019-08-15 07:37:54 +02:00
commit 1774921dca
33 changed files with 710 additions and 196 deletions

View File

@ -10,6 +10,7 @@
import DefaultLayout from '@/layouts/DefaultLayout';
import SimpleLayout from '@/layouts/SimpleLayout';
import BlankLayout from '@/layouts/BlankLayout';
import FullScreenLayout from '@/layouts/FullScreenLayout';
import Modal from '@/components/Modal';
import MobileNavigation from '@/components/MobileNavigation';
import NewContentBlockWizard from '@/components/content-block-form/NewContentBlockWizard';
@ -33,6 +34,7 @@
DefaultLayout,
SimpleLayout,
BlankLayout,
FullScreenLayout,
Modal,
MobileNavigation,
NewContentBlockWizard,

View File

@ -61,6 +61,7 @@
import EyeIcon from '@/components/icons/EyeIcon';
import PenIcon from '@/components/icons/PenIcon';
import TrashIcon from '@/components/icons/TrashIcon';
import ModuleRoomSlug from '@/components/content-blocks/ModuleRoomSlug'
import CHAPTER_QUERY from '@/graphql/gql/chapterQuery.gql';
import DELETE_CONTENT_BLOCK_MUTATION from '@/graphql/gql/mutations/deleteContentBlock.gql';
@ -93,6 +94,7 @@
'genially_block': GeniallyBlock,
'subtitle': SubtitleBlock,
'content_list': ContentListBlock,
'module_room_slug': ModuleRoomSlug,
Survey,
Solution,
Assignment,

View File

@ -0,0 +1,20 @@
<template>
<div class="module-slug">
<router-link class="button button--primary" :to="{name: 'moduleRoom', params: { slug: value.slug }}">Raum anzeigen
</router-link>
</div>
</template>
<script>
export default {
props: ['value']
}
</script>
<style scoped lang="scss">
@import "@/styles/_variables.scss";
.module-slug {
margin-bottom: $large-spacing;
}
</style>

View File

@ -0,0 +1,38 @@
import AddRoomEntryButton from '@/components/rooms/AddRoomEntryButton.vue';
import RoomEntry from '@/components/rooms/RoomEntry.vue';
import RoomGroupWidget from '@/components/rooms/RoomGroupWidget';
import EntryCountWidget from '@/components/rooms/EntryCountWidget';
import RoomActions from '@/components/rooms/RoomActions';
export default {
components: {
EntryCountWidget,
RoomGroupWidget,
AddRoomEntryButton,
RoomEntry,
RoomActions
},
beforeDestroy() {
this.$store.dispatch('setSpecialContainerClass', '');
},
created() {
},
data() {
return {
room: [],
entries: []
}
},
computed: {
roomEntryCount() {
return (this.room && this.room.roomEntries) ? this.room.roomEntries.length : 0
},
roomAppearance() {
return this.room ? this.room.appearance : ''
}
}
}

View File

@ -10,7 +10,7 @@
:key="assignment.id"
class="module-navigation__anchor sub-navigation-item__link"
exact-active-class="module-navigation__anchor--active"
>{{assignmentTitle(assignment)}}
>{{assignment.value.assignment}}
</router-link>
</sub-navigation-item>
<div class="module-navigation__module-content" v-if="false"> <!-- Do not display this for now, might be used later again though -->
@ -41,8 +41,6 @@
class="module-navigation__solution-toggle"
data-cy="toggle-enable-solutions"></toggle-solutions-for-module>
</div>
</nav>
</template>
@ -79,13 +77,14 @@
showResults() {
return this.me.permissions.includes('users.can_manage_school_class_content');
},
assignments() {
return [...this.module.assignments].sort((a, b) => {
return a.title.toLowerCase() > b.title.toLowerCase() ? 1 : -1;
})
},
canManageContent() {
return this.me.permissions.includes('users.can_manage_school_class_content');
},
assignments() {
if (!this.module.chapters) {
return [];
}
return this.extractAssignmentsFromChapters(this.module.chapters, []);
}
},
@ -105,10 +104,47 @@
return `#chapter-${index}`
},
submissionsLink(assignment) {
return `/module/${this.module.slug}/submissions/${assignment.id}`;
return `/module/${this.module.slug}/submissions/${assignment.value.id}`;
},
assignmentTitle(assignment) {
return assignment.assignment.length > 25 ? assignment.assignment.substring(0, 22) + '...' : assignment.assignment;
extractAssignmentsFromChapters(chapters, assignments) {
chapters.forEach(node => {
if (node.contentBlocks) { // in chapter node
// if chapter information is required then do it here like so:
// assignments.push({
// chapterTitle: node.title
// });
// return this.extractAssignmentsFromChapters(node.contentBlocks, assignments);
assignments = this.extractAssignmentsFromChapters(node.contentBlocks, assignments);
} else if (node.contents) {
let foundAssignments = [];
node.contents.forEach(contentNode => {
foundAssignments = this.concatAssignments(foundAssignments, contentNode);
});
assignments = [...assignments, ...foundAssignments];
}
});
return assignments;
},
concatAssignments(foundAssignments, node) {
let foundAssignment = this.findAssignment(node);
return foundAssignment ? [...foundAssignments, ...foundAssignment] : foundAssignments;
},
findAssignment(node) {
if (node.type && node.type === 'assignment') {
return [node];
} else if (node.type && node.type === 'content_list_item') {
let foundAssignments = [];
node.value.forEach(contentNode => {
foundAssignments = this.concatAssignments(foundAssignments, contentNode);
});
return this.flattenArray(foundAssignments)
} else {
return null;
}
},
flattenArray(arrayToFlatten) {
// https://stackoverflow.com/questions/10865025/merge-flatten-an-array-of-arrays
return [].concat.apply([], arrayToFlatten);
}
}
}
@ -156,6 +192,9 @@
font-size: 0.875rem;
line-height: 1.2rem;
margin-bottom: .6875rem;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
&--active {
color: $color-brand;

View File

@ -59,8 +59,8 @@
display: grid;
margin-bottom: 0;
}
grid-template-rows: 150px 1fr;
-ms-grid-rows: 150px 48px;
grid-template-rows: 175px 1fr;
-ms-grid-rows: 175px 48px;
-ms-grid-columns: 1fr;
&__content {

View File

@ -25,8 +25,10 @@
mounted () {
if (this.avatarUrl !== '') {
this.$refs.fakeImage.addEventListener('load', () => {
this.$refs.fakeImage.remove();
this.isAvatarLoaded = true;
if (this.$refs.fakeImage) {
this.$refs.fakeImage.remove();
this.isAvatarLoaded = true;
}
});
};
}

View File

@ -1,5 +1,5 @@
<template>
<checkbox v-if="canToggleSolutions" label="Lösungen für Schüler anzeigen" :checked="enabled"
<checkbox v-if="canToggleSolutions" label="Lösungen für Lernende anzeigen" :checked="enabled"
@input="toggleSolutions"></checkbox>
</template>

View File

@ -0,0 +1,14 @@
#import "./fragments/roomParts.gql"
#import "./fragments/roomEntryParts.gql"
query ModuleRoomEntriesQuery($slug: String, $classId: ID!) {
moduleRoom(slug: $slug, classId: $classId) {
...RoomParts
roomEntries {
edges {
node {
...RoomEntryParts
}
}
}
}
}

View File

@ -35,74 +35,6 @@
<style lang="scss" scoped>
@import "@/styles/_variables.scss";
@import "@/styles/_mixins.scss";
@import "@/styles/_default-layout.scss";
.skillbox {
margin: 0 auto;
width: 100%;
@supports (display: grid) {
display: grid;
}
grid-template-rows: auto 1fr;
min-height: 100vh;
grid-auto-rows: 1fr;
grid-template-areas: "h" "c";
padding-bottom: 50px;
&--show-filter {
grid-template-rows: auto auto 1fr;
-ms-grid-rows: 50px 50px 30px 1fr; // 1 extra row for gap
grid-template-areas: "h" "." "c";
}
/*
* For IE10+
*/
&--show-filter &__content {
-ms-grid-row: 4;
-ms-grid-column: 1;
}
&--show-filter &__filter-bar {
-ms-grid-row: 2;
-ms-grid-column: 1;
}
/*
* For IE10+
*/
display: -ms-grid;
-ms-grid-rows: 50px 30px auto; // 1 extra row for gap
-ms-grid-columns: 1fr;
@include skillbox-colors;
&__header {
grid-area: h;
-ms-grid-row: 1;
}
&__content {
-ms-grid-row: 3;
-ms-grid-column: 1;
}
&__footer {
grid-area: f;
display: none;
}
/*
* For IE10+
*/
& > :nth-child(2) {
}
& > :nth-child(3) {
-ms-grid-row: 4;
-ms-grid-column: 1;
}
}
</style>

View File

@ -0,0 +1,51 @@
<template>
<div class="container skillbox" :class="specialContainerClass">
<div class="close-button" v-on:click="back">
<cross class="close-button__icon"></cross>
</div>
<router-view class="skillbox__content"></router-view>
</div>
</template>
<script>
import Cross from '@/components/icons/Cross';
export default {
components: {
Cross
},
computed: {
specialContainerClass() {
let cls = this.$store.state.specialContainerClass;
return [cls ? `skillbox--${cls}` : '']
}
},
methods: {
back() {
this.$router.go(-1);
}
}
}
</script>
<style lang="scss" scoped>
@import "@/styles/_default-layout.scss";
.close-button {
margin-top: $medium-spacing;
margin-right: $medium-spacing;
justify-self: end;
cursor: pointer;
display:flex;
justify-content:end;
&__icon {
width: 40px;
height: 40px;
}
}
</style>

View File

@ -7,7 +7,7 @@
</div>
</template>
<style lang="scss">
<style lang="scss" scoped>
@import "@/styles/_mixins.scss";
.layout {

View File

@ -0,0 +1,75 @@
<template>
<div class="room">
<div class="room__header">
<h1 class="room__title">{{room.title}}</h1>
<p class="room__intro">
{{room.description}}
</p>
<div class="room__meta">
<room-group-widget v-bind="room.schoolClass"></room-group-widget>
<entry-count-widget :entry-count="roomEntryCount"></entry-count-widget>
</div>
</div>
<div class="room__content">
<add-room-entry-button :parent="room" v-if="room.id">
<!--
the v-if is there for the case where the room hasn't loaded yet, but there is already an attempt to create
a new room entry. mainly happens during cypress testing, but could also happen on a very slow connection
-->
</add-room-entry-button>
<room-entry v-for="entry in room.roomEntries" v-bind="entry" :key="entry.id"></room-entry>
</div>
</div>
</template>
<script>
import MODULE_ROOM_ENTRIES_QUERY from '@/graphql/gql/moduleRoomEntryQuery.gql';
import ME_QUERY from '@/graphql/gql/meQuery.gql';
import roomMixin from '@/components/mixins/room'
export default {
props: ['slug'],
mixins: [roomMixin],
data() {
return {
room: [],
entries: [],
me: {
selectedClass: {
id: ''
},
permissions: []
}
}
},
apollo: {
moduleRoom: {
query: MODULE_ROOM_ENTRIES_QUERY,
variables() {
return {
slug: this.slug,
classId: this.me.selectedClass.id
}
},
// manual: true,
// todo: do we really need manual here? update should do the trick too
result({data, loading, networkStatus}) {
if (!loading) {
this.room = Object.assign({}, this.$getRidOfEdges(data).moduleRoom);
this.$store.dispatch('setSpecialContainerClass', this.room.appearance);
}
},
pollInterval: 5000,
},
me: {
query: ME_QUERY,
}
}
}
</script>
<style scoped lang="scss">
@import "@/styles/_room.scss";
</style>

View File

@ -66,7 +66,7 @@
grid-template-columns: minmax(max-content, 840px);
}
grid-row-gap: 30px;
grid-auto-rows: 200px;
grid-auto-rows: 225px;
max-width: 840px;
width: 100vw;
/*justify-self: center;*/

View File

@ -25,46 +25,11 @@
<script>
import ROOM_ENTRIES_QUERY from '@/graphql/gql/roomEntriesQuery.gql';
import AddRoomEntryButton from '@/components/rooms/AddRoomEntryButton.vue';
import RoomEntry from '@/components/rooms/RoomEntry.vue';
import RoomGroupWidget from '@/components/rooms/RoomGroupWidget';
import EntryCountWidget from '@/components/rooms/EntryCountWidget';
import RoomActions from '@/components/rooms/RoomActions';
import roomMixin from '@/components/mixins/room'
export default {
props: ['slug'],
components: {
EntryCountWidget,
RoomGroupWidget,
AddRoomEntryButton,
RoomEntry,
RoomActions
},
beforeDestroy() {
this.$store.dispatch('setSpecialContainerClass', '');
},
created() {
},
data() {
return {
room: [],
entries: []
}
},
computed: {
roomEntryCount() {
return (this.room && this.room.roomEntries) ? this.room.roomEntries.length : 0
},
roomAppearance() {
return this.room ? this.room.appearance : ''
}
},
mixins: [roomMixin],
apollo: {
modules: {
@ -89,54 +54,5 @@
</script>
<style scoped lang="scss">
@import "@/styles/_variables.scss";
@import "@/styles/_functions.scss";
@import "@/styles/_mixins.scss";
.room {
display: grid;
grid-template-rows: auto 1fr;
margin-bottom: -50px;
&__header {
padding: 30px;
}
&__intro {
font-family: $sans-serif-font-family;
font-weight: $font-weight-regular;
font-size: toRem(17px);
max-width: 900px;
line-height: 1.5;
margin-bottom: 25px;
}
&__meta {
display: flex;
flex-direction: column;
@include desktop {
flex-direction: row-reverse;
}
justify-content: start;
position: relative;
& > :first-child {
margin-left: $large-spacing;
}
& > :nth-child(2) {
margin-left: $large-spacing;
}
}
&__content {
padding: 50px 15px;
background-color: rgba($color-charcoal-dark, 0.18);
@include desktop {
columns: 4;
padding: 50px 60px;
}
}
}
@import "@/styles/_room.scss";
</style>

View File

@ -35,16 +35,16 @@
<portfolio-illustration></portfolio-illustration>
</section-block>
</div>
<div class="start-page__news news">
<h2 class="news__title">News</h2>
<news-teaser date="19. Dezember 2018" title="Bilder eines Jahres"
url="https://www.brennpunkt-welt.ch/jahresrückblick-2018/"></news-teaser>
<news-teaser date="20. November 2018" title="100 Jahre Erster Weltkrieg"
url="http://abunews-1f178193a10edaabff3ab828e30af44.webflow.io/"></news-teaser>
<news-teaser date="31. Oktober 2018" title="Sommerzeit - Festivalzeit"
url="https://abunews.webflow.io/"></news-teaser>
<div class="news__more">Mehr...</div>
</div>
<!-- <div class="start-page__news news">-->
<!-- <h2 class="news__title">News</h2>-->
<!-- <news-teaser date="19. Dezember 2018" title="Bilder eines Jahres"-->
<!-- url="https://www.brennpunkt-welt.ch/jahresrückblick-2018/"></news-teaser>-->
<!-- <news-teaser date="20. November 2018" title="100 Jahre Erster Weltkrieg"-->
<!-- url="http://abunews-1f178193a10edaabff3ab828e30af44.webflow.io/"></news-teaser>-->
<!-- <news-teaser date="31. Oktober 2018" title="Sommerzeit - Festivalzeit"-->
<!-- url="https://abunews.webflow.io/"></news-teaser>-->
<!-- <div class="news__more">Mehr...</div>-->
<!-- </div>-->
</div>
</template>
<script>

View File

@ -26,6 +26,7 @@ import editProject from '@/pages/editProject'
import newProject from '@/pages/newProject'
import surveyPage from '@/pages/survey'
import styleGuidePage from '@/pages/styleguide'
import moduleRoom from '@/pages/moduleRoom'
import store from '@/store/index';
@ -46,13 +47,20 @@ const routes = [
name: 'submissions',
component: submissions,
meta: {filter: true}
},
}
]
},
{path: '/rooms', name: 'rooms', component: rooms, meta: {filter: true}},
{path: '/new-room/', name: 'new-room', component: newRoom},
{path: '/edit-room/:id', name: 'edit-room', component: editRoom, props: true},
{path: '/room/:slug', name: 'room', component: room, props: true},
{
path: '/module-room/:slug',
name: 'moduleRoom',
component: moduleRoom,
props: true,
meta: {layout: 'fullScreen'}
},
{path: '/article/:slug', name: 'article', component: article, meta: {layout: 'simple'}},
{
path: '/instruments/:slug',

View File

@ -0,0 +1,72 @@
@import "@/styles/_variables.scss";
@import "@/styles/_mixins.scss";
.skillbox {
margin: 0 auto;
width: 100%;
@supports (display: grid) {
display: grid;
}
grid-template-rows: auto 1fr;
min-height: 100vh;
grid-auto-rows: 1fr;
grid-template-areas: "h" "c";
padding-bottom: 50px;
&--show-filter {
grid-template-rows: auto auto 1fr;
-ms-grid-rows: 50px 50px 30px 1fr; // 1 extra row for gap
grid-template-areas: "h" "." "c";
}
/*
* For IE10+
*/
&--show-filter &__content {
-ms-grid-row: 4;
-ms-grid-column: 1;
}
&--show-filter &__filter-bar {
-ms-grid-row: 2;
-ms-grid-column: 1;
}
/*
* For IE10+
*/
display: -ms-grid;
-ms-grid-rows: 50px 30px auto; // 1 extra row for gap
-ms-grid-columns: 1fr;
@include skillbox-colors;
&__header {
grid-area: h;
-ms-grid-row: 1;
}
&__content {
-ms-grid-row: 3;
-ms-grid-column: 1;
}
&__footer {
grid-area: f;
display: none;
}
/*
* For IE10+
*/
& > :nth-child(2) {
}
& > :nth-child(3) {
-ms-grid-row: 4;
-ms-grid-column: 1;
}
}

View File

@ -0,0 +1,50 @@
@import "@/styles/_variables.scss";
@import "@/styles/_functions.scss";
@import "@/styles/_mixins.scss";
.room {
display: grid;
grid-template-rows: auto 1fr;
margin-bottom: -50px;
&__header {
padding: 30px;
}
&__intro {
font-family: $sans-serif-font-family;
font-weight: $font-weight-regular;
font-size: toRem(17px);
max-width: 900px;
line-height: 1.5;
margin-bottom: 25px;
}
&__meta {
display: flex;
flex-direction: column;
@include desktop {
flex-direction: row-reverse;
}
justify-content: start;
position: relative;
& > :first-child {
margin-left: $large-spacing;
}
& > :nth-child(2) {
margin-left: $large-spacing;
}
}
&__content {
padding: 50px 15px;
background-color: rgba($color-charcoal-dark, 0.18);
@include desktop {
columns: 4;
padding: 50px 60px;
}
}
}

View File

@ -6,6 +6,7 @@
@import "reset";
@import "typography";
@import "variables";
@import "default-layout";
@import "buttons";
@import "forms";
@import "uploadcare_overwrite";

View File

@ -18,12 +18,12 @@ from portfolio.schema import PortfolioQuery
from surveys.schema import SurveysQuery
from surveys.mutations import SurveysMutations
from rooms.mutations import RoomMutations
from rooms.schema import RoomsQuery
from rooms.schema import RoomsQuery, ModuleRoomsQuery
from users.schema import UsersQuery
from users.mutations import ProfileMutations
class Query(UsersQuery, RoomsQuery, ObjectivesQuery, BookQuery, AssignmentsQuery, StudentSubmissionQuery,
class Query(UsersQuery, ModuleRoomsQuery, RoomsQuery, ObjectivesQuery, BookQuery, AssignmentsQuery, StudentSubmissionQuery,
BasicKnowledgeQuery, PortfolioQuery, MyActivityQuery, SurveysQuery, graphene.ObjectType):
node = relay.Node.Field()

View File

@ -98,6 +98,13 @@ class InstrumentTextBlock(blocks.StructBlock):
text = blocks.RichTextBlock(features=INSTRUMENTS_RICH_TEXT_FEATURES)
class ModuleRoomSlugBlock(blocks.StructBlock):
class Meta:
icon = 'link'
title = blocks.TextBlock()
# 'text_block' 'task' 'basic_knowledge' 'student_entry' 'image_block'
#
# url = blocks.URLBlock()

View File

@ -0,0 +1,24 @@
# Generated by Django 2.0.6 on 2019-08-08 06:49
import assignments.models
from django.db import migrations
import surveys.models
import wagtail.core.blocks
import wagtail.core.fields
import wagtail.images.blocks
import wagtail.snippets.blocks
class Migration(migrations.Migration):
dependencies = [
('books', '0012_auto_20190722_0932'),
]
operations = [
migrations.AlterField(
model_name='contentblock',
name='contents',
field=wagtail.core.fields.StreamField([('text_block', wagtail.core.blocks.StructBlock([('text', wagtail.core.blocks.RichTextBlock(features=['ul']))])), ('basic_knowledge', wagtail.core.blocks.StructBlock([('description', wagtail.core.blocks.RichTextBlock(required=False)), ('basic_knowledge', wagtail.core.blocks.PageChooserBlock(required=True, target_model=['basicknowledge.BasicKnowledge']))])), ('assignment', wagtail.core.blocks.StructBlock([('assignment_id', wagtail.snippets.blocks.SnippetChooserBlock(assignments.models.Assignment))])), ('survey', wagtail.core.blocks.StructBlock([('survey_id', wagtail.snippets.blocks.SnippetChooserBlock(surveys.models.Survey))])), ('image_block', wagtail.images.blocks.ImageChooserBlock()), ('image_url_block', wagtail.core.blocks.StructBlock([('title', wagtail.core.blocks.TextBlock()), ('url', wagtail.core.blocks.URLBlock())])), ('link_block', wagtail.core.blocks.StructBlock([('text', wagtail.core.blocks.TextBlock()), ('url', wagtail.core.blocks.URLBlock())])), ('solution', wagtail.core.blocks.StructBlock([('text', wagtail.core.blocks.RichTextBlock(features=['ul']))], icon='tick')), ('video_block', wagtail.core.blocks.StructBlock([('url', wagtail.core.blocks.URLBlock())])), ('document_block', wagtail.core.blocks.StructBlock([('url', wagtail.core.blocks.URLBlock())])), ('infogram_block', wagtail.core.blocks.StructBlock([('id', wagtail.core.blocks.TextBlock()), ('title', wagtail.core.blocks.TextBlock())])), ('genially_block', wagtail.core.blocks.StructBlock([('id', wagtail.core.blocks.TextBlock())])), ('subtitle', wagtail.core.blocks.StructBlock([('text', wagtail.core.blocks.TextBlock())])), ('module_room_slug', wagtail.core.blocks.StructBlock([('title', wagtail.core.blocks.TextBlock())])), ('content_list_item', wagtail.core.blocks.StreamBlock([('text_block', wagtail.core.blocks.StructBlock([('text', wagtail.core.blocks.RichTextBlock(features=['ul']))])), ('basic_knowledge', wagtail.core.blocks.StructBlock([('description', wagtail.core.blocks.RichTextBlock(required=False)), ('basic_knowledge', wagtail.core.blocks.PageChooserBlock(required=True, target_model=['basicknowledge.BasicKnowledge']))])), ('assignment', wagtail.core.blocks.StructBlock([('assignment_id', wagtail.snippets.blocks.SnippetChooserBlock(assignments.models.Assignment))])), ('survey', wagtail.core.blocks.StructBlock([('survey_id', wagtail.snippets.blocks.SnippetChooserBlock(surveys.models.Survey))])), ('image_block', wagtail.images.blocks.ImageChooserBlock()), ('image_url_block', wagtail.core.blocks.StructBlock([('title', wagtail.core.blocks.TextBlock()), ('url', wagtail.core.blocks.URLBlock())])), ('link_block', wagtail.core.blocks.StructBlock([('text', wagtail.core.blocks.TextBlock()), ('url', wagtail.core.blocks.URLBlock())])), ('solution', wagtail.core.blocks.StructBlock([('text', wagtail.core.blocks.RichTextBlock(features=['ul']))], icon='tick')), ('video_block', wagtail.core.blocks.StructBlock([('url', wagtail.core.blocks.URLBlock())])), ('document_block', wagtail.core.blocks.StructBlock([('url', wagtail.core.blocks.URLBlock())])), ('infogram_block', wagtail.core.blocks.StructBlock([('id', wagtail.core.blocks.TextBlock()), ('title', wagtail.core.blocks.TextBlock())])), ('genially_block', wagtail.core.blocks.StructBlock([('id', wagtail.core.blocks.TextBlock())])), ('subtitle', wagtail.core.blocks.StructBlock([('text', wagtail.core.blocks.TextBlock())])), ('module_room_slug', wagtail.core.blocks.StructBlock([('title', wagtail.core.blocks.TextBlock())]))]))], blank=True, null=True),
),
]

View File

@ -7,7 +7,7 @@ from wagtail.core.fields import StreamField
from wagtail.images.blocks import ImageChooserBlock
from books.blocks import TextBlock, BasicKnowledgeBlock, LinkBlock, VideoBlock, DocumentBlock, \
ImageUrlBlock, AssignmentBlock, InfogramBlock, GeniallyBlock, SubtitleBlock, SurveyBlock
ImageUrlBlock, AssignmentBlock, InfogramBlock, GeniallyBlock, SubtitleBlock, SurveyBlock, ModuleRoomSlugBlock
from core.wagtail_utils import StrictHierarchyPage
from users.models import SchoolClass
@ -48,7 +48,8 @@ class ContentBlock(StrictHierarchyPage):
('document_block', DocumentBlock()),
('infogram_block', InfogramBlock()),
('genially_block', GeniallyBlock()),
('subtitle', SubtitleBlock())
('subtitle', SubtitleBlock()),
('module_room_slug', ModuleRoomSlugBlock())
]
content_list_item = StreamBlock(content_blocks)

View File

@ -5,6 +5,7 @@ from graphene_django.filter import DjangoFilterConnectionField
from api.utils import get_object
from books.utils import are_solutions_enabled_for
from rooms.models import ModuleRoomSlug
from ..models import Book, Topic, Module, Chapter, ContentBlock
@ -25,9 +26,23 @@ class ContentBlockNode(DjangoObjectType):
return self.owner is not None and self.owner.pk == info.context.user.pk
def resolve_contents(self, info, **kwargs):
if not are_solutions_enabled_for(info.context.user, self.module):
self.contents.stream_data = [content for content in self.contents.stream_data if
content['type'] != 'solution']
updated_stream_data = []
for content in self.contents.stream_data:
if not are_solutions_enabled_for(info.context.user, self.module) and content['type'] == 'solution':
continue
if content['type'] == 'module_room_slug':
try:
module_room_slug = ModuleRoomSlug.objects.get(title=content['value']['title'])
content['value'] = {
'title': content['value']['title'],
'slug': module_room_slug.slug
}
except ModuleRoomSlug.DoesNotExist:
pass
updated_stream_data.append(content)
self.contents.stream_data = updated_stream_data
return self.contents

View File

@ -1,6 +1,6 @@
from django.contrib import admin
from rooms.models import Room, RoomEntry
from rooms.models import Room, RoomEntry, ModuleRoomSlug
@admin.register(Room)
@ -13,3 +13,9 @@ class RoomAdmin(admin.ModelAdmin):
class RoomEntryAdmin(admin.ModelAdmin):
list_display = ('id', 'slug', 'title', 'room', 'author')
list_filter = ('room', 'author')
@admin.register(ModuleRoomSlug)
class AdminGeneratedRoomSlugAdmin(admin.ModelAdmin):
list_display = ('id', 'slug', 'title')
list_filter = ('slug', 'title')

View File

@ -8,7 +8,7 @@ from wagtail.core.rich_text import RichText
from books.factories import TextBlockFactory, ImageUrlBlockFactory, LinkBlockFactory
from core.factories import fake, fake_paragraph
from rooms.models import Room, RoomEntry
from rooms.models import Room, RoomEntry, ModuleRoomSlug
from users.models import SchoolClass
@ -77,3 +77,12 @@ class RoomEntryFactory(factory.django.DjangoModelFactory):
def create(cls, **kwargs):
cls.stream_field_magic(kwargs, 'contents')
return cls._generate(CREATE_STRATEGY, kwargs)
class ModuleRoomSlugFactory(factory.django.DjangoModelFactory):
class Meta:
model = ModuleRoomSlug
slug = factory.Sequence(lambda n: u'slug-{:d}'.format(n))
title = factory.Sequence(lambda n: u'Title {:d}'.format(n))

View File

@ -0,0 +1,31 @@
# Generated by Django 2.0.6 on 2019-08-08 06:49
from django.db import migrations, models
import django_extensions.db.fields
class Migration(migrations.Migration):
dependencies = [
('rooms', '0006_auto_20190722_0932'),
]
operations = [
migrations.CreateModel(
name='ModuleRoomSlug',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=255, verbose_name='title')),
('description', models.TextField(blank=True, null=True, verbose_name='description')),
('slug', django_extensions.db.fields.AutoSlugField(blank=True, editable=False, populate_from='title', verbose_name='slug')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='room',
name='user_created',
field=models.BooleanField(default=True),
),
]

View File

@ -15,6 +15,7 @@ class Room(TitleSlugDescriptionModel):
school_class = models.ForeignKey(SchoolClass, blank=False, null=False, on_delete=models.CASCADE, related_name='rooms')
appearance = models.CharField(blank=True, null=False, max_length=255)
user_created = models.BooleanField(blank=False, null=False, default=True)
def __str__(self):
return 'Room {}-{}-{}'.format(self.id, self.title, self.school_class)
@ -41,3 +42,9 @@ class RoomEntry(TitleSlugDescriptionModel):
def can_user_see_entry(self, user):
return user.is_superuser or self.room.school_class.is_user_in_schoolclass(user)
class ModuleRoomSlug(TitleSlugDescriptionModel):
def __str__(self):
return 'ModuleRoomSlug {}-{}'.format(self.id, self.title)

View File

@ -6,7 +6,8 @@ from graphene_django import DjangoObjectType
from graphene_django.filter import DjangoFilterConnectionField
from api.utils import get_object, get_by_id_or_slug
from rooms.models import Room, RoomEntry
from rooms.models import Room, RoomEntry, ModuleRoomSlug
from users.models import SchoolClass
from users.schema import UserNode
logger = logging.getLogger(__name__)
@ -53,7 +54,7 @@ class RoomsQuery(object):
user = info.context.user
if user.is_superuser:
return Room.objects.all()
return Room.objects.filter(school_class__in=user.school_classes.all())
return Room.objects.filter(school_class__in=user.school_classes.all()).exclude(user_created=False)
def resolve_room(self, info, **kwargs):
room = get_by_id_or_slug(Room, **kwargs)
@ -82,3 +83,30 @@ class RoomsQuery(object):
return RoomEntry.objects.none()
else:
return RoomEntry.objects.all()
class ModuleRoomsQuery(object):
module_room = graphene.Field(RoomNode, slug=graphene.String(), class_id=graphene.ID())
def resolve_module_room(self, info, **kwargs):
schoolclass = get_object(SchoolClass, kwargs.get('class_id'))
try:
slug = ModuleRoomSlug.objects.get(slug=kwargs.get('slug'))
except ModuleRoomSlug.DoesNotExist:
return None
if schoolclass is None or not schoolclass.is_user_in_schoolclass(info.context.user):
return None
room, created = Room.objects.get_or_create(school_class=schoolclass, user_created=False, slug=slug.slug,
title=slug.title, appearance='blue')
if created:
room.slug = slug.slug
room.save()
if not room.user_created and room.school_class.is_user_in_schoolclass(info.context.user):
return room
else:
return None

View File

@ -0,0 +1,96 @@
# -*- coding: utf-8 -*-
#
# ITerativ GmbH
# http://www.iterativ.ch/
#
# Copyright (c) 2019 ITerativ GmbH. All rights reserved.
#
# Created on 2019-08-07
# @author: chrigu <christian.cueni@iterativ.ch>
from django.test import TestCase, RequestFactory
from graphene.test import Client
from graphql_relay import to_global_id
from api.schema import schema
from core.factories import UserFactory
from rooms.factories import RoomFactory, ModuleRoomSlugFactory
from users.factories import SchoolClassFactory
class AdminRoomQueryPermission(TestCase):
def setUp(self):
self.user = UserFactory(username='aschi')
self.another_user = UserFactory(username='pesche')
self.sc1 = SchoolClassFactory(users=[self.user])
sc2 = SchoolClassFactory(users=[self.another_user])
self.room1 = RoomFactory(school_class=self.sc1)
self.room2 = RoomFactory(school_class=sc2)
self.module_room_slug = ModuleRoomSlugFactory(title='some title')
self.sc1_id = to_global_id('SchoolClass', self.sc1.pk)
self.sc2_id = to_global_id('SchoolClass', sc2.pk)
request = RequestFactory().get('/')
request.user = self.user
self.client = Client(schema=schema, context_value=request)
self.query = '''
query ModuleRoomEntriesQuery($slug: String, $classId: ID!) {
moduleRoom(slug: $slug, classId: $classId) {
title
}
}
'''
def test_should_return_none_if_slug_does_not_exist(self):
result = self.client.execute(self.query, variables={
'slug': 'no-slug',
'classId': 'norealId'
})
self.assertIsNone(result.get('errors'))
self.assertIsNone(result.get('data').get('moduleRoom'))
def test_should_return_none_if_class_id_does_not_exist(self):
result = self.client.execute(self.query, variables={
'slug': 'no-slug',
'classId': 'norealId'
})
self.assertIsNone(result.get('errors'))
self.assertIsNone(result.get('data').get('moduleRoom'))
def test_user_should_not_be_able_to_create_room_for_other_class(self):
result = self.client.execute(self.query, variables={
'slug': self.module_room_slug.slug,
'classId': self.sc2_id
})
self.assertIsNone(result.get('errors'))
self.assertIsNone(result.get('data').get('moduleRoom'))
def test_should_create_room_if_none_exists(self):
result = self.client.execute(self.query, variables={
'slug': self.module_room_slug.slug,
'classId': self.sc1_id
})
self.assertIsNone(result.get('errors'))
self.assertEqual(result.get('data').get('moduleRoom').get('title'), self.module_room_slug.title)
def test_should_return_room_if_one_exists(self):
existing_room = RoomFactory(school_class=self.sc1, user_created=False)
admin_slug = ModuleRoomSlugFactory(slug=existing_room.slug, title=existing_room.title)
result = self.client.execute(self.query, variables={
'slug': admin_slug.slug,
'classId': self.sc1_id
})
self.assertIsNone(result.get('errors'))
self.assertEqual(result.get('data').get('moduleRoom').get('title'), existing_room.title)

View File

@ -61,6 +61,27 @@ class RoomQueryPermission(TestCase):
self.assertIsNone(result.get('errors'))
self.assertEqual(result.get('data').get('room'), None)
def test_student_should_only_user_created_rooms(self):
modlue_room = RoomFactory(school_class=self.room1.school_class, user_created=False)
query = '''
query {
rooms {
edges {
node {
title
}
}
}
}
'''
result = self.client.execute(query)
self.assertIsNone(result.get('errors'))
self.assertEqual(len(result.get('data').get('rooms').get('edges')), 1)
self.assertNotEqual(result.get('data').get('rooms').get('edges')[0].get('node').get('title'), modlue_room.title)
class RoomEntryQueryPermissions(TestCase):

View File

@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-
#
# ITerativ GmbH
# http://www.iterativ.ch/
#
# Copyright (c) 2019 ITerativ GmbH. All rights reserved.
#
# Created on 2019-08-05
# @author: chrigu <christian.cueni@iterativ.ch>
from wagtail.core import hooks
from rooms.models import ModuleRoomSlug
@hooks.register('after_edit_page')
@hooks.register('after_create_page')
def do_after_page_edit(request, page):
blocks = get_room_blocks(page)
for block in blocks:
if isinstance(block, tuple):
title = block[1]['title']
if isinstance(block, dict):
title = block['value']['title']
ModuleRoomSlug.objects.get_or_create(title=title)
def get_room_blocks(page):
top_level_module_room_slug_blocks = get_block_from_stream_data(page.contents.stream_data, 'module_room_slug')
content_list_module_room_slug_blocks = get_admin_slugs_from_content_list(page.contents.stream_data)
return top_level_module_room_slug_blocks + content_list_module_room_slug_blocks
def get_block_from_stream_data(stream_data, block_name):
if isinstance(stream_data[0], tuple):
return [block for block in stream_data if block[0] in [block_name]]
if isinstance(stream_data[0], dict):
return [block for block in stream_data if block['type'] in [block_name]]
return []
def get_admin_slugs_from_content_list(stream_data):
module_room_slug_blocks = []
content_list_items = get_block_from_stream_data(stream_data, 'content_list_item')
for content_list_item in content_list_items:
module_room_slug_blocks = module_room_slug_blocks + get_block_from_stream_data(content_list_item[1].stream_data,
'module_room_slug')
return module_room_slug_blocks