Add django rest framework with CRUD views
This commit is contained in:
parent
221fc560a7
commit
abb8eaae13
10 changed files with 401 additions and 11 deletions
|
|
@ -245,7 +245,7 @@ IN_GAME_ERRORS = True
|
||||||
# ENGINE - path to the the database backend. Possible choices are:
|
# ENGINE - path to the the database backend. Possible choices are:
|
||||||
# 'django.db.backends.sqlite3', (default)
|
# 'django.db.backends.sqlite3', (default)
|
||||||
# 'django.db.backends.mysql',
|
# 'django.db.backends.mysql',
|
||||||
# 'django.db.backends.postgresql_psycopg2',
|
# 'django.db.backends.postgresql',
|
||||||
# 'django.db.backends.oracle' (untested).
|
# 'django.db.backends.oracle' (untested).
|
||||||
# NAME - database name, or path to the db file for sqlite3
|
# NAME - database name, or path to the db file for sqlite3
|
||||||
# USER - db admin (unused in sqlite3)
|
# USER - db admin (unused in sqlite3)
|
||||||
|
|
@ -255,7 +255,9 @@ IN_GAME_ERRORS = True
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
"default": {
|
"default": {
|
||||||
"ENGINE": "django.db.backends.sqlite3",
|
"ENGINE": "django.db.backends.sqlite3",
|
||||||
"NAME": os.getenv("TEST_DB_PATH", os.path.join(GAME_DIR, "server", "evennia.db3")),
|
"NAME": os.getenv(
|
||||||
|
"TEST_DB_PATH", os.path.join(GAME_DIR, "server", "evennia.db3")
|
||||||
|
),
|
||||||
"USER": "",
|
"USER": "",
|
||||||
"PASSWORD": "",
|
"PASSWORD": "",
|
||||||
"HOST": "",
|
"HOST": "",
|
||||||
|
|
@ -472,7 +474,12 @@ SERVER_SESSION_CLASS = "evennia.server.serversession.ServerSession"
|
||||||
# immediately entered path fail to find a typeclass. It allows for
|
# immediately entered path fail to find a typeclass. It allows for
|
||||||
# shorter input strings. They must either base off the game directory
|
# shorter input strings. They must either base off the game directory
|
||||||
# or start from the evennia library.
|
# or start from the evennia library.
|
||||||
TYPECLASS_PATHS = ["typeclasses", "evennia", "evennia.contrib", "evennia.contrib.tutorial_examples"]
|
TYPECLASS_PATHS = [
|
||||||
|
"typeclasses",
|
||||||
|
"evennia",
|
||||||
|
"evennia.contrib",
|
||||||
|
"evennia.contrib.tutorial_examples",
|
||||||
|
]
|
||||||
|
|
||||||
# Typeclass for account objects (linked to a character) (fallback)
|
# Typeclass for account objects (linked to a character) (fallback)
|
||||||
BASE_ACCOUNT_TYPECLASS = "typeclasses.accounts.Account"
|
BASE_ACCOUNT_TYPECLASS = "typeclasses.accounts.Account"
|
||||||
|
|
@ -550,7 +557,11 @@ VALIDATOR_FUNC_MODULES = ["evennia.utils.validatorfuncs"]
|
||||||
|
|
||||||
# Python path to a directory to be searched for batch scripts
|
# Python path to a directory to be searched for batch scripts
|
||||||
# for the batch processors (.ev and/or .py files).
|
# for the batch processors (.ev and/or .py files).
|
||||||
BASE_BATCHPROCESS_PATHS = ["world", "evennia.contrib", "evennia.contrib.tutorial_examples"]
|
BASE_BATCHPROCESS_PATHS = [
|
||||||
|
"world",
|
||||||
|
"evennia.contrib",
|
||||||
|
"evennia.contrib.tutorial_examples",
|
||||||
|
]
|
||||||
|
|
||||||
######################################################################
|
######################################################################
|
||||||
# Game Time setup
|
# Game Time setup
|
||||||
|
|
@ -861,7 +872,9 @@ TEMPLATES = [
|
||||||
os.path.join(GAME_DIR, "web", "template_overrides"),
|
os.path.join(GAME_DIR, "web", "template_overrides"),
|
||||||
os.path.join(EVENNIA_DIR, "web", "website", "templates", WEBSITE_TEMPLATE),
|
os.path.join(EVENNIA_DIR, "web", "website", "templates", WEBSITE_TEMPLATE),
|
||||||
os.path.join(EVENNIA_DIR, "web", "website", "templates"),
|
os.path.join(EVENNIA_DIR, "web", "website", "templates"),
|
||||||
os.path.join(EVENNIA_DIR, "web", "webclient", "templates", WEBCLIENT_TEMPLATE),
|
os.path.join(
|
||||||
|
EVENNIA_DIR, "web", "webclient", "templates", WEBCLIENT_TEMPLATE
|
||||||
|
),
|
||||||
os.path.join(EVENNIA_DIR, "web", "webclient", "templates"),
|
os.path.join(EVENNIA_DIR, "web", "webclient", "templates"),
|
||||||
],
|
],
|
||||||
"APP_DIRS": True,
|
"APP_DIRS": True,
|
||||||
|
|
@ -912,6 +925,8 @@ INSTALLED_APPS = [
|
||||||
"django.contrib.sites",
|
"django.contrib.sites",
|
||||||
"django.contrib.staticfiles",
|
"django.contrib.staticfiles",
|
||||||
"django.contrib.messages",
|
"django.contrib.messages",
|
||||||
|
"rest_framework",
|
||||||
|
"django_filters",
|
||||||
"sekizai",
|
"sekizai",
|
||||||
"evennia.utils.idmapper",
|
"evennia.utils.idmapper",
|
||||||
"evennia.server",
|
"evennia.server",
|
||||||
|
|
@ -931,7 +946,9 @@ AUTH_USER_MODEL = "accounts.AccountDB"
|
||||||
# Password validation plugins
|
# Password validation plugins
|
||||||
# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators
|
# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators
|
||||||
AUTH_PASSWORD_VALIDATORS = [
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
{"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
|
{
|
||||||
|
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
||||||
"OPTIONS": {"min_length": 8},
|
"OPTIONS": {"min_length": 8},
|
||||||
|
|
@ -944,8 +961,14 @@ AUTH_PASSWORD_VALIDATORS = [
|
||||||
# Username validation plugins
|
# Username validation plugins
|
||||||
AUTH_USERNAME_VALIDATORS = [
|
AUTH_USERNAME_VALIDATORS = [
|
||||||
{"NAME": "django.contrib.auth.validators.ASCIIUsernameValidator"},
|
{"NAME": "django.contrib.auth.validators.ASCIIUsernameValidator"},
|
||||||
{"NAME": "django.core.validators.MinLengthValidator", "OPTIONS": {"limit_value": 3}},
|
{
|
||||||
{"NAME": "django.core.validators.MaxLengthValidator", "OPTIONS": {"limit_value": 30}},
|
"NAME": "django.core.validators.MinLengthValidator",
|
||||||
|
"OPTIONS": {"limit_value": 3},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"NAME": "django.core.validators.MaxLengthValidator",
|
||||||
|
"OPTIONS": {"limit_value": 30},
|
||||||
|
},
|
||||||
{"NAME": "evennia.server.validators.EvenniaUsernameAvailabilityValidator"},
|
{"NAME": "evennia.server.validators.EvenniaUsernameAvailabilityValidator"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -956,6 +979,38 @@ TEST_RUNNER = "evennia.server.tests.testrunner.EvenniaTestSuiteRunner"
|
||||||
# messages.error() to Bootstrap 'danger' classes.
|
# messages.error() to Bootstrap 'danger' classes.
|
||||||
MESSAGE_TAGS = {messages.ERROR: "danger"}
|
MESSAGE_TAGS = {messages.ERROR: "danger"}
|
||||||
|
|
||||||
|
# Django REST Framework settings
|
||||||
|
REST_FRAMEWORK = {
|
||||||
|
# django_filters allows you to specify search fields for models in an API View
|
||||||
|
'DEFAULT_FILTER_BACKENDS': (
|
||||||
|
'django_filters.rest_framework.DjangoFilterBackend',
|
||||||
|
),
|
||||||
|
# whether to paginate results and how many per page
|
||||||
|
"DEFAULT_PAGINATION_CLASS": 'rest_framework.pagination.LimitOffsetPagination',
|
||||||
|
'PAGE_SIZE': 25,
|
||||||
|
# require logged in users to call API so that access checks can work on them
|
||||||
|
'DEFAULT_PERMISSION_CLASSES': [
|
||||||
|
'rest_framework.permissions.IsAuthenticated',
|
||||||
|
],
|
||||||
|
# These are the different ways people can authenticate for API requests - via
|
||||||
|
# session or with user/password. Other ways are possible, such as via tokens
|
||||||
|
# or oauth, but require additional dependencies.
|
||||||
|
'DEFAULT_AUTHENTICATION_CLASSES': [
|
||||||
|
'rest_framework.authentication.BasicAuthentication',
|
||||||
|
'rest_framework.authentication.SessionAuthentication',
|
||||||
|
],
|
||||||
|
# default permission checks used by the EvenniaPermission class
|
||||||
|
"DEFAULT_CREATE_PERMISSION": "builder",
|
||||||
|
"DEFAULT_LIST_PERMISSION": "builder",
|
||||||
|
"DEFAULT_VIEW_LOCKS": ["examine"],
|
||||||
|
"DEFAULT_DESTROY_LOCKS": ["delete"],
|
||||||
|
"DEFAULT_UPDATE_LOCKS": ["control", "edit"],
|
||||||
|
# No throttle class set by default. Setting one also requires a cache backend to be specified.
|
||||||
|
}
|
||||||
|
|
||||||
|
# To enable the REST api, turn this to True
|
||||||
|
REST_API_ENABLED = False
|
||||||
|
|
||||||
######################################################################
|
######################################################################
|
||||||
# Django extensions
|
# Django extensions
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
|
||||||
0
evennia/web/api/__init__.py
Normal file
0
evennia/web/api/__init__.py
Normal file
27
evennia/web/api/filters.py
Normal file
27
evennia/web/api/filters.py
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
from django_filters.rest_framework.filterset import FilterSet
|
||||||
|
|
||||||
|
from evennia.objects.models import ObjectDB
|
||||||
|
from evennia.accounts.models import AccountDB
|
||||||
|
from evennia.scripts.models import ScriptDB
|
||||||
|
|
||||||
|
SHARED_FIELDS = ["db_key", "db_typeclass_path", "db_tags__db_key", "db_tags__db_category"]
|
||||||
|
|
||||||
|
|
||||||
|
class ObjectDBFilterSet(FilterSet):
|
||||||
|
class Meta:
|
||||||
|
model = ObjectDB
|
||||||
|
fields = SHARED_FIELDS + ["db_location__db_key", "db_home__db_key", "db_location__id",
|
||||||
|
"db_home__id"]
|
||||||
|
|
||||||
|
|
||||||
|
class AccountDBFilterSet(FilterSet):
|
||||||
|
class Meta:
|
||||||
|
model = AccountDB
|
||||||
|
fields = SHARED_FIELDS + ["username", "db_is_connected", "db_is_bot"]
|
||||||
|
|
||||||
|
|
||||||
|
class ScriptDBFilterSet(FilterSet):
|
||||||
|
class Meta:
|
||||||
|
model = ScriptDB
|
||||||
|
fields = SHARED_FIELDS + ["db_desc", "db_obj__db_key", "db_obj__id", "db_account__id",
|
||||||
|
"db_account__username", "db_is_active", "db_persistent"]
|
||||||
59
evennia/web/api/permissions.py
Normal file
59
evennia/web/api/permissions.py
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
from rest_framework import permissions
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
|
class EvenniaPermission(permissions.BasePermission):
|
||||||
|
"""
|
||||||
|
A Django Rest Framework permission class that allows us to use
|
||||||
|
Evennia's permission structure. Based on the action in a given
|
||||||
|
view, we'll check a corresponding Evennia access/lock check.
|
||||||
|
"""
|
||||||
|
# subclass this to change these permissions
|
||||||
|
MINIMUM_LIST_PERMISSION = settings.REST_FRAMEWORK["DEFAULT_LIST_PERMISSION"]
|
||||||
|
MINIMUM_CREATE_PERMISSION = settings.REST_FRAMEWORK["DEFAULT_CREATE_PERMISSION"]
|
||||||
|
view_locks = settings.REST_FRAMEWORK["DEFAULT_VIEW_LOCKS"]
|
||||||
|
destroy_locks = settings.REST_FRAMEWORK["DEFAULT_DESTROY_LOCKS"]
|
||||||
|
update_locks = settings.REST_FRAMEWORK["DEFAULT_UPDATE_LOCKS"]
|
||||||
|
|
||||||
|
def has_permission(self, request, view):
|
||||||
|
"""
|
||||||
|
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 also called if this returns True. If we return False, a permission denied
|
||||||
|
error is raised.
|
||||||
|
"""
|
||||||
|
# Only allow authenticated users to call the API
|
||||||
|
if not request.user.is_authenticated:
|
||||||
|
return False
|
||||||
|
if request.user.is_superuser:
|
||||||
|
return True
|
||||||
|
# these actions don't support object-level permissions, so use the above definitions
|
||||||
|
if view.action == "list":
|
||||||
|
return request.user.has_permistring(self.MINIMUM_LIST_PERMISSION)
|
||||||
|
if view.action == "create":
|
||||||
|
return request.user.has_permistring(self.MINIMUM_CREATE_PERMISSION)
|
||||||
|
return True # this means we'll check object-level permissions
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def check_locks(obj, user, locks):
|
||||||
|
return any([obj.access(user, lock) for lock in locks])
|
||||||
|
|
||||||
|
def has_object_permission(self, request, view, obj):
|
||||||
|
"""
|
||||||
|
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"):
|
||||||
|
# access_type is based on the examine command
|
||||||
|
return self.check_locks(obj, request.user, self.view_locks)
|
||||||
|
if view.action == "destroy":
|
||||||
|
# access type based on the destroy command
|
||||||
|
return self.check_locks(obj, request.user, self.destroy_locks)
|
||||||
|
if view.action in ("update", "partial_update"):
|
||||||
|
# access type based on set command
|
||||||
|
return self.check_locks(obj, request.user, self.update_locks)
|
||||||
58
evennia/web/api/serializers.py
Normal file
58
evennia/web/api/serializers.py
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from evennia.objects.models import ObjectDB
|
||||||
|
from evennia.accounts.models import AccountDB
|
||||||
|
from evennia.scripts.models import ScriptDB
|
||||||
|
from evennia.typeclasses.attributes import Attribute
|
||||||
|
from evennia.typeclasses.tags import Tag
|
||||||
|
|
||||||
|
|
||||||
|
class AttributeSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Attribute
|
||||||
|
fields = ["db_key", "db_value", "db_category", "db_attrtype"]
|
||||||
|
|
||||||
|
|
||||||
|
class TagSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Tag
|
||||||
|
fields = ["db_key", "db_category", "db_data", "db_tagtype"]
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleObjectDBSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = ObjectDB
|
||||||
|
fields = ["id", "db_key"]
|
||||||
|
|
||||||
|
|
||||||
|
class TypeclassSerializerMixin(object):
|
||||||
|
db_attributes = AttributeSerializer(many=True)
|
||||||
|
db_tags = TagSerializer(many=True)
|
||||||
|
|
||||||
|
shared_fields = ["id", "db_key", "db_attributes", "db_tags", "db_typeclass_path"]
|
||||||
|
|
||||||
|
|
||||||
|
class ObjectDBSerializer(TypeclassSerializerMixin, serializers.ModelSerializer):
|
||||||
|
contents = SimpleObjectDBSerializer(source="locations_set", many=True, read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ObjectDB
|
||||||
|
fields = ["db_location", "db_home", "contents"] + TypeclassSerializerMixin.shared_fields
|
||||||
|
read_only_fields = ["id", "db_attributes", "db_tags"]
|
||||||
|
|
||||||
|
|
||||||
|
class AccountDBSerializer(TypeclassSerializerMixin, serializers.ModelSerializer):
|
||||||
|
db_key = serializers.CharField(required=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = AccountDB
|
||||||
|
fields = ["username"] + TypeclassSerializerMixin.shared_fields
|
||||||
|
read_only_fields = ["id", "db_attributes", "db_tags"]
|
||||||
|
|
||||||
|
|
||||||
|
class ScriptDBSerializer(TypeclassSerializerMixin, serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = ScriptDB
|
||||||
|
fields = ["db_interval", "db_persistent", "db_start_delay",
|
||||||
|
"db_is_active", "db_repeats"] + TypeclassSerializerMixin.shared_fields
|
||||||
|
read_only_fields = ["id", "db_attributes", "db_tags"]
|
||||||
93
evennia/web/api/tests.py
Normal file
93
evennia/web/api/tests.py
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
"""Tests for the REST API"""
|
||||||
|
from evennia.utils.test_resources import EvenniaTest
|
||||||
|
from evennia.web.api import serializers
|
||||||
|
from rest_framework.test import APIClient
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.test import override_settings
|
||||||
|
from collections import namedtuple
|
||||||
|
from django.conf.urls import url, include
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
url(r"^", include("evennia.web.website.urls")),
|
||||||
|
url(r"^api/", include("evennia.web.api.urls", namespace="api")),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
REST_API_ENABLED=True, ROOT_URLCONF=__name__, AUTH_USERNAME_VALIDATORS=[]
|
||||||
|
)
|
||||||
|
class TestEvenniaRESTApi(EvenniaTest):
|
||||||
|
client_class = APIClient
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.account.is_superuser = True
|
||||||
|
self.account.save()
|
||||||
|
self.client.force_login(self.account)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
try:
|
||||||
|
super().tearDown()
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_view_details(self, action):
|
||||||
|
"""Helper function for generating list of named tuples"""
|
||||||
|
View = namedtuple("View", ["view_name", "obj", "list", "serializer"])
|
||||||
|
views = [
|
||||||
|
View("object-%s" % action, self.obj1, [self.obj1, self.char1, self.exit, self.room1, self.room2, self.obj2,
|
||||||
|
self.char2], serializers.ObjectDBSerializer),
|
||||||
|
View("character-%s" % action, self.char1, [self.char1, self.char2], serializers.ObjectDBSerializer),
|
||||||
|
View("exit-%s" % action, self.exit, [self.exit], serializers.ObjectDBSerializer),
|
||||||
|
View("room-%s" % action, self.room1, [self.room1, self.room2], serializers.ObjectDBSerializer),
|
||||||
|
View("script-%s" % action, self.script, [self.script], serializers.ScriptDBSerializer),
|
||||||
|
View("account-%s" % action, self.account2, [self.account, self.account2], serializers.AccountDBSerializer),
|
||||||
|
]
|
||||||
|
return views
|
||||||
|
|
||||||
|
def test_retrieve(self):
|
||||||
|
views = self.get_view_details("detail")
|
||||||
|
for view in views:
|
||||||
|
with self.subTest(msg="Testing {} retrieve".format(view.view_name)):
|
||||||
|
view_url = reverse(
|
||||||
|
"api:{}".format(view.view_name), kwargs={"pk": view.obj.pk}
|
||||||
|
)
|
||||||
|
response = self.client.get(view_url)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_update(self):
|
||||||
|
views = self.get_view_details("detail")
|
||||||
|
for view in views:
|
||||||
|
with self.subTest(msg="Testing {} update".format(view.view_name)):
|
||||||
|
view_url = reverse(
|
||||||
|
"api:{}".format(view.view_name), kwargs={"pk": view.obj.pk}
|
||||||
|
)
|
||||||
|
# test both PUT (update) and PATCH (partial update) here
|
||||||
|
for new_key, method in (("foobar", "put"), ("fizzbuzz", "patch")):
|
||||||
|
field = "username" if "account" in view.view_name else "db_key"
|
||||||
|
data = {field: new_key}
|
||||||
|
response = getattr(self.client, method)(view_url, data=data)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
view.obj.refresh_from_db()
|
||||||
|
self.assertEqual(getattr(view.obj, field), new_key)
|
||||||
|
|
||||||
|
def test_delete(self):
|
||||||
|
views = self.get_view_details("detail")
|
||||||
|
for view in views:
|
||||||
|
with self.subTest(msg="Testing {} delete".format(view.view_name)):
|
||||||
|
view_url = reverse(
|
||||||
|
"api:{}".format(view.view_name), kwargs={"pk": view.obj.pk}
|
||||||
|
)
|
||||||
|
response = self.client.delete(view_url)
|
||||||
|
self.assertEqual(response.status_code, 204)
|
||||||
|
with self.assertRaises(ObjectDoesNotExist):
|
||||||
|
view.obj.refresh_from_db()
|
||||||
|
|
||||||
|
def test_list(self):
|
||||||
|
views = self.get_view_details("list")
|
||||||
|
for view in views:
|
||||||
|
with self.subTest(msg=f"Testing {view.view_name} "):
|
||||||
|
view_url = reverse(f"api:{view.view_name}")
|
||||||
|
response = self.client.get(view_url)
|
||||||
|
self.assertCountEqual(response.data['results'], [view.serializer(obj).data for obj in view.list])
|
||||||
21
evennia/web/api/urls.py
Normal file
21
evennia/web/api/urls.py
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
from rest_framework import routers
|
||||||
|
from evennia.web.api.views import (
|
||||||
|
ObjectDBViewSet,
|
||||||
|
AccountDBViewSet,
|
||||||
|
CharacterViewSet,
|
||||||
|
ExitViewSet,
|
||||||
|
RoomViewSet,
|
||||||
|
ScriptDBViewSet
|
||||||
|
)
|
||||||
|
|
||||||
|
app_name = "api"
|
||||||
|
|
||||||
|
router = routers.DefaultRouter()
|
||||||
|
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")
|
||||||
|
|
||||||
|
urlpatterns = router.urls
|
||||||
70
evennia/web/api/views.py
Normal file
70
evennia/web/api/views.py
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
"""
|
||||||
|
Views are the functions that are called by different url endpoints.
|
||||||
|
The Django Rest Framework provides collections called 'ViewSets', which
|
||||||
|
can generate a number of views for the common CRUD operations.
|
||||||
|
"""
|
||||||
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
from rest_framework.decorators import action
|
||||||
|
|
||||||
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
|
|
||||||
|
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, AccountDBSerializer, ScriptDBSerializer, AttributeSerializer
|
||||||
|
from evennia.web.api.filters import ObjectDBFilterSet, AccountDBFilterSet, ScriptDBFilterSet
|
||||||
|
from evennia.web.api.permissions import EvenniaPermission
|
||||||
|
|
||||||
|
|
||||||
|
class TypeclassViewSetMixin(object):
|
||||||
|
permission_classes = [EvenniaPermission]
|
||||||
|
filter_backends = [DjangoFilterBackend]
|
||||||
|
|
||||||
|
|
||||||
|
class ObjectDBViewSet(TypeclassViewSetMixin, ModelViewSet):
|
||||||
|
serializer_class = ObjectDBSerializer
|
||||||
|
queryset = ObjectDB.objects.all()
|
||||||
|
filterset_class = ObjectDBFilterSet
|
||||||
|
|
||||||
|
@action(detail=True, methods=["put", "post"])
|
||||||
|
def add_attribute(self, request, pk=None):
|
||||||
|
attr = AttributeSerializer(data=request.data)
|
||||||
|
obj = self.get_object()
|
||||||
|
if attr.is_valid(raise_exception=True):
|
||||||
|
key = attr.validated_data["db_key"]
|
||||||
|
value = attr.validated_data.get("db_value")
|
||||||
|
category = attr.validated_data.get("db_category")
|
||||||
|
attr_type = attr.validated_data.get("db_attrtype")
|
||||||
|
if attr_type == "nick":
|
||||||
|
handler = obj.nicks
|
||||||
|
else:
|
||||||
|
handler = obj.attributes
|
||||||
|
if value:
|
||||||
|
handler.add(key=key, value=value, category=category)
|
||||||
|
else:
|
||||||
|
handler.remove(key=key, category=category)
|
||||||
|
|
||||||
|
|
||||||
|
class CharacterViewSet(ObjectDBViewSet):
|
||||||
|
queryset = DefaultCharacter.objects.typeclass_search(DefaultCharacter.path, include_children=True)
|
||||||
|
|
||||||
|
|
||||||
|
class RoomViewSet(ObjectDBViewSet):
|
||||||
|
queryset = DefaultRoom.objects.typeclass_search(DefaultRoom.path, include_children=True)
|
||||||
|
|
||||||
|
|
||||||
|
class ExitViewSet(ObjectDBViewSet):
|
||||||
|
queryset = DefaultExit.objects.typeclass_search(DefaultExit.path, include_children=True)
|
||||||
|
|
||||||
|
|
||||||
|
class AccountDBViewSet(TypeclassViewSetMixin, ModelViewSet):
|
||||||
|
serializer_class = AccountDBSerializer
|
||||||
|
queryset = AccountDB.objects.all()
|
||||||
|
filterset_class = AccountDBFilterSet
|
||||||
|
|
||||||
|
|
||||||
|
class ScriptDBViewSet(TypeclassViewSetMixin, ModelViewSet):
|
||||||
|
serializer_class = ScriptDBSerializer
|
||||||
|
queryset = ScriptDB.objects.all()
|
||||||
|
filterset_class = ScriptDBFilterSet
|
||||||
|
|
@ -6,7 +6,9 @@
|
||||||
# http://diveintopython.org/regular_expressions/street_addresses.html#re.matching.2.3
|
# http://diveintopython.org/regular_expressions/street_addresses.html#re.matching.2.3
|
||||||
#
|
#
|
||||||
|
|
||||||
|
from django.urls import path
|
||||||
from django.conf.urls import url, include
|
from django.conf.urls import url, include
|
||||||
|
from django.conf import settings
|
||||||
from django.views.generic import RedirectView
|
from django.views.generic import RedirectView
|
||||||
|
|
||||||
# Setup the root url tree from /
|
# Setup the root url tree from /
|
||||||
|
|
@ -14,9 +16,12 @@ from django.views.generic import RedirectView
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# Front page (note that we shouldn't specify namespace here since we will
|
# Front page (note that we shouldn't specify namespace here since we will
|
||||||
# not be able to load django-auth/admin stuff (will probably work in Django>1.9)
|
# not be able to load django-auth/admin stuff (will probably work in Django>1.9)
|
||||||
url(r"^", include("evennia.web.website.urls")), # , namespace='website', app_name='website')),
|
path("", include("evennia.web.website.urls")),
|
||||||
# webclient
|
# webclient
|
||||||
url(r"^webclient/", include("evennia.web.webclient.urls", namespace="webclient")),
|
path("webclient/", include("evennia.web.webclient.urls")),
|
||||||
# favicon
|
# favicon
|
||||||
url(r"^favicon\.ico$", RedirectView.as_view(url="/media/images/favicon.ico", permanent=False)),
|
path("favicon.ico", RedirectView.as_view(url="/media/images/favicon.ico", permanent=False)),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if settings.REST_API_ENABLED:
|
||||||
|
urlpatterns += [url(r'^api/', include("evennia.web.api.urls", namespace="api"))]
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@
|
||||||
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
|
||||||
|
django-filter >= 2.2.0
|
||||||
django-sekizai
|
django-sekizai
|
||||||
inflect
|
inflect
|
||||||
autobahn >= 17.9.3
|
autobahn >= 17.9.3
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue