Add more fields to serializers

This commit is contained in:
TehomCD 2020-03-01 19:11:04 -05:00
parent ec570a17cd
commit 1c4fabbfa2
5 changed files with 161 additions and 38 deletions

View file

@ -17,11 +17,20 @@ class EvenniaPermission(permissions.BasePermission):
update_locks = settings.REST_FRAMEWORK.get("DEFAULT_UPDATE_LOCKS", ["control", "edit"]) update_locks = settings.REST_FRAMEWORK.get("DEFAULT_UPDATE_LOCKS", ["control", "edit"])
def has_permission(self, request, view): def has_permission(self, request, view):
""" """Checks for permissions
This method is a check that always happens first. If there's an object involved,
such as with retrieve, update, or delete, then the has_object_permission method Args:
is also called if this returns True. If we return False, a permission denied request (Request): The incoming request object.
error is raised. view (View): The django view we are checking permission for.
Returns:
bool: If permission is granted or not. If we return False here, a PermissionDenied
error will be raised from the view.
Notes:
This method is a check that always happens first. If there's an object involved,
such as with retrieve, update, or delete, then the has_object_permission method
is called after this, assuming this returns `True`.
""" """
# Only allow authenticated users to call the API # Only allow authenticated users to call the API
if not request.user.is_authenticated: if not request.user.is_authenticated:
@ -37,17 +46,34 @@ class EvenniaPermission(permissions.BasePermission):
@staticmethod @staticmethod
def check_locks(obj, user, locks): def check_locks(obj, user, locks):
"""Checks access for user for object with given locks
Args:
obj: Object instance we're checking
user (Account): User who we're checking permissions
locks (list): list of lockstrings
Returns:
bool: True if they have access, False if they don't
"""
return any([obj.access(user, lock) for lock in locks]) return any([obj.access(user, lock) for lock in locks])
def has_object_permission(self, request, view, obj): def has_object_permission(self, request, view, obj):
"""Checks object-level permissions after has_permission
Args:
request (Request): The incoming request object.
view (View): The django view we are checking permission for.
obj: Object we're checking object-level permissions for
Returns:
bool: If permission is granted or not. If we return False here, a PermissionDenied
error will be raised from the view.
Notes:
This method assumes that has_permission has already returned True. We check
equivalent Evennia permissions in the request.user to determine if they can
complete the action.
""" """
This method assumes that has_permission has already returned True. We check
equivalent Evennia permissions in the request.user to determine if they can
complete the action. If so, we return True. Otherwise we return False, and
a permission denied error will be raised.
"""
if request.user.is_superuser:
return True
if view.action in ("list", "retrieve"): if view.action in ("list", "retrieve"):
# access_type is based on the examine command # access_type is based on the examine command
return self.check_locks(obj, request.user, self.view_locks) return self.check_locks(obj, request.user, self.view_locks)

View file

@ -11,8 +11,8 @@ often django model instances, that we can use (deserialization).
from rest_framework import serializers from rest_framework import serializers
from evennia.objects.models import ObjectDB from evennia.objects.objects import DefaultObject
from evennia.accounts.models import AccountDB from evennia.accounts.accounts import DefaultAccount
from evennia.scripts.models import ScriptDB from evennia.scripts.models import ScriptDB
from evennia.typeclasses.attributes import Attribute from evennia.typeclasses.attributes import Attribute
from evennia.typeclasses.tags import Tag from evennia.typeclasses.tags import Tag
@ -32,36 +32,122 @@ class TagSerializer(serializers.ModelSerializer):
class SimpleObjectDBSerializer(serializers.ModelSerializer): class SimpleObjectDBSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = ObjectDB model = DefaultObject
fields = ["id", "db_key"] fields = ["id", "db_key"]
class TypeclassSerializerMixin(object): 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.
"""
db_attributes = AttributeSerializer(many=True) db_attributes = AttributeSerializer(many=True)
db_tags = TagSerializer(many=True)
shared_fields = ["id", "db_key", "db_attributes", "db_tags", "db_typeclass_path"] shared_fields = ["id", "db_key", "db_attributes", "db_typeclass_path", "aliases", "tags", "permissions"]
def get_tags(self, obj):
"""
Serializes tags from the object's Tagshandler
Args:
obj: Typeclassed object being serialized
Returns:
List of TagSerializer data
"""
return TagSerializer(obj.tags.get(return_tagobj=True, return_list=True), many=True).data
def get_aliases(self, obj):
"""
Serializes tags from the object's Aliashandler
Args:
obj: Typeclassed object being serialized
Returns:
List of TagSerializer data
"""
return TagSerializer(obj.aliases.get(return_tagobj=True, return_list=True), many=True).data
def get_permissions(self, obj):
"""
Serializes tags from the object's Permissionshandler
Args:
obj: Typeclassed object being serialized
Returns:
List of TagSerializer data
"""
return TagSerializer(obj.permissions.get(return_tagobj=True, return_list=True), many=True).data
class ObjectDBSerializer(TypeclassSerializerMixin, serializers.ModelSerializer): class ObjectDBSerializer(TypeclassSerializerMixin, serializers.ModelSerializer):
contents = SimpleObjectDBSerializer(source="locations_set", many=True, read_only=True) contents = serializers.SerializerMethodField()
exits = serializers.SerializerMethodField()
tags = serializers.SerializerMethodField()
aliases = serializers.SerializerMethodField()
permissions = serializers.SerializerMethodField()
class Meta: class Meta:
model = ObjectDB model = DefaultObject
fields = ["db_location", "db_home", "contents"] + TypeclassSerializerMixin.shared_fields fields = ["db_location", "db_home", "contents", "exits"] + TypeclassSerializerMixin.shared_fields
read_only_fields = ["id", "db_attributes", "db_tags"] read_only_fields = ["id", "db_attributes"]
def get_exits(self, obj):
"""
Gets exits for the object
Args:
obj: Object being serialized
Returns:
List of data from SimpleObjectDBSerializer
"""
exits = [ob for ob in obj.contents if ob.destination]
return SimpleObjectDBSerializer(exits, many=True).data
def get_contents(self, obj):
"""
Gets non-exits for the object
Args:
obj: Object being serialized
Returns:
List of data from SimpleObjectDBSerializer
"""
non_exits = [ob for ob in obj.contents if not ob.destination]
return SimpleObjectDBSerializer(non_exits, many=True).data
class AccountDBSerializer(TypeclassSerializerMixin, serializers.ModelSerializer): class AccountSerializer(TypeclassSerializerMixin, serializers.ModelSerializer):
"""This uses the DefaultAccount object to have access to the sessions property"""
db_key = serializers.CharField(required=False) db_key = serializers.CharField(required=False)
session_ids = serializers.SerializerMethodField()
tags = serializers.SerializerMethodField()
aliases = serializers.SerializerMethodField()
permissions = serializers.SerializerMethodField()
def get_session_ids(self, obj):
"""
Gets a list of session IDs connected to this Account
Args:
obj (DefaultAccount): Account we're grabbing sessions from
Returns:
List of session IDs
"""
return [sess.sessid for sess in obj.sessions.all() if hasattr(sess, "sessid")]
class Meta: class Meta:
model = AccountDB model = DefaultAccount
fields = ["username"] + TypeclassSerializerMixin.shared_fields fields = ["username", "session_ids"] + TypeclassSerializerMixin.shared_fields
read_only_fields = ["id", "db_attributes", "db_tags"] read_only_fields = ["id", "db_attributes", "db_tags", "session_ids"]
class ScriptDBSerializer(TypeclassSerializerMixin, serializers.ModelSerializer): class ScriptDBSerializer(TypeclassSerializerMixin, serializers.ModelSerializer):
tags = serializers.SerializerMethodField()
aliases = serializers.SerializerMethodField()
permissions = serializers.SerializerMethodField()
class Meta: class Meta:
model = ScriptDB model = ScriptDB
fields = ["db_interval", "db_persistent", "db_start_delay", fields = ["db_interval", "db_persistent", "db_start_delay",

View file

@ -26,6 +26,8 @@ class TestEvenniaRESTApi(EvenniaTest):
self.account.is_superuser = True self.account.is_superuser = True
self.account.save() self.account.save()
self.client.force_login(self.account) self.client.force_login(self.account)
# scripts do not have default locks. Without them, even superuser access check fails
self.script.locks.add("edit: perm(Admin); examine: perm(Admin); delete: perm(Admin)")
def tearDown(self): def tearDown(self):
try: try:
@ -35,21 +37,27 @@ class TestEvenniaRESTApi(EvenniaTest):
def get_view_details(self, action): def get_view_details(self, action):
"""Helper function for generating list of named tuples""" """Helper function for generating list of named tuples"""
View = namedtuple("View", ["view_name", "obj", "list", "serializer", "create_data"]) View = namedtuple("View", ["view_name", "obj", "list", "serializer", "create_data", "retrieve_data"])
views = [ views = [
View("object-%s" % action, self.obj1, [self.obj1, self.char1, self.exit, self.room1, self.room2, self.obj2, View("object-%s" % action, self.obj1, [self.obj1, self.char1, self.exit, self.room1, self.room2, self.obj2,
self.char2], serializers.ObjectDBSerializer, self.char2], serializers.ObjectDBSerializer,
{"db_key": "object-create-test-name"}), {"db_key": "object-create-test-name"},
serializers.ObjectDBSerializer(self.obj1).data),
View("character-%s" % action, self.char1, [self.char1, self.char2], serializers.ObjectDBSerializer, View("character-%s" % action, self.char1, [self.char1, self.char2], serializers.ObjectDBSerializer,
{"db_key": "character-create-test-name"}), {"db_key": "character-create-test-name"},
serializers.ObjectDBSerializer(self.char1).data),
View("exit-%s" % action, self.exit, [self.exit], serializers.ObjectDBSerializer, View("exit-%s" % action, self.exit, [self.exit], serializers.ObjectDBSerializer,
{"db_key": "exit-create-test-name"}), {"db_key": "exit-create-test-name"},
serializers.ObjectDBSerializer(self.exit).data),
View("room-%s" % action, self.room1, [self.room1, self.room2], serializers.ObjectDBSerializer, View("room-%s" % action, self.room1, [self.room1, self.room2], serializers.ObjectDBSerializer,
{"db_key": "room-create-test-name"}), {"db_key": "room-create-test-name"},
serializers.ObjectDBSerializer(self.room1).data),
View("script-%s" % action, self.script, [self.script], serializers.ScriptDBSerializer, View("script-%s" % action, self.script, [self.script], serializers.ScriptDBSerializer,
{"db_key": "script-create-test-name"}), {"db_key": "script-create-test-name"},
View("account-%s" % action, self.account2, [self.account, self.account2], serializers.AccountDBSerializer, serializers.ScriptDBSerializer(self.script).data),
{"username": "account-create-test-name"}), View("account-%s" % action, self.account2, [self.account, self.account2], serializers.AccountSerializer,
{"username": "account-create-test-name"},
serializers.AccountSerializer(self.account2).data),
] ]
return views return views
@ -62,6 +70,7 @@ class TestEvenniaRESTApi(EvenniaTest):
) )
response = self.client.get(view_url) response = self.client.get(view_url)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertDictEqual(response.data, view.retrieve_data)
def test_update(self): def test_update(self):
views = self.get_view_details("detail") views = self.get_view_details("detail")
@ -78,6 +87,7 @@ class TestEvenniaRESTApi(EvenniaTest):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
view.obj.refresh_from_db() view.obj.refresh_from_db()
self.assertEqual(getattr(view.obj, field), new_key) self.assertEqual(getattr(view.obj, field), new_key)
self.assertEqual(response.data[field], new_key)
def test_delete(self): def test_delete(self):
views = self.get_view_details("detail") views = self.get_view_details("detail")

View file

@ -14,7 +14,7 @@ from evennia.objects.models import ObjectDB
from evennia.objects.objects import DefaultCharacter, DefaultExit, DefaultRoom from evennia.objects.objects import DefaultCharacter, DefaultExit, DefaultRoom
from evennia.accounts.models import AccountDB from evennia.accounts.models import AccountDB
from evennia.scripts.models import ScriptDB from evennia.scripts.models import ScriptDB
from evennia.web.api.serializers import ObjectDBSerializer, AccountDBSerializer, ScriptDBSerializer, AttributeSerializer from evennia.web.api.serializers import ObjectDBSerializer, AccountSerializer, ScriptDBSerializer, AttributeSerializer
from evennia.web.api.filters import ObjectDBFilterSet, AccountDBFilterSet, ScriptDBFilterSet from evennia.web.api.filters import ObjectDBFilterSet, AccountDBFilterSet, ScriptDBFilterSet
from evennia.web.api.permissions import EvenniaPermission from evennia.web.api.permissions import EvenniaPermission
@ -26,7 +26,8 @@ class TypeclassViewSetMixin(object):
""" """
# permission classes determine who is authorized to call the view # permission classes determine who is authorized to call the view
permission_classes = [EvenniaPermission] permission_classes = [EvenniaPermission]
# the filter backend allows for retrieval views to have filter arguments passed to it # the filter backend allows for retrieval views to have filter arguments passed to it,
# for example: mygame.com/api/objects?db_key=bob to find matches based on objects having a db_key of bob
filter_backends = [DjangoFilterBackend] filter_backends = [DjangoFilterBackend]
@action(detail=True, methods=["put", "post"]) @action(detail=True, methods=["put", "post"])
@ -93,7 +94,7 @@ class ExitViewSet(ObjectDBViewSet):
class AccountDBViewSet(TypeclassViewSetMixin, ModelViewSet): class AccountDBViewSet(TypeclassViewSetMixin, ModelViewSet):
"""Viewset for Account objects""" """Viewset for Account objects"""
serializer_class = AccountDBSerializer serializer_class = AccountSerializer
queryset = AccountDB.objects.all() queryset = AccountDB.objects.all()
filterset_class = AccountDBFilterSet filterset_class = AccountDBFilterSet

View file

@ -5,8 +5,8 @@ attrs >= 19.2.0
django >= 2.2.5, < 2.3 django >= 2.2.5, < 2.3
twisted >= 19.2.1, < 20.0.0 twisted >= 19.2.1, < 20.0.0
pytz pytz
djangorestframework >= 3.10.3 djangorestframework >= 3.10.3, < 3.12
django-filter >= 2.2.0 django-filter >= 2.2.0, < 2.3
django-sekizai django-sekizai
inflect inflect
autobahn >= 17.9.3 autobahn >= 17.9.3