Merged in feature/MS743-ModuleCategorization (pull request #131)
Feature/MS743 ModuleCategorization Approved-by: Ramon Wenger
This commit is contained in:
commit
0ae86202c7
File diff suppressed because it is too large
Load Diff
|
|
@ -4,12 +4,22 @@
|
||||||
class="module"
|
class="module"
|
||||||
v-if="module.id"
|
v-if="module.id"
|
||||||
>
|
>
|
||||||
|
<div class="module__header">
|
||||||
|
|
||||||
<h2
|
<h2
|
||||||
class="module__meta-title"
|
class="module__meta-title"
|
||||||
id="meta-title"
|
id="meta-title"
|
||||||
>
|
>
|
||||||
{{ module.metaTitle }}
|
{{ module.metaTitle }}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
|
<div class="module__categoryindicators">
|
||||||
|
<pill :text="module.level?.name"></pill>
|
||||||
|
<pill :text="module.category?.name"></pill>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
<h1
|
<h1
|
||||||
class="module__title"
|
class="module__title"
|
||||||
data-cy="module-title"
|
data-cy="module-title"
|
||||||
|
|
@ -85,11 +95,12 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import ObjectiveGroups from '@/components/objective-groups/ObjectiveGroups.vue';
|
import ObjectiveGroups from '@/components/objective-groups/ObjectiveGroups.vue';
|
||||||
import Chapter from '@/components/Chapter.vue';
|
import Chapter from '@/components/Chapter.vue';
|
||||||
import BookmarkActions from '@/components/notes/BookmarkActions.vue';
|
import BookmarkActions from '@/components/notes/BookmarkActions.vue';
|
||||||
|
import Pill from "@/components/ui/Pill.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
module: {
|
module: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
|
@ -98,6 +109,7 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
|
Pill,
|
||||||
BookmarkActions,
|
BookmarkActions,
|
||||||
ObjectiveGroups,
|
ObjectiveGroups,
|
||||||
Chapter,
|
Chapter,
|
||||||
|
|
@ -126,13 +138,13 @@ export default {
|
||||||
return this.module.bookmark.note;
|
return this.module.bookmark.note;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@import 'styles/helpers';
|
@import 'styles/helpers';
|
||||||
|
|
||||||
.module {
|
.module {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-self: center;
|
justify-self: center;
|
||||||
max-width: 100vw;
|
max-width: 100vw;
|
||||||
|
|
@ -161,8 +173,17 @@ export default {
|
||||||
line-height: 25px;
|
line-height: 25px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: stretch;
|
||||||
|
margin-bottom: $small-spacing;
|
||||||
|
}
|
||||||
|
|
||||||
&__meta-title {
|
&__meta-title {
|
||||||
@include meta-title;
|
@include meta-title;
|
||||||
|
margin-right: $medium-spacing;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__intro-wrapper {
|
&__intro-wrapper {
|
||||||
|
|
@ -196,5 +217,5 @@ export default {
|
||||||
&__objective-groups {
|
&__objective-groups {
|
||||||
margin-bottom: 2 * $large-spacing;
|
margin-bottom: 2 * $large-spacing;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,199 @@
|
||||||
|
<template>
|
||||||
|
<div class="module-filter">
|
||||||
|
<div class="module-filter__filterselection">
|
||||||
|
<Dropdown
|
||||||
|
class="module-filter__dropdown"
|
||||||
|
:selected-item="selectedLevel"
|
||||||
|
:items="moduleLevels"
|
||||||
|
@update:selectedItem="updateLevel"
|
||||||
|
></Dropdown>
|
||||||
|
|
||||||
|
<Dropdown
|
||||||
|
class="module-filter__dropdown"
|
||||||
|
:selected-item="selectedCategory"
|
||||||
|
:items="moduleCategories"
|
||||||
|
@update:selectedItem="newItem => selectedCategory = newItem"
|
||||||
|
></Dropdown>
|
||||||
|
<pill-radio-buttons
|
||||||
|
:selectableItems="languageOptions"
|
||||||
|
:defaultSelectedItem="initialLanguage"
|
||||||
|
class="module-filter__language-selection"
|
||||||
|
@update:selectedItem="item => selectedLanguage = item"/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="topic__modules">
|
||||||
|
<module-teaser
|
||||||
|
v-for="module in filteredModules"
|
||||||
|
v-bind="module"
|
||||||
|
:key="module.slug"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {computed, ref} from "vue";
|
||||||
|
import ModuleTeaser from "@/components/modules/ModuleTeaser.vue";
|
||||||
|
import {useQuery, useMutation} from "@vue/apollo-composable";
|
||||||
|
import Dropdown from "@/components/ui/Dropdown.vue";
|
||||||
|
import PillRadioButtons from "@/components/ui/PillRadioButtons.vue";
|
||||||
|
import gql from "graphql-tag";
|
||||||
|
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modules: Object[];
|
||||||
|
me: any;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const languageOptions: Array<{ id: number, label: string }> = ref([
|
||||||
|
{id: 1, label: 'De'},
|
||||||
|
{id: 2, label: 'Fr'},
|
||||||
|
{id: 3, label: 'En'}
|
||||||
|
])
|
||||||
|
const initialLanguage = languageOptions.value[0]
|
||||||
|
const selectedLanguage = ref(initialLanguage)
|
||||||
|
|
||||||
|
const initialCategory = {
|
||||||
|
name: 'Alle Lernfelder',
|
||||||
|
id: null,
|
||||||
|
filterAttributeType: 'ALL'
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialLevel = {
|
||||||
|
name: 'Alle Lehrjahre',
|
||||||
|
id: null,
|
||||||
|
filterAttributeType: 'ALL'
|
||||||
|
};
|
||||||
|
|
||||||
|
const {result: moduleFilterResult} = useQuery(gql`
|
||||||
|
query ModuleFilterQuery{
|
||||||
|
moduleLevels {
|
||||||
|
name
|
||||||
|
id
|
||||||
|
filterAttributeType
|
||||||
|
}
|
||||||
|
moduleCategories {
|
||||||
|
name
|
||||||
|
id
|
||||||
|
filterAttributeType
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const selectedLevel = ref(initialLevel);
|
||||||
|
const selectedCategory = ref(initialCategory);
|
||||||
|
|
||||||
|
selectedLevel.value = loadDefaultLevel(props.me);
|
||||||
|
|
||||||
|
const moduleLevels = computed(() => moduleFilterResult.value?.moduleLevels || []);
|
||||||
|
const moduleCategories = computed(() => moduleFilterResult.value?.moduleCategories || []);
|
||||||
|
const filteredModules = computed(() => props.modules?.filter(module => filterModule(module,
|
||||||
|
selectedLevel.value, selectedCategory.value, selectedLanguage.value)) || []);
|
||||||
|
|
||||||
|
|
||||||
|
function loadDefaultLevel(me) {
|
||||||
|
return me?.lastModuleLevel || initialLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterModule(module: Object, selectedLevel, selectedCategory, selectedLanguage) {
|
||||||
|
let filterExpression = true;
|
||||||
|
|
||||||
|
filterExpression = filterByLevel(module, selectedLevel) &&
|
||||||
|
filterByCategory(module, selectedCategory) &&
|
||||||
|
filterByLanguage(module, selectedLanguage);
|
||||||
|
return filterExpression;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateLevel = (level) => {
|
||||||
|
selectedLevel.value = level;
|
||||||
|
updateLastModuleLevelUser(level);
|
||||||
|
}
|
||||||
|
|
||||||
|
const filterByLevel = (module, level) => {
|
||||||
|
return level.filterAttributeType === 'ALL' || module.level?.id === level.id
|
||||||
|
}
|
||||||
|
|
||||||
|
const filterByCategory = (module, category) => {
|
||||||
|
return category.filterAttributeType === 'ALL' || module.category?.id === category.id
|
||||||
|
}
|
||||||
|
|
||||||
|
const filterByLanguage = (module: Object, language: Object) => {
|
||||||
|
// TODO: implement filter by language here.
|
||||||
|
console.log("selectedLanguage", selectedLanguage.value, language);
|
||||||
|
console.log("module.languages", module);
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const {mutate: updateLastModuleLevel} = useMutation(gql `
|
||||||
|
mutation ($input: UpdateLastModuleLevelInput!){
|
||||||
|
updateLastModuleLevel(input: $input) {
|
||||||
|
clientMutationId
|
||||||
|
user {
|
||||||
|
username
|
||||||
|
lastModuleLevel {
|
||||||
|
name
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`);
|
||||||
|
|
||||||
|
const updateLastModuleLevelUser = (moduleLevel: Object) => {
|
||||||
|
updateLastModuleLevel({
|
||||||
|
input: {
|
||||||
|
id: moduleLevel.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@import 'styles/helpers';
|
||||||
|
|
||||||
|
.module-filter {
|
||||||
|
// TODO: how do I correcty set the with of the whole thig including the grid for the modules?
|
||||||
|
// TODO: Farbe des Arrows für Dropdowns muss platfrom habhängig sein MS-775
|
||||||
|
|
||||||
|
width: 75%;
|
||||||
|
|
||||||
|
&__filterselection {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__dropdown {
|
||||||
|
margin-right: $medium-spacing;
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__language-selection {
|
||||||
|
margin-left: auto;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.topic {
|
||||||
|
&__modules {
|
||||||
|
margin-top: 40px;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
@supports (display: grid) {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
grid-column-gap: $large-spacing;
|
||||||
|
grid-row-gap: $large-spacing;
|
||||||
|
|
||||||
|
@include desktop {
|
||||||
|
grid-template-columns: repeat(3, minmax(auto, 380px));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
@ -17,13 +17,23 @@
|
||||||
<p class="module-teaser__description">
|
<p class="module-teaser__description">
|
||||||
{{ teaser }}
|
{{ teaser }}
|
||||||
</p>
|
</p>
|
||||||
|
<div class="module-teaser__pills">
|
||||||
|
<pill :text="level?.name"></pill>
|
||||||
|
<pill :text="category?.name"></pill>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</router-link>
|
</router-link>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
import Pill from "@/components/ui/Pill.vue";
|
||||||
props: ['metaTitle', 'title', 'teaser', 'id', 'slug', 'heroImage'],
|
|
||||||
|
export default {
|
||||||
|
|
||||||
|
props: ['metaTitle', 'title', 'teaser', 'id', 'slug', 'heroImage', 'level', 'category'],
|
||||||
|
|
||||||
|
components: {Pill},
|
||||||
|
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
moduleLink() {
|
moduleLink() {
|
||||||
|
|
@ -39,16 +49,16 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@import 'styles/helpers';
|
@import 'styles/helpers';
|
||||||
|
|
||||||
.module-teaser {
|
.module-teaser {
|
||||||
box-shadow: 0 3px 9px 0 rgba(0, 0, 0, 0.12);
|
box-shadow: 0 3px 9px 0 rgba(0, 0, 0, 0.12);
|
||||||
border: 1px solid #e2e2e2;
|
border: 1px solid #e2e2e2;
|
||||||
height: 330px;
|
height: 390px;
|
||||||
max-width: 380px;
|
max-width: 380px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
|
|
@ -88,5 +98,9 @@ export default {
|
||||||
line-height: $default-line-height;
|
line-height: $default-line-height;
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
&__pills {
|
||||||
|
margin-top: $medium-spacing;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,128 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="dropdown"
|
||||||
|
v-if="selectedItem"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
data-cy="dropdown"
|
||||||
|
class="dropdown__selected-item"
|
||||||
|
@click.stop="toggle()"
|
||||||
|
>
|
||||||
|
<span class="dropdown__selected-item-text"> {{ selectedItem.name }}</span>
|
||||||
|
<ChevronDown class="dropdown__dropdown-icon" :class="{'rotate-chevron': showPopover}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<transition name="scaleY">
|
||||||
|
<WidgetPopover
|
||||||
|
class="dropdown__popover"
|
||||||
|
v-if="showPopover"
|
||||||
|
@hide-me="showPopover = false"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
:label="selectedItem.name"
|
||||||
|
:item="selectedItem"
|
||||||
|
data-cy="dropdown-item"
|
||||||
|
class="popover-links__link popover-links__link--large"
|
||||||
|
v-for="item in items"
|
||||||
|
:key="item.id"
|
||||||
|
@click="updateSelectedItem(item)"
|
||||||
|
>{{ item.name }}
|
||||||
|
</li>
|
||||||
|
</WidgetPopover>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import WidgetPopover from '@/components/ui/WidgetPopover.vue';
|
||||||
|
import {ref} from "vue";
|
||||||
|
import ChevronDown from "@/components/icons/ChevronDown.vue";
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
selectedItem: Object,
|
||||||
|
items: Object[],
|
||||||
|
}>();
|
||||||
|
|
||||||
|
let showPopover = ref(false)
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
showPopover.value = !showPopover.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSelectedItem(item: Object) {
|
||||||
|
emit('update:selectedItem', item);
|
||||||
|
showPopover.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:selectedItem']);
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@import 'styles/helpers';
|
||||||
|
|
||||||
|
.dropdown {
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-bottom: $medium-spacing;
|
||||||
|
border-radius: $input-border-radius;
|
||||||
|
|
||||||
|
|
||||||
|
&__popover {
|
||||||
|
white-space: nowrap;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
transform-origin: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__selected-item {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
border: solid $color-silver 1px;
|
||||||
|
border-radius: $input-border-radius;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__selected-item-text {
|
||||||
|
@include small-text;
|
||||||
|
margin-right: $small-spacing;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__dropdown-icon {
|
||||||
|
width: $default-icon-dimension;
|
||||||
|
height: $default-icon-dimension;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
fill: $color-brand;
|
||||||
|
|
||||||
|
&.rotate-chevron {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__popover {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: $default-border-radius;
|
||||||
|
margin-top: 4px;
|
||||||
|
|
||||||
|
.popover-links__link {
|
||||||
|
&:hover {
|
||||||
|
background-color: $color-silver-light;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.scaleY-enter-active,
|
||||||
|
.scaleY-leave-active {
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scaleY-enter,
|
||||||
|
.scaleY-leave-to {
|
||||||
|
transform: scaleY(0);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
<template>
|
||||||
|
<div class="pill" v-if="props.text">{{ props.text }}</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const props = defineProps<{
|
||||||
|
text: String;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@import 'styles/helpers';
|
||||||
|
|
||||||
|
.pill {
|
||||||
|
background-color: white;
|
||||||
|
color: $color-charcoal-dark; /* Replace with your desired text color */
|
||||||
|
padding: $small-spacing $medium-spacing;
|
||||||
|
border-radius: $round-border-radius;
|
||||||
|
border: 1px solid $color-silver;
|
||||||
|
display: inline-block; /* Ensures the pill takes only the necessary width */
|
||||||
|
margin-right: $small-spacing;
|
||||||
|
margin-top: 2px;
|
||||||
|
@include small-text;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
<template>
|
||||||
|
<div class="pill-radio">
|
||||||
|
<button
|
||||||
|
:class="['pill-radio__button', { 'pill-radio__button--active': item === selectedItem }]"
|
||||||
|
v-for="item in props.selectableItems"
|
||||||
|
:key="item.id"
|
||||||
|
@click="updateSelectedItem(item)"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {ref} from 'vue';
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:selectedItem']);
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
selectableItems: Object;
|
||||||
|
defaultSelectedItem: Object;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const selectedItem = ref(props.defaultSelectedItem)
|
||||||
|
|
||||||
|
function updateSelectedItem(item: object) {
|
||||||
|
emit('update:selectedItem', item);
|
||||||
|
selectedItem.value = item
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@import 'styles/helpers';
|
||||||
|
|
||||||
|
.pill-radio {
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 40px;
|
||||||
|
|
||||||
|
|
||||||
|
&__button {
|
||||||
|
flex: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: $color-silver-light;
|
||||||
|
|
||||||
|
outline: none;
|
||||||
|
padding-left: 1rem;
|
||||||
|
padding-right: 1rem;
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
&--active {
|
||||||
|
background-color: white;
|
||||||
|
border: solid $color-silver 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__button:first-child {
|
||||||
|
border-top-left-radius: $round-border-radius;
|
||||||
|
border-bottom-left-radius: $round-border-radius;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
&__button:last-child {
|
||||||
|
border-top-right-radius: $round-border-radius;
|
||||||
|
border-bottom-right-radius: $round-border-radius;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -9,6 +9,14 @@ fragment ModuleParts on ModuleNode {
|
||||||
heroSource
|
heroSource
|
||||||
solutionsEnabled
|
solutionsEnabled
|
||||||
inEditMode @client
|
inEditMode @client
|
||||||
|
level {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
category {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
topic {
|
topic {
|
||||||
slug
|
slug
|
||||||
title
|
title
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,11 @@ fragment UserParts on PrivateUserNode {
|
||||||
avatarUrl
|
avatarUrl
|
||||||
expiryDate
|
expiryDate
|
||||||
readOnly
|
readOnly
|
||||||
|
lastModuleLevel {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
filterAttributeType
|
||||||
|
}
|
||||||
lastModule {
|
lastModule {
|
||||||
id
|
id
|
||||||
slug
|
slug
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ export interface Me {
|
||||||
team: any;
|
team: any;
|
||||||
lastTopic: any;
|
lastTopic: any;
|
||||||
readOnly: boolean;
|
readOnly: boolean;
|
||||||
|
lastModuleLevel: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MeQuery {
|
export interface MeQuery {
|
||||||
|
|
@ -30,6 +31,7 @@ export interface Location {
|
||||||
|
|
||||||
type RouteLocation = Location | string;
|
type RouteLocation = Location | string;
|
||||||
|
|
||||||
|
// TODO: ME_QUERY existiert an einem weiteren Ort. Dieser sollte entfernt werden.
|
||||||
const defaultMe: MeQuery = {
|
const defaultMe: MeQuery = {
|
||||||
me: {
|
me: {
|
||||||
selectedClass: {
|
selectedClass: {
|
||||||
|
|
@ -42,6 +44,7 @@ const defaultMe: MeQuery = {
|
||||||
team: null,
|
team: null,
|
||||||
readOnly: false,
|
readOnly: false,
|
||||||
lastTopic: undefined,
|
lastTopic: undefined,
|
||||||
|
lastModuleLevel: undefined,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
<p class="topic__teaser">
|
<p class="topic__teaser">
|
||||||
{{ topic.teaser }}
|
{{ topic.teaser }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="topic__links">
|
<div class="topic__links">
|
||||||
<div
|
<div
|
||||||
class="topic__video-link topic__link"
|
class="topic__video-link topic__link"
|
||||||
|
|
@ -33,19 +34,15 @@
|
||||||
<span class="topic__link-description">Anweisungen zum {{ $flavor.textTopic }} anzeigen</span>
|
<span class="topic__link-description">Anweisungen zum {{ $flavor.textTopic }} anzeigen</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="topic__modules">
|
<div class="topic__modulefilter">
|
||||||
<module-teaser
|
<module-filter :modules="modules" :me="me" v-if="modules.length > 0" ></module-filter>
|
||||||
v-for="module in modules"
|
|
||||||
v-bind="module"
|
|
||||||
:key="module.slug"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import ModuleTeaser from '@/components/modules/ModuleTeaser.vue';
|
import ModuleFilter from '@/components/modules/ModuleFilter.vue';
|
||||||
import { defineAsyncComponent } from 'vue';
|
import { defineAsyncComponent } from 'vue';
|
||||||
import TOPIC_QUERY from '@/graphql/gql/queries/topicQuery.gql';
|
import TOPIC_QUERY from '@/graphql/gql/queries/topicQuery.gql';
|
||||||
import me from '@/mixins/me';
|
import me from '@/mixins/me';
|
||||||
|
|
@ -61,9 +58,9 @@ export default {
|
||||||
mixins: [me],
|
mixins: [me],
|
||||||
components: {
|
components: {
|
||||||
TopicNavigation,
|
TopicNavigation,
|
||||||
ModuleTeaser,
|
|
||||||
PlayIcon,
|
PlayIcon,
|
||||||
BulbIcon,
|
BulbIcon,
|
||||||
|
ModuleFilter,
|
||||||
},
|
},
|
||||||
|
|
||||||
apollo: {
|
apollo: {
|
||||||
|
|
@ -170,6 +167,7 @@ export default {
|
||||||
grid-template-columns: 300px 1fr;
|
grid-template-columns: 300px 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
&__navigation {
|
&__navigation {
|
||||||
padding: 0 $medium-spacing;
|
padding: 0 $medium-spacing;
|
||||||
display: none;
|
display: none;
|
||||||
|
|
@ -210,19 +208,5 @@ export default {
|
||||||
@include heading-3;
|
@include heading-3;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__modules {
|
|
||||||
margin-top: 40px;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
@supports (display: grid) {
|
|
||||||
display: grid;
|
|
||||||
}
|
|
||||||
grid-column-gap: $large-spacing;
|
|
||||||
grid-row-gap: $large-spacing;
|
|
||||||
|
|
||||||
@include desktop {
|
|
||||||
grid-template-columns: repeat(3, minmax(auto, 380px));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,101 +1,16 @@
|
||||||
{
|
{
|
||||||
"name": "cariot",
|
"name": "myskillbox",
|
||||||
"version": "1.0.1",
|
"version": "1.1.0",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "cariot",
|
"name": "myskillbox",
|
||||||
"version": "1.0.1",
|
"version": "1.1.0",
|
||||||
"dependencies": {
|
|
||||||
"babel-polyfill": "^6.26.0",
|
|
||||||
"unfetch": "^3.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "8.x"
|
"node": "20.x",
|
||||||
|
"npm": ">= 8.x"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"node_modules/babel-polyfill": {
|
|
||||||
"version": "6.26.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.26.0.tgz",
|
|
||||||
"integrity": "sha1-N5k3q8Z9eJWXCtxiHyhM2WbPIVM=",
|
|
||||||
"dependencies": {
|
|
||||||
"babel-runtime": "^6.26.0",
|
|
||||||
"core-js": "^2.5.0",
|
|
||||||
"regenerator-runtime": "^0.10.5"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/babel-runtime": {
|
|
||||||
"version": "6.26.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz",
|
|
||||||
"integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=",
|
|
||||||
"dependencies": {
|
|
||||||
"core-js": "^2.4.0",
|
|
||||||
"regenerator-runtime": "^0.11.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/babel-runtime/node_modules/regenerator-runtime": {
|
|
||||||
"version": "0.11.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz",
|
|
||||||
"integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg=="
|
|
||||||
},
|
|
||||||
"node_modules/core-js": {
|
|
||||||
"version": "2.5.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz",
|
|
||||||
"integrity": "sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw=="
|
|
||||||
},
|
|
||||||
"node_modules/regenerator-runtime": {
|
|
||||||
"version": "0.10.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz",
|
|
||||||
"integrity": "sha1-M2w+/BIgrc7dosn6tntaeVWjNlg="
|
|
||||||
},
|
|
||||||
"node_modules/unfetch": {
|
|
||||||
"version": "3.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/unfetch/-/unfetch-3.1.1.tgz",
|
|
||||||
"integrity": "sha512-syDl3htvM56w0HC0PTVA5jEEknOCJ3dWgWGDuaEtQUno8ORDCfZQbm12RzfWO3AC3YhWDoP61dlgmo8Z05Y97g=="
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"babel-polyfill": {
|
|
||||||
"version": "6.26.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.26.0.tgz",
|
|
||||||
"integrity": "sha1-N5k3q8Z9eJWXCtxiHyhM2WbPIVM=",
|
|
||||||
"requires": {
|
|
||||||
"babel-runtime": "^6.26.0",
|
|
||||||
"core-js": "^2.5.0",
|
|
||||||
"regenerator-runtime": "^0.10.5"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"babel-runtime": {
|
|
||||||
"version": "6.26.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz",
|
|
||||||
"integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=",
|
|
||||||
"requires": {
|
|
||||||
"core-js": "^2.4.0",
|
|
||||||
"regenerator-runtime": "^0.11.0"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"regenerator-runtime": {
|
|
||||||
"version": "0.11.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz",
|
|
||||||
"integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg=="
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"core-js": {
|
|
||||||
"version": "2.5.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz",
|
|
||||||
"integrity": "sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw=="
|
|
||||||
},
|
|
||||||
"regenerator-runtime": {
|
|
||||||
"version": "0.10.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz",
|
|
||||||
"integrity": "sha1-M2w+/BIgrc7dosn6tntaeVWjNlg="
|
|
||||||
},
|
|
||||||
"unfetch": {
|
|
||||||
"version": "3.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/unfetch/-/unfetch-3.1.1.tgz",
|
|
||||||
"integrity": "sha512-syDl3htvM56w0HC0PTVA5jEEknOCJ3dWgWGDuaEtQUno8ORDCfZQbm12RzfWO3AC3YhWDoP61dlgmo8Z05Y97g=="
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
from users.models import User
|
||||||
|
from .models.module import Module, ModuleLevel, ModuleCategory
|
||||||
|
|
||||||
|
def analyze_module_meta_titles():
|
||||||
|
all_nodes = []
|
||||||
|
|
||||||
|
for module in Module.objects.all():
|
||||||
|
print(f"{module.get_parent() } {module.meta_title} : {module.title}")
|
||||||
|
|
||||||
|
nodes = [i.strip() for i in [module.get_parent().title] + module.meta_title.split(' – ')]
|
||||||
|
all_nodes.append(nodes)
|
||||||
|
|
||||||
|
print("")
|
||||||
|
|
||||||
|
for i in range(3):
|
||||||
|
leafs = [node[i] for node in all_nodes if len(node) > i+1]
|
||||||
|
leafs.sort()
|
||||||
|
print(f"{i}.: {set(leafs)}")
|
||||||
|
|
||||||
|
|
||||||
|
def create_default_levels_and_categories():
|
||||||
|
ModuleLevel.objects.get_or_create(name=f"Alle Lehrjahre", filter_attribute_type="all")
|
||||||
|
|
||||||
|
for lehrjahr in range(1,4):
|
||||||
|
module_category, created = ModuleLevel.objects.get_or_create(name=f"{lehrjahr}. Lehrjahr")
|
||||||
|
|
||||||
|
|
||||||
|
ModuleCategory.objects.get_or_create(name=f"Alle Lernfelder", filter_attribute_type="all")
|
||||||
|
|
||||||
|
for category in range(1, 10):
|
||||||
|
ModuleCategory.objects.get_or_create(name=f"Lernfeld {category}")
|
||||||
|
|
||||||
|
|
||||||
|
def categorize_modules():
|
||||||
|
for level in ModuleLevel.objects.all():
|
||||||
|
modules = Module.objects.filter(level__isnull=True, meta_title__icontains=level.name)
|
||||||
|
print(f"{level.name}: {modules.count()}")
|
||||||
|
modules.update(level=level)
|
||||||
|
|
||||||
|
for category in ModuleCategory.objects.all():
|
||||||
|
modules = Module.objects.filter(category__isnull=True, meta_title__contains=category.name)
|
||||||
|
print(f"{category.name}: {modules.count()}")
|
||||||
|
modules.update(category=category)
|
||||||
|
|
||||||
|
|
||||||
|
def uncategorize_modules():
|
||||||
|
User.objects.all().update(last_module_level=None)
|
||||||
|
#
|
||||||
|
ModuleCategory.objects.all().delete()
|
||||||
|
ModuleLevel.objects.all().delete()
|
||||||
|
|
||||||
|
def delete_unused_levels():
|
||||||
|
for level in ModuleLevel.objects.filter(filter_attribute_type="exact"):
|
||||||
|
if not level.module_set.exists():
|
||||||
|
level.delete()
|
||||||
|
|
||||||
|
def delete_unused_categories():
|
||||||
|
for category in ModuleCategory.objects.filter(filter_attribute_type="exact"):
|
||||||
|
if not category.module_set.exists():
|
||||||
|
category.delete()
|
||||||
|
|
@ -28,7 +28,7 @@ from books.blocks import (
|
||||||
SurveyBlock,
|
SurveyBlock,
|
||||||
VideoBlock,
|
VideoBlock,
|
||||||
)
|
)
|
||||||
from books.models import Book, Chapter, ContentBlock, Module, TextBlock, Topic
|
from books.models import Book, Chapter, ContentBlock, Module, TextBlock, Topic, ModuleLevel, ModuleCategory
|
||||||
from core.factories import (
|
from core.factories import (
|
||||||
BasePageFactory,
|
BasePageFactory,
|
||||||
DummyImageFactory,
|
DummyImageFactory,
|
||||||
|
|
@ -201,6 +201,17 @@ class VideoBlockFactory(wagtail_factories.StructBlockFactory):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VideoBlock
|
model = VideoBlock
|
||||||
|
|
||||||
|
class ModuleLevelFactory(factory.DjangoModelFactory):
|
||||||
|
class Meta:
|
||||||
|
model = ModuleLevel
|
||||||
|
name = '1. Lehrjahr'
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleTypeFactory(factory.DjangoModelFactory):
|
||||||
|
class Meta:
|
||||||
|
model = ModuleCategory
|
||||||
|
name = 'Lernfeld 1'
|
||||||
|
|
||||||
|
|
||||||
block_types = [
|
block_types = [
|
||||||
"text_block",
|
"text_block",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
from django.core.management import BaseCommand
|
||||||
|
from books.categorize_modules import categorize_modules, delete_unused_levels, uncategorize_modules, \
|
||||||
|
delete_unused_categories
|
||||||
|
from books.categorize_modules import create_default_levels_and_categories
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
self.stdout.write("Categorizing modules")
|
||||||
|
uncategorize_modules()
|
||||||
|
create_default_levels_and_categories()
|
||||||
|
categorize_modules()
|
||||||
|
delete_unused_levels()
|
||||||
|
delete_unused_categories()
|
||||||
|
|
||||||
|
self.stdout.write("Finish categorizing modules")
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
from django.core.management import BaseCommand
|
||||||
|
from books.categorize_modules import categorize_modules, create_default_levels_and_categories
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
self.stdout.write("Create defaut categories")
|
||||||
|
create_default_levels_and_categories()
|
||||||
|
self.stdout.write("Finish")
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
# Generated by Django 3.2.16 on 2023-08-22 12:19
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('books', '0042_alter_contentblock_contents'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ModuleCategory',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=255)),
|
||||||
|
('filter_attribute_type', models.CharField(choices=[('all', 'All'), ('exact', 'Exact')], default='exact', max_length=16)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'module type',
|
||||||
|
'verbose_name_plural': 'module types',
|
||||||
|
'ordering': ('name',),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ModuleLevel',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=255, unique=True)),
|
||||||
|
('filter_attribute_type', models.CharField(choices=[('all', 'All'), ('exact', 'Exact')], default='exact', max_length=16)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'module level',
|
||||||
|
'verbose_name_plural': 'module Levels',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='module',
|
||||||
|
name='category',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='books.modulecategory'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='module',
|
||||||
|
name='level',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='books.modulelevel'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -1,13 +1,46 @@
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from wagtail.admin.panels import FieldPanel, InlinePanel, TabbedInterface, ObjectList
|
|
||||||
from wagtail.fields import RichTextField
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from wagtail.admin.panels import FieldPanel, TabbedInterface, ObjectList
|
||||||
|
from wagtail.fields import RichTextField
|
||||||
|
|
||||||
from core.constants import DEFAULT_RICH_TEXT_FEATURES
|
from core.constants import DEFAULT_RICH_TEXT_FEATURES
|
||||||
from core.wagtail_utils import StrictHierarchyPage, get_default_settings
|
from core.wagtail_utils import StrictHierarchyPage, get_default_settings
|
||||||
from users.models import SchoolClass
|
from users.models import SchoolClass
|
||||||
|
|
||||||
|
EXACT = "exact"
|
||||||
|
|
||||||
|
FILTER_ATTRIBUTE_TYPE = (("all", "All"), (EXACT, "Exact"))
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleLevel(models.Model):
|
||||||
|
name = models.CharField(max_length=255, unique=True)
|
||||||
|
filter_attribute_type = models.CharField(
|
||||||
|
max_length=16, choices=FILTER_ATTRIBUTE_TYPE, default=EXACT
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name_plural = _("module Levels")
|
||||||
|
verbose_name = _("module level")
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleCategory(models.Model):
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("module type")
|
||||||
|
verbose_name_plural = _("module types")
|
||||||
|
ordering = ("name",)
|
||||||
|
|
||||||
|
name = models.CharField(max_length=255)
|
||||||
|
filter_attribute_type = models.CharField(
|
||||||
|
max_length=16, choices=FILTER_ATTRIBUTE_TYPE, default=EXACT
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.name}"
|
||||||
|
|
||||||
|
|
||||||
class Module(StrictHierarchyPage):
|
class Module(StrictHierarchyPage):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
@ -15,6 +48,13 @@ class Module(StrictHierarchyPage):
|
||||||
verbose_name_plural = "Module"
|
verbose_name_plural = "Module"
|
||||||
|
|
||||||
meta_title = models.CharField(max_length=255, help_text="e.g. 'Intro' or 'Modul 1'")
|
meta_title = models.CharField(max_length=255, help_text="e.g. 'Intro' or 'Modul 1'")
|
||||||
|
level = models.ForeignKey(
|
||||||
|
ModuleLevel, on_delete=models.SET_NULL, blank=True, null=True
|
||||||
|
)
|
||||||
|
category = models.ForeignKey(
|
||||||
|
ModuleCategory, on_delete=models.SET_NULL, blank=True, null=True
|
||||||
|
)
|
||||||
|
|
||||||
hero_image = models.ForeignKey(
|
hero_image = models.ForeignKey(
|
||||||
"wagtailimages.Image",
|
"wagtailimages.Image",
|
||||||
null=True,
|
null=True,
|
||||||
|
|
@ -34,30 +74,12 @@ class Module(StrictHierarchyPage):
|
||||||
content_panels = [
|
content_panels = [
|
||||||
FieldPanel("title", classname="full title"),
|
FieldPanel("title", classname="full title"),
|
||||||
FieldPanel("meta_title", classname="full title"),
|
FieldPanel("meta_title", classname="full title"),
|
||||||
|
FieldPanel("level"),
|
||||||
|
FieldPanel("category"),
|
||||||
FieldPanel("hero_image"),
|
FieldPanel("hero_image"),
|
||||||
FieldPanel("hero_source"),
|
FieldPanel("hero_source"),
|
||||||
FieldPanel("teaser"),
|
FieldPanel("teaser"),
|
||||||
FieldPanel("intro"),
|
FieldPanel("intro"),
|
||||||
# InlinePanel(
|
|
||||||
# "assignments",
|
|
||||||
# label=_("Assignment"),
|
|
||||||
# classname="collapsed",
|
|
||||||
# heading=_("linked assignments"),
|
|
||||||
# help_text=_(
|
|
||||||
# "These %s are automatically linked, they are shown here only to provide an overview. Please don't change anything here."
|
|
||||||
# )
|
|
||||||
# % _("assignments"),
|
|
||||||
# ),
|
|
||||||
# InlinePanel(
|
|
||||||
# "surveys",
|
|
||||||
# heading=_("linked surveys"),
|
|
||||||
# label=_("Survey"),
|
|
||||||
# classname="collapsed",
|
|
||||||
# help_text=_(
|
|
||||||
# "These %s are automatically linked, they are shown here only to provide an overview. Please don't change anything here."
|
|
||||||
# )
|
|
||||||
# % _("surveys"),
|
|
||||||
# ),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
edit_handler = TabbedInterface(
|
edit_handler = TabbedInterface(
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
from books.schema.mutations.chapter import UpdateChapterVisibility
|
from books.schema.mutations.chapter import UpdateChapterVisibility
|
||||||
from books.schema.mutations.contentblock import DuplicateContentBlock, MutateContentBlock, AddContentBlock, \
|
from books.schema.mutations.contentblock import DuplicateContentBlock, MutateContentBlock, AddContentBlock, \
|
||||||
DeleteContentBlock
|
DeleteContentBlock
|
||||||
from books.schema.mutations.module import UpdateSolutionVisibility, UpdateLastModule, SyncModuleVisibility
|
from books.schema.mutations.module import UpdateSolutionVisibility, UpdateLastModule, SyncModuleVisibility, UpdateLastModuleLevel
|
||||||
from books.schema.mutations.snapshot import CreateSnapshot, ApplySnapshot, ShareSnapshot, UpdateSnapshot, DeleteSnapshot
|
from books.schema.mutations.snapshot import CreateSnapshot, ApplySnapshot, ShareSnapshot, UpdateSnapshot, DeleteSnapshot
|
||||||
from books.schema.mutations.topic import UpdateLastTopic
|
from books.schema.mutations.topic import UpdateLastTopic
|
||||||
|
|
||||||
|
|
@ -14,6 +14,7 @@ class BookMutations(object):
|
||||||
update_solution_visibility = UpdateSolutionVisibility.Field()
|
update_solution_visibility = UpdateSolutionVisibility.Field()
|
||||||
update_last_module = UpdateLastModule.Field()
|
update_last_module = UpdateLastModule.Field()
|
||||||
update_last_topic = UpdateLastTopic.Field()
|
update_last_topic = UpdateLastTopic.Field()
|
||||||
|
update_last_module_level = UpdateLastModuleLevel.Field()
|
||||||
update_chapter_visibility = UpdateChapterVisibility.Field()
|
update_chapter_visibility = UpdateChapterVisibility.Field()
|
||||||
sync_module_visibility = SyncModuleVisibility.Field()
|
sync_module_visibility = SyncModuleVisibility.Field()
|
||||||
create_snapshot = CreateSnapshot.Field()
|
create_snapshot = CreateSnapshot.Field()
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
import graphene
|
import graphene
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from graphene import relay
|
from graphene import relay
|
||||||
|
|
||||||
from api.utils import get_object
|
from api.utils import get_object
|
||||||
from books.models import Module, RecentModule
|
from books.models import Module, RecentModule, ModuleLevel
|
||||||
from books.schema.nodes import ModuleNode
|
from books.schema.nodes import ModuleNode
|
||||||
from users.models import SchoolClass
|
from users.models import SchoolClass, User
|
||||||
|
from users.schema import PrivateUserNode
|
||||||
|
|
||||||
|
|
||||||
class UpdateSolutionVisibility(relay.ClientIDMutation):
|
class UpdateSolutionVisibility(relay.ClientIDMutation):
|
||||||
|
|
@ -104,3 +103,19 @@ class SyncModuleVisibility(relay.ClientIDMutation):
|
||||||
module.sync_from_school_class(template, school_class)
|
module.sync_from_school_class(template, school_class)
|
||||||
|
|
||||||
return cls(success=True)
|
return cls(success=True)
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateLastModuleLevel(relay.ClientIDMutation):
|
||||||
|
class Input:
|
||||||
|
id = graphene.ID()
|
||||||
|
|
||||||
|
user = graphene.Field(PrivateUserNode)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def mutate_and_get_payload(cls, root, info, **args):
|
||||||
|
user = info.context.user
|
||||||
|
id = args.get('id')
|
||||||
|
module_level = get_object(ModuleLevel, id)
|
||||||
|
|
||||||
|
User.objects.filter(pk=user.id).update(last_module_level_id=module_level.id)
|
||||||
|
return cls(user=user)
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ from graphene import relay
|
||||||
|
|
||||||
from api.types import FailureNode, Success
|
from api.types import FailureNode, Success
|
||||||
from api.utils import get_object
|
from api.utils import get_object
|
||||||
from books.models import Module, ContentBlock, Chapter
|
from books.models import Module
|
||||||
from books.models.snapshot import Snapshot
|
from books.models.snapshot import Snapshot
|
||||||
from books.schema.nodes import SnapshotNode, ModuleNode
|
from books.schema.nodes import SnapshotNode, ModuleNode
|
||||||
from users.models import SchoolClass
|
from users.models import SchoolClass
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,16 @@ from graphene_django.filter import DjangoFilterConnectionField
|
||||||
|
|
||||||
from assignments.models import StudentSubmission
|
from assignments.models import StudentSubmission
|
||||||
from assignments.schema.types import AssignmentNode, StudentSubmissionNode
|
from assignments.schema.types import AssignmentNode, StudentSubmissionNode
|
||||||
from books.models import Module, Chapter, ContentBlock, RecentModule
|
from books.models import (
|
||||||
|
Module,
|
||||||
|
Chapter,
|
||||||
|
ContentBlock,
|
||||||
|
RecentModule,
|
||||||
|
)
|
||||||
from books.schema.interfaces.module import ModuleInterface
|
from books.schema.interfaces.module import ModuleInterface
|
||||||
from books.schema.nodes.chapter import ChapterNode
|
from books.schema.nodes.chapter import ChapterNode
|
||||||
|
from books.schema.nodes.module_category import ModuleCategoryNode
|
||||||
|
from books.schema.nodes.module_level import ModuleLevelNode
|
||||||
from notes.models import ModuleBookmark, ContentBlockBookmark, ChapterBookmark
|
from notes.models import ModuleBookmark, ContentBlockBookmark, ChapterBookmark
|
||||||
from notes.schema import (
|
from notes.schema import (
|
||||||
ModuleBookmarkNode,
|
ModuleBookmarkNode,
|
||||||
|
|
@ -34,6 +41,8 @@ class ModuleNode(DjangoObjectType):
|
||||||
"hero_image",
|
"hero_image",
|
||||||
"hero_source",
|
"hero_source",
|
||||||
"topic",
|
"topic",
|
||||||
|
"level",
|
||||||
|
"category",
|
||||||
]
|
]
|
||||||
filter_fields = {
|
filter_fields = {
|
||||||
"slug": ["exact", "icontains", "in"],
|
"slug": ["exact", "icontains", "in"],
|
||||||
|
|
@ -46,12 +55,13 @@ class ModuleNode(DjangoObjectType):
|
||||||
bookmark = graphene.Field(ModuleBookmarkNode)
|
bookmark = graphene.Field(ModuleBookmarkNode)
|
||||||
my_submissions = DjangoFilterConnectionField(StudentSubmissionNode)
|
my_submissions = DjangoFilterConnectionField(StudentSubmissionNode)
|
||||||
my_answers = DjangoFilterConnectionField(AnswerNode)
|
my_answers = DjangoFilterConnectionField(AnswerNode)
|
||||||
my_content_bookmarks = DjangoFilterConnectionField(
|
my_content_bookmarks = DjangoFilterConnectionField(ContentBlockBookmarkNode)
|
||||||
ContentBlockBookmarkNode)
|
|
||||||
my_chapter_bookmarks = DjangoFilterConnectionField(ChapterBookmarkNode)
|
my_chapter_bookmarks = DjangoFilterConnectionField(ChapterBookmarkNode)
|
||||||
snapshots = graphene.List("books.schema.nodes.SnapshotNode")
|
snapshots = graphene.List("books.schema.nodes.SnapshotNode")
|
||||||
objective_groups = graphene.List(ObjectiveGroupNode)
|
objective_groups = graphene.List(ObjectiveGroupNode)
|
||||||
assignments = graphene.List(AssignmentNode)
|
assignments = graphene.List(AssignmentNode)
|
||||||
|
level = graphene.Field(ModuleLevelNode)
|
||||||
|
category = graphene.Field(ModuleCategoryNode)
|
||||||
|
|
||||||
def resolve_chapters(self, info, **kwargs):
|
def resolve_chapters(self, info, **kwargs):
|
||||||
return Chapter.get_by_parent(self)
|
return Chapter.get_by_parent(self)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
from graphene import relay
|
||||||
|
from graphene_django import DjangoObjectType
|
||||||
|
|
||||||
|
from books.models import ModuleCategory
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleCategoryNode(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = ModuleCategory
|
||||||
|
interfaces = (relay.Node,)
|
||||||
|
only_fields = "__all__"
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
from graphene import relay
|
||||||
|
from graphene_django import DjangoObjectType
|
||||||
|
|
||||||
|
from books.models import ModuleLevel
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleLevelNode(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = ModuleLevel
|
||||||
|
interfaces = (relay.Node,)
|
||||||
|
only_fields = "__all__"
|
||||||
|
|
@ -5,9 +5,17 @@ from graphene_django.filter import DjangoFilterConnectionField
|
||||||
from api.utils import get_object
|
from api.utils import get_object
|
||||||
from core.logger import get_logger
|
from core.logger import get_logger
|
||||||
from .connections import TopicConnection, ModuleConnection
|
from .connections import TopicConnection, ModuleConnection
|
||||||
from .nodes import ContentBlockNode, ChapterNode, ModuleNode, NotFoundFailure, SnapshotNode, \
|
from .nodes import (
|
||||||
TopicOr404Node
|
ContentBlockNode,
|
||||||
from ..models import Book, Topic, Module, Chapter, Snapshot
|
ChapterNode,
|
||||||
|
ModuleNode,
|
||||||
|
NotFoundFailure,
|
||||||
|
SnapshotNode,
|
||||||
|
TopicOr404Node,
|
||||||
|
)
|
||||||
|
from .nodes.module_category import ModuleCategoryNode
|
||||||
|
from .nodes.module_level import ModuleLevelNode
|
||||||
|
from ..models import Book, Topic, Module, Chapter, Snapshot, ModuleLevel, ModuleCategory
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
@ -15,8 +23,7 @@ logger = get_logger(__name__)
|
||||||
class BookQuery(object):
|
class BookQuery(object):
|
||||||
node = relay.Node.Field()
|
node = relay.Node.Field()
|
||||||
topic = graphene.Field(TopicOr404Node, slug=graphene.String())
|
topic = graphene.Field(TopicOr404Node, slug=graphene.String())
|
||||||
module = graphene.Field(
|
module = graphene.Field(ModuleNode, slug=graphene.String(), id=graphene.ID())
|
||||||
ModuleNode, slug=graphene.String(), id=graphene.ID())
|
|
||||||
chapter = relay.Node.Field(ChapterNode)
|
chapter = relay.Node.Field(ChapterNode)
|
||||||
content_block = relay.Node.Field(ContentBlockNode)
|
content_block = relay.Node.Field(ContentBlockNode)
|
||||||
snapshot = relay.Node.Field(SnapshotNode)
|
snapshot = relay.Node.Field(SnapshotNode)
|
||||||
|
|
@ -25,6 +32,12 @@ class BookQuery(object):
|
||||||
modules = relay.ConnectionField(ModuleConnection)
|
modules = relay.ConnectionField(ModuleConnection)
|
||||||
chapters = DjangoFilterConnectionField(ChapterNode)
|
chapters = DjangoFilterConnectionField(ChapterNode)
|
||||||
|
|
||||||
|
module_level = graphene.Field(ModuleLevelNode, id=graphene.ID(required=True))
|
||||||
|
module_levels = graphene.List(ModuleLevelNode)
|
||||||
|
|
||||||
|
module_category = graphene.Field(ModuleCategoryNode, id=graphene.ID(required=True))
|
||||||
|
module_categories = graphene.List(ModuleCategoryNode)
|
||||||
|
|
||||||
def resolve_books(self, *args, **kwargs):
|
def resolve_books(self, *args, **kwargs):
|
||||||
return Book.objects.filter(**kwargs).live()
|
return Book.objects.filter(**kwargs).live()
|
||||||
|
|
||||||
|
|
@ -38,13 +51,13 @@ class BookQuery(object):
|
||||||
return Chapter.objects.filter(**kwargs).live()
|
return Chapter.objects.filter(**kwargs).live()
|
||||||
|
|
||||||
def resolve_snapshot(self, info, **kwargs):
|
def resolve_snapshot(self, info, **kwargs):
|
||||||
id = kwargs.get('id')
|
id = kwargs.get("id")
|
||||||
snapshot = get_object(Snapshot, id)
|
snapshot = get_object(Snapshot, id)
|
||||||
return snapshot
|
return snapshot
|
||||||
|
|
||||||
def resolve_module(self, info, **kwargs):
|
def resolve_module(self, info, **kwargs):
|
||||||
slug = kwargs.get('slug')
|
slug = kwargs.get("slug")
|
||||||
id = kwargs.get('id')
|
id = kwargs.get("id")
|
||||||
module = None
|
module = None
|
||||||
try:
|
try:
|
||||||
if id is not None:
|
if id is not None:
|
||||||
|
|
@ -59,8 +72,8 @@ class BookQuery(object):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def resolve_topic(self, info, **kwargs):
|
def resolve_topic(self, info, **kwargs):
|
||||||
slug = kwargs.get('slug')
|
slug = kwargs.get("slug")
|
||||||
id = kwargs.get('id')
|
id = kwargs.get("id")
|
||||||
|
|
||||||
if id is not None:
|
if id is not None:
|
||||||
return get_object(Topic, id)
|
return get_object(Topic, id)
|
||||||
|
|
@ -70,3 +83,33 @@ class BookQuery(object):
|
||||||
except Topic.DoesNotExist:
|
except Topic.DoesNotExist:
|
||||||
return NotFoundFailure
|
return NotFoundFailure
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def resolve_module_level(self, info, **kwargs):
|
||||||
|
module_level_id = kwargs.get("id")
|
||||||
|
try:
|
||||||
|
if module_level_id is not None:
|
||||||
|
return get_object(Module, module_level_id)
|
||||||
|
|
||||||
|
except Module.DoesNotExist:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def resolve_module_levels(self, *args, **kwargs):
|
||||||
|
return ModuleLevel.objects.all()
|
||||||
|
|
||||||
|
def resolve_module_category(self, info, **kwargs):
|
||||||
|
id = kwargs.get("id")
|
||||||
|
try:
|
||||||
|
if id is not None:
|
||||||
|
return get_object(Module, id)
|
||||||
|
|
||||||
|
except Module.DoesNotExist:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def resolve_module_categories(self, *args, **kwargs):
|
||||||
|
return ModuleCategory.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleTypeQuery(graphene.ObjectType):
|
||||||
|
node = relay.Node.Field()
|
||||||
|
name = graphene.String()
|
||||||
|
id = graphene.ID()
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
from django.test import TestCase, RequestFactory
|
from django.test import TestCase, RequestFactory
|
||||||
|
from unittest import skip
|
||||||
from graphene.test import Client
|
from graphene.test import Client
|
||||||
from graphql_relay import to_global_id
|
from graphql_relay import to_global_id
|
||||||
|
|
||||||
from api.schema import schema
|
from api.schema import schema
|
||||||
from api.utils import get_object
|
from api.utils import get_object
|
||||||
from books.models import ContentBlock, Chapter
|
from books.models import ContentBlock, Chapter
|
||||||
from books.factories import ModuleFactory
|
from books.factories import ModuleFactory, ModuleLevelFactory
|
||||||
from core.factories import UserFactory
|
from core.factories import UserFactory
|
||||||
|
from users.models import User
|
||||||
|
|
||||||
|
|
||||||
class NewContentBlockMutationTest(TestCase):
|
class NewContentBlockMutationTest(TestCase):
|
||||||
|
|
@ -14,12 +16,12 @@ class NewContentBlockMutationTest(TestCase):
|
||||||
module = ModuleFactory()
|
module = ModuleFactory()
|
||||||
chapter = Chapter(title='Hello')
|
chapter = Chapter(title='Hello')
|
||||||
module.add_child(instance=chapter)
|
module.add_child(instance=chapter)
|
||||||
user = UserFactory(username='aschi')
|
self.user = UserFactory(username='aschi')
|
||||||
content_block = ContentBlock(title='bla', slug='bla')
|
content_block = ContentBlock(title='bla', slug='bla')
|
||||||
chapter.specific.add_child(instance=content_block)
|
chapter.specific.add_child(instance=content_block)
|
||||||
|
|
||||||
request = RequestFactory().get('/')
|
request = RequestFactory().get('/')
|
||||||
request.user = user
|
request.user = self.user
|
||||||
self.client = Client(schema=schema, context_value=request)
|
self.client = Client(schema=schema, context_value=request)
|
||||||
|
|
||||||
self.sibling_id = to_global_id('ContentBlockNode', content_block.pk)
|
self.sibling_id = to_global_id('ContentBlockNode', content_block.pk)
|
||||||
|
|
@ -120,3 +122,30 @@ class NewContentBlockMutationTest(TestCase):
|
||||||
content = new_content_block['contents'][0]
|
content = new_content_block['contents'][0]
|
||||||
self.assertEqual(content.get('type'), 'image_url_block')
|
self.assertEqual(content.get('type'), 'image_url_block')
|
||||||
self.assertEqual(content.get('value'), {'url': '/test.png'})
|
self.assertEqual(content.get('value'), {'url': '/test.png'})
|
||||||
|
|
||||||
|
def test_updateLastModuleLevel(self):
|
||||||
|
self.assertIsNone(self.user.last_module_level, None)
|
||||||
|
|
||||||
|
moduleLevel = ModuleLevelFactory(name='1. Lehrjahr')
|
||||||
|
moduleLevel1 = ModuleLevelFactory(name='2. Lehrjahr')
|
||||||
|
|
||||||
|
mutation = """
|
||||||
|
mutation ($input: UpdateLastModuleLevelInput!) {
|
||||||
|
updateLastModuleLevel(input: $input) {
|
||||||
|
clientMutationId
|
||||||
|
user {
|
||||||
|
username
|
||||||
|
lastModuleLevel {
|
||||||
|
name
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
module_level_gql_id = to_global_id('ModuleLevelNode', moduleLevel1.pk)
|
||||||
|
result = self.client.execute(mutation, variables={"input": {"id": module_level_gql_id}})
|
||||||
|
self.assertIsNone(result.get('errors'))
|
||||||
|
|
||||||
|
updated_user = User.objects.get(id=self.user.id)
|
||||||
|
self.assertEqual(updated_user.last_module_level.name, moduleLevel1.name)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
from wagtail.contrib.modeladmin.options import (
|
||||||
|
ModelAdmin,
|
||||||
|
ModelAdminGroup,
|
||||||
|
modeladmin_register,
|
||||||
|
)
|
||||||
|
from .models.module import ModuleLevel, ModuleCategory, Module
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleAdmin(ModelAdmin):
|
||||||
|
model = Module
|
||||||
|
list_display = ("title", "meta_title", "level", "category")
|
||||||
|
search_fields = ("title", "meta_title")
|
||||||
|
list_filter = ("level", "category")
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleLevelAdmin(ModelAdmin):
|
||||||
|
model = ModuleLevel
|
||||||
|
list_display = ("name",)
|
||||||
|
ordering = ("name",)
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleCategoryAdmin(ModelAdmin):
|
||||||
|
model = ModuleCategory
|
||||||
|
list_display = ("name",)
|
||||||
|
ordering = ("name",)
|
||||||
|
inspect_view_fields = ("name",)
|
||||||
|
|
||||||
|
|
||||||
|
class InstrumentGroup(ModelAdminGroup):
|
||||||
|
menu_label = _("Modules")
|
||||||
|
items = (
|
||||||
|
ModuleAdmin,
|
||||||
|
ModuleLevelAdmin,
|
||||||
|
ModuleCategoryAdmin,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
modeladmin_register(InstrumentGroup)
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
#!/bin/bash
|
||||||
|
python manage.py graphql_schema
|
||||||
|
sed -i '' 's/Node, Node/Node & Node/g' schema.graphql
|
||||||
|
|
||||||
|
python manage.py graphql_schema --schema api.schema_public.schema --out schema-public.graphql
|
||||||
|
sed -i '' 's/Node, Node/Node & Node/g' schema-public.graphql
|
||||||
|
|
@ -236,7 +236,7 @@ interface ChapterInterface {
|
||||||
title: String
|
title: String
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChapterNode implements Node & ChapterInterface {
|
type ChapterNode implements Node, ChapterInterface {
|
||||||
title: String
|
title: String
|
||||||
slug: String!
|
slug: String!
|
||||||
description: String
|
description: String
|
||||||
|
|
@ -306,7 +306,7 @@ interface ContentBlockInterface {
|
||||||
type: String
|
type: String
|
||||||
}
|
}
|
||||||
|
|
||||||
type ContentBlockNode implements Node & ContentBlockInterface {
|
type ContentBlockNode implements Node, ContentBlockInterface {
|
||||||
title: String
|
title: String
|
||||||
slug: String!
|
slug: String!
|
||||||
hiddenFor: [SchoolClassNode]
|
hiddenFor: [SchoolClassNode]
|
||||||
|
|
@ -621,6 +621,17 @@ type ModuleBookmarkNode {
|
||||||
module: ModuleNode!
|
module: ModuleNode!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum ModuleCategoryFilterAttributeType {
|
||||||
|
ALL
|
||||||
|
EXACT
|
||||||
|
}
|
||||||
|
|
||||||
|
type ModuleCategoryNode implements Node {
|
||||||
|
id: ID!
|
||||||
|
name: String!
|
||||||
|
filterAttributeType: ModuleCategoryFilterAttributeType!
|
||||||
|
}
|
||||||
|
|
||||||
type ModuleConnection {
|
type ModuleConnection {
|
||||||
pageInfo: PageInfo!
|
pageInfo: PageInfo!
|
||||||
edges: [ModuleEdge]!
|
edges: [ModuleEdge]!
|
||||||
|
|
@ -639,10 +650,23 @@ interface ModuleInterface {
|
||||||
topic: TopicNode
|
topic: TopicNode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum ModuleLevelFilterAttributeType {
|
||||||
|
ALL
|
||||||
|
EXACT
|
||||||
|
}
|
||||||
|
|
||||||
|
type ModuleLevelNode implements Node {
|
||||||
|
id: ID!
|
||||||
|
name: String!
|
||||||
|
filterAttributeType: ModuleLevelFilterAttributeType!
|
||||||
|
}
|
||||||
|
|
||||||
type ModuleNode implements ModuleInterface {
|
type ModuleNode implements ModuleInterface {
|
||||||
title: String!
|
title: String!
|
||||||
slug: String!
|
slug: String!
|
||||||
metaTitle: String!
|
metaTitle: String!
|
||||||
|
level: ModuleLevelNode
|
||||||
|
category: ModuleCategoryNode
|
||||||
heroImage: String!
|
heroImage: String!
|
||||||
heroSource: String!
|
heroSource: String!
|
||||||
teaser: String!
|
teaser: String!
|
||||||
|
|
@ -735,6 +759,7 @@ type Mutation {
|
||||||
updateSolutionVisibility(input: UpdateSolutionVisibilityInput!): UpdateSolutionVisibilityPayload
|
updateSolutionVisibility(input: UpdateSolutionVisibilityInput!): UpdateSolutionVisibilityPayload
|
||||||
updateLastModule(input: UpdateLastModuleInput!): UpdateLastModulePayload
|
updateLastModule(input: UpdateLastModuleInput!): UpdateLastModulePayload
|
||||||
updateLastTopic(input: UpdateLastTopicInput!): UpdateLastTopicPayload
|
updateLastTopic(input: UpdateLastTopicInput!): UpdateLastTopicPayload
|
||||||
|
updateLastModuleLevel(input: UpdateLastModuleLevelInput!): UpdateLastModuleLevelPayload
|
||||||
updateChapterVisibility(input: UpdateChapterVisibilityInput!): UpdateChapterVisibilityPayload
|
updateChapterVisibility(input: UpdateChapterVisibilityInput!): UpdateChapterVisibilityPayload
|
||||||
syncModuleVisibility(input: SyncModuleVisibilityInput!): SyncModuleVisibilityPayload
|
syncModuleVisibility(input: SyncModuleVisibilityInput!): SyncModuleVisibilityPayload
|
||||||
createSnapshot(input: CreateSnapshotInput!): CreateSnapshotPayload
|
createSnapshot(input: CreateSnapshotInput!): CreateSnapshotPayload
|
||||||
|
|
@ -847,6 +872,7 @@ type PrivateUserNode implements Node {
|
||||||
avatarUrl: String!
|
avatarUrl: String!
|
||||||
username: String!
|
username: String!
|
||||||
lastModule: ModuleNode
|
lastModule: ModuleNode
|
||||||
|
lastModuleLevel: ModuleLevelNode
|
||||||
lastTopic: TopicNode
|
lastTopic: TopicNode
|
||||||
email: String!
|
email: String!
|
||||||
onboardingVisited: Boolean!
|
onboardingVisited: Boolean!
|
||||||
|
|
@ -928,6 +954,10 @@ type Query {
|
||||||
topics(before: String, after: String, first: Int, last: Int): TopicConnection
|
topics(before: String, after: String, first: Int, last: Int): TopicConnection
|
||||||
modules(before: String, after: String, first: Int, last: Int): ModuleConnection
|
modules(before: String, after: String, first: Int, last: Int): ModuleConnection
|
||||||
chapters(offset: Int, before: String, after: String, first: Int, last: Int, slug: String, title: String): ChapterNodeConnection
|
chapters(offset: Int, before: String, after: String, first: Int, last: Int, slug: String, title: String): ChapterNodeConnection
|
||||||
|
moduleLevel(id: ID!): ModuleLevelNode
|
||||||
|
moduleLevels: [ModuleLevelNode]
|
||||||
|
moduleCategory(id: ID!): ModuleCategoryNode
|
||||||
|
moduleCategories: [ModuleCategoryNode]
|
||||||
objectiveGroup(id: ID!): ObjectiveGroupNode
|
objectiveGroup(id: ID!): ObjectiveGroupNode
|
||||||
objectiveGroups(offset: Int, before: String, after: String, first: Int, last: Int, title: String, module_Slug: String): ObjectiveGroupNodeConnection
|
objectiveGroups(offset: Int, before: String, after: String, first: Int, last: Int, title: String, module_Slug: String): ObjectiveGroupNodeConnection
|
||||||
roomEntry(id: ID, slug: String): RoomEntryNode
|
roomEntry(id: ID, slug: String): RoomEntryNode
|
||||||
|
|
@ -1013,7 +1043,7 @@ type SnapshotChangesNode {
|
||||||
newContentBlocks: Int!
|
newContentBlocks: Int!
|
||||||
}
|
}
|
||||||
|
|
||||||
type SnapshotChapterNode implements Node & ChapterInterface {
|
type SnapshotChapterNode implements Node, ChapterInterface {
|
||||||
id: ID!
|
id: ID!
|
||||||
description: String
|
description: String
|
||||||
title: String
|
title: String
|
||||||
|
|
@ -1022,7 +1052,7 @@ type SnapshotChapterNode implements Node & ChapterInterface {
|
||||||
titleHidden: Boolean
|
titleHidden: Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type SnapshotContentBlockNode implements Node & ContentBlockInterface {
|
type SnapshotContentBlockNode implements Node, ContentBlockInterface {
|
||||||
id: ID!
|
id: ID!
|
||||||
title: String
|
title: String
|
||||||
contents: GenericStreamFieldType
|
contents: GenericStreamFieldType
|
||||||
|
|
@ -1306,6 +1336,16 @@ input UpdateLastModuleInput {
|
||||||
clientMutationId: String
|
clientMutationId: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input UpdateLastModuleLevelInput {
|
||||||
|
id: ID
|
||||||
|
clientMutationId: String
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateLastModuleLevelPayload {
|
||||||
|
user: PrivateUserNode
|
||||||
|
clientMutationId: String
|
||||||
|
}
|
||||||
|
|
||||||
type UpdateLastModulePayload {
|
type UpdateLastModulePayload {
|
||||||
lastModule: ModuleNode
|
lastModule: ModuleNode
|
||||||
clientMutationId: String
|
clientMutationId: String
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
# Generated by Django 3.2.16 on 2023-08-22 12:19
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('books', '0043_auto_20230822_1219'),
|
||||||
|
('users', '0033_alter_license_isbn'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='last_module_level',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='books.modulelevel'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -33,6 +33,7 @@ class User(AbstractUser):
|
||||||
|
|
||||||
last_module = models.ForeignKey('books.Module', related_name='+', on_delete=models.SET_NULL, null=True)
|
last_module = models.ForeignKey('books.Module', related_name='+', on_delete=models.SET_NULL, null=True)
|
||||||
recent_modules = models.ManyToManyField('books.Module', related_name='+', through='books.RecentModule')
|
recent_modules = models.ManyToManyField('books.Module', related_name='+', through='books.RecentModule')
|
||||||
|
last_module_level = models.ForeignKey('books.ModuleLevel', related_name='+', on_delete=models.SET_NULL, null=True)
|
||||||
last_topic = models.ForeignKey('books.Topic', related_name='+', on_delete=models.SET_NULL, null=True)
|
last_topic = models.ForeignKey('books.Topic', related_name='+', on_delete=models.SET_NULL, null=True)
|
||||||
avatar_url = models.CharField(max_length=254, blank=True, default='')
|
avatar_url = models.CharField(max_length=254, blank=True, default='')
|
||||||
email = models.EmailField(_('email address'), unique=True)
|
email = models.EmailField(_('email address'), unique=True)
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ from graphql_relay import to_global_id
|
||||||
from api.types import FailureNode
|
from api.types import FailureNode
|
||||||
|
|
||||||
from books.models import Module
|
from books.models import Module
|
||||||
|
from books.schema.nodes import ModuleLevelNode
|
||||||
from books.schema.queries import ModuleNode
|
from books.schema.queries import ModuleNode
|
||||||
from users.models import SchoolClass, SchoolClassMember, Team, User
|
from users.models import SchoolClass, SchoolClassMember, Team, User
|
||||||
|
|
||||||
|
|
@ -103,6 +104,7 @@ class PrivateUserNode(DjangoObjectType):
|
||||||
"onboarding_visited",
|
"onboarding_visited",
|
||||||
"team",
|
"team",
|
||||||
"read_only",
|
"read_only",
|
||||||
|
"last_module_level"
|
||||||
]
|
]
|
||||||
interfaces = (relay.Node,)
|
interfaces = (relay.Node,)
|
||||||
|
|
||||||
|
|
@ -113,6 +115,7 @@ class PrivateUserNode(DjangoObjectType):
|
||||||
is_teacher = graphene.Boolean()
|
is_teacher = graphene.Boolean()
|
||||||
old_classes = graphene.List(SchoolClassNode)
|
old_classes = graphene.List(SchoolClassNode)
|
||||||
school_classes = graphene.List(SchoolClassNode)
|
school_classes = graphene.List(SchoolClassNode)
|
||||||
|
|
||||||
recent_modules = DjangoFilterConnectionField(
|
recent_modules = DjangoFilterConnectionField(
|
||||||
ModuleNode, filterset_class=RecentModuleFilter
|
ModuleNode, filterset_class=RecentModuleFilter
|
||||||
)
|
)
|
||||||
|
|
@ -161,6 +164,7 @@ class PrivateUserNode(DjangoObjectType):
|
||||||
return self.team
|
return self.team
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ClassMemberNode(ObjectType):
|
class ClassMemberNode(ObjectType):
|
||||||
"""
|
"""
|
||||||
We need to build this ourselves, because we want the active property on the node, because providing it on the
|
We need to build this ourselves, because we want the active property on the node, because providing it on the
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue