Add api customization templates
This commit is contained in:
parent
cc9f42a398
commit
4250ca1a29
24 changed files with 334 additions and 170 deletions
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
118
docs/source/Components/Permissions.md
Normal file
118
docs/source/Components/Permissions.md
Normal 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.
|
||||
136
docs/source/Components/Web-API.md
Normal file
136
docs/source/Components/Web-API.md
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
0
evennia/game_template/web/api/__init__.py
Normal file
0
evennia/game_template/web/api/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Evennia API static files
|
||||
|
||||
Overrides for API files.
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# Static files for API
|
||||
|
||||
Override images here.
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# Templates for the Evennia API
|
||||
|
||||
Override templates here.
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue