diff --git a/client/src/components/modules/ModuleFilter.vue b/client/src/components/modules/ModuleFilter.vue
index 5f952422..731b143a 100644
--- a/client/src/components/modules/ModuleFilter.vue
+++ b/client/src/components/modules/ModuleFilter.vue
@@ -2,9 +2,9 @@
selectedCategory = newItem"
+ :selected-item="selectedLevel"
+ :items="levelOptions"
+ @update:selectedItem="newItem => selectedLevel = newItem"
>
selectedLernfeld = newItem"
>
selectedLanguage = item"/>
+ :defaultSelectedItem="initialLanguage"
+ class="module-filter__language-selection"
+ @update:selectedItem="item => selectedLanguage = item"/>
@@ -58,9 +58,9 @@
const selectedLanguage = ref(initialLanguage)
- const {result: moduleCategoriesResult} = useQuery(gql`
- query ModuleCategoriesQuery {
- moduleCategories {
+ const {result: moduleLevelsResult} = useQuery(gql`
+ query ModuleLevelsQuery {
+ moduleLevels {
name
id
}
@@ -71,12 +71,12 @@
name: '---',
id: null,
};
- let defaultCategory = loadDefaultCategory(props.me);
+ let defaultLevel = loadDefaultLevel(props.me);
- let selectedCategory = ref(defaultCategory);
+ let selectedLevel = ref(defaultLevel);
- const firstLevelCategories = computed(() => {
- return [nullCategory, ...moduleCategoriesResult.value?.moduleCategories || []];
+ const levelOptions = computed(() => {
+ return [nullCategory, ...moduleLevelsResult.value?.moduleLevels || []];
});
const nullLernfeld = {
@@ -104,8 +104,8 @@
return filterModules();
});
- function loadDefaultCategory(me) {
- return me?.lastModuleCategory || nullCategory;
+ function loadDefaultLevel(me) {
+ return me?.lastModuleLevel || nullCategory;
}
@@ -113,14 +113,14 @@
let filteredModules = props.modules;
- if (selectedCategory.value === null) {
+ if (selectedLevel.value === null) {
return props.modules;
}
// filter by Lehrjahr
- if (selectedCategory.value.name !== '---') {
+ if (selectedLevel.value.name !== '---') {
filteredModules = filteredModules.filter((module) => {
- return module.category?.id == selectedCategory.value.id;
+ return module.level?.id == selectedLevel.value.id;
});
}
@@ -130,8 +130,8 @@
return module.categoryType?.id == selectedLernfeld.value.id;
});
}
- updateLastModuleCategory(selectedCategory.value);
- filteredModules = filterByLanguage(selectedLanguage.value, filteredModules);
+ updateLastModuleLevel(selectedLevel.value);
+ filteredModules = filterByLanguage(selectedLanguage.value, filteredModules);
return filteredModules;
}
@@ -142,24 +142,23 @@
}
- function updateLastModuleCategory(moduleCategory: Object) {
- console.log(moduleCategory);
+ function updateLastModuleLevel(moduleLevel: Object) {
- const {mutate: updateLastModuleCategory} = useMutation(gql`
- mutation ($input: UpdateLastModuleCategoryInput!){updateLastModuleCategory(input: $input) {
+ const {mutate: updateLastModuleLevel} = useMutation(gql`
+ mutation ($input: UpdateLastModuleLevelInput!){updateLastModuleLevel(input: $input) {
clientMutationId
user {
username
- lastModuleCategory {
+ lastModuleLevel {
name
id
}
}
}}`);
- updateLastModuleCategory({
+ updateLastModuleLevel({
input: {
- id: moduleCategory.id,
+ id: moduleLevel.id,
},
});
diff --git a/client/src/components/modules/ModuleTeaser.vue b/client/src/components/modules/ModuleTeaser.vue
index 1943bf5c..57a459a2 100644
--- a/client/src/components/modules/ModuleTeaser.vue
+++ b/client/src/components/modules/ModuleTeaser.vue
@@ -18,7 +18,7 @@
{{ teaser }}
@@ -30,7 +30,7 @@
export default {
components: {Pill},
- props: ['metaTitle', 'title', 'teaser', 'id', 'slug', 'heroImage', 'category', 'categoryType'],
+ props: ['metaTitle', 'title', 'teaser', 'id', 'slug', 'heroImage', 'level', 'categoryType'],
computed: {
diff --git a/client/src/graphql/gql/fragments/moduleParts.gql b/client/src/graphql/gql/fragments/moduleParts.gql
index eec8e9f3..9a03d012 100644
--- a/client/src/graphql/gql/fragments/moduleParts.gql
+++ b/client/src/graphql/gql/fragments/moduleParts.gql
@@ -9,7 +9,7 @@ fragment ModuleParts on ModuleNode {
heroSource
solutionsEnabled
inEditMode @client
- category {
+ level {
id
name
}
diff --git a/client/src/graphql/gql/fragments/userParts.gql b/client/src/graphql/gql/fragments/userParts.gql
index d7631fb0..453aaf49 100644
--- a/client/src/graphql/gql/fragments/userParts.gql
+++ b/client/src/graphql/gql/fragments/userParts.gql
@@ -10,7 +10,7 @@ fragment UserParts on PrivateUserNode {
avatarUrl
expiryDate
readOnly
- lastModuleCategory {
+ lastModuleLevel {
id
name
}
diff --git a/client/src/mixins/me.ts b/client/src/mixins/me.ts
index baa9353d..de2136ec 100644
--- a/client/src/mixins/me.ts
+++ b/client/src/mixins/me.ts
@@ -14,7 +14,7 @@ export interface Me {
team: any;
lastTopic: any;
readOnly: boolean;
- lastModuleCategory: any;
+ lastModuleLevel: any;
}
export interface MeQuery {
@@ -45,7 +45,7 @@ const defaultMe: MeQuery = {
team: null,
readOnly: false,
lastTopic: undefined,
- lastModuleCategory: undefined,
+ lastModuleLevel: undefined,
},
};
@@ -80,8 +80,8 @@ const getCurrentClassName = (me: Me) => {
return currentClass ? currentClass.name : me.schoolClasses.length ? me.schoolClasses[0].name : '';
};
-const getLastModuleCategory = (me: Me) => {
- return me.lastModuleCategory;
+const getLastModuleLevel = (me: Me) => {
+ return me.lastModuleLevel;
}
const getMe = () => {
@@ -134,9 +134,9 @@ const meMixin = {
// @ts-ignore
return getCurrentClassName(this.$data.me);
},
- lastModuleCategory(): any {
+ lastModuleLevel(): any {
// @ts-ignore
- return getLastModuleCategory(this.$data.me);
+ return getLastModuleLevel(this.$data.me);
}
},
diff --git a/server/books/categorize_modules.py b/server/books/categorize_modules.py
index d47de0a5..4cd8f329 100644
--- a/server/books/categorize_modules.py
+++ b/server/books/categorize_modules.py
@@ -1,4 +1,4 @@
-from .models.module import Module, ModuleCategory, ModuleType
+from .models.module import Module, ModuleLevel, ModuleType
def analyze_module_meta_titles():
all_nodes = []
@@ -19,14 +19,14 @@ def analyze_module_meta_titles():
def create_default_categories():
for lehrjahr in range(1,4):
- module_category, created = ModuleCategory.objects.get_or_create(name=f"{lehrjahr}. Lehrjahr")
+ module_category, created = ModuleLevel.objects.get_or_create(name=f"{lehrjahr}. Lehrjahr")
for type in range(1, 10):
ModuleType.objects.get_or_create(name=f"Lernfeld {type}")
def categorize_modules():
- for category in ModuleCategory.objects.all():
+ for category in ModuleLevel.objects.all():
modules = Module.objects.filter(category__isnull=True, meta_title__icontains=category.name)
print(f"{category.name}: {modules.count()}")
modules.update(category=category)
@@ -39,10 +39,10 @@ def categorize_modules():
def uncategorize_modules():
ModuleType.objects.all().delete()
- ModuleCategory.objects.all().delete()
+ ModuleLevel.objects.all().delete()
def delete_unused_categories():
- for category in ModuleCategory.objects.all():
+ for category in ModuleLevel.objects.all():
if not category.module_set.exists():
category.delete()
diff --git a/server/books/factories.py b/server/books/factories.py
index db270cdc..5b89ffe7 100644
--- a/server/books/factories.py
+++ b/server/books/factories.py
@@ -28,7 +28,7 @@ from books.blocks import (
SurveyBlock,
VideoBlock,
)
-from books.models import Book, Chapter, ContentBlock, Module, TextBlock, Topic, ModuleCategory, ModuleType
+from books.models import Book, Chapter, ContentBlock, Module, TextBlock, Topic, ModuleLevel, ModuleType
from core.factories import (
BasePageFactory,
DummyImageFactory,
@@ -201,9 +201,9 @@ class VideoBlockFactory(wagtail_factories.StructBlockFactory):
class Meta:
model = VideoBlock
-class ModuleCategoryFactory(factory.DjangoModelFactory):
+class ModuleLevelFactory(factory.DjangoModelFactory):
class Meta:
- model = ModuleCategory
+ model = ModuleLevel
name = '1. Lehrjahr'
diff --git a/server/books/models/module.py b/server/books/models/module.py
index 638034a1..e236bfee 100644
--- a/server/books/models/module.py
+++ b/server/books/models/module.py
@@ -9,19 +9,19 @@ from core.wagtail_utils import StrictHierarchyPage, get_default_settings
from users.models import SchoolClass
from django.utils.text import slugify
-class ModuleCategory(models.Model):
+class ModuleLevel(models.Model):
name = models.CharField(max_length=255, unique=True)
def __str__(self):
return self.name
class Meta:
- verbose_name_plural = _("module categories")
- verbose_name = _("module category")
+ verbose_name_plural = _("module Levels")
+ verbose_name = _("module level")
def default_category():
- return ModuleCategory.objects.first().pk
+ return ModuleLevel.objects.first().pk
class ModuleType(models.Model):
@@ -42,7 +42,7 @@ class Module(StrictHierarchyPage):
verbose_name_plural = "Module"
meta_title = models.CharField(max_length=255, help_text="e.g. 'Intro' or 'Modul 1'")
- category = models.ForeignKey(ModuleCategory, on_delete=models.SET_NULL, blank=True, null=True)
+ level = models.ForeignKey(ModuleLevel, on_delete=models.SET_NULL, blank=True, null=True)
category_type = models.ForeignKey(ModuleType, on_delete=models.SET_NULL, blank=True, null=True)
@@ -62,12 +62,10 @@ class Module(StrictHierarchyPage):
solutions_enabled_for = models.ManyToManyField(SchoolClass)
- # TODO: Filter category_type by category
-
content_panels = [
FieldPanel("title", classname="full title"),
FieldPanel("meta_title", classname="full title"),
- FieldPanel("category"),
+ FieldPanel("level"),
FieldPanel("category_type"),
FieldPanel("hero_image"),
FieldPanel("hero_source"),
@@ -161,12 +159,6 @@ class Module(StrictHierarchyPage):
def get_admin_display_title(self):
return f"{self.meta_title} - {self.title}"
- @property
- def category_name(self) -> str:
- return self.category.name if self.category else ""
- @property
- def category_type_name(self) -> str:
- return self.category_type.name if self.category_type else ""
class RecentModule(models.Model):
diff --git a/server/books/schema/mutations/__init__.py b/server/books/schema/mutations/__init__.py
index 0dcc09c7..f25027d5 100644
--- a/server/books/schema/mutations/__init__.py
+++ b/server/books/schema/mutations/__init__.py
@@ -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, UpdateLastModuleCategory
+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,7 +14,7 @@ class BookMutations(object):
update_solution_visibility = UpdateSolutionVisibility.Field()
update_last_module = UpdateLastModule.Field()
update_last_topic = UpdateLastTopic.Field()
- update_last_module_category = UpdateLastModuleCategory.Field()
+ update_last_module_level = UpdateLastModuleLevel.Field()
update_chapter_visibility = UpdateChapterVisibility.Field()
sync_module_visibility = SyncModuleVisibility.Field()
create_snapshot = CreateSnapshot.Field()
diff --git a/server/books/schema/mutations/module.py b/server/books/schema/mutations/module.py
index f551c97b..1df5bf00 100644
--- a/server/books/schema/mutations/module.py
+++ b/server/books/schema/mutations/module.py
@@ -3,7 +3,7 @@ from django.utils import timezone
from graphene import relay
from api.utils import get_object
-from books.models import Module, RecentModule, ModuleCategory
+from books.models import Module, RecentModule, ModuleLevel
from books.schema.nodes import ModuleNode
from users.models import SchoolClass, User
from users.schema import PrivateUserNode
@@ -105,7 +105,7 @@ class SyncModuleVisibility(relay.ClientIDMutation):
return cls(success=True)
-class UpdateLastModuleCategory(relay.ClientIDMutation):
+class UpdateLastModuleLevel(relay.ClientIDMutation):
class Input:
id = graphene.ID()
@@ -115,8 +115,8 @@ class UpdateLastModuleCategory(relay.ClientIDMutation):
def mutate_and_get_payload(cls, root, info, **args):
user = info.context.user
id = args.get('id')
- module_category = get_object(ModuleCategory, id)
+ module_level = get_object(ModuleLevel, id)
- User.objects.filter(pk=user.id).update(last_module_category_id=module_category.id)
+ User.objects.filter(pk=user.id).update(last_module_level_id=module_level.id)
return cls(user=user)
diff --git a/server/books/schema/nodes/module.py b/server/books/schema/nodes/module.py
index 66e89fe8..dbbd2fbd 100644
--- a/server/books/schema/nodes/module.py
+++ b/server/books/schema/nodes/module.py
@@ -6,10 +6,10 @@ 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, ModuleCategory, ModuleType
+from books.models import Module, Chapter, ContentBlock, RecentModule, ModuleLevel, ModuleType
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_category import ModuleLevelNode
from books.schema.nodes.module_category_type import ModuleCategoryTypeNode
from notes.models import ModuleBookmark, ContentBlockBookmark, ChapterBookmark
from notes.schema import (
@@ -36,7 +36,7 @@ class ModuleNode(DjangoObjectType):
"hero_image",
"hero_source",
"topic",
- "category",
+ "level",
"category_type",
]
filter_fields = {
@@ -56,7 +56,7 @@ class ModuleNode(DjangoObjectType):
snapshots = graphene.List("books.schema.nodes.SnapshotNode")
objective_groups = graphene.List(ObjectiveGroupNode)
assignments = graphene.List(AssignmentNode)
- category = graphene.Field(ModuleCategoryNode)
+ category = graphene.Field(ModuleLevelNode)
category_type = graphene.Field(ModuleCategoryTypeNode)
def resolve_chapters(self, info, **kwargs):
@@ -111,7 +111,7 @@ class ModuleNode(DjangoObjectType):
return parent.objective_groups.all().prefetch_related("hidden_for")
def resolve_category(self, info, **kwargs):
- return ModuleCategory.objects.get(pk=self.category_id) if self.category_id else None
+ return ModuleLevel.objects.get(pk=self.category_id) if self.category_id else None
def resolve_category_type(self, info, **kwargs):
return ModuleType.objects.get(pk=self.category_type_id) if self.category_type_id else None
diff --git a/server/books/schema/nodes/module_category.py b/server/books/schema/nodes/module_category.py
index add10b08..165d8e83 100644
--- a/server/books/schema/nodes/module_category.py
+++ b/server/books/schema/nodes/module_category.py
@@ -2,12 +2,12 @@ from graphene import relay
from graphene import relay
from graphene_django import DjangoObjectType
-from books.models import ModuleCategory
+from books.models import ModuleLevel
-class ModuleCategoryNode(DjangoObjectType):
+class ModuleLevelNode(DjangoObjectType):
class Meta:
- model = ModuleCategory
+ model = ModuleLevel
interfaces = (relay.Node,)
only_fields = [
"id",
diff --git a/server/books/schema/queries.py b/server/books/schema/queries.py
index 74e24d29..7793140b 100644
--- a/server/books/schema/queries.py
+++ b/server/books/schema/queries.py
@@ -7,9 +7,9 @@ from core.logger import get_logger
from .connections import TopicConnection, ModuleConnection
from .nodes import ContentBlockNode, ChapterNode, ModuleNode, NotFoundFailure, SnapshotNode, \
TopicOr404Node
-from .nodes.module_category import ModuleCategoryNode
+from .nodes.module_category import ModuleLevelNode
from .nodes.module_category_type import ModuleCategoryTypeNode
-from ..models import Book, Topic, Module, Chapter, Snapshot, ModuleCategory, ModuleType
+from ..models import Book, Topic, Module, Chapter, Snapshot, ModuleLevel, ModuleType
logger = get_logger(__name__)
@@ -27,8 +27,8 @@ class BookQuery(object):
modules = relay.ConnectionField(ModuleConnection)
chapters = DjangoFilterConnectionField(ChapterNode)
- module_category = graphene.Field(ModuleCategoryNode, id=graphene.ID(required=True))
- module_categories = graphene.List(ModuleCategoryNode)
+ module_level = graphene.Field(ModuleLevelNode, id=graphene.ID(required=True))
+ module_levels = graphene.List(ModuleLevelNode)
module_category_type= graphene.Field(ModuleCategoryTypeNode, id=graphene.ID(required=True))
module_category_types = graphene.List(ModuleCategoryTypeNode)
@@ -80,18 +80,17 @@ class BookQuery(object):
return None
- def resolve_module_category(self, info, **kwargs):
- id = kwargs.get('id')
+ def resolve_module_level(self, info, **kwargs):
+ module_level_id = kwargs.get('id')
try:
- if id is not None:
- module_category = get_object(Module, id)
- return module_category
+ if module_level_id is not None:
+ return get_object(Module, module_level_id)
except Module.DoesNotExist:
return None
- def resolve_module_categories(self, *args, **kwargs):
- return ModuleCategory.objects.filter(**kwargs)
+ def resolve_module_levels(self, *args, **kwargs):
+ return ModuleLevel.objects.filter(**kwargs)
def resolve_module_category_type(self, info, **kwargs):
id = kwargs.get('id')
diff --git a/server/books/tests/test_module_mutations.py b/server/books/tests/test_module_mutations.py
index 68faf7aa..08f32b22 100644
--- a/server/books/tests/test_module_mutations.py
+++ b/server/books/tests/test_module_mutations.py
@@ -5,7 +5,7 @@ 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, ModuleCategoryFactory
+from books.factories import ModuleFactory, ModuleLevelFactory
from core.factories import UserFactory
from users.models import User
@@ -122,26 +122,26 @@ class NewContentBlockMutationTest(TestCase):
self.assertEqual(content.get('type'), 'image_url_block')
self.assertEqual(content.get('value'), {'url': '/test.png'})
- def test_updateLastModuleCategory(self):
+ def test_updateLastModuleLevel(self):
self.assertIsNone(self.user.last_module_category, None)
- moduleCategory = ModuleCategoryFactory(name='1. Lehrjahr')
- moduleCategory1 = ModuleCategoryFactory(name='2. Lehrjahr')
+ moduleLevel= ModuleLevelFactory(name='1. Lehrjahr')
+ moduleLevel1 = ModuleLevelFactory(name='2. Lehrjahr')
mutation = """
- mutation ($input: UpdateLastModuleCategoryInput!){updateLastModuleCategory(input: $input) {
+ mutation ($input: UpdateLastModuleLevelInput!){updateLastModuleLevel(input: $input) {
clientMutationId
user {
username
- lastModuleCategory {
+ lastModuleLevel {
name
id
}
}
}}
"""
- result = self.client.execute(mutation, variables={"input": {"id": moduleCategory1.id}})
+ result = self.client.execute(mutation, variables={"input": {"id": moduleLevel1.id}})
self.assertIsNone(result.get('errors'))
updated_user = User.objects.get(id=self.user.id)
- self.assertEqual(updated_user.last_module_category.name, moduleCategory1.name)
+ self.assertEqual(updated_user.last_module_category.name, moduleLevel1.name)
diff --git a/server/books/wagtail_hooks.py b/server/books/wagtail_hooks.py
index 75c3d9d3..570e006c 100644
--- a/server/books/wagtail_hooks.py
+++ b/server/books/wagtail_hooks.py
@@ -5,19 +5,19 @@ from wagtail.contrib.modeladmin.options import (
)
from wagtail import hooks
-from .models.module import ModuleCategory, ModuleType, Module
+from .models.module import ModuleLevel, ModuleType, Module
from django.utils.translation import gettext_lazy as _
class ModuleAdmin(ModelAdmin):
model = Module
- list_display = ("title", "meta_title", "category", "category_type")
+ list_display = ("title", "meta_title", "level", "category_type")
search_fields = ("title", "meta_title")
- list_filter = ("category", "category_type")
+ list_filter = ("level", "category_type")
-class ModuleCategoryAdmin(ModelAdmin):
- model = ModuleCategory
+class ModuleLevelAdmin(ModelAdmin):
+ model = ModuleLevel
list_display = ("name",)
ordering = ("name",)
@@ -33,7 +33,7 @@ class InstrumentGroup(ModelAdminGroup):
menu_label = _("Modules")
items = (
ModuleAdmin,
- ModuleCategoryAdmin,
+ ModuleLevelAdmin,
ModuleTypeAdmin,
)
diff --git a/server/users/migrations/0034_user_last_module_level.py b/server/users/migrations/0034_user_last_module_level.py
new file mode 100644
index 00000000..06f30efb
--- /dev/null
+++ b/server/users/migrations/0034_user_last_module_level.py
@@ -0,0 +1,20 @@
+# Generated by Django 3.2.16 on 2023-08-21 12:07
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('books', '0048_auto_20230821_0922'),
+ ('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'),
+ ),
+ ]
diff --git a/server/users/models.py b/server/users/models.py
index 77208ca1..5fc79d38 100644
--- a/server/users/models.py
+++ b/server/users/models.py
@@ -33,7 +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_category = models.ForeignKey('books.ModuleCategory', related_name='+', on_delete=models.SET_NULL, null=True)
+ 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)
diff --git a/server/users/schema/types.py b/server/users/schema/types.py
index afebcfd4..031330f6 100644
--- a/server/users/schema/types.py
+++ b/server/users/schema/types.py
@@ -11,7 +11,7 @@ from graphql_relay import to_global_id
from api.types import FailureNode
from books.models import Module
-from books.schema.nodes import ModuleCategoryNode
+from books.schema.nodes import ModuleLevelNode
from books.schema.queries import ModuleNode
from users.models import SchoolClass, SchoolClassMember, Team, User
@@ -104,7 +104,7 @@ class PrivateUserNode(DjangoObjectType):
"onboarding_visited",
"team",
"read_only",
- "last_module_category"
+ "last_module_level"
]
interfaces = (relay.Node,)