mirror of
https://github.com/evennia/evennia.git
synced 2026-03-16 21:06:30 +01:00
Customize api and add redoc autodocs
This commit is contained in:
parent
43d678d8ca
commit
87e0796f05
14 changed files with 243 additions and 36 deletions
|
|
@ -204,7 +204,59 @@ pages look the same without rewriting the same thing over and over)
|
|||
|
||||
There's a lot more information to be found in the [Django template language documentation](https://docs.djangoproject.com/en/3.2/ref/templates/language/).
|
||||
|
||||
#### Change front page functionality
|
||||
### Change webpage colors and styling
|
||||
|
||||
You can tweak the [CSS](https://en.wikipedia.org/wiki/Cascading_Style_Sheets) of the entire
|
||||
website. If you investigate the `evennia/web/templates/website/base.html` file you'll see that we
|
||||
use the [Bootstrap
|
||||
4](https://getbootstrap.com/docs/4.6/getting-started/introduction/) toolkit.
|
||||
|
||||
Much structural HTML functionality is actually coming from bootstrap, so you
|
||||
will often be able to just add bootstrap CSS classes to elements in the HTML
|
||||
file to get various effects like text-centering or similar.
|
||||
|
||||
The website's custom CSS is found in
|
||||
`evennia/web/static/website/css/website.css` but we also look for a (currently
|
||||
empty) `custom.css` in the same location. You can override either, but it may
|
||||
be easier to revert your changes if you only add things to `custom.css`.
|
||||
|
||||
Copy the CSS file you want to modify to the corresponding location in `mygame/web`.
|
||||
Modify it and reload the server to see your changes.
|
||||
|
||||
You can also apply static files without reloading, but running this in the
|
||||
terminal:
|
||||
|
||||
evennia collectstatic --no-input
|
||||
|
||||
(this is run automatically when reloading the server).
|
||||
|
||||
> Note that before you see new CSS files applied you may need to refresh your
|
||||
> browser without cache (Ctrl-F5 in Firefox, for example).
|
||||
|
||||
As an example, add/copy `custom.css` to `mygame/web/static/website/css/` and
|
||||
add the following:
|
||||
|
||||
|
||||
```css
|
||||
|
||||
.navbar {
|
||||
background-color: #7a3d54;
|
||||
}
|
||||
|
||||
.footer {
|
||||
background-color: #7a3d54;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Reload and your website now has a red theme!
|
||||
|
||||
> Hint: Learn to use your web browser's [Developer tools](https://torquemag.io/2020/06/browser-developer-tools-tutorial/).
|
||||
> These allow you to tweak CSS 'live' to find a look you like and copy it into
|
||||
> the .css file only when you want to make the changes permanent.
|
||||
|
||||
|
||||
### Change front page functionality
|
||||
|
||||
The logic is all in the view. To find where the index-page view is found, we
|
||||
look in `evennia/web/website/urls.py`. Here we find the following line:
|
||||
|
|
|
|||
|
|
@ -880,7 +880,7 @@ STATIC_URL = "/static/"
|
|||
# served by webserver.
|
||||
STATIC_ROOT = os.path.join(GAME_DIR, "server", ".static")
|
||||
# Location of static data to overload the defaults from
|
||||
# evennia/web/webclient and evennia/web/website's static/ dirs.
|
||||
# evennia/web/static.
|
||||
STATICFILES_DIRS = [os.path.join(GAME_DIR, "web", "static")]
|
||||
# Patterns of files in the static directories. Used here to make sure that
|
||||
# its readme file is preserved but unused.
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ permissions. Individual objects will check lockstrings to determine if the
|
|||
user has permission to perform retrieve/update/delete actions upon them.
|
||||
To start with, you can view a synopsis of endpoints by making a GET request
|
||||
to the `yourgame/api/` endpoint by using the excellent [requests library][requests]:
|
||||
|
||||
```pythonstub
|
||||
>>> import requests
|
||||
>>> r = requests.get("https://www.mygame.com/api", auth=("user", "pw"))
|
||||
|
|
@ -66,6 +67,7 @@ to the `yourgame/api/` endpoint by using the excellent [requests library][reques
|
|||
```
|
||||
|
||||
To view an object, you might make a request like this:
|
||||
|
||||
```pythonstub
|
||||
>>> import requests
|
||||
>>> response = requests.get("https://www.mygame.com/api/objects/57",
|
||||
|
|
@ -77,6 +79,7 @@ The above example makes a GET request to the /objects/ endpoint to retrieve the
|
|||
object with an ID of 57, retrieving basic data for it.
|
||||
|
||||
For listing a number of objects, you might do this:
|
||||
|
||||
```pythonstub
|
||||
>>> response = requests.get("https://www.mygame.com/api/objects",
|
||||
auth=("Myusername", "password123"))
|
||||
|
|
@ -87,11 +90,16 @@ For listing a number of objects, you might do this:
|
|||
"previous": null,
|
||||
"results" : [{"db_key": "A rusty longsword", "id": 57, "db_location": 213, ...}]}
|
||||
```
|
||||
In the above example, it now displays the objects inside the "results" array, while it has a "count" value
|
||||
for the number of total objects, and "next" and "previous" links for the next and previous page, if any.
|
||||
This is called [pagination][pagination], and the link displays "limit" and "offset" as query parameters that
|
||||
can be added to the url to control the output. Other query parameters can be defined as [filters][filters] which
|
||||
allow you to further narrow the results. For example, to only get accounts with developer permissions:
|
||||
|
||||
In the above example, it now displays the objects inside the "results" array,
|
||||
while it has a "count" value for the number of total objects, and "next" and
|
||||
"previous" links for the next and previous page, if any. This is called
|
||||
[pagination][pagination], and the link displays "limit" and "offset" as query
|
||||
parameters that can be added to the url to control the output. Other query
|
||||
parameters can be defined as [filters][filters] which allow you to further
|
||||
narrow the results. For example, to only get accounts with developer
|
||||
permissions:
|
||||
|
||||
```pythonstub
|
||||
>>> response = requests.get("https://www.mygame.com/api/accounts/?permission=developer",
|
||||
auth=("user", "pw"))
|
||||
|
|
@ -104,6 +112,7 @@ allow you to further narrow the results. For example, to only get accounts with
|
|||
|
||||
Now suppose that you want
|
||||
to use the API to create an object:
|
||||
|
||||
```pythonstub
|
||||
>>> data = {"db_key": "A shiny sword"}
|
||||
>>> response = requests.post("https://www.mygame.com/api/objects",
|
||||
|
|
@ -111,16 +120,19 @@ to use the API to create an object:
|
|||
>>> response.json()
|
||||
{"db_key": "A shiny sword", "id": 214, "db_location": None, ...}
|
||||
```
|
||||
|
||||
In the above example, you make a POST request to the /objects/ endpoint with
|
||||
the name of the object you wish to create passed along as data. Now suppose you
|
||||
decided you didn't like the name, and wanted to change it for the newly created
|
||||
object:
|
||||
|
||||
```pythonstub
|
||||
>>> data = {"db_key": "An even SHINIER sword", "db_location": 50}
|
||||
>>> response = requests.put("https://www.mygame.com/api/objects/214",
|
||||
data=data, auth=("Alsoauser", "Badpassword"))
|
||||
>>> response.json()
|
||||
{"db_key": "An even SHINIER sword", "id": 214, "db_location": 50, ...}
|
||||
|
||||
```
|
||||
By making a PUT request to the endpoint that includes the object ID, it becomes
|
||||
a request to update the object with the specified data you pass along.
|
||||
|
|
|
|||
17
evennia/web/api/root.py
Normal file
17
evennia/web/api/root.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
"""
|
||||
Set a more useful description on the Api root.
|
||||
|
||||
"""
|
||||
|
||||
from rest_framework import routers
|
||||
|
||||
|
||||
class EvenniaAPIRoot(routers.APIRootView):
|
||||
"""
|
||||
Root of the Evennia API tree.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
class APIRootRouter(routers.DefaultRouter):
|
||||
APIRootView = EvenniaAPIRoot
|
||||
|
|
@ -14,9 +14,14 @@ retrieve object: action: GET, url: /objects/<:pk>, view name: object-detail
|
|||
update object: action: POST, url: /objects/<:pk>, view name: object-detail
|
||||
delete object: action: DELETE, url: /objects/<:pk>, view name: object-detail
|
||||
set attribute: action: POST, url: /objects/<:pk>/set-attribute, view name: object-set-attribute
|
||||
|
||||
"""
|
||||
|
||||
from rest_framework import routers
|
||||
from django.urls import path
|
||||
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,
|
||||
|
|
@ -28,7 +33,7 @@ from evennia.web.api.views import (
|
|||
|
||||
app_name = "api"
|
||||
|
||||
router = routers.DefaultRouter()
|
||||
router = APIRootRouter()
|
||||
router.trailing_slash = "/?"
|
||||
router.register(r"accounts", AccountDBViewSet, basename="account")
|
||||
router.register(r"objects", ObjectDBViewSet, basename="object")
|
||||
|
|
@ -38,3 +43,21 @@ router.register(r"rooms", RoomViewSet, basename="room")
|
|||
router.register(r"scripts", ScriptDBViewSet, basename="script")
|
||||
|
||||
urlpatterns = router.urls
|
||||
|
||||
urlpatterns += [
|
||||
# openapi schema
|
||||
path('openapi',
|
||||
get_schema_view(
|
||||
title="Evennia API",
|
||||
description="Evennia OpenAPI Schema",
|
||||
version="1.0"),
|
||||
name='openapi',
|
||||
),
|
||||
# redoc auto-doc (based on openapi schema)
|
||||
path('redoc/',
|
||||
TemplateView.as_view(
|
||||
template_name="rest_framework/redoc.html" ,
|
||||
extra_context={'schema_url': 'api:openapi'}),
|
||||
name='redoc'
|
||||
)
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
"""
|
||||
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.
|
||||
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
|
||||
|
|
@ -24,10 +25,19 @@ from evennia.web.api.filters import ObjectDBFilterSet, AccountDBFilterSet, Scrip
|
|||
from evennia.web.api.permissions import EvenniaPermission
|
||||
|
||||
|
||||
class TypeclassViewSetMixin(object):
|
||||
class TypeclassViewSetMixin:
|
||||
"""
|
||||
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.
|
||||
|
||||
The `set_atribute` action is an example of a custom action added to a
|
||||
viewset. Based on the name of the method, it will create a default url_name
|
||||
(used for reversing) and url_path. The 'pk' argument is automatically
|
||||
passed to this action because it has a url path of the format <object
|
||||
type>/:pk/set-attribute. The get_object method is automatically set in the
|
||||
expected viewset classes that will inherit this, using the pk that's passed
|
||||
along to retrieve the object.
|
||||
|
||||
"""
|
||||
|
||||
# permission classes determine who is authorized to call the view
|
||||
|
|
@ -39,15 +49,9 @@ class TypeclassViewSetMixin(object):
|
|||
@action(detail=True, methods=["put", "post"])
|
||||
def set_attribute(self, request, pk=None):
|
||||
"""
|
||||
This is an example of a custom action added to a viewset. Based on the name of the
|
||||
method, it will create a default url_name (used for reversing) and url_path.
|
||||
The 'pk' argument is automatically passed to this action because it has a url path
|
||||
of the format <object type>/:pk/set-attribute. The get_object method is automatically
|
||||
set in the expected viewset classes that will inherit this, using the pk that's
|
||||
passed along to retrieve the object.
|
||||
This action will set an attribute if the db_value is defined, or remove
|
||||
it if no db_value is provided.
|
||||
|
||||
This action will set an attribute if the db_value is defined, or remove it
|
||||
if no db_value is provided.
|
||||
"""
|
||||
attr = AttributeSerializer(data=request.data)
|
||||
obj = self.get_object()
|
||||
|
|
@ -73,11 +77,14 @@ class TypeclassViewSetMixin(object):
|
|||
|
||||
class ObjectDBViewSet(TypeclassViewSetMixin, ModelViewSet):
|
||||
"""
|
||||
An example of a basic viewset for all ObjectDB instances. It declares the
|
||||
serializer to use for both retrieving and changing/creating/deleting
|
||||
instances. Serializers are similar to django forms, used for the
|
||||
transmitting of data (typically json).
|
||||
The Object is the parent for all in-game entities that have a location
|
||||
(rooms, exits, characters etc).
|
||||
|
||||
"""
|
||||
# An example of a basic viewset for all ObjectDB instances. It declares the
|
||||
# serializer to use for both retrieving and changing/creating/deleting
|
||||
# instances. Serializers are similar to django forms, used for the
|
||||
# transmitting of data (typically json).
|
||||
|
||||
serializer_class = ObjectDBSerializer
|
||||
queryset = ObjectDB.objects.all()
|
||||
|
|
@ -86,8 +93,8 @@ class ObjectDBViewSet(TypeclassViewSetMixin, ModelViewSet):
|
|||
|
||||
class CharacterViewSet(ObjectDBViewSet):
|
||||
"""
|
||||
This overrides the queryset to only retrieve Character objects
|
||||
based on your DefaultCharacter typeclass path.
|
||||
Characters are a type of Object commonly used as player avatars in-game.
|
||||
|
||||
"""
|
||||
|
||||
queryset = DefaultCharacter.objects.typeclass_search(
|
||||
|
|
@ -96,19 +103,29 @@ class CharacterViewSet(ObjectDBViewSet):
|
|||
|
||||
|
||||
class RoomViewSet(ObjectDBViewSet):
|
||||
"""Viewset for Room objects"""
|
||||
"""
|
||||
Rooms indicate discrete locations in-game.
|
||||
|
||||
"""
|
||||
|
||||
queryset = DefaultRoom.objects.typeclass_search(DefaultRoom.path, include_children=True)
|
||||
|
||||
|
||||
class ExitViewSet(ObjectDBViewSet):
|
||||
"""Viewset for Exit objects"""
|
||||
"""
|
||||
Exits are objects with a destination and allows for traversing from one
|
||||
location to another.
|
||||
|
||||
"""
|
||||
|
||||
queryset = DefaultExit.objects.typeclass_search(DefaultExit.path, include_children=True)
|
||||
|
||||
|
||||
class AccountDBViewSet(TypeclassViewSetMixin, ModelViewSet):
|
||||
"""Viewset for Account objects"""
|
||||
"""
|
||||
Accounts represent the players connected to the game
|
||||
|
||||
"""
|
||||
|
||||
serializer_class = AccountSerializer
|
||||
queryset = AccountDB.objects.all()
|
||||
|
|
@ -116,7 +133,11 @@ class AccountDBViewSet(TypeclassViewSetMixin, ModelViewSet):
|
|||
|
||||
|
||||
class ScriptDBViewSet(TypeclassViewSetMixin, ModelViewSet):
|
||||
"""Viewset for Script objects"""
|
||||
"""
|
||||
Scripts are meta-objects for storing system data, running timers etc. They
|
||||
have no in-game existence.
|
||||
|
||||
"""
|
||||
|
||||
serializer_class = ScriptDBSerializer
|
||||
queryset = ScriptDB.objects.all()
|
||||
|
|
|
|||
23
evennia/web/static/rest_framework/css/api.css
Normal file
23
evennia/web/static/rest_framework/css/api.css
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
/* CSS overrides for Evennia rest-api page */
|
||||
|
||||
.navbar {
|
||||
background-color: #3d5c7a;
|
||||
border-top: 0px;
|
||||
padding-bottom: 7px;
|
||||
}
|
||||
|
||||
ul.breadcrumb {
|
||||
/* margin: 70px 0 0 0; */
|
||||
margin: 85px 0 0 0;
|
||||
}
|
||||
|
||||
.str, .atv {
|
||||
color: #114edd;
|
||||
}
|
||||
|
||||
body a {
|
||||
color: #adb5ce;
|
||||
}
|
||||
body a:hover {
|
||||
color: #fff
|
||||
}
|
||||
BIN
evennia/web/static/rest_framework/images/favicon.ico
Normal file
BIN
evennia/web/static/rest_framework/images/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
35
evennia/web/templates/rest_framework/api.html
Normal file
35
evennia/web/templates/rest_framework/api.html
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<!DOCTYPE HTML>
|
||||
|
||||
<!--
|
||||
Extend and customize the Django REST Framework api look.
|
||||
|
||||
-->
|
||||
{% extends "rest_framework/base.html" %}
|
||||
{% load static sekizai_tags %}
|
||||
|
||||
{% block title %}
|
||||
Evennia API
|
||||
{% endblock %}
|
||||
|
||||
<!-- Plug in custom Evennia CSS -->
|
||||
{% block style %}
|
||||
{{ block.super }}
|
||||
<link rel="icon" type="image/x-icon" href="{% static "website/images/evennia_logo.png" %}" />
|
||||
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/api.css" %}">
|
||||
{% endblock %}
|
||||
|
||||
<!-- Header -->
|
||||
{% block branding %}
|
||||
<h3>Evennia REST API</h3>
|
||||
Access game database from external services (requires login and proper access)
|
||||
{% endblock %}
|
||||
|
||||
<!-- Sidebar links -->
|
||||
{% block userlinks %}
|
||||
{{ block.super }}
|
||||
<li><a href="/api">Root</a></li>
|
||||
<li><a href="{% url 'api:openapi' %}">Schema</a></li>
|
||||
<li><a href="{% url 'api:redoc' %}">Autodoc</a></li>
|
||||
<li><a href="/">Home</a></li>
|
||||
{% endblock %}
|
||||
|
||||
20
evennia/web/templates/rest_framework/redoc.html
Normal file
20
evennia/web/templates/rest_framework/redoc.html
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<!DOCTYPE html>
|
||||
<!--ReDoc is used for generating documentation from OpenAPI schemas -- >
|
||||
|
||||
<!DOCTYPE html>
|
||||
|
||||
{% extends "rest_framework/api.html" %}
|
||||
|
||||
{% block body %}
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<redoc spec-url='{% url schema_url %}'></redoc>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"> </script>
|
||||
|
||||
{% endblock %}
|
||||
|
|
@ -30,20 +30,21 @@ folder and edit it to add/remove links to the menu.
|
|||
<li><a class="nav-link" href="{% url 'channels' %}">Channels</a></li>
|
||||
<li><a class="nav-link" href="{% url 'help' %}">Help</a></li>
|
||||
<!-- end game views -->
|
||||
|
||||
|
||||
{% if webclient_enabled %}
|
||||
<li><a class="nav-link" href="{% url 'webclient:index' %}">Play Online</a></li>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if user.is_staff %}
|
||||
<li><a class="nav-link" href="{% url 'admin:index' %}">Admin</a></li>
|
||||
<li><a class="nav-link" href="/api">API</a></li>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
</ul>
|
||||
<ul class="nav navbar-nav ml-auto w-120 justify-content-end">
|
||||
{% block navbar_right %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block navbar_user %}
|
||||
{% if account %}
|
||||
<li class="nav-item dropdown">
|
||||
|
|
@ -60,7 +61,7 @@ folder and edit it to add/remove links to the menu.
|
|||
<div class="dropdown-divider"></div>
|
||||
{% for character in account.characters|slice:"10" %}
|
||||
<a class="dropdown-item" href="{{ character.web_get_puppet_url }}?next={{ request.path }}">{{ character }}</a>
|
||||
{% empty %}
|
||||
{% empty %}
|
||||
<a class="dropdown-item" href="#">No characters found!</a>
|
||||
{% endfor %}
|
||||
<div class="dropdown-divider"></div>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
|
||||
<link rel="icon" type="image/x-icon" href="/static/website/images/evennia_logo.png" />
|
||||
<link rel="icon" type="image/x-icon" href="{% static "website/images/evennia_logo.png" %}" />
|
||||
|
||||
<!-- Bootstrap CSS -->
|
||||
<!--link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.6.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous"-->
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ urlpatterns = [
|
|||
path("webclient/", include("evennia.web.webclient.urls")),
|
||||
# admin
|
||||
path("admin/", include("evennia.web.admin.urls")),
|
||||
# api
|
||||
path("api/", include("evennia.web.api.urls")),
|
||||
# favicon
|
||||
path("favicon.ico", RedirectView.as_view(url="/media/images/favicon.ico", permanent=False)),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ django >= 3.2, < 3.3
|
|||
twisted >= 20.3.0, < 22.0.0
|
||||
pytz
|
||||
djangorestframework >= 3.10.3, < 3.12
|
||||
pyyaml
|
||||
django-filter == 2.4
|
||||
django-sekizai == 2.0
|
||||
inflect >= 5.2.0
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue