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"
|
||||
v-if="module.id"
|
||||
>
|
||||
<div class="module__header">
|
||||
|
||||
<h2
|
||||
class="module__meta-title"
|
||||
id="meta-title"
|
||||
>
|
||||
{{ module.metaTitle }}
|
||||
</h2>
|
||||
|
||||
<div class="module__categoryindicators">
|
||||
<pill :text="module.level?.name"></pill>
|
||||
<pill :text="module.category?.name"></pill>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<h1
|
||||
class="module__title"
|
||||
data-cy="module-title"
|
||||
|
|
@ -88,6 +98,7 @@
|
|||
import ObjectiveGroups from '@/components/objective-groups/ObjectiveGroups.vue';
|
||||
import Chapter from '@/components/Chapter.vue';
|
||||
import BookmarkActions from '@/components/notes/BookmarkActions.vue';
|
||||
import Pill from "@/components/ui/Pill.vue";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
|
|
@ -98,6 +109,7 @@ export default {
|
|||
},
|
||||
|
||||
components: {
|
||||
Pill,
|
||||
BookmarkActions,
|
||||
ObjectiveGroups,
|
||||
Chapter,
|
||||
|
|
@ -161,8 +173,17 @@ export default {
|
|||
line-height: 25px;
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
margin-bottom: $small-spacing;
|
||||
}
|
||||
|
||||
&__meta-title {
|
||||
@include meta-title;
|
||||
margin-right: $medium-spacing;
|
||||
|
||||
}
|
||||
|
||||
&__intro-wrapper {
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
{{ teaser }}
|
||||
</p>
|
||||
<div class="module-teaser__pills">
|
||||
<pill :text="level?.name"></pill>
|
||||
<pill :text="category?.name"></pill>
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Pill from "@/components/ui/Pill.vue";
|
||||
|
||||
export default {
|
||||
props: ['metaTitle', 'title', 'teaser', 'id', 'slug', 'heroImage'],
|
||||
|
||||
props: ['metaTitle', 'title', 'teaser', 'id', 'slug', 'heroImage', 'level', 'category'],
|
||||
|
||||
components: {Pill},
|
||||
|
||||
|
||||
computed: {
|
||||
moduleLink() {
|
||||
|
|
@ -48,7 +58,7 @@ export default {
|
|||
.module-teaser {
|
||||
box-shadow: 0 3px 9px 0 rgba(0, 0, 0, 0.12);
|
||||
border: 1px solid #e2e2e2;
|
||||
height: 330px;
|
||||
height: 390px;
|
||||
max-width: 380px;
|
||||
width: 100%;
|
||||
border-radius: 12px;
|
||||
|
|
@ -88,5 +98,9 @@ export default {
|
|||
line-height: $default-line-height;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
&__pills {
|
||||
margin-top: $medium-spacing;
|
||||
}
|
||||
}
|
||||
</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
|
||||
solutionsEnabled
|
||||
inEditMode @client
|
||||
level {
|
||||
id
|
||||
name
|
||||
}
|
||||
category {
|
||||
id
|
||||
name
|
||||
}
|
||||
topic {
|
||||
slug
|
||||
title
|
||||
|
|
|
|||
|
|
@ -10,6 +10,11 @@ fragment UserParts on PrivateUserNode {
|
|||
avatarUrl
|
||||
expiryDate
|
||||
readOnly
|
||||
lastModuleLevel {
|
||||
id
|
||||
name
|
||||
filterAttributeType
|
||||
}
|
||||
lastModule {
|
||||
id
|
||||
slug
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ export interface Me {
|
|||
team: any;
|
||||
lastTopic: any;
|
||||
readOnly: boolean;
|
||||
lastModuleLevel: any;
|
||||
}
|
||||
|
||||
export interface MeQuery {
|
||||
|
|
@ -30,6 +31,7 @@ export interface Location {
|
|||
|
||||
type RouteLocation = Location | string;
|
||||
|
||||
// TODO: ME_QUERY existiert an einem weiteren Ort. Dieser sollte entfernt werden.
|
||||
const defaultMe: MeQuery = {
|
||||
me: {
|
||||
selectedClass: {
|
||||
|
|
@ -42,6 +44,7 @@ const defaultMe: MeQuery = {
|
|||
team: null,
|
||||
readOnly: false,
|
||||
lastTopic: undefined,
|
||||
lastModuleLevel: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
<p class="topic__teaser">
|
||||
{{ topic.teaser }}
|
||||
</p>
|
||||
|
||||
<div class="topic__links">
|
||||
<div
|
||||
class="topic__video-link topic__link"
|
||||
|
|
@ -33,19 +34,15 @@
|
|||
<span class="topic__link-description">Anweisungen zum {{ $flavor.textTopic }} anzeigen</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="topic__modules">
|
||||
<module-teaser
|
||||
v-for="module in modules"
|
||||
v-bind="module"
|
||||
:key="module.slug"
|
||||
/>
|
||||
<div class="topic__modulefilter">
|
||||
<module-filter :modules="modules" :me="me" v-if="modules.length > 0" ></module-filter>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ModuleTeaser from '@/components/modules/ModuleTeaser.vue';
|
||||
import ModuleFilter from '@/components/modules/ModuleFilter.vue';
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
import TOPIC_QUERY from '@/graphql/gql/queries/topicQuery.gql';
|
||||
import me from '@/mixins/me';
|
||||
|
|
@ -61,9 +58,9 @@ export default {
|
|||
mixins: [me],
|
||||
components: {
|
||||
TopicNavigation,
|
||||
ModuleTeaser,
|
||||
PlayIcon,
|
||||
BulbIcon,
|
||||
ModuleFilter,
|
||||
},
|
||||
|
||||
apollo: {
|
||||
|
|
@ -170,6 +167,7 @@ export default {
|
|||
grid-template-columns: 300px 1fr;
|
||||
}
|
||||
|
||||
|
||||
&__navigation {
|
||||
padding: 0 $medium-spacing;
|
||||
display: none;
|
||||
|
|
@ -210,19 +208,5 @@ export default {
|
|||
@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>
|
||||
|
|
|
|||
|
|
@ -1,101 +1,16 @@
|
|||
{
|
||||
"name": "cariot",
|
||||
"version": "1.0.1",
|
||||
"name": "myskillbox",
|
||||
"version": "1.1.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "cariot",
|
||||
"version": "1.0.1",
|
||||
"dependencies": {
|
||||
"babel-polyfill": "^6.26.0",
|
||||
"unfetch": "^3.0.0"
|
||||
},
|
||||
"name": "myskillbox",
|
||||
"version": "1.1.0",
|
||||
"engines": {
|
||||
"node": "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=="
|
||||
"node": "20.x",
|
||||
"npm": ">= 8.x"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
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 (
|
||||
BasePageFactory,
|
||||
DummyImageFactory,
|
||||
|
|
@ -201,6 +201,17 @@ class VideoBlockFactory(wagtail_factories.StructBlockFactory):
|
|||
class Meta:
|
||||
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 = [
|
||||
"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.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 wagtail.admin.panels import FieldPanel, TabbedInterface, ObjectList
|
||||
from wagtail.fields import RichTextField
|
||||
|
||||
from core.constants import DEFAULT_RICH_TEXT_FEATURES
|
||||
from core.wagtail_utils import StrictHierarchyPage, get_default_settings
|
||||
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 Meta:
|
||||
|
|
@ -15,6 +48,13 @@ class Module(StrictHierarchyPage):
|
|||
verbose_name_plural = "Module"
|
||||
|
||||
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(
|
||||
"wagtailimages.Image",
|
||||
null=True,
|
||||
|
|
@ -34,30 +74,12 @@ class Module(StrictHierarchyPage):
|
|||
content_panels = [
|
||||
FieldPanel("title", classname="full title"),
|
||||
FieldPanel("meta_title", classname="full title"),
|
||||
FieldPanel("level"),
|
||||
FieldPanel("category"),
|
||||
FieldPanel("hero_image"),
|
||||
FieldPanel("hero_source"),
|
||||
FieldPanel("teaser"),
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from books.schema.mutations.chapter import UpdateChapterVisibility
|
||||
from books.schema.mutations.contentblock import DuplicateContentBlock, MutateContentBlock, AddContentBlock, \
|
||||
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.topic import UpdateLastTopic
|
||||
|
||||
|
|
@ -14,6 +14,7 @@ class BookMutations(object):
|
|||
update_solution_visibility = UpdateSolutionVisibility.Field()
|
||||
update_last_module = UpdateLastModule.Field()
|
||||
update_last_topic = UpdateLastTopic.Field()
|
||||
update_last_module_level = UpdateLastModuleLevel.Field()
|
||||
update_chapter_visibility = UpdateChapterVisibility.Field()
|
||||
sync_module_visibility = SyncModuleVisibility.Field()
|
||||
create_snapshot = CreateSnapshot.Field()
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
from datetime import datetime
|
||||
|
||||
import graphene
|
||||
from django.utils import timezone
|
||||
from graphene import relay
|
||||
|
||||
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 users.models import SchoolClass
|
||||
from users.models import SchoolClass, User
|
||||
from users.schema import PrivateUserNode
|
||||
|
||||
|
||||
class UpdateSolutionVisibility(relay.ClientIDMutation):
|
||||
|
|
@ -104,3 +103,19 @@ class SyncModuleVisibility(relay.ClientIDMutation):
|
|||
module.sync_from_school_class(template, school_class)
|
||||
|
||||
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.utils import get_object
|
||||
from books.models import Module, ContentBlock, Chapter
|
||||
from books.models import Module
|
||||
from books.models.snapshot import Snapshot
|
||||
from books.schema.nodes import SnapshotNode, ModuleNode
|
||||
from users.models import SchoolClass
|
||||
|
|
|
|||
|
|
@ -6,9 +6,16 @@ from graphene_django.filter import DjangoFilterConnectionField
|
|||
|
||||
from assignments.models import StudentSubmission
|
||||
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.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.schema import (
|
||||
ModuleBookmarkNode,
|
||||
|
|
@ -34,6 +41,8 @@ class ModuleNode(DjangoObjectType):
|
|||
"hero_image",
|
||||
"hero_source",
|
||||
"topic",
|
||||
"level",
|
||||
"category",
|
||||
]
|
||||
filter_fields = {
|
||||
"slug": ["exact", "icontains", "in"],
|
||||
|
|
@ -46,12 +55,13 @@ class ModuleNode(DjangoObjectType):
|
|||
bookmark = graphene.Field(ModuleBookmarkNode)
|
||||
my_submissions = DjangoFilterConnectionField(StudentSubmissionNode)
|
||||
my_answers = DjangoFilterConnectionField(AnswerNode)
|
||||
my_content_bookmarks = DjangoFilterConnectionField(
|
||||
ContentBlockBookmarkNode)
|
||||
my_content_bookmarks = DjangoFilterConnectionField(ContentBlockBookmarkNode)
|
||||
my_chapter_bookmarks = DjangoFilterConnectionField(ChapterBookmarkNode)
|
||||
snapshots = graphene.List("books.schema.nodes.SnapshotNode")
|
||||
objective_groups = graphene.List(ObjectiveGroupNode)
|
||||
assignments = graphene.List(AssignmentNode)
|
||||
level = graphene.Field(ModuleLevelNode)
|
||||
category = graphene.Field(ModuleCategoryNode)
|
||||
|
||||
def resolve_chapters(self, info, **kwargs):
|
||||
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 core.logger import get_logger
|
||||
from .connections import TopicConnection, ModuleConnection
|
||||
from .nodes import ContentBlockNode, ChapterNode, ModuleNode, NotFoundFailure, SnapshotNode, \
|
||||
TopicOr404Node
|
||||
from ..models import Book, Topic, Module, Chapter, Snapshot
|
||||
from .nodes import (
|
||||
ContentBlockNode,
|
||||
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__)
|
||||
|
||||
|
|
@ -15,8 +23,7 @@ logger = get_logger(__name__)
|
|||
class BookQuery(object):
|
||||
node = relay.Node.Field()
|
||||
topic = graphene.Field(TopicOr404Node, slug=graphene.String())
|
||||
module = graphene.Field(
|
||||
ModuleNode, slug=graphene.String(), id=graphene.ID())
|
||||
module = graphene.Field(ModuleNode, slug=graphene.String(), id=graphene.ID())
|
||||
chapter = relay.Node.Field(ChapterNode)
|
||||
content_block = relay.Node.Field(ContentBlockNode)
|
||||
snapshot = relay.Node.Field(SnapshotNode)
|
||||
|
|
@ -25,6 +32,12 @@ class BookQuery(object):
|
|||
modules = relay.ConnectionField(ModuleConnection)
|
||||
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):
|
||||
return Book.objects.filter(**kwargs).live()
|
||||
|
||||
|
|
@ -38,13 +51,13 @@ class BookQuery(object):
|
|||
return Chapter.objects.filter(**kwargs).live()
|
||||
|
||||
def resolve_snapshot(self, info, **kwargs):
|
||||
id = kwargs.get('id')
|
||||
id = kwargs.get("id")
|
||||
snapshot = get_object(Snapshot, id)
|
||||
return snapshot
|
||||
|
||||
def resolve_module(self, info, **kwargs):
|
||||
slug = kwargs.get('slug')
|
||||
id = kwargs.get('id')
|
||||
slug = kwargs.get("slug")
|
||||
id = kwargs.get("id")
|
||||
module = None
|
||||
try:
|
||||
if id is not None:
|
||||
|
|
@ -59,8 +72,8 @@ class BookQuery(object):
|
|||
return None
|
||||
|
||||
def resolve_topic(self, info, **kwargs):
|
||||
slug = kwargs.get('slug')
|
||||
id = kwargs.get('id')
|
||||
slug = kwargs.get("slug")
|
||||
id = kwargs.get("id")
|
||||
|
||||
if id is not None:
|
||||
return get_object(Topic, id)
|
||||
|
|
@ -70,3 +83,33 @@ class BookQuery(object):
|
|||
except Topic.DoesNotExist:
|
||||
return NotFoundFailure
|
||||
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 unittest import skip
|
||||
from graphene.test import Client
|
||||
from graphql_relay import to_global_id
|
||||
|
||||
from api.schema import schema
|
||||
from api.utils import get_object
|
||||
from books.models import ContentBlock, Chapter
|
||||
from books.factories import ModuleFactory
|
||||
from books.factories import ModuleFactory, ModuleLevelFactory
|
||||
from core.factories import UserFactory
|
||||
from users.models import User
|
||||
|
||||
|
||||
class NewContentBlockMutationTest(TestCase):
|
||||
|
|
@ -14,12 +16,12 @@ class NewContentBlockMutationTest(TestCase):
|
|||
module = ModuleFactory()
|
||||
chapter = Chapter(title='Hello')
|
||||
module.add_child(instance=chapter)
|
||||
user = UserFactory(username='aschi')
|
||||
self.user = UserFactory(username='aschi')
|
||||
content_block = ContentBlock(title='bla', slug='bla')
|
||||
chapter.specific.add_child(instance=content_block)
|
||||
|
||||
request = RequestFactory().get('/')
|
||||
request.user = user
|
||||
request.user = self.user
|
||||
self.client = Client(schema=schema, context_value=request)
|
||||
|
||||
self.sibling_id = to_global_id('ContentBlockNode', content_block.pk)
|
||||
|
|
@ -120,3 +122,30 @@ class NewContentBlockMutationTest(TestCase):
|
|||
content = new_content_block['contents'][0]
|
||||
self.assertEqual(content.get('type'), 'image_url_block')
|
||||
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
|
||||
}
|
||||
|
||||
type ChapterNode implements Node & ChapterInterface {
|
||||
type ChapterNode implements Node, ChapterInterface {
|
||||
title: String
|
||||
slug: String!
|
||||
description: String
|
||||
|
|
@ -306,7 +306,7 @@ interface ContentBlockInterface {
|
|||
type: String
|
||||
}
|
||||
|
||||
type ContentBlockNode implements Node & ContentBlockInterface {
|
||||
type ContentBlockNode implements Node, ContentBlockInterface {
|
||||
title: String
|
||||
slug: String!
|
||||
hiddenFor: [SchoolClassNode]
|
||||
|
|
@ -621,6 +621,17 @@ type ModuleBookmarkNode {
|
|||
module: ModuleNode!
|
||||
}
|
||||
|
||||
enum ModuleCategoryFilterAttributeType {
|
||||
ALL
|
||||
EXACT
|
||||
}
|
||||
|
||||
type ModuleCategoryNode implements Node {
|
||||
id: ID!
|
||||
name: String!
|
||||
filterAttributeType: ModuleCategoryFilterAttributeType!
|
||||
}
|
||||
|
||||
type ModuleConnection {
|
||||
pageInfo: PageInfo!
|
||||
edges: [ModuleEdge]!
|
||||
|
|
@ -639,10 +650,23 @@ interface ModuleInterface {
|
|||
topic: TopicNode
|
||||
}
|
||||
|
||||
enum ModuleLevelFilterAttributeType {
|
||||
ALL
|
||||
EXACT
|
||||
}
|
||||
|
||||
type ModuleLevelNode implements Node {
|
||||
id: ID!
|
||||
name: String!
|
||||
filterAttributeType: ModuleLevelFilterAttributeType!
|
||||
}
|
||||
|
||||
type ModuleNode implements ModuleInterface {
|
||||
title: String!
|
||||
slug: String!
|
||||
metaTitle: String!
|
||||
level: ModuleLevelNode
|
||||
category: ModuleCategoryNode
|
||||
heroImage: String!
|
||||
heroSource: String!
|
||||
teaser: String!
|
||||
|
|
@ -735,6 +759,7 @@ type Mutation {
|
|||
updateSolutionVisibility(input: UpdateSolutionVisibilityInput!): UpdateSolutionVisibilityPayload
|
||||
updateLastModule(input: UpdateLastModuleInput!): UpdateLastModulePayload
|
||||
updateLastTopic(input: UpdateLastTopicInput!): UpdateLastTopicPayload
|
||||
updateLastModuleLevel(input: UpdateLastModuleLevelInput!): UpdateLastModuleLevelPayload
|
||||
updateChapterVisibility(input: UpdateChapterVisibilityInput!): UpdateChapterVisibilityPayload
|
||||
syncModuleVisibility(input: SyncModuleVisibilityInput!): SyncModuleVisibilityPayload
|
||||
createSnapshot(input: CreateSnapshotInput!): CreateSnapshotPayload
|
||||
|
|
@ -847,6 +872,7 @@ type PrivateUserNode implements Node {
|
|||
avatarUrl: String!
|
||||
username: String!
|
||||
lastModule: ModuleNode
|
||||
lastModuleLevel: ModuleLevelNode
|
||||
lastTopic: TopicNode
|
||||
email: String!
|
||||
onboardingVisited: Boolean!
|
||||
|
|
@ -928,6 +954,10 @@ type Query {
|
|||
topics(before: String, after: String, first: Int, last: Int): TopicConnection
|
||||
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
|
||||
moduleLevel(id: ID!): ModuleLevelNode
|
||||
moduleLevels: [ModuleLevelNode]
|
||||
moduleCategory(id: ID!): ModuleCategoryNode
|
||||
moduleCategories: [ModuleCategoryNode]
|
||||
objectiveGroup(id: ID!): ObjectiveGroupNode
|
||||
objectiveGroups(offset: Int, before: String, after: String, first: Int, last: Int, title: String, module_Slug: String): ObjectiveGroupNodeConnection
|
||||
roomEntry(id: ID, slug: String): RoomEntryNode
|
||||
|
|
@ -1013,7 +1043,7 @@ type SnapshotChangesNode {
|
|||
newContentBlocks: Int!
|
||||
}
|
||||
|
||||
type SnapshotChapterNode implements Node & ChapterInterface {
|
||||
type SnapshotChapterNode implements Node, ChapterInterface {
|
||||
id: ID!
|
||||
description: String
|
||||
title: String
|
||||
|
|
@ -1022,7 +1052,7 @@ type SnapshotChapterNode implements Node & ChapterInterface {
|
|||
titleHidden: Boolean
|
||||
}
|
||||
|
||||
type SnapshotContentBlockNode implements Node & ContentBlockInterface {
|
||||
type SnapshotContentBlockNode implements Node, ContentBlockInterface {
|
||||
id: ID!
|
||||
title: String
|
||||
contents: GenericStreamFieldType
|
||||
|
|
@ -1306,6 +1336,16 @@ input UpdateLastModuleInput {
|
|||
clientMutationId: String
|
||||
}
|
||||
|
||||
input UpdateLastModuleLevelInput {
|
||||
id: ID
|
||||
clientMutationId: String
|
||||
}
|
||||
|
||||
type UpdateLastModuleLevelPayload {
|
||||
user: PrivateUserNode
|
||||
clientMutationId: String
|
||||
}
|
||||
|
||||
type UpdateLastModulePayload {
|
||||
lastModule: ModuleNode
|
||||
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)
|
||||
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)
|
||||
avatar_url = models.CharField(max_length=254, blank=True, default='')
|
||||
email = models.EmailField(_('email address'), unique=True)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ from graphql_relay import to_global_id
|
|||
from api.types import FailureNode
|
||||
|
||||
from books.models import Module
|
||||
from books.schema.nodes import ModuleLevelNode
|
||||
from books.schema.queries import ModuleNode
|
||||
from users.models import SchoolClass, SchoolClassMember, Team, User
|
||||
|
||||
|
|
@ -103,6 +104,7 @@ class PrivateUserNode(DjangoObjectType):
|
|||
"onboarding_visited",
|
||||
"team",
|
||||
"read_only",
|
||||
"last_module_level"
|
||||
]
|
||||
interfaces = (relay.Node,)
|
||||
|
||||
|
|
@ -113,6 +115,7 @@ class PrivateUserNode(DjangoObjectType):
|
|||
is_teacher = graphene.Boolean()
|
||||
old_classes = graphene.List(SchoolClassNode)
|
||||
school_classes = graphene.List(SchoolClassNode)
|
||||
|
||||
recent_modules = DjangoFilterConnectionField(
|
||||
ModuleNode, filterset_class=RecentModuleFilter
|
||||
)
|
||||
|
|
@ -161,6 +164,7 @@ class PrivateUserNode(DjangoObjectType):
|
|||
return self.team
|
||||
|
||||
|
||||
|
||||
class ClassMemberNode(ObjectType):
|
||||
"""
|
||||
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