mirror of
https://github.com/evennia/evennia.git
synced 2026-03-16 21:06:30 +01:00
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:
|
||||
# 'django.db.backends.sqlite3', (default)
|
||||
# 'django.db.backends.mysql',
|
||||
# 'django.db.backends.postgresql_psycopg2',
|
||||
# 'django.db.backends.postgresql',
|
||||
# 'django.db.backends.oracle' (untested).
|
||||
# NAME - database name, or path to the db file for sqlite3
|
||||
# USER - db admin (unused in sqlite3)
|
||||
|
|
@ -255,7 +255,9 @@ IN_GAME_ERRORS = True
|
|||
DATABASES = {
|
||||
"default": {
|
||||
"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": "",
|
||||
"PASSWORD": "",
|
||||
"HOST": "",
|
||||
|
|
@ -472,7 +474,12 @@ SERVER_SESSION_CLASS = "evennia.server.serversession.ServerSession"
|
|||
# immediately entered path fail to find a typeclass. It allows for
|
||||
# shorter input strings. They must either base off the game directory
|
||||
# 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)
|
||||
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
|
||||
# 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
|
||||
|
|
@ -861,7 +872,9 @@ TEMPLATES = [
|
|||
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"),
|
||||
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"),
|
||||
],
|
||||
"APP_DIRS": True,
|
||||
|
|
@ -912,6 +925,8 @@ INSTALLED_APPS = [
|
|||
"django.contrib.sites",
|
||||
"django.contrib.staticfiles",
|
||||
"django.contrib.messages",
|
||||
"rest_framework",
|
||||
"django_filters",
|
||||
"sekizai",
|
||||
"evennia.utils.idmapper",
|
||||
"evennia.server",
|
||||
|
|
@ -931,7 +946,9 @@ AUTH_USER_MODEL = "accounts.AccountDB"
|
|||
# Password validation plugins
|
||||
# https://docs.djangoproject.com/en/1.11/ref/settings/#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",
|
||||
"OPTIONS": {"min_length": 8},
|
||||
|
|
@ -944,8 +961,14 @@ AUTH_PASSWORD_VALIDATORS = [
|
|||
# Username validation plugins
|
||||
AUTH_USERNAME_VALIDATORS = [
|
||||
{"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"},
|
||||
]
|
||||
|
||||
|
|
@ -956,6 +979,38 @@ TEST_RUNNER = "evennia.server.tests.testrunner.EvenniaTestSuiteRunner"
|
|||
# messages.error() to Bootstrap 'danger' classes.
|
||||
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
|
||||
######################################################################
|
||||
|
|
|
|||
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
|
||||
#
|
||||
|
||||
from django.urls import path
|
||||
from django.conf.urls import url, include
|
||||
from django.conf import settings
|
||||
from django.views.generic import RedirectView
|
||||
|
||||
# Setup the root url tree from /
|
||||
|
|
@ -14,9 +16,12 @@ from django.views.generic import RedirectView
|
|||
urlpatterns = [
|
||||
# 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)
|
||||
url(r"^", include("evennia.web.website.urls")), # , namespace='website', app_name='website')),
|
||||
path("", include("evennia.web.website.urls")),
|
||||
# webclient
|
||||
url(r"^webclient/", include("evennia.web.webclient.urls", namespace="webclient")),
|
||||
path("webclient/", include("evennia.web.webclient.urls")),
|
||||
# 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
|
||||
twisted >= 19.2.1, < 20.0.0
|
||||
pytz
|
||||
djangorestframework >= 3.10.3
|
||||
django-filter >= 2.2.0
|
||||
django-sekizai
|
||||
inflect
|
||||
autobahn >= 17.9.3
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue