diff --git a/evennia/web/api/filters.py b/evennia/web/api/filters.py index 592668ea48..d74932956b 100644 --- a/evennia/web/api/filters.py +++ b/evennia/web/api/filters.py @@ -5,6 +5,7 @@ that is retrieved in GET requests. By default, Django Rest Framework uses the documentation specifically regarding DRF integration. https://django-filter.readthedocs.io/en/latest/guide/rest_framework.html + """ from typing import Union from django.db.models import Q @@ -25,6 +26,7 @@ def get_tag_query(tag_type: Union[str, None], key: str) -> Q: Returns: A Q object that for searching by this tag type and name + """ return Q(db_tags__db_tagtype=tag_type) & Q(db_tags__db_key__iexact=key) @@ -32,6 +34,7 @@ def get_tag_query(tag_type: Union[str, None], key: str) -> Q: class TagTypeFilter(CharFilter): """ This class lets you create different filters for tags of a specified db_tagtype. + """ tag_type = None @@ -60,11 +63,14 @@ SHARED_FIELDS = ["db_key", "db_typeclass_path", "db_tags__db_key", "db_tags__db_ class BaseTypeclassFilterSet(FilterSet): - """A parent class with filters for aliases and permissions""" + """ + A parent class with filters for aliases and permissions + """ + + name = CharFilter(lookup_expr="iexact", method="filter_name", field_name="db_key") alias = AliasFilter(lookup_expr="iexact") permission = PermissionFilter(lookup_expr="iexact") - name = CharFilter(lookup_expr="iexact", method="filter_name", field_name="db_key") @staticmethod def filter_name(queryset, name, value): @@ -84,7 +90,10 @@ class BaseTypeclassFilterSet(FilterSet): class ObjectDBFilterSet(BaseTypeclassFilterSet): - """This adds filters for ObjectDB instances - characters, rooms, exits, etc""" + """ + This adds filters for ObjectDB instances - characters, rooms, exits, etc + + """ class Meta: model = ObjectDB @@ -103,7 +112,9 @@ class AccountDBFilterSet(BaseTypeclassFilterSet): class Meta: model = AccountDB - fields = SHARED_FIELDS + ["username", "db_is_connected", "db_is_bot"] + fields = [fi for fi in (SHARED_FIELDS + ["username", "db_is_connected", "db_is_bot"]) + if fi != 'db_key'] + class ScriptDBFilterSet(BaseTypeclassFilterSet): @@ -121,3 +132,16 @@ class ScriptDBFilterSet(BaseTypeclassFilterSet): "db_persistent", "db_interval", ] + + +class HelpFilterSet(FilterSet): + + """ + Filter for help entries + + """ + name = CharFilter(lookup_expr="iexact", method="filter_name", field_name="db_key") + category = CharFilter(lookup_expr="iexact", method="filter_name", field_name="db_category") + alias = AliasFilter(lookup_expr="iexact") + + diff --git a/evennia/web/api/serializers.py b/evennia/web/api/serializers.py index 29132699a9..d5a8166e85 100644 --- a/evennia/web/api/serializers.py +++ b/evennia/web/api/serializers.py @@ -7,6 +7,7 @@ those decisions in the hands of clients, and are more focused on converting data from the server to JSON (serialization) for a response, and validating and converting JSON data sent from clients to our enpoints into python objects, often django model instances, that we can use (deserialization). + """ from rest_framework import serializers @@ -16,9 +17,14 @@ from evennia.accounts.accounts import DefaultAccount from evennia.scripts.models import ScriptDB from evennia.typeclasses.attributes import Attribute from evennia.typeclasses.tags import Tag +from evennia.help.models import HelpEntry class AttributeSerializer(serializers.ModelSerializer): + """ + Serialize Attribute views. + + """ value_display = serializers.SerializerMethodField(source="value") db_value = serializers.CharField(write_only=True, required=False) @@ -35,6 +41,7 @@ class AttributeSerializer(serializers.ModelSerializer): Returns: The Attribute's value in string format + """ if obj.db_strvalue: return obj.db_strvalue @@ -53,13 +60,17 @@ class SimpleObjectDBSerializer(serializers.ModelSerializer): fields = ["id", "db_key"] -class TypeclassSerializerMixin(object): - """Mixin that contains types shared by typeclasses. A note about tags, aliases, and permissions. You - might note that the methods and fields are defined here, but they're included explicitly in each child - class. What gives? It's a DRF error: serializer method fields which are inherited do not resolve correctly - in child classes, and as of this current version (3.11) you must have them in the child classes explicitly - to avoid field errors. Similarly, the child classes must contain the attribute serializer explicitly to - not have them render PK-related fields. +class TypeclassSerializerMixin: + """ + Mixin that contains types shared by typeclasses. A note about tags, + aliases, and permissions. You might note that the methods and fields are + defined here, but they're included explicitly in each child class. What + gives? It's a DRF error: serializer method fields which are inherited do + not resolve correctly in child classes, and as of this current version + (3.11) you must have them in the child classes explicitly to avoid field + errors. Similarly, the child classes must contain the attribute serializer + explicitly to not have them render PK-related fields. + """ shared_fields = [ @@ -135,7 +146,24 @@ class TypeclassSerializerMixin(object): return AttributeSerializer(obj.nicks.all(), many=True).data +class TypeclassListSerializerMixin: + """ + Shortened serializer for list views. + + """ + shared_fields = [ + "id", + "db_key", + "db_typeclass_path", + ] + + class ObjectDBSerializer(TypeclassSerializerMixin, serializers.ModelSerializer): + """ + Serializing Objects. + + """ + attributes = serializers.SerializerMethodField() nicks = serializers.SerializerMethodField() contents = serializers.SerializerMethodField() @@ -182,8 +210,25 @@ class ObjectDBSerializer(TypeclassSerializerMixin, serializers.ModelSerializer): return SimpleObjectDBSerializer(non_exits, many=True).data +class ObjectListSerializer(TypeclassListSerializerMixin, serializers.ModelSerializer): + """ + Shortened representation for listings.] + + """ + class Meta: + model = DefaultObject + fields = [ + "db_location", + "db_home", + ] + TypeclassListSerializerMixin.shared_fields + read_only_fields = ["id"] + + class AccountSerializer(TypeclassSerializerMixin, serializers.ModelSerializer): - """This uses the DefaultAccount object to have access to the sessions property""" + """ + This uses the DefaultAccount object to have access to the sessions property + + """ attributes = serializers.SerializerMethodField() nicks = serializers.SerializerMethodField() @@ -211,7 +256,23 @@ class AccountSerializer(TypeclassSerializerMixin, serializers.ModelSerializer): read_only_fields = ["id"] +class AccountListSerializer(TypeclassListSerializerMixin, serializers.ModelSerializer): + """ + A shortened form for listing. + + """ + class Meta: + model = DefaultAccount + fields = ["username"] + [ + fi for fi in TypeclassListSerializerMixin.shared_fields if fi != "db_key"] + read_only_fields = ["id"] + + class ScriptDBSerializer(TypeclassSerializerMixin, serializers.ModelSerializer): + """ + Serializing Account. + + """ attributes = serializers.SerializerMethodField() tags = serializers.SerializerMethodField() aliases = serializers.SerializerMethodField() @@ -227,3 +288,47 @@ class ScriptDBSerializer(TypeclassSerializerMixin, serializers.ModelSerializer): "db_repeats", ] + TypeclassSerializerMixin.shared_fields read_only_fields = ["id"] + + +class ScriptListSerializer(TypeclassListSerializerMixin, serializers.ModelSerializer): + """ + Shortened form for listing. + + """ + class Meta: + model = ScriptDB + fields = [ + "db_interval", + "db_persistent", + "db_start_delay", + "db_is_active", + "db_repeats", + ] + TypeclassListSerializerMixin.shared_fields + read_only_fields = ["id"] + + +class HelpSerializer(TypeclassSerializerMixin, serializers.ModelSerializer): + """ + Serializers Help entries (not a typeclass). + + """ + tags = serializers.SerializerMethodField() + aliases = serializers.SerializerMethodField() + + class Meta: + model = HelpEntry + fields = [ + "id", "db_key", "db_help_category", "db_entrytext", "db_date_created", + "tags", "aliases" + ] + +class HelpListSerializer(TypeclassListSerializerMixin, serializers.ModelSerializer): + """ + Shortened form for listings. + + """ + class Meta: + model = HelpEntry + fields = [ + "id", "db_key", "db_help_category", "db_date_created", + ] diff --git a/evennia/web/api/tests.py b/evennia/web/api/tests.py index 3d07ad2311..32c1aa14cc 100644 --- a/evennia/web/api/tests.py +++ b/evennia/web/api/tests.py @@ -1,4 +1,7 @@ -"""Tests for the REST API""" +""" +Tests for the REST API. + +""" from evennia.utils.test_resources import EvenniaTest from evennia.web.api import serializers from rest_framework.test import APIClient diff --git a/evennia/web/api/urls.py b/evennia/web/api/urls.py index 1d17892aba..57c5c9ddfa 100644 --- a/evennia/web/api/urls.py +++ b/evennia/web/api/urls.py @@ -22,25 +22,19 @@ from django.views.generic import TemplateView from rest_framework.schemas import get_schema_view from evennia.web.api.root import APIRootRouter -from evennia.web.api.views import ( - ObjectDBViewSet, - AccountDBViewSet, - CharacterViewSet, - ExitViewSet, - RoomViewSet, - ScriptDBViewSet, -) +from evennia.web.api import views app_name = "api" router = APIRootRouter() router.trailing_slash = "/?" -router.register(r"accounts", AccountDBViewSet, basename="account") -router.register(r"objects", ObjectDBViewSet, basename="object") -router.register(r"characters", CharacterViewSet, basename="character") -router.register(r"exits", ExitViewSet, basename="exit") -router.register(r"rooms", RoomViewSet, basename="room") -router.register(r"scripts", ScriptDBViewSet, basename="script") +router.register(r"accounts", views.AccountDBViewSet, basename="account") +router.register(r"objects", views.ObjectDBViewSet, basename="object") +router.register(r"characters", views.CharacterViewSet, basename="character") +router.register(r"exits", views.ExitViewSet, basename="exit") +router.register(r"rooms", views.RoomViewSet, basename="room") +router.register(r"scripts", views.ScriptDBViewSet, basename="script") +router.register(r"helpentries", views.HelpViewSet, basename="script") urlpatterns = router.urls diff --git a/evennia/web/api/views.py b/evennia/web/api/views.py index d5806fd871..698616f2f2 100644 --- a/evennia/web/api/views.py +++ b/evennia/web/api/views.py @@ -15,17 +15,29 @@ from evennia.objects.models import ObjectDB from evennia.objects.objects import DefaultCharacter, DefaultExit, DefaultRoom from evennia.accounts.models import AccountDB from evennia.scripts.models import ScriptDB -from evennia.web.api.serializers import ( - ObjectDBSerializer, - AccountSerializer, - ScriptDBSerializer, - AttributeSerializer, -) -from evennia.web.api.filters import ObjectDBFilterSet, AccountDBFilterSet, ScriptDBFilterSet +from evennia.help.models import HelpEntry +from evennia.web.api import serializers +from evennia.web.api import filters from evennia.web.api.permissions import EvenniaPermission -class TypeclassViewSetMixin: +class GeneralViewSetMixin: + """ + Mixin for both typeclass- and non-typeclass entities. + + """ + def get_serializer_class(self): + """ + Allow different serializers for certain actions. + + """ + if self.action == 'list': + if hasattr(self, "list_serializer_class"): + return self.list_serializer_class + return self.serializer_class + + +class TypeclassViewSetMixin(GeneralViewSetMixin): """ This mixin adds some shared functionality to each viewset of a typeclass. They all use the same permission classes and filter backend. You can override any of these in your own viewsets. @@ -53,7 +65,7 @@ class TypeclassViewSetMixin: it if no db_value is provided. """ - attr = AttributeSerializer(data=request.data) + attr = serializers.AttributeSerializer(data=request.data) obj = self.get_object() if attr.is_valid(raise_exception=True): key = attr.validated_data["db_key"] @@ -69,7 +81,7 @@ class TypeclassViewSetMixin: else: handler.remove(key=key, category=category) return Response( - AttributeSerializer(obj.db_attributes.all(), many=True).data, + serializers.AttributeSerializer(obj.db_attributes.all(), many=True).data, status=status.HTTP_200_OK, ) return Response(attr.errors, status=status.HTTP_400_BAD_REQUEST) @@ -86,9 +98,10 @@ class ObjectDBViewSet(TypeclassViewSetMixin, ModelViewSet): # instances. Serializers are similar to django forms, used for the # transmitting of data (typically json). - serializer_class = ObjectDBSerializer + serializer_class = serializers.ObjectDBSerializer queryset = ObjectDB.objects.all() - filterset_class = ObjectDBFilterSet + filterset_class = filters.ObjectDBFilterSet + list_serializer_class = serializers.ObjectListSerializer class CharacterViewSet(ObjectDBViewSet): @@ -96,10 +109,10 @@ class CharacterViewSet(ObjectDBViewSet): Characters are a type of Object commonly used as player avatars in-game. """ - queryset = DefaultCharacter.objects.typeclass_search( DefaultCharacter.path, include_children=True ) + list_serializer_class = serializers.ObjectListSerializer class RoomViewSet(ObjectDBViewSet): @@ -109,6 +122,7 @@ class RoomViewSet(ObjectDBViewSet): """ queryset = DefaultRoom.objects.typeclass_search(DefaultRoom.path, include_children=True) + list_serializer_class = serializers.ObjectListSerializer class ExitViewSet(ObjectDBViewSet): @@ -119,6 +133,7 @@ class ExitViewSet(ObjectDBViewSet): """ queryset = DefaultExit.objects.typeclass_search(DefaultExit.path, include_children=True) + list_serializer_class = serializers.ObjectListSerializer class AccountDBViewSet(TypeclassViewSetMixin, ModelViewSet): @@ -127,9 +142,10 @@ class AccountDBViewSet(TypeclassViewSetMixin, ModelViewSet): """ - serializer_class = AccountSerializer + serializer_class = serializers.AccountSerializer queryset = AccountDB.objects.all() - filterset_class = AccountDBFilterSet + filterset_class = filters.AccountDBFilterSet + list_serializer_class = serializers.AccountListSerializer class ScriptDBViewSet(TypeclassViewSetMixin, ModelViewSet): @@ -139,6 +155,19 @@ class ScriptDBViewSet(TypeclassViewSetMixin, ModelViewSet): """ - serializer_class = ScriptDBSerializer + serializer_class = serializers.ScriptDBSerializer queryset = ScriptDB.objects.all() - filterset_class = ScriptDBFilterSet + filterset_class = filters.ScriptDBFilterSet + list_serializer_class = serializers.ScriptListSerializer + + +class HelpViewSet(GeneralViewSetMixin, ModelViewSet): + """ + Database-stored help entries. + Note that command auto-help and file-based help entries are not accessible this way. + + """ + serializer_class = serializers.HelpSerializer + queryset = HelpEntry.objects.all() + filterset_class = filters.HelpFilterSet + list_serializer_class = serializers.HelpListSerializer