Add api customization templates

This commit is contained in:
Griatch 2021-05-23 17:00:02 +02:00
parent cc9f42a398
commit 4250ca1a29
24 changed files with 334 additions and 170 deletions

View file

@ -57,6 +57,9 @@ Up requirements to Django 3.2+
concept of a dynamically created `ChannelCmdSet`.
- Add `Msg.db_receiver_external` field to allowe external, string-id message-receivers.
- Renamed `app.css` to `website.css` for consistency. Removed old prosimii-css files.
- Remove `mygame/web/static_overrides` and -`template_overrides`, reorganize website/admin/client/api
into a more consistent structure for overriding. Expanded webpage documentation considerably.
- REST API list-view was shortened (#2401). New CSS/HTML. Add ReDoc for API autodoc page.
### Evennia 0.9.5 (2019-2020)

View file

@ -43,7 +43,7 @@ command:
```python
if not obj.access(accessing_obj, 'delete'):
accessing_obj.msg("Sorry, you may not delete that.")
return
return
```
## Defining locks
@ -51,16 +51,16 @@ command:
Defining a lock (i.e. an access restriction) in Evennia is done by adding simple strings of lock
definitions to the object's `locks` property using `obj.locks.add()`.
Here are some examples of lock strings (not including the quotes):
Here are some examples of lock strings (not including the quotes):
```python
delete:id(34) # only allow obj #34 to delete
edit:all() # let everyone edit
edit:all() # let everyone edit
# only those who are not "very_weak" or are Admins may pick this up
get: not attr(very_weak) or perm(Admin)
get: not attr(very_weak) or perm(Admin)
```
Formally, a lockstring has the following syntax:
Formally, a lockstring has the following syntax:
```python
access_type: [NOT] lockfunc1([arg1,..]) [AND|OR] [NOT] lockfunc2([arg1,...]) [...]
@ -77,7 +77,7 @@ total result is `True`, the lock is passed.
You can create several lock types one after the other by separating them with a semicolon (`;`) in
the lockstring. The string below yields the same result as the previous example:
delete:id(34);edit:all();get: not attr(very_weak) or perm(Admin)
delete:id(34);edit:all();get: not attr(very_weak) or perm(Admin)
### Valid access_types
@ -92,7 +92,7 @@ the default command set) actually checks for, as in the example of `delete` abov
Below are the access_types checked by the default commandset.
- [Commands](./Commands)
- [Commands](./Commands)
- `cmd` - this defines who may call this command at all.
- [Objects](./Objects):
- `control` - who is the "owner" of the object. Can set locks, delete it etc. Defaults to the
@ -109,10 +109,10 @@ something like `call:false()`.
- `get`- who may pick up the object and carry it around.
- `puppet` - who may "become" this object and control it as their "character".
- `attrcreate` - who may create new attributes on the object (default True)
- [Characters](./Objects#Characters):
- [Characters](./Objects#Characters):
- Same as for Objects
- [Exits](./Objects#Exits):
- Same as for Objects
- [Exits](./Objects#Exits):
- Same as for Objects
- `traverse` - who may pass the exit.
- [Accounts](./Accounts):
- `examine` - who may examine the account's properties.
@ -147,7 +147,7 @@ read a board or post to a board. You could then define locks such as:
```python
obj.locks.add("read:perm(Player);post:perm(Admin)")
```
```
This will create a 'read' access type for Characters having the `Player` permission or above and a
'post' access type for those with `Admin` permissions or above (see below how the `perm()` lock
@ -158,7 +158,7 @@ trying to read the board):
```python
if not obj.access(accessing_obj, 'read'):
accessing_obj.msg("Sorry, you may not read that.")
return
return
```
### Lock functions
@ -178,15 +178,15 @@ arguments explicitly given in the lock definition will appear as extra arguments
```python
# A simple example lock function. Called with e.g. `id(34)`. This is
# defined in, say mygame/server/conf/lockfuncs.py
def id(accessing_obj, accessed_obj, *args, **kwargs):
if args:
wanted_id = args[0]
return accessing_obj.id == wanted_id
return False
return False
```
The above could for example be used in a lock function like this:
The above could for example be used in a lock function like this:
```python
# we have `obj` and `owner_object` from before
@ -202,7 +202,7 @@ We could check if the "edit" lock is passed with something like this:
return
```
In this example, everyone except the `caller` with the right `id` will get the error.
In this example, everyone except the `caller` with the right `id` will get the error.
> (Using the `*` and `**` syntax causes Python to magically put all extra arguments into a list
`args` and all keyword arguments into a dictionary `kwargs` respectively. If you are unfamiliar with
@ -258,123 +258,6 @@ child object to change the default. Also creation commands like `create` changes
objects you create - for example it sets the `control` lock_type so as to allow you, its creator, to
control and delete the object.
# Permissions
> This section covers the underlying code use of permissions. If you just want to learn how to
practically assign permissions in-game, refer to the [Building Permissions](../Concepts/Building-Permissions)
page, which details how you use the `perm` command.
A *permission* is simply a list of text strings stored in the handler `permissions` on `Objects`
and `Accounts`. Permissions can be used as a convenient way to structure access levels and
hierarchies. It is set by the `perm` command. Permissions are especially handled by the `perm()` and
`pperm()` lock functions listed above.
Let's say we have a `red_key` object. We also have red chests that we want to unlock with this key.
perm red_key = unlocks_red_chests
This gives the `red_key` object the permission "unlocks_red_chests". Next we lock our red chests:
lock red chest = unlock:perm(unlocks_red_chests)
What this lock will expect is to the fed the actual key object. The `perm()` lock function will
check the permissions set on the key and only return true if the permission is the one given.
Finally we need to actually check this lock somehow. Let's say the chest has an command `open <key>`
sitting on itself. Somewhere in its code the command needs to figure out which key you are using and
test if this key has the correct permission:
```python
# self.obj is the chest
# and used_key is the key we used as argument to
# the command. The self.caller is the one trying
# to unlock the chest
if not self.obj.access(used_key, "unlock"):
self.caller.msg("The key does not fit!")
return
```
All new accounts are given a default set of permissions defined by
`settings.PERMISSION_ACCOUNT_DEFAULT`.
Selected permission strings can be organized in a *permission hierarchy* by editing the tuple
`settings.PERMISSION_HIERARCHY`. Evennia's default permission hierarchy is as follows:
Developer # like superuser but affected by locks
Admin # can administrate accounts
Builder # can edit the world
Helper # can edit help files
Player # can chat and send tells (default level)
(Also the plural form works, so you could use `Developers` etc too).
> There is also a `Guest` level below `Player` that is only active if `settings.GUEST_ENABLED` is
set. This is never part of `settings.PERMISSION_HIERARCHY`.
The main use of this is that if you use the lock function `perm()` mentioned above, a lock check for
a particular permission in the hierarchy will *also* grant access to those with *higher* hierarchy
access. So if you have the permission "Admin" you will also pass a lock defined as `perm(Builder)`
or any of those levels below "Admin".
When doing an access check from an [Object](./Objects) or Character, the `perm()` lock function will
always first use the permissions of any Account connected to that Object before checking for
permissions on the Object. In the case of hierarchical permissions (Admins, Builders etc), the
Account permission will always be used (this stops an Account from escalating their permission by
puppeting a high-level Character). If the permission looked for is not in the hierarchy, an exact
match is required, first on the Account and if not found there (or if no Account is connected), then
on the Object itself.
Here is how you use `perm` to give an account more permissions:
perm/account Tommy = Builders
perm/account/del Tommy = Builders # remove it again
Note the use of the `/account` switch. It means you assign the permission to the
[Accounts](./Accounts) Tommy instead of any [Character](./Objects) that also happens to be named
"Tommy".
Putting permissions on the *Account* guarantees that they are kept, *regardless* of which Character
they are currently puppeting. This is especially important to remember when assigning permissions
from the *hierarchy tree* - as mentioned above, an Account's permissions will overrule that of its
character. So to be sure to avoid confusion you should generally put hierarchy permissions on the
Account, not on their Characters (but see also [quelling](./Locks#Quelling)).
Below is an example of an object without any connected account
```python
obj1.permissions = ["Builders", "cool_guy"]
obj2.locks.add("enter:perm_above(Accounts) and perm(cool_guy)")
obj2.access(obj1, "enter") # this returns True!
```
And one example of a puppet with a connected account:
```python
account.permissions.add("Accounts")
puppet.permissions.add("Builders", "cool_guy")
obj2.locks.add("enter:perm_above(Accounts) and perm(cool_guy)")
obj2.access(puppet, "enter") # this returns False!
```
## Superusers
There is normally only one *superuser* account and that is the one first created when starting
Evennia (User #1). This is sometimes known as the "Owner" or "God" user. A superuser has more than
full access - it completely *bypasses* all locks so no checks are even run. This allows for the
superuser to always have access to everything in an emergency. But it also hides any eventual errors
you might have made in your lock definitions. So when trying out game systems you should either use
quelling (see below) or make a second Developer-level character so your locks get tested correctly.
## Quelling
The `quell` command can be used to enforce the `perm()` lockfunc to ignore permissions on the
Account and instead use the permissions on the Character only. This can be used e.g. by staff to
test out things with a lower permission level. Return to the normal operation with `unquell`. Note
that quelling will use the smallest of any hierarchical permission on the Account or Character, so
one cannot escalate one's Account permission by quelling to a high-permission Character. Also the
superuser can quell their powers this way, making them affectable by locks.
## More Lock definition examples
@ -384,7 +267,7 @@ You are only allowed to do *examine* on this object if you have 'excellent' eyes
an Attribute `eyesight` with the value `excellent` defined on yourself) or if you have the
"Builders" permission string assigned to you.
open: holds('the green key') or perm(Builder)
open: holds('the green key') or perm(Builder)
This could be called by the `open` command on a "door" object. The check is passed if you are a
Builder or has the right key in your inventory.
@ -453,7 +336,7 @@ object has the attribute *strength* of the right value. For this we would need t
function that checks if attributes have a value greater than a given value. Luckily there is already
such a one included in evennia (see `evennia/locks/lockfuncs.py`), called `attr_gt`.
So the lock string will look like this: `get:attr_gt(strength, 50)`. We put this on the box now:
So the lock string will look like this: `get:attr_gt(strength, 50)`. We put this on the box now:
lock box = get:attr_gt(strength, 50)
@ -463,20 +346,20 @@ strength above 50 however and you'll pick it up no problem. Done! A very heavy b
If you wanted to set this up in python code, it would look something like this:
```python
from evennia import create_object
# create, then set the lock
box = create_object(None, key="box")
box.locks.add("get:attr_gt(strength, 50)")
# or we can assign locks in one go right away
box = create_object(None, key="box", locks="get:attr_gt(strength, 50)")
# set the attributes
box.db.desc = "This is a very big and heavy box."
box.db.get_err_msg = "You are not strong enough to lift this box."
# one heavy box, ready to withstand all but the strongest...
```
@ -492,4 +375,4 @@ when we try to hide away as much of the underlying architecture as possible.
The django permissions are not completely gone however. We use it for validating passwords during
login. It is also used exclusively for managing Evennia's web-based admin site, which is a graphical
front-end for the database of Evennia. You edit and assign such permissions directly from the web
interface. It's stand-alone from the permissions described above.
interface. It's stand-alone from the permissions described above.

View file

@ -0,0 +1,118 @@
# Permissions
A *permission* is simply a text string stored in the handler `permissions` on `Objects`
and `Accounts`. Think of it as a specialized sort of [Tag](./Tags) - one specifically dedicated
to access checking. They are thus often tightly coupled to [Locks](./Locks).
Permissions are used as a convenient way to structure access levels and
hierarchies. It is set by the `perm` command. Permissions are especially
handled by the `perm()` and `pperm()` [lock functions](./Locks).
Let's say we have a `red_key` object. We also have red chests that we want to unlock with this key.
perm red_key = unlocks_red_chests
This gives the `red_key` object the permission "unlocks_red_chests". Next we
lock our red chests:
lock red chest = unlock:perm(unlocks_red_chests)
When trying to unlock the red chest with this key, the chest Typeclass could
then take the key and do an access check:
```python
# in some typeclass file where chest is defined
class TreasureChest(Object):
# ...
def open_chest(self, who, tried_key):
if not chest.access(who, tried_key, "unlock"):
who.msg("The key does not fit!")
return
```
All new accounts are given a default set of permissions defined by
`settings.PERMISSION_ACCOUNT_DEFAULT`.
Selected permission strings can be organized in a *permission hierarchy* by editing the tuple
`settings.PERMISSION_HIERARCHY`. Evennia's default permission hierarchy is as follows:
Developer # like superuser but affected by locks
Admin # can administrate accounts
Builder # can edit the world
Helper # can edit help files
Player # can chat and send tells (default level)
(Also the plural form works, so you could use `Developers` etc too).
> There is also a `Guest` level below `Player` that is only active if `settings.GUEST_ENABLED` is
set. This is never part of `settings.PERMISSION_HIERARCHY`.
The main use of this is that if you use the lock function `perm()` mentioned above, a lock check for
a particular permission in the hierarchy will *also* grant access to those with *higher* hierarchy
access. So if you have the permission "Admin" you will also pass a lock defined as `perm(Builder)`
or any of those levels below "Admin".
When doing an access check from an [Object](./Objects) or Character, the `perm()` lock function will
always first use the permissions of any Account connected to that Object before checking for
permissions on the Object. In the case of hierarchical permissions (Admins, Builders etc), the
Account permission will always be used (this stops an Account from escalating their permission by
puppeting a high-level Character). If the permission looked for is not in the hierarchy, an exact
match is required, first on the Account and if not found there (or if no Account is connected), then
on the Object itself.
Here is how you use `perm` to give an account more permissions:
perm/account Tommy = Builders
perm/account/del Tommy = Builders # remove it again
Note the use of the `/account` switch. It means you assign the permission to the
[Accounts](./Accounts) Tommy instead of any [Character](./Objects) that also happens to be named
"Tommy".
Putting permissions on the *Account* guarantees that they are kept, *regardless* of which Character
they are currently puppeting. This is especially important to remember when assigning permissions
from the *hierarchy tree* - as mentioned above, an Account's permissions will overrule that of its
character. So to be sure to avoid confusion you should generally put hierarchy permissions on the
Account, not on their Characters (but see also [quelling](./Locks#Quelling)).
Below is an example of an object without any connected account
```python
obj1.permissions = ["Builders", "cool_guy"]
obj2.locks.add("enter:perm_above(Accounts) and perm(cool_guy)")
obj2.access(obj1, "enter") # this returns True!
```
And one example of a puppet with a connected account:
```python
account.permissions.add("Accounts")
puppet.permissions.add("Builders", "cool_guy")
obj2.locks.add("enter:perm_above(Accounts) and perm(cool_guy)")
obj2.access(puppet, "enter") # this returns False!
```
## Superusers
There is normally only one *superuser* account and that is the one first created when starting
Evennia (User #1). This is sometimes known as the "Owner" or "God" user. A superuser has more than
full access - it completely *bypasses* all locks so no checks are even run. This allows for the
superuser to always have access to everything in an emergency. But it also hides any eventual errors
you might have made in your lock definitions. So when trying out game systems you should either use
quelling (see below) or make a second Developer-level character so your locks get tested correctly.
## Quelling
The `quell` command can be used to enforce the `perm()` lockfunc to ignore permissions on the
Account and instead use the permissions on the Character only. This can be used e.g. by staff to
test out things with a lower permission level. Return to the normal operation with `unquell`. Note
that quelling will use the smallest of any hierarchical permission on the Account or Character, so
one cannot escalate one's Account permission by quelling to a high-permission Character. Also the
superuser can quell their powers this way, making them affectable by locks.

View file

@ -0,0 +1,136 @@
# Evennia REST API
Evennia makes its database accessible via a REST API found on
[http://localhost:4001/api](http://localhost:4001/api) if running locally with
default setup. The API allows you to retrieve, edit and create resources from
outside the game, for example with your own custom client or game editor.
While you can view and learn about the api in the web browser, it is really
meant to be accessed in code, by other programs.
The API is using [Django Rest Framework][drf]. This automates the process
of setting up _views_ (Python code) to process the result of web requests.
The process of retrieving data is similar to that explained on the
[Webserver](./Webserver) page, except the views will here return [JSON][json]
data for the resource you want. You can also _send_ such JSON data
in order to update the database from the outside.
## Usage
To activate the API, add this to your settings file.
REST_API_ENABLED = True
The main controlling setting is `REST_FRAMEWORK`, which is a dict. The keys
`DEFAULT_LIST_PERMISSION` and `DEFAULT_CREATE_PERMISSIONS` control who may
view and create new objects via the api respectively. By default, users with
['Builder'-level permission](./Permissions) or higher may access both actions.
While the api is meant to be expanded upon, Evennia supplies several operations
out of the box. If you click the `Autodoc` button in the upper right of the `/api`
website you'll get a fancy graphical presentation of the available endpoints.
Here is an example of calling the api in Python using the standard `requests` library.
>>> import requests
>>> response = requests.get("https://www.mygame.com/api", auth=("MyUsername", "password123"))
>>> response.json()
{'accounts': 'http://www.mygame.com/api/accounts/',
'objects': 'http://www.mygame.com/api/objects/',
'characters': 'http://www.mygame.comg/api/characters/',
'exits': 'http://www.mygame.com/api/exits/',
'rooms': 'http://www.mygame.com/api/rooms/',
'scripts': 'http://www.mygame.com/api/scripts/'
'helpentries': 'http://www.mygame.com/api/helpentries/' }
To list a specific type of object:
>>> response = requests.get("https://www.mygame.com/api/objects",
auth=("Myusername", "password123"))
>>> response.json()
{
"count": 125,
"next": "https://www.mygame.com/api/objects/?limit=25&offset=25",
"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:
>>> response = requests.get("https://www.mygame.com/api/accounts/?permission=developer",
auth=("MyUserName", "password123"))
>>> response.json()
{
"count": 1,
"results": [{"username": "bob",...}]
}
Now suppose that you want to use the API to create an [Object](./Objects):
>>> data = {"db_key": "A shiny sword"}
>>> response = requests.post("https://www.mygame.com/api/objects",
data=data, auth=("Anotherusername", "mypassword"))
>>> response.json()
{"db_key": "A shiny sword", "id": 214, "db_location": None, ...}
Here we made a HTTP POST request to the `/api/objects` endpoint with the `db_key`
we wanted. We got back info for the newly created object. You can now make
another request with PUT (replace everything) or PATCH (replace only what you
provide). By providing the id to the endpoint (`/api/objects/214`),
we make sure to update the right sword:
>>> data = {"db_key": "An even SHINIER sword", "db_location": 50}
>>> response = requests.put("https://www.mygame.com/api/objects/214",
data=data, auth=("Anotherusername", "mypassword"))
>>> response.json()
{"db_key": "An even SHINIER sword", "id": 214, "db_location": 50, ...}
In most cases, you won't be making API requests to the backend with Python,
but with Javascript from some frontend application.
There are many Javascript libraries which are meant to make this process
easier for requests from the frontend, such as [AXIOS][axios], or using
the native [Fetch][fetch].
## Customizing the API
Overall, reading up on [Django Rest Framework ViewSets](https://www.django-rest-framework.org/api-guide/viewsets) and
other parts of their documentation is required for expanding and
customizing the API.
Check out the [Website](Website) page for help on how to override code, templates
and static files.
- API templates (for the web-display) is located in `evennia/web/api/templates/rest_framework/` (it must
be named such to allow override of the original REST framework templates).
- Static files is in `evennia/web/api/static/rest_framework/`
- The api code is located in `evennia/web/api/` - the `url.py` file here is responsible for
collecting all view-classes.
Contrary to other web components, there is no pre-made urls.py set up for
`mygame/web/api/`. This is because the registration of models with the api is
strongly integrated with the REST api functionality. Easiest is probably to
copy over `evennia/web/api/urls.py` and modify it in place.
[wiki-api]: https://en.wikipedia.org/wiki/Application_programming_interface
[drf]: https://www.django-rest-framework.org/
[pagination]: https://www.django-rest-framework.org/api-guide/pagination/
[filters]: https://www.django-rest-framework.org/api-guide/filtering/#filtering
[json]: https://en.wikipedia.org/wiki/JSON
[crud]: https://en.wikipedia.org/wiki/Create,_read,_update_and_delete
[serializers]: https://www.django-rest-framework.org/api-guide/serializers/
[ajax]: https://en.wikipedia.org/wiki/Ajax_(programming)
[rest]: https://en.wikipedia.org/wiki/Representational_state_transfer
[requests]: https://requests.readthedocs.io/en/master/
[axios]: https://github.com/axios/axios
[fetch]: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API

View file

@ -86,7 +86,7 @@ Only Superusers can change the `Superuser status` flag, and grant new
permissions to accounts. The superuser is the only permission level that is
also relevant in-game. `User Permissions` and `Groups` found on the `Account`
admin page _only_ affects the admin - they have no connection to the in-game
[Permissions](Permissions) (Player, Builder, Admin etc).
[Permissions](./Permissions) (Player, Builder, Admin etc).
For a staffer with `Staff status` to be able to actually do anything, the
superuser must grant at least some permissions for them on their Account. This

View file

@ -22,6 +22,8 @@ environment. It leverages the Django web framework and provides:
- The [Webclient](./Webclient) page is served by the webserver, but the actual
game communication (sending/receiving data) is done by the javascript client
on the page opening a websocket connection directly to Evennia's Portal.
- The [Evennia REST-API](./Web-API) allows for accessing the database from outside the game
(only if `REST_API_ENABLED=True).
## Basic Webserver data flow

View file

@ -38,6 +38,7 @@
- [Components/Nicks](Components/Nicks)
- [Components/Objects](Components/Objects)
- [Components/Outputfuncs](Components/Outputfuncs)
- [Components/Permissions](Components/Permissions)
- [Components/Portal And Server](Components/Portal-And-Server)
- [Components/Prototypes](Components/Prototypes)
- [Components/Scripts](Components/Scripts)
@ -48,6 +49,7 @@
- [Components/Tags](Components/Tags)
- [Components/TickerHandler](Components/TickerHandler)
- [Components/Typeclasses](Components/Typeclasses)
- [Components/Web API](Components/Web-API)
- [Components/Web Admin](Components/Web-Admin)
- [Components/Webclient](Components/Webclient)
- [Components/Webserver](Components/Webserver)

View file

@ -7,7 +7,7 @@ The main web/urls.py includes these routes for all urls starting with `admin/`
"""
from django.conf.urls import path
from django.urls import path
from evennia.web.admin.urls import urlpatterns as evennia_admin_urlpatterns
# add patterns here

View file

@ -0,0 +1,3 @@
# Evennia API static files
Overrides for API files.

View file

@ -0,0 +1,3 @@
# Static files for API
Override images here.

View file

@ -0,0 +1,3 @@
# Templates for the Evennia API
Override templates here.

View file

@ -12,8 +12,7 @@ should modify urls.py in those sub directories.
Search the Django documentation for "URL dispatcher" for more help.
"""
from django.conf.urls import path, include
from django.urls import path, include
# default evennia patterns
from evennia.web.urls import urlpatterns as evennia_default_urlpatterns

View file

@ -6,8 +6,7 @@ The main web/urls.py includes these routes for all urls starting with `webclient
"""
from django.conf.urls import path
from django.urls import path
from evennia.web.webclient.urls import urlpatterns as evennia_webclient_urlpatterns
# add patterns here

View file

@ -6,8 +6,7 @@ so it can reroute to all website pages.
"""
from django.conf.urls import path
from django.urls import path
from evennia.web.website.urls import urlpatterns as evennia_website_urlpatterns
# add patterns here

View file

@ -168,7 +168,6 @@ def perm(accessing_obj, accessed_obj, *args, **kwargs):
permission = args[0].lower()
perms_object = accessing_obj.permissions.all()
except (AttributeError, IndexError) as err:
print("accessing_obj err:", err)
return False
gtmode = kwargs.pop("_greater_than", False)

View file

@ -1,5 +1,8 @@
# Evennia API
This folder contains the code implementing the REST-API of Evennia, based on
Django Rest Framework.
## Synopsis
An API, or [Application Programming Interface][wiki-api], is a way of establishing rules
@ -18,7 +21,7 @@ can convert into python objects for you, a process called deserialization.
When returning a response, it can also convert python objects into JSON
strings to send back to a client, which is called serialization. Because it's
such a common task to want to handle [CRUD][crud] operations for the django models that you use to represent database
objects (such as your Character typeclass, Room typeclass, etc), DRF makes
objects (such as your Character typeclass, Room typeclass, etc), DRF makes
this process very easy by letting you define [Serializers][serializers]
that largely automate the process of serializing your in-game objects into
JSON representations for sending them to a client, or for turning a JSON string
@ -54,7 +57,7 @@ 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
```python
>>> import requests
>>> r = requests.get("https://www.mygame.com/api", auth=("user", "pw"))
>>> r.json()
@ -131,16 +134,16 @@ object:
>>> 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, ...}
{"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.
In most cases, you won't be making API requests to the backend with python,
but with Javascript from your frontend application.
There are many Javascript libraries which are meant to make this process
easier for requests from the frontend, such as [AXIOS][axios], or using
There are many Javascript libraries which are meant to make this process
easier for requests from the frontend, such as [AXIOS][axios], or using
the native [Fetch][fetch].
[wiki-api]: https://en.wikipedia.org/wiki/Application_programming_interface

View file

@ -321,6 +321,7 @@ class HelpSerializer(TypeclassSerializerMixin, serializers.ModelSerializer):
"id", "db_key", "db_help_category", "db_entrytext", "db_date_created",
"tags", "aliases"
]
read_only_fields = ["id"]
class HelpListSerializer(TypeclassListSerializerMixin, serializers.ModelSerializer):
"""
@ -332,3 +333,4 @@ class HelpListSerializer(TypeclassListSerializerMixin, serializers.ModelSerializ
fields = [
"id", "db_key", "db_help_category", "db_date_created",
]
read_only_fields = ["id"]

View file

@ -39,7 +39,7 @@ class TestEvenniaRESTApi(EvenniaTest):
def get_view_details(self, action):
"""Helper function for generating list of named tuples"""
View = namedtuple(
"View", ["view_name", "obj", "list", "serializer", "create_data", "retrieve_data"]
"View", ["view_name", "obj", "list", "serializer", "list_serializer", "create_data", "retrieve_data"]
)
views = [
View(
@ -47,6 +47,7 @@ class TestEvenniaRESTApi(EvenniaTest):
self.obj1,
[self.obj1, self.char1, self.exit, self.room1, self.room2, self.obj2, self.char2],
serializers.ObjectDBSerializer,
serializers.ObjectListSerializer,
{"db_key": "object-create-test-name"},
serializers.ObjectDBSerializer(self.obj1).data,
),
@ -55,6 +56,7 @@ class TestEvenniaRESTApi(EvenniaTest):
self.char1,
[self.char1, self.char2],
serializers.ObjectDBSerializer,
serializers.ObjectListSerializer,
{"db_key": "character-create-test-name"},
serializers.ObjectDBSerializer(self.char1).data,
),
@ -63,6 +65,7 @@ class TestEvenniaRESTApi(EvenniaTest):
self.exit,
[self.exit],
serializers.ObjectDBSerializer,
serializers.ObjectListSerializer,
{"db_key": "exit-create-test-name"},
serializers.ObjectDBSerializer(self.exit).data,
),
@ -71,6 +74,7 @@ class TestEvenniaRESTApi(EvenniaTest):
self.room1,
[self.room1, self.room2],
serializers.ObjectDBSerializer,
serializers.ObjectListSerializer,
{"db_key": "room-create-test-name"},
serializers.ObjectDBSerializer(self.room1).data,
),
@ -79,6 +83,7 @@ class TestEvenniaRESTApi(EvenniaTest):
self.script,
[self.script],
serializers.ScriptDBSerializer,
serializers.ScriptListSerializer,
{"db_key": "script-create-test-name"},
serializers.ScriptDBSerializer(self.script).data,
),
@ -87,6 +92,7 @@ class TestEvenniaRESTApi(EvenniaTest):
self.account2,
[self.account, self.account2],
serializers.AccountSerializer,
serializers.AccountListSerializer,
{"username": "account-create-test-name"},
serializers.AccountSerializer(self.account2).data,
),
@ -135,7 +141,7 @@ class TestEvenniaRESTApi(EvenniaTest):
response = self.client.get(view_url)
self.assertEqual(response.status_code, 200)
self.assertCountEqual(
response.data["results"], [view.serializer(obj).data for obj in view.list]
response.data["results"], [view.list_serializer(obj).data for obj in view.list]
)
def test_create(self):

View file

@ -34,7 +34,7 @@ router.register(r"characters", views.CharacterViewSet, basename="character")
router.register(r"exits", views.ExitViewSet, basename="exit")
router.register(r"rooms", views.RoomViewSet, basename="room")
router.register(r"scripts", views.ScriptDBViewSet, basename="script")
router.register(r"helpentries", views.HelpViewSet, basename="script")
router.register(r"helpentries", views.HelpViewSet, basename="helpentry")
urlpatterns = router.urls

View file

@ -36,8 +36,10 @@ folder and edit it to add/remove links to the menu.
{% 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>
<li><a class="nav-link" href="{% url 'admin:index' %}">Admin</a></li>
{% if rest_api_enabled %}
<li><a class="nav-link" href="/api">API</a></li>
{% endif %}
{% endif %}
{% endblock %}
</ul>

View file

@ -30,8 +30,6 @@ 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

@ -1,9 +1,9 @@
"""
This file defines global variables that will always be available in a view
context without having to repeatedly include it.
context without having to repeatedly include it.
For this to work, this file is included in the settings file, in the
TEMPLATE_CONTEXT_PROCESSORS tuple.
TEMPLATES["OPTIONS"]["context_processors"] list.
"""
@ -20,6 +20,7 @@ GAME_ENTITIES = ["Objects", "Scripts", "Comms", "Help"]
GAME_SETUP = ["Permissions", "Config"]
CONNECTIONS = ["Irc"]
WEBSITE = ["Flatpages", "News", "Sites"]
REST_API_ENABLED = False
# Determine the site name and server version
def set_game_name_and_slogan():
@ -31,7 +32,7 @@ def set_game_name_and_slogan():
This function is used for unit testing the values of the globals.
"""
global GAME_NAME, GAME_SLOGAN, SERVER_VERSION
global GAME_NAME, GAME_SLOGAN, SERVER_VERSION, REST_API_ENABLED
try:
GAME_NAME = settings.SERVERNAME.strip()
except AttributeError:
@ -42,6 +43,7 @@ def set_game_name_and_slogan():
except AttributeError:
GAME_SLOGAN = SERVER_VERSION
REST_API_ENABLED = settings.REST_API_ENABLED
def set_webclient_settings():
"""
@ -98,4 +100,5 @@ def general_context(request):
"websocket_enabled": WEBSOCKET_CLIENT_ENABLED,
"websocket_port": WEBSOCKET_PORT,
"websocket_url": WEBSOCKET_URL,
"rest_api_enabled": REST_API_ENABLED,
}

View file

@ -9,13 +9,12 @@ class TestGeneralContext(TestCase):
@patch("evennia.web.utils.general_context.GAME_NAME", "test_name")
@patch("evennia.web.utils.general_context.GAME_SLOGAN", "test_game_slogan")
@patch(
"evennia.web.utils.general_context.WEBSOCKET_CLIENT_ENABLED",
"websocket_client_enabled_testvalue",
)
@patch("evennia.web.utils.general_context.WEBSOCKET_CLIENT_ENABLED",
"websocket_client_enabled_testvalue")
@patch("evennia.web.utils.general_context.WEBCLIENT_ENABLED", "webclient_enabled_testvalue")
@patch("evennia.web.utils.general_context.WEBSOCKET_PORT", "websocket_client_port_testvalue")
@patch("evennia.web.utils.general_context.WEBSOCKET_URL", "websocket_client_url_testvalue")
@patch("evennia.web.utils.general_context.REST_API_ENABLED", True)
def test_general_context(self):
request = RequestFactory().get("/")
request.user = AnonymousUser()
@ -39,6 +38,7 @@ class TestGeneralContext(TestCase):
"websocket_enabled": "websocket_client_enabled_testvalue",
"websocket_port": "websocket_client_port_testvalue",
"websocket_url": "websocket_client_url_testvalue",
"rest_api_enabled": True,
},
)
@ -48,6 +48,7 @@ class TestGeneralContext(TestCase):
def test_set_game_name_and_slogan(self, mock_get_version, mock_settings):
mock_get_version.return_value = "version 1"
# test default/fallback values
mock_settings.REST_API_ENABLED = False
general_context.set_game_name_and_slogan()
self.assertEqual(general_context.GAME_NAME, "Evennia")
self.assertEqual(general_context.GAME_SLOGAN, "version 1")