diff --git a/docs/source/Components/Website.md b/docs/source/Components/Website.md index c3df4603cc..0b1eabfbbf 100644 --- a/docs/source/Components/Website.md +++ b/docs/source/Components/Website.md @@ -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: diff --git a/evennia/settings_default.py b/evennia/settings_default.py index 65f753f4a1..7870daef3a 100644 --- a/evennia/settings_default.py +++ b/evennia/settings_default.py @@ -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. diff --git a/evennia/web/api/README.md b/evennia/web/api/README.md index efa5cd0596..95dd080fe2 100644 --- a/evennia/web/api/README.md +++ b/evennia/web/api/README.md @@ -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. diff --git a/evennia/web/api/root.py b/evennia/web/api/root.py new file mode 100644 index 0000000000..4a89639435 --- /dev/null +++ b/evennia/web/api/root.py @@ -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 diff --git a/evennia/web/api/urls.py b/evennia/web/api/urls.py index 3b42bf9ec6..1d17892aba 100644 --- a/evennia/web/api/urls.py +++ b/evennia/web/api/urls.py @@ -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' + ) +] diff --git a/evennia/web/api/views.py b/evennia/web/api/views.py index eefb717982..d5806fd871 100644 --- a/evennia/web/api/views.py +++ b/evennia/web/api/views.py @@ -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 /: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 /: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() diff --git a/evennia/web/static/rest_framework/css/api.css b/evennia/web/static/rest_framework/css/api.css new file mode 100644 index 0000000000..022a006c1d --- /dev/null +++ b/evennia/web/static/rest_framework/css/api.css @@ -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 +} diff --git a/evennia/web/static/rest_framework/images/favicon.ico b/evennia/web/static/rest_framework/images/favicon.ico new file mode 100644 index 0000000000..547a1686d6 Binary files /dev/null and b/evennia/web/static/rest_framework/images/favicon.ico differ diff --git a/evennia/web/templates/rest_framework/api.html b/evennia/web/templates/rest_framework/api.html new file mode 100644 index 0000000000..2251a3f853 --- /dev/null +++ b/evennia/web/templates/rest_framework/api.html @@ -0,0 +1,35 @@ + + + +{% extends "rest_framework/base.html" %} +{% load static sekizai_tags %} + +{% block title %} + Evennia API +{% endblock %} + + +{% block style %} + {{ block.super }} + + +{% endblock %} + + +{% block branding %} +

Evennia REST API

+ Access game database from external services (requires login and proper access) +{% endblock %} + + +{% block userlinks %} + {{ block.super }} +
  • Root
  • +
  • Schema
  • +
  • Autodoc
  • +
  • Home
  • +{% endblock %} + diff --git a/evennia/web/templates/rest_framework/redoc.html b/evennia/web/templates/rest_framework/redoc.html new file mode 100644 index 0000000000..f2422cdf54 --- /dev/null +++ b/evennia/web/templates/rest_framework/redoc.html @@ -0,0 +1,20 @@ + + - + {% if webclient_enabled %}
  • Play Online
  • {% endif %} - + {% if user.is_staff %}
  • Admin
  • +
  • API
  • {% endif %} {% endblock %}