Customize api and add redoc autodocs

This commit is contained in:
Griatch 2021-05-23 11:48:41 +02:00
parent 43d678d8ca
commit 87e0796f05
14 changed files with 243 additions and 36 deletions

View file

@ -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:

View file

@ -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.

View file

@ -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
View 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

View file

@ -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'
)
]

View file

@ -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()

View 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
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View 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 %}

View 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 %}

View file

@ -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>

View file

@ -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"-->

View file

@ -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)),
]

View file

@ -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