Made a lot of progress on the search tutorial

This commit is contained in:
Griatch 2020-07-08 00:14:37 +02:00
parent 3f1a3612e4
commit c54625b305
15 changed files with 786 additions and 636 deletions

View file

@ -1,44 +0,0 @@
## Evennia API overview
If you cloned the GIT repo following the instructions, you will have a folder named `evennia`. The
top level of it contains Python package specific stuff such as a readme file, `setup.py` etc. It
also has two subfolders`bin/` and `evennia/` (again).
The `bin/` directory holds OS-specific binaries that will be used when installing Evennia with `pip`
as per the [Getting started](../Setup/Getting-Started) instructions. The library itself is in the `evennia`
subfolder. From your code you will access this subfolder simply by `import evennia`.
- evennia
- [`__init__.py`](Evennia-API) - The "flat API" of Evennia resides here.
- [`commands/`](Commands) - The command parser and handler.
- `default/` - The [default commands](../../Component/Default-Command-Help) and cmdsets.
- [`comms/`](Communications) - Systems for communicating in-game.
- `contrib/` - Optional plugins too game-specific for core Evennia.
- `game_template/` - Copied to become the "game directory" when using `evennia --init`.
- [`help/`](Help-System) - Handles the storage and creation of help entries.
- `locale/` - Language files ([i18n](../../Concept/Internationalization)).
- [`locks/`](Locks) - Lock system for restricting access to in-game entities.
- [`objects/`](Objects) - In-game entities (all types of items and Characters).
- [`prototypes/`](Spawner-and-Prototypes) - Object Prototype/spawning system and OLC menu
- [`accounts/`](Accounts) - Out-of-game Session-controlled entities (accounts, bots etc)
- [`scripts/`](Scripts) - Out-of-game entities equivalence to Objects, also with timer support.
- [`server/`](Portal-And-Server) - Core server code and Session handling.
- `portal/` - Portal proxy and connection protocols.
- [`settings_default.py`](Server-Conf#Settings-file) - Root settings of Evennia. Copy settings
from here to `mygame/server/settings.py` file.
- [`typeclasses/`](Typeclasses) - Abstract classes for the typeclass storage and database system.
- [`utils/`](Coding-Utils) - Various miscellaneous useful coding resources.
- [`web/`](Web-Features) - Web resources and webserver. Partly copied into game directory on
initialization.
All directories contain files ending in `.py`. These are Python *modules* and are the basic units of
Python code. The roots of directories also have (usually empty) files named `__init__.py`. These are
required by Python so as to be able to find and import modules in other directories. When you have
run Evennia at least once you will find that there will also be `.pyc` files appearing, these are
pre-compiled binary versions of the `.py` files to speed up execution.
The root of the `evennia` folder has an `__init__.py` file containing the "[flat API](../../Evennia-API)".
This holds shortcuts to various subfolders in the evennia library. It is provided to make it easier
to find things; it allows you to just import `evennia` and access things from that rather than
having to import from their actual locations inside the source tree.

View file

@ -1,6 +1,6 @@
# Our own commands
[prev lesson](Python-classes-and-objects) | [next lesson](More-on-Commands)
[prev lesson](Searching-Things) | [next lesson]()
In this lesson we'll learn how to create our own Evennia _Commands_. If you are new to Python you'll
also learn some more basics about how to manipulate strings and get information out of Evennia.
@ -393,5 +393,4 @@ We also upset a dragon.
In the next lesson we'll learn how to hit Smaug with different weapons. We'll also
get into how we replace and extend Evennia's default Commands.
[prev lesson](Python-classes-and-objects) | [next lesson](More-on-Commands)
[prev lesson](Searching-Things) | [next lesson]()

View file

@ -0,0 +1,53 @@
# Creating things
[prev lesson](Learning-Typeclasses) | [next lesson](Searching-Things)
We have already created some things - dragons for example. There are many different things to create
in Evennia though. In the last lesson we learned about typeclasses, the way to make objects persistent in the database.
Given the path to a Typeclass, there are three ways to create an instance of it:
- Firstly, you can call the class directly, and then `.save()` it:
obj = SomeTypeClass(db_key=...)
obj.save()
This has the drawback of being two operations; you must also import the class and have to pass
the actual database field names, such as `db_key` instead of `key` as keyword arguments.
- Secondly you can use the Evennia creation helpers:
obj = evennia.create_object(SomeTypeClass, key=...)
This is the recommended way if you are trying to create things in Python. The first argument can either be
the class _or_ the python-path to the typeclass, like `"path.to.SomeTypeClass"`. It can also be `None` in which
case the Evennia default will be used. While all the creation methods
are available on `evennia`, they are actually implemented in [evennia/utils/create.py](api:evennia.utils.create).
- Finally, you can create objects using an in-game command, such as
create/drop obj:path.to.SomeTypeClass
As a developer you are usually best off using the two other methods, but a command is usually the only way
to let regular players or builders without Python-access help build the game world.
## Creating Objects
This is one of the most common creation-types. These are entities that inherits from `DefaultObject` at any distance.
They have an existence in the game world and includes rooms, characters, exits, weapons, flower pots and castles.
> py
> import evennia
> rose = evennia.create_object(key="rose")
Since we didn't specify the `typeclass` as the first argument, the default given by `settings.BASE_OBJECT_TYPECLASS`
(`typeclasses.objects.Object`) will be used.
## Creating Accounts
An _Account_ is an out-of-character (OOC) entity, with no existence in the game world.
You can find the parent class for Accounts in `typeclasses/accounts.py`.
_TODO_
[prev lesson](Learning-Typeclasses) | [next lesson](Searching-Things)

View file

@ -1,562 +0,0 @@
# Overview of the Evennia API
In the last few lessons we have explored the gamedir, learned about typeclasses and commands. In the process
we have used several resources from the Evennia library. Some examples:
- `evennia.DefaultObject`, `evennia.DefaultCharacter` and (inherited) methods on these classes like `.msg`
and `at_object_create` but also `.cmdset.add` for adding new cmdsets.
- `evennia.search_object` for finding lists of objects anywhere.
- `evennia.create_object` for creating objects in code instead of using the in-game `create` command.
- `evennia.Command` with methods like `func` and `parse` to implement new commands
- `evennia.CmdSet` for storing commands
- `evennia.default_cmds` holding references to all default Command classes like `look`, `dig` and so on.
Evennia has a lot of resources to help you make your game. We have just given a selection of them for you to try
out so far (and we'll show off many more in the lessons to come). Now we'll teach you how to find them
for yourself.
## Exploring the API
The Evennia _API_
([Application Programming Interface](https://en.wikipedia.org/wiki/Application_programming_interface)) is what
you use to access things inside the `evennia` package.
Open the [API frontpage](../../../Evennia-API). This page sums up the main components of Evennia with a short
description of each. Try clicking through to a few entries - once you get deep enough you'll see full descriptions
of each component along with their documentation. You can also click `[source]` to see the full Python source
for each thing.
### Browsing the code
You can browse [the evennia repository on github](https://github.com/evennia/evennia). This is exactly
what you can download from us. The github repo is also searchable.
You can also clone the evennia repo to your own computer and read the sources locally. This is necessary
if you want to help with Evennia's development itself. See the
[extended install instructions](../../../Setup/Extended-Installation) if you want to do this. The short of is to install `git` and run
git clone https://github.com/evennia/evennia.git
In the terminal/console you can search for anything using `git` (make sure you are inside the repo):
git grep -n "class DefaultObject"
will quickly tell you which file the DefaultObject class is defined and on which line.
If you read the code on `github` or cloned the repo yourself, you will find this being the outermost folder
structure:
evennia/
bin/
CHANGELOG.md
...
...
docs/
evennia/
That internal folder `evennia/evennia/` is the actual library, the thing covered by the API auto-docs and
what you get when you do `import evennia`. The outermost level is part of the Evennia package distribution and
installation. It's not something we'll bother with for this tutorial.
> The `evennia/docs/` folder contains, well, this documentation. See [contributing to the docs](../../../Contributing-Docs) if you
want to learn more about how this works.
## Overview of the `evennia` root package
```
evennia/
__init__.py
settings_default.py
accounts/
commands/
comms/
game_template/
help/
locale/
locks/
objects/
prototypes/
scripts/
server/
typeclasses/
utils/
web/
```
```sidebar:: __init__.py
The `__init__.py` file is a special Python filename used to represent a Python 'package'.
When you import `evennia` on its own, you import this file. When you do `evennia.foo` Python will
first look for a property `.foo` in `__init__.py` and then for a module or folder in the same
location.
```
While all the actual Evennia code is found in the various folders, the `__init__.py` contains "shortcuts"
to useful things you will often need. This allows you to do things like `from evennia import DefaultObject`
even though the `DefaultObject` is not actually defined there. Let's see how that works:
[Look at Line 189](evennia/__init__.py#L189) of `evennia/__init__.py` and you'll find this line:
```python
# ...
from .objects.objects import DefaultObject
# ...
```
```sidebar:: Relative and absolute imports
The first full-stop in `from .objects.objects ...` means that
we are importing from the current location. This is called a `relative import`.
By comparison, `from evennia.objects.objects` is an `absolute import`. In this particular
case, the two would give the same result.
```
Since `DefaultObject` is imported into `__init__.py`, it means we can do
`from evennia import DefaultObject` even though the code for it is not actually here.
So if we want to find the code for `DefaultObject` we need to look in
`evennia/objects/objects.py`. Here's how to look it up in these docs:
1. Open the [API frontpage](../../../Evennia-API)
2. Locate the link to [evennia.objects](api:evennia.objects) and click on it.
3. Click through to [evennia.objects.objects](api:evennia.objects.objects).
4. You are now in the python module. Scroll down (or search in your web browser) to find the `DefaultObject` class.
5. You can now read what this does and what methods are on it. If you want to see the full source, click the
\[[source](src:evennia.objects.objects#DefaultObject)\] link.
# Tutorial Searching For Objects
You will often want to operate on a specific object in the database. For example when a player
attacks a named target you'll need to find that target so it can be attacked. Or when a rain storm
draws in you need to find all outdoor-rooms so you can show it raining in them. This tutorial
explains Evennia's tools for searching.
## Things to search for
The first thing to consider is the base type of the thing you are searching for. Evennia organizes
its database into a few main tables: [Objects](../../../Component/Objects), [Accounts](../../../Component/Accounts), [Scripts](../../../Component/Scripts),
[Channels](../../../Component/Communications#channels), [Messages](Communication#Msg) and [Help Entries](../../../Component/Help-System).
Most of the time you'll likely spend your time searching for Objects and the occasional Accounts.
So to find an entity, what can be searched for?
- The `key` is the name of the entity. While you can get this from `obj.key` the *database field*
is actually named `obj.db_key` - this is useful to know only when you do [direct database
queries](Tutorial-Searching-For-Objects#queries-in-django). The one exception is `Accounts`, where
the database field for `.key` is instead named `username` (this is a Django requirement). When you
don't specify search-type, you'll usually search based on key. *Aliases* are extra names given to
Objects using something like `@alias` or `obj.aliases.add('name')`. The main search functions (see
below) will automatically search for aliases whenever you search by-key.
- [Tags](../../../Component/Tags) are the main way to group and identify objects in Evennia. Tags can most often be
used (sometimes together with keys) to uniquely identify an object. For example, even though you
have two locations with the same name, you can separate them by their tagging (this is how Evennia
implements 'zones' seen in other systems). Tags can also have categories, to further organize your
data for quick lookups.
- An object's [Attributes](../../../Component/Attributes) can also used to find an object. This can be very useful but
since Attributes can store almost any data they are far less optimized to search for than Tags or
keys.
- The object's [Typeclass](../../../Component/Typeclasses) indicate the sub-type of entity. A Character, Flower or
Sword are all types of Objects. A Bot is a kind of Account. The database field is called
`typeclass_path` and holds the full Python-path to the class. You can usually specify the
`typeclass` as an argument to Evennia's search functions as well as use the class directly to limit
queries.
- The `location` is only relevant for [Objects](../../../Component/Objects) but is a very common way to weed down the
number of candidates before starting to search. The reason is that most in-game commands tend to
operate on things nearby (in the same room) so the choices can be limited from the start.
- The database id or the '#dbref' is unique (and never re-used) within each database table. So while
there is one and only one Object with dbref `#42` there could also be an Account or Script with the
dbref `#42` at the same time. In almost all search methods you can replace the "key" search
criterion with `"#dbref"` to search for that id. This can occasionally be practical and may be what
you are used to from other code bases. But it is considered *bad practice* in Evennia to rely on
hard-coded #dbrefs to do your searches. It makes your code tied to the exact layout of the database.
It's also not very maintainable to have to remember abstract numbers. Passing the actual objects
around and searching by Tags and/or keys will usually get you what you need.
## Getting objects inside another
All in-game [Objects](../../../Component/Objects) have a `.contents` property that returns all objects 'inside' them
(that is, all objects which has its `.location` property set to that object. This is a simple way to
get everything in a room and is also faster since this lookup is cached and won't hit the database.
- `roomobj.contents` returns a list of all objects inside `roomobj`.
- `obj.contents` same as for a room, except this usually represents the object's inventory
- `obj.location.contents` gets everything in `obj`'s location (including `obj` itself).
- `roomobj.exits` returns all exits starting from `roomobj` (Exits are here defined as Objects with
their `destination` field set).
- `obj.location.contents_get(exclude=obj)` - this helper method returns all objects in `obj`'s
location except `obj`.
## Searching using `Object.search`
Say you have a [command](../../../Component/Commands), and you want it to do something to a target. You might be
wondering how you retrieve that target in code, and that's where Evennia's search utilities come in.
In the most common case, you'll often use the `search` method of the `Object` or `Account`
typeclasses. In a command, the `.caller` property will refer back to the object using the command
(usually a `Character`, which is a type of `Object`) while `.args` will contain Command's arguments:
```python
# e.g. in file mygame/commands/command.py
from evennia import default_cmds
class CmdPoke(default_cmds.MuxCommand):
"""
Pokes someone.
Usage: poke <target>
"""
key = "poke"
def func(self):
"""Executes poke command"""
target = self.caller.search(self.args)
if not target:
# we didn't find anyone, but search has already let the
# caller know. We'll just return, since we're done
return
# we found a target! we'll do stuff to them.
target.msg("You have been poked by %s." % self.caller)
self.caller.msg("You have poked %s." % target)
```
By default, the search method of a Character will attempt to find a unique object match for the
string sent to it (`self.args`, in this case, which is the arguments passed to the command by the
player) in the surroundings of the Character - the room or their inventory. If there is no match
found, the return value (which is assigned to `target`) will be `None`, and an appropriate failure
message will be sent to the Character. If there's not a unique match, `None` will again be returned,
and a different error message will be sent asking them to disambiguate the multi-match. By default,
the user can then pick out a specific match using with a number and dash preceding the name of the
object: `character.search("2-pink unicorn")` will try to find the second pink unicorn in the room.
The search method has many [arguments](github:evennia.objects.objects#defaultcharactersearch) that
allow you to refine the search, such as by designating the location to search in or only matching
specific typeclasses.
## Searching using `utils.search`
Sometimes you will want to find something that isn't tied to the search methods of a character or
account. In these cases, Evennia provides a [utility module with a number of search
functions](github:evennia.utils.search). For example, suppose you want a command that will find and
display all the rooms that are tagged as a 'hangout', for people to gather by. Here's a simple
Command to do this:
```python
# e.g. in file mygame/commands/command.py
from evennia import default_cmds
from evennia.utils.search import search_tag
class CmdListHangouts(default_cmds.MuxCommand):
"""Lists hangouts"""
key = "hangouts"
def func(self):
"""Executes 'hangouts' command"""
hangouts = search_tag(key="hangout",
category="location tags")
self.caller.msg("Hangouts available: {}".format(
", ".join(str(ob) for ob in hangouts)))
```
This uses the `search_tag` function to find all objects previously tagged with [Tags](../../../Component/Tags)
"hangout" and with category "location tags".
Other important search methods in `utils.search` are
- `search_object`
- `search_account`
- `search_scripts`
- `search_channel`
- `search_message`
- `search_help`
- `search_tag` - find Objects with a given Tag.
- `search_account_tag` - find Accounts with a given Tag.
- `search_script_tag` - find Scripts with a given Tag.
- `search_channel_tag` - find Channels with a given Tag.
- `search_object_attribute` - find Objects with a given Attribute.
- `search_account_attribute` - find Accounts with a given Attribute.
- `search_attribute_object` - this returns the actual Attribute, not the object it sits on.
> Note: All search functions return a Django `queryset` which is technically a list-like
representation of the database-query it's about to do. Only when you convert it to a real list, loop
over it or try to slice or access any of its contents will the datbase-lookup happen. This means you
could yourself customize the query further if you know what you are doing (see the next section).
## Queries in Django
*This is an advanced topic.*
Evennia's search methods should be sufficient for the vast majority of situations. But eventually
you might find yourself trying to figure out how to get searches for unusual circumstances: Maybe
you want to find all characters who are *not* in rooms tagged as hangouts *and* have the lycanthrope
tag *and* whose names start with a vowel, but *not* with 'Ab', and *only if* they have 3 or more
objects in their inventory ... You could in principle use one of the earlier search methods to find
all candidates and then loop over them with a lot of if statements in raw Python. But you can do
this much more efficiently by querying the database directly.
Enter [django's querysets](https://docs.djangoproject.com/en/1.11/ref/models/querysets/). A QuerySet
is the representation of a database query and can be modified as desired. Only once one tries to
retrieve the data of that query is it *evaluated* and does an actual database request. This is
useful because it means you can modify a query as much as you want (even pass it around) and only
hit the database once you are happy with it.
Evennia's search functions are themselves an even higher level wrapper around Django's queries, and
many search methods return querysets. That means that you could get the result from a search
function and modify the resulting query to your own ends to further tweak what you search for.
Evaluated querysets can either contain objects such as Character objects, or lists of values derived
from the objects. Queries usually use the 'manager' object of a class, which by convention is the
`.objects` attribute of a class. For example, a query of Accounts that contain the letter 'a' could
be:
```python
from typeclasses.accounts import Account
queryset = Account.objects.filter(username__contains='a')
```
The `filter` method of a manager takes arguments that allow you to define the query, and you can
continue to refine the query by calling additional methods until you evaluate the queryset, causing
the query to be executed and return a result. For example, if you have the result above, you could,
without causing the queryset to be evaluated yet, get rid of matches that contain the letter 'e by
doing this:
```python
queryset = result.exclude(username__contains='e')
```
> You could also have chained `.exclude` directly to the end of the previous line.
Once you try to access the result, the queryset will be evaluated automatically under the hood:
```python
accounts = list(queryset) # this fills list with matches
for account in queryset:
# do something with account
accounts = queryset[:4] # get first four matches
account = queryset[0] # get first match
# etc
```
### Limiting by typeclass
Although `Character`s, `Exit`s, `Room`s, and other children of `DefaultObject` all shares the same
underlying database table, Evennia provides a shortcut to do more specific queries only for those
typeclasses. For example, to find only `Character`s whose names start with 'A', you might do:
```python
Character.objects.filter(db_key__startswith="A")
```
If Character has a subclass `Npc` and you wanted to find only Npc's you'd instead do
```python
Npc.objects.filter(db_key__startswith="A")
```
If you wanted to search both Characters and all its subclasses (like Npc) you use the `*_family`
method which is added by Evennia:
```python
Character.objects.filter_family(db_key__startswith="A")
```
The higher up in the inheritance hierarchy you go the more objects will be included in these
searches. There is one special case, if you really want to include *everything* from a given
database table. You do that by searching on the database model itself. These are named `ObjectDB`,
`AccountDB`, `ScriptDB` etc.
```python
from evennia import AccountDB
# all Accounts in the database, regardless of typeclass
all = AccountDB.objects.all()
```
Here are the most commonly used methods to use with the `objects` managers:
- `filter` - query for a listing of objects based on search criteria. Gives empty queryset if none
were found.
- `get` - query for a single match - raises exception if none were found, or more than one was
found.
- `all` - get all instances of the particular type.
- `filter_family` - like `filter`, but search all sub classes as well.
- `get_family` - like `get`, but search all sub classes as well.
- `all_family` - like `all`, but return entities of all subclasses as well.
## Multiple conditions
If you pass more than one keyword argument to a query method, the query becomes an `AND`
relationship. For example, if we want to find characters whose names start with "A" *and* are also
werewolves (have the `lycanthrope` tag), we might do:
```python
queryset = Character.objects.filter(db_key__startswith="A", db_tags__db_key="lycanthrope")
```
To exclude lycanthropes currently in rooms tagged as hangouts, we might tack on an `.exclude` as
before:
```python
queryset = quersyet.exclude(db_location__db_tags__db_key="hangout")
```
Note the syntax of the keywords in building the queryset. For example, `db_location` is the name of
the database field sitting on (in this case) the `Character` (Object). Double underscore `__` works
like dot-notation in normal Python (it's used since dots are not allowed in keyword names). So the
instruction `db_location__db_tags__db_key="hangout"` should be read as such:
1. "On the `Character` object ... (this comes from us building this queryset using the
`Character.objects` manager)
2. ... get the value of the `db_location` field ... (this references a Room object, normally)
3. ... on that location, get the value of the `db_tags` field ... (this is a many-to-many field that
can be treated like an object for this purpose. It references all tags on the location)
4. ... through the `db_tag` manager, find all Tags having a field `db_key` set to the value
"hangout"."
This may seem a little complex at first, but this syntax will work the same for all queries. Just
remember that all *database-fields* in Evennia are prefaced with `db_`. So even though Evennia is
nice enough to alias the `db_key` field so you can normally just do `char.key` to get a character's
name, the database field is actually called `db_key` and the real name must be used for the purpose
of building a query.
> Don't confuse database fields with [Attributes](../../../Component/Attributes) you set via `obj.db.attr = 'foo'` or
`obj.attributes.add()`. Attributes are custom database entities *linked* to an object. They are not
separate fields *on* that object like `db_key` or `db_location` are. You can get attached Attributes
manually through the `db_attributes` many-to-many field in the same way as `db_tags` above.
### Complex queries
What if you want to have a query with with `OR` conditions or negated requirements (`NOT`)? Enter
Django's Complex Query object,
[Q](https://docs.djangoproject.com/en/1.11/topics/db/queries/#complex-lookups-with-q-objects). `Q()`
objects take a normal django keyword query as its arguments. The special thing is that these Q
objects can then be chained together with set operations: `|` for OR, `&` for AND, and preceded with
`~` for NOT to build a combined, complex query.
In our original Lycanthrope example we wanted our werewolves to have names that could start with any
vowel except for the specific beginning "ab".
```python
from django.db.models import Q
from typeclasses.characters import Character
query = Q()
for letter in ("aeiouy"):
query |= Q(db_key__istartswith=letter)
query &= ~Q(db_key__istartswith="ab")
query = Character.objects.filter(query)
list_of_lycanthropes = list(query)
```
In the above example, we construct our query our of several Q objects that each represent one part
of the query. We iterate over the list of vowels, and add an `OR` condition to the query using `|=`
(this is the same idea as using `+=` which may be more familiar). Each `OR` condition checks that
the name starts with one of the valid vowels. Afterwards, we add (using `&=`) an `AND` condition
that is negated with the `~` symbol. In other words we require that any match should *not* start
with the string "ab". Note that we don't actually hit the database until we convert the query to a
list at the end (we didn't need to do that either, but could just have kept the query until we
needed to do something with the matches).
### Annotations and `F` objects
What if we wanted to filter on some condition that isn't represented easily by a field on the
object? Maybe we want to find rooms only containing five or more objects?
We *could* retrieve all interesting candidates and run them through a for-loop to get and count
their `.content` properties. We'd then just return a list of only those objects with enough
contents. It would look something like this (note: don't actually do this!):
```python
# probably not a good idea to do it this way
from typeclasses.rooms import Room
queryset = Room.objects.all() # get all Rooms
rooms = [room for room in queryset if len(room.contents) >= 5]
```
Once the number of rooms in your game increases, this could become quite expensive. Additionally, in
some particular contexts, like when using the web features of Evennia, you must have the result as a
queryset in order to use it in operations, such as in Django's admin interface when creating list
filters.
Enter [F objects](https://docs.djangoproject.com/en/1.11/ref/models/expressions/#f-expressions) and
*annotations*. So-called F expressions allow you to do a query that looks at a value of each object
in the database, while annotations allow you to calculate and attach a value to a query. So, let's
do the same example as before directly in the database:
```python
from typeclasses.rooms import Room
from django.db.models import Count
room_count = Room.objects.annotate(num_objects=Count('locations_set'))
queryset = room_count.filter(num_objects__gte=5)
rooms = (Room.objects.annotate(num_objects=Count('locations_set'))
.filter(num_objects__gte=5))
rooms = list(rooms)
```
Here we first create an annotation `num_objects` of type `Count`, which is a Django class. Note that
use of `location_set` in that `Count`. The `*_set` is a back-reference automatically created by
Django. In this case it allows you to find all objects that *has the current object as location*.
Once we have those, they are counted.
Next we filter on this annotation, using the name `num_objects` as something we can filter for. We
use `num_objects__gte=5` which means that `num_objects` should be greater than 5. This is a little
harder to get one's head around but much more efficient than lopping over all objects in Python.
What if we wanted to compare two parameters against one another in a query? For example, what if
instead of having 5 or more objects, we only wanted objects that had a bigger inventory than they
had tags? Here an F-object comes in handy:
```python
from django.db.models import Count, F
from typeclasses.rooms import Room
result = (Room.objects.annotate(num_objects=Count('locations_set'),
num_tags=Count('db_tags'))
.filter(num_objects__gt=F('num_tags')))
```
F-objects allows for wrapping an annotated structure on the right-hand-side of the expression. It
will be evaluated on-the-fly as needed.
### Grouping By and Values
Suppose you used tags to mark someone belonging an organization. Now you want to make a list and
need to get the membership count of every organization all at once. That's where annotations and the
`.values_list` queryset method come in. Values/Values Lists are an alternate way of returning a
queryset - instead of objects, you get a list of dicts or tuples that hold selected properties from
the the matches. It also allows you a way to 'group up' queries for returning information. For
example, to get a display about each tag per Character and the names of the tag:
```python
result = (Character.objects.filter(db_tags__db_category="organization")
.values_list('db_tags__db_key')
.annotate(cnt=Count('id'))
.order_by('-cnt'))
```
The result queryset will be a list of tuples ordered in descending order by the number of matches,
in a format like the following:
```
[('Griatch Fanclub', 3872), ("Chainsol's Ainneve Testers", 2076), ("Blaufeuer's Whitespace Fixers",
1903),
("Volund's Bikeshed Design Crew", 1764), ("Tehom's Misanthropes", 1)]

View file

@ -0,0 +1,129 @@
# Overview of the Evennia library
[prev lesson](Python-classes-and-objects) | [next lesson](Learning-Typeclasses)
```sidebar:: API
API stands for `Application Programming Interface`, a description for how to access
the resources of a program or library.
```
A good place to start exploring Evennia is the [Evenia-API frontpage](../../../Evennia-API).
This page sums up the main components of Evennia with a short description of each. Try clicking through
to a few entries - once you get deep enough you'll see full descriptions
of each component along with their documentation. You can also click `[source]` to see the full Python source
for each thing.
You can also browse [the evennia repository on github](https://github.com/evennia/evennia). This is exactly
what you can download from us. The github repo is also searchable.
Finally, you can clone the evennia repo to your own computer and read the sources locally. This is necessary
if you want to help with Evennia's development itself. See the
[extended install instructions](../../../Setup/Extended-Installation) if you want to do this.
### Where is it?
If Evennia is installed, you can import from it simply with
import evennia
from evennia import some_module
from evennia.some_module.other_module import SomeClass
and so on.
If you installed Evennia with `pip install`, the library folder will be installed deep inside your Python
installation. If you cloned the repo there will be a folder `evennia` on your hard drive there.
If you cloned the repo or read the code on `github` you'll find this being the outermost structure:
evennia/
bin/
CHANGELOG.md
...
...
docs/
evennia/
This outer layer is for Evennia's installation and package distribution. That internal folder `evennia/evennia/` is
the _actual_ library, the thing covered by the API auto-docs and what you get when you do `import evennia`.
> The `evennia/docs/` folder contains the sources for this documentation. See
> [contributing to the docs](../../../Contributing-Docs) if you want to learn more about how this works.
This the the structure of the Evennia library:
- evennia
- [`__init__.py`](Evennia-API#shortcuts) - The "flat API" of Evennia resides here.
- [`settings_default.py`](Server-Conf#Settings-file) - Root settings of Evennia. Copy settings
from here to `mygame/server/settings.py` file.
- [`commands/`](Commands) - The command parser and handler.
- `default/` - The [default commands](../../../Component/Default-Command-Help) and cmdsets.
- [`comms/`](Communications) - Systems for communicating in-game.
- `contrib/` - Optional plugins too game-specific for core Evennia.
- `game_template/` - Copied to become the "game directory" when using `evennia --init`.
- [`help/`](Help-System) - Handles the storage and creation of help entries.
- `locale/` - Language files ([i18n](../../../Concept/Internationalization)).
- [`locks/`](Locks) - Lock system for restricting access to in-game entities.
- [`objects/`](Objects) - In-game entities (all types of items and Characters).
- [`prototypes/`](Spawner-and-Prototypes) - Object Prototype/spawning system and OLC menu
- [`accounts/`](Accounts) - Out-of-game Session-controlled entities (accounts, bots etc)
- [`scripts/`](Scripts) - Out-of-game entities equivalence to Objects, also with timer support.
- [`server/`](Portal-And-Server) - Core server code and Session handling.
- `portal/` - Portal proxy and connection protocols.
- [`typeclasses/`](Typeclasses) - Abstract classes for the typeclass storage and database system.
- [`utils/`](Coding-Utils) - Various miscellaneous useful coding resources.
- [`web/`](Web-Features) - Web resources and webserver. Partly copied into game directory on initialization.
```sidebar:: __init__.py
The `__init__.py` file is a special Python filename used to represent a Python 'package'.
When you import `evennia` on its own, you import this file. When you do `evennia.foo` Python will
first look for a property `.foo` in `__init__.py` and then for a module or folder of that name
in the same location.
```
While all the actual Evennia code is found in the various folders, the `__init__.py` represents the entire
package `evennia`. It contains "shortcuts" to code that is actually located elsewhere. Most of these shortcuts
are listed if you [scroll down a bit](../../../Evennia-API) on the Evennia-API page.
## An example of exploring the library
In the previous lesson we took a brief look at `mygame/typeclasses/objects` as an example of a Python module. Let's
open it again. Inside is the `Object` class, which inherits from `DefaultObject`.
Near the top of the module is this line:
from evennia import DefaultObject
We want to figure out just what this DefaultObject offers. Since this is imported directly from `evennia`, we
are actually importing from `evennia/__init__.py`.
[Look at Line 189](evennia/__init__.py#L189) of `evennia/__init__.py` and you'll find this line:
from .objects.objects import DefaultObject
```sidebar:: Relative and absolute imports
The first full-stop in `from .objects.objects ...` means that
we are importing from the current location. This is called a `relative import`.
By comparison, `from evennia.objects.objects` is an `absolute import`. In this particular
case, the two would give the same result.
```
> You can also look at [the right section of the API frontpage](../../../Evennia-API#typeclasses) and click through
> to the code that way.
The fact that `DefaultObject` is imported into `__init__.py` here is what makes it possible to also import
it as `from evennia import DefaultObject` even though the code for the class is not actually here.
So to find the code for `DefaultObject` we need to look in `evennia/objects/objects.py`. Here's how
to look it up in the docs:
1. Open the [API frontpage](../../../Evennia-API)
2. Locate the link to [evennia.objects](api:evennia.objects) and click on it.
3. Click through to [evennia.objects.objects](api:evennia.objects.objects).
4. You are now in the python module. Scroll down (or search in your web browser) to find the `DefaultObject` class.
5. You can now read what this does and what methods are on it. If you want to see the full source, click the
\[[source](src:evennia.objects.objects#DefaultObject)\] link.
[prev lesson](Python-classes-and-objects) | [next lesson](Learning-Typeclasses)

View file

@ -1,9 +1,11 @@
# Persistent objects and typeclasses
[prev lesson](Python-classes-and-objects) | [next lesson](Adding-Commands)
[prev lesson](Evennia-Library-Overview) | [next lesson](Creating-Things)
In the last lesson we created the dragons Fluffy, Cuddly and Smaug and made the fly and breathe fire. We
learned a bit about _classes_ in the process. But so far our dragons are short-lived - whenever we `restart`
Now that we have learned a little about how to find things in the Evennia library, let's use it.
In the [Python classes and objects](Python-classes-and-objects) lesson we created the dragons Fluffy, Cuddly
and Smaug and made them fly and breathe fire. So far our dragons are short-lived - whenever we `restart`
the server or `quit()` out of python mode they are gone.
This is what you should have in `mygame/typeclasses/monsters.py` so far:
@ -620,4 +622,4 @@ Typeclasses are a fundamental part of Evennia and we will see a lot of more uses
this tutorial. But that's enough of them for now. It's time to take some action. Let's learn about _Commands_.
[prev lesson](Python-classes-and-objects) | [next lesson](Adding-Commands)
[prev lesson](Evennia-Library-Overview) | [next lesson](Creating-Things)

View file

@ -1,6 +1,6 @@
# More about Commands
[prev lesson](Adding-Commands) | [next lesson](Evennia-API-Overview)
[prev lesson](Adding-Commands) | [next lesson](Creating-Things)
In this lesson we learn some basics about parsing the input of Commands. We will
also learn how to add, modify and extend Evennia's default commands.
@ -497,8 +497,4 @@ In this lesson we got into some more advanced string formatting - many of those
the future! We also made a functional sword. Finally we got into how to add to, extend and replace a default
command on ourselves.
In the last few lessons we have made use of resources from Evennia. Now that we have had some experience of how
classes and inheritance work, we can start exploring this in earnest.
[prev lesson](Adding-Commands) | [next lesson](Evennia-API-Overview)
[prev lesson](Adding-Commands) | [next lesson](Creating-Things)

View file

@ -1,6 +1,6 @@
# Python Classes and objects
[prev lesson](Gamedir-Overview) | [next lesson](Learning-Typeclasses)
[prev lesson](Gamedir-Overview) | [next lesson](Evennia-Library-Overview)
We have now learned how to run some simple Python code from inside (and outside) your game server.
We have also taken a look at what our game dir looks and what is where. Now we'll start to use it.
@ -409,8 +409,7 @@ We have created our first dragons from classes. We have learned a little about h
into an _object_. We have seen some examples of _inheritance_ and we tested to _override_ a method in the parent
with one in the child class. We also used `super()` to good effect.
But so far our dragons are gone as soon as we `restart` the server or `quit()` the Python interpreter. In the
next lesson we'll get up close and personal with Smaug.
We have used pretty much raw Python so far. In the coming lessons we'll start to look at the extra bits that Evennia
provides. But first we need to learn just where to find everything.
[prev lesson](Gamedir-Overview) | [next lesson](Learning-Typeclasses)
[prev lesson](Gamedir-Overview) | [next lesson](Evennia-Library-Overview)

View file

@ -0,0 +1,576 @@
# Searching for things
[prev lesson](Creating-Things) | [next lesson]()
We have gone through how to create the various entities in Evennia. But creating something is of little use
if we cannot find and use it afterwards.
## Main search functions
The base tools are the `evennia.search_*` functions, such as `evennia.search_object`.
rose = evennia.search_object(key="rose")
acct = evennia.search_account(key="MyAccountName", email="foo@bar.com")
```sidebar:: Querysets
What is returned from the main search functions is actually a `queryset`. They can be
treated like lists except that they can't modified in-place. We'll discuss querysets at
the end of this lesson.
```
Strings are always case-insensitive, so searching for `"rose"`, `"Rose"` or `"rOsE"` give the same results.
It's important to remember that what is returned from these search methods is a _listing_ of 0, one or more
elements - all the matches to your search. To get the first match:
rose = rose[0]
Often you really want all matches to the search parameters you specify. In other situations, having zero or
more than one match is a sign of a problem and you need to handle this case yourself.
the_one_ring = evennia.search_object(key="The one Ring")
if not the_one_ring:
# handle not finding the ring at all
elif len(the_one_ring) > 1:
# handle finding more than one ring
else:
# ok - exactly one ring found
the_one_ring = the_one_ring[0]
There are equivalent search functions for all the main resources. You can find a listing of them
[in the Search functions section](../../../Evennia-API) of the API frontpage.
## Searching using Object.search
On the `DefaultObject` is a `.search` method which we have already tried out when we made Commands. For
this to be used you must already have an object available:
rose = obj.search("rose")
The `.search` method wraps `evennia.search_object` and handles its output in various ways.
- By default it will always search for objects among those in `obj.location.contents` and `obj.contents` (that is,
things in obj's inventory or in the same room).
- It will always return exactly one match. If it found zero or more than one match, the return is `None`.
- On a no-match or multimatch, `.search` will automatically send an error message to `obj`.
So this method handles error messaging for you. A very common way to use it is in commands:
```python
from evennia import Command
class MyCommand(Command):
key = "findfoo"
def func(self):
foo = self.caller.search("foo")
if not foo:
return
```
Remember, `self.caller` is the one calling the command. This is usually a Character, which
inherits from `DefaultObject`! This (rather stupid) Command searches for an object named "foo" in
the same location. If it can't find it, `foo` will be `None`. The error has already been reported
to `self.caller` so we just abort with `return`.
You can use `.search` to find anything, not just stuff in the same room:
volcano = self.caller.search("Volcano", global=True)
If you only want to search for a specific list of things, you can do so too:
stone = self.caller.search("MyStone", candidates=[obj1, obj2, obj3, obj4])
This will only return a match if MyStone is one of the four provided candidate objects. This is quite powerful,
here's how you'd find something only in your inventory:
potion = self.caller.search("Healing potion", candidates=self.caller.contents)
You can also turn off the automatic error handling:
swords = self.search("Sword", quiet=True)
With `quiet=True` the user will not be notified on zero or multi-match errors. Instead you are expected to handle this
yourself and what you get back is now a list of zero, one or more matches!
## What can be searched for
These are the main database entities one can search for:
- [Objects](../../../Component/Objects)
- [Accounts](../../../Component/Accounts)
- [Scripts](../../../Component/Scripts),
- [Channels](../../../Component/Communications#channels),
- [Messages](Communication#Msg)
- [Help Entries](../../../Component/Help-System).
Most of the time you'll likely spend your time searching for Objects and the occasional Accounts.
So to find an entity, what can be searched for?
### Search by key
The `key` is the name of the entity. Searching for this is always case-insensitive.
### Search by aliases
Objects and Accounts can have any number of aliases. When searching for `key` these will searched too,
you can't easily search only for aliases.
rose.aliases.add("flower")
If the above `rose` has a `key` `"Rose"`, it can now also be found by searching for `flower`. In-game
you can assign new aliases to things with the `alias` command.
### Search by location
Only Objects (things inheriting from `evennia.DefaultObject`) has a location. This is usually a room.
The `Object.search` method will automatically limit it search by location, but it also works for the
general search function. If we assume `room` is a particular Room instance,
chest = evennia.search_object("Treasure chest", location=room)
### Search by Tags
Think of a [Tag](../../../Component/Tags) as the label the airport puts on your luggage when flying.
Everyone going on the same plane gets a tag grouping them together so the airport can know what should
go to which plane. Entities in Evennia can be grouped in the same way. Any number of tags can be attached
to each object.
rose.tags.add("flowers")
daffodil.tags.add("flowers")
tulip.tags.add("flowers")
You can now find all flowers using the `search_tag` function:
all_flowers = evennia.search_tag("flowers")
Tags can also have categories. By default this category is `None` which is also considered a category.
silmarillion.tags.add("fantasy", category="books")
ice_and_fire.tags.add("fantasy", category="books")
mona_lisa_overdrive.tags.add("cyberpunk", category="books")
Note that if you specify the tag you _must_ also include its category, otherwise that category
will be `None` and find no matches.
all_fantasy_books = evennia.search_tag("fantasy") # no matches!
all_fantasy_books = evennia.search_tag("fantasy", category="books")
Only the second line above returns the two fantasy books. If we specify a category however,
we can get all tagged entities within that category:
all_books = evennia.search_tag(category="books")
This gets all three books.
### Search by Attribute
We can also search by the [Attributes](../../../Component/Attributes) associated with entities.
For example, let's give our rose thorns:
rose.db.has_thorns = True
wines.db.has_thorns = True
daffodil.db.has_thorns = False
Now we can find things attribute and the value we want it to have:
is_ouch = evennia.search_object_attribute("has_thorns", True)
This returns the rose and the wines.
> Searching by Attribute can be very practical. But if you plan to do a search very often, searching
> by-tag is generally faster.
### Search by Typeclass
Sometimes it's useful to find all objects of a specific Typeclass. All of Evennia's search tools support this.
all_roses = evennia.search_object(typeclass="typeclasses.flowers.Rose")
If you have the `Rose` class already imported you can also pass it directly:
all_roses = evennia.search_object(typeclass=Rose)
You can also search using the typeclass itself:
all_roses = Rose.objects.all()
This last way of searching is a simple form of a Django _query_. This is a way to express SQL queries using
Python. We'll cover this some more as an [Extra-credits](#Extra-Credits) section at the end of this lesson.
### Search by dbref
The database id or `#dbref` is unique and never-reused within each database table. In search methods you can
replace the search for `key` with the dbref to search for. This must be written as a string `#dbref`:
the_answer = self.caller.search("#42")
eightball = evennia.search_object("#8")
Since `#dbref` is always unique, this search is always global.
```warning:: Relying on #dbrefs
You may be used to using #dbrefs a lot from other codebases. It is however considered
`bad practice` in Evennia to rely on hard-coded #dbrefs. It makes your code hard to maintain
and tied to the exact layout of the database. In 99% of cases you should pass the actual objects
around and search by key/tags/attribute instead.
```
## Finding objects relative each other
Let's consider a `chest` with a `coin` inside it. The chests stand in a room `dungeon`. In the dungeon is also
a `door`. This is an exit leading outside.
- `coin.location` is `chest`.
- `chest.location` is `dungeon`.
- `door.location` is `dungeon`.
- `room.location` is `None` since it's not inside something else.
One can use this to find what is inside what. For example, `coin.location.location` is the `room`.
We can also find what is inside each object. This is a list of things.
- `room.contents` is `[chest, door]`
- `chest.contents` is `[coin]`
- `coin.contents` is `[]`, the empty list since there's nothing 'inside' the coin.
- `door.contents` is `[]` too.
A convenient helper is `.contents_get` - this allows to restrict what is returned:
- `room.contents_get(exclude=chest)` - this returns everything in the room except the chest (maybe it's hidden?)
There is a special property for finding exits:
- `room.exits` is `[door]`
- `coin.exits` is `[]` (same for all the other objects)
There is a property `.destination` which is only used by exits:
- `door.destination` is `outside` (or wherever the door leads)
- `room.destination` is `None` (same for all the other non-exit objects)
## Database queries
The search functions and methods above are enough for most cases. But sometimes you need to be
more specific:
- You want to find all `Characters` ...
- ... who are in Rooms tagged as `moonlit` ...
- ... _and_ who has the Attribute `lycantrophy` with a level higher than 2 ...
- ... because they'll immediately become werewolves!
In principle you could achieve this with the existing search functions combined with a lot of loops
and if statements. But for something non-standard like this querying the database directly will be
more efficient.
A [django queryset](https://docs.djangoproject.com/en/3.0/ref/models/querysets/) represents
a database query. One can add querysets together to build ever-more complicated queries. Only when
you are trying to use the results of the queryset will it actually call the database.
The normal way to build a queryset is to define what class of entity you want to search by getting its
`.objects` resource, and then call various methods on that. We've seen this one before:
all_weapons = Weapon.objects.all()
This is now a queryset representing all instances of `Weapon`. If `Weapon` had a subclass `Cannon` and we
only wanted the cannons, we would do
all_cannons = Cannon.objects.all()
Note that `Weapon` and `Cannon` are different typeclasses. You won't find any `Cannon` instances in
the `all_weapon` result above, confusing as that may sound. To get instances of a Typeclass _and_ the
instances of all its children classes you need to use `_family`:
```sidebar:: _family
The all_family, filter_family etc is an Evennia-specific
thing. It's not part of regular Django.
```
really_all_weapons = Weapon.objects.all_family()
This result now contains both `Weapon` and `Cannon` instances.
To actually limit your search by other criteria than the Typeclass you need to use `.filter`
(or `.filter_family`) instead:
roses = Flower.objects.filter(db_key="rose")
This is a queryset representing all objects having a `db_key` equal to `"rose"`.
Since this is a queryset you can keep adding to it:
local_roses = roses.filter(db_location=myroom)
We could also have written this in one statement:
local_roses = Flower.objects.filter(db_key="rose", db_location=myroom)
We can also `.exclude` something from results
local_non_red_roses = local_roses.exclude(db_key="red_rose")
Only until we actually try to examine the result will the database be called. Here it's called when we
try to loop over the queryset:
for rose in local_non_red_roses:
print(rose)
From now on, the queryset is _evaluated_ and we can't keep adding more queries to it - we'd need to
create a new queryset if we wanted to find some other result.
Note how we use `db_key` and `db_location`. This is the actual names of these database fields. By convention
Evennia uses `db_` in front of every database field, but when you access it in Python you can skip the `db_`. This
is why you can use `obj.key` and `obj.location` in normal code. Here we are calling the database directly though
and need to use the 'real' names.
Here are the most commonly used methods to use with the `objects` managers:
- `filter` - query for a listing of objects based on search criteria. Gives empty queryset if none
were found.
- `get` - query for a single match - raises exception if none were found, or more than one was
found.
- `all` - get all instances of the particular type.
- `filter_family` - like `filter`, but search all sub classes as well.
- `get_family` - like `get`, but search all sub classes as well.
- `all_family` - like `all`, but return entities of all subclasses as well.
> All of Evennia search functions use querysets under the hood. The `evennia.search_*` functions actually
> return querysets, which means you could in principle keep adding queries to their results as well.
### Queryset field lookups
Above we found roses with exactly the `db_key` `"rose"`. This is an _exact_ match that is _case sensitive_,
so it would not find `"Rose"`.
# this is case-sensitive and the same as =
roses = Flower.objects.filter(db_key__exact="rose"
# the i means it's case-insensitive
roses = Flower.objects.filter(db_key__iequals="rose")
The Django field query language uses `__` in the same way as Python uses `.` to access resources. This
is because `.` is not allowed in a function keyword.
roses = Flower.objects.filter(db_key__icontains="rose")
This will find all flowers whose name contains the string `"rose"`, like `"roses"`, `"wild rose"` etc. The
`i` in the beginning makes the search case-insensitive. Other useful variations to use
are `__istartswith` and `__iendswith`. You can also use `__gt`, `__ge` for "greater-than"/"greater-or-equal-than"
comparisons (same for `__lt` and `__le`). There is also `__in`:
swords = Weapons.objects.filter(db_key__in=("rapier", "two-hander", "shortsword"))
For more field lookups, see the
[django docs](https://docs.djangoproject.com/en/3.0/ref/models/querysets/#field-lookups) on the subject.
### Get that werewolf ...
Let's see if we can make a query for the werewolves in the moonlight we mentioned at the beginning
of this section.
Firstly, we make ourselves and our current location match the criteria, so we can test:
> py here.tags.add("moonlit")
> py me.db.lycantrophy = 3
This is an example of a more complex query. We'll consider it an example of what is
possible.
```sidebar:: Line breaks
Note the way of writing this code. It would have been very hard to read if we just wrote it in
one long line. But since we wrapped it in `(...)` we can spread it out over multiple lines
without worrying about line breaks!
```
```python
from typeclasses.characters import Character
will_transform = (
Character.objects
.filter(
db_location__db_tags__db_key__iexact="moonlit",
db_attributes__db_key="lycantrophy",
db_attributes__db_value__gt=2)
)
```
- **Line 3** - We want to find `Character`s, so we access `.objects` on the `Character` typeclass.
- **Line 4** - We start to filter ...
- **Line 5**
- ... by accessing the `db_location` field (usually this is a Room)
- ... and on that location, we get the value of `db_tags` (this is a _many-to-many_ database field
that we can treat like an object for this purpose; it references all Tags on the location)
- ... and from those `Tags`, we looking for `Tags` whose `db_key` is "monlit" (non-case sensitive).
- **Line 6** - ... We also want only Characters with `Attributes` whose `db_key` is exactly `"lycantrophy"`
- **Line 7** - ... at the same time as the `Attribute`'s `db_value` is greater-than 2.
Running this query makes our newly lycantrrophic Character appear in `will_transform`. Success!
> Don't confuse database fields with [Attributes](../../../Component/Attributes) you set via `obj.db.attr = 'foo'` or
`obj.attributes.add()`. Attributes are custom database entities *linked* to an object. They are not
separate fields *on* that object like `db_key` or `db_location` are.
### Complex queries
All examples so far used `AND` relations. The arguments to `.filter` are added together with `AND`
("we want tag room to be "monlit" _and_ lycantrhopy be > 2").
For queries using `OR` and `NOT` we need Django's
[Q object](https://docs.djangoproject.com/en/1.11/topics/db/queries/#complex-lookups-with-q-objects). It is
import from Django directly:
from django.db.models import Q
`Q()` objects take the same arguments like `.filter`:
Q(db_key="foo")
The special thing is that these `Q` objects can then be chained together with special symbols:
`|` for `OR`, `&` for `AND`. A tilde `~` in front negates the expression inside the `Q` and thus works like `NOT`.
Let us expand our original werewolf query. Not only do we want to find all Characters in a moonlit room
with a certain level of `lycanthrophy`. Now we also want the full moon to immediately transform people who were
recently bitten, even if their `lycantrophy` level is not yet high enough (more dramatic this way!). Let's say there is
a Tag "recently_bitten" that controls this.
This is how we'd change our query:
```python
from django.db.models import Q
will_transform = (
Character.objects
.filter(
Q(db_location__db_tags__db_key__iexact="moonlit")
& (
Q(db_attributes__db_key="lycantrophy",
db_attributes__db_value__gt=2)
| Q(db__tags__db__key__iexact="recently_bitten")
)
)
)
```
We now grouped the filter
In our original Lycanthrope example we wanted our werewolves to have names that could start with any
vowel except for the specific beginning "ab".
```python
from django.db.models import Q
from typeclasses.characters import Character
query = Q()
for letter in ("aeiouy"):
query |= Q(db_key__istartswith=letter)
query &= ~Q(db_key__istartswith="ab")
query = Character.objects.filter(query)
list_of_lycanthropes = list(query)
```
In the above example, we construct our query our of several Q objects that each represent one part
of the query. We iterate over the list of vowels, and add an `OR` condition to the query using `|=`
(this is the same idea as using `+=` which may be more familiar). Each `OR` condition checks that
the name starts with one of the valid vowels. Afterwards, we add (using `&=`) an `AND` condition
that is negated with the `~` symbol. In other words we require that any match should *not* start
with the string "ab". Note that we don't actually hit the database until we convert the query to a
list at the end (we didn't need to do that either, but could just have kept the query until we
needed to do something with the matches).
### Annotations and `F` objects
What if we wanted to filter on some condition that isn't represented easily by a field on the
object? Maybe we want to find rooms only containing five or more objects?
We *could* retrieve all interesting candidates and run them through a for-loop to get and count
their `.content` properties. We'd then just return a list of only those objects with enough
contents. It would look something like this (note: don't actually do this!):
```python
# probably not a good idea to do it this way
from typeclasses.rooms import Room
queryset = Room.objects.all() # get all Rooms
rooms = [room for room in queryset if len(room.contents) >= 5]
```
Once the number of rooms in your game increases, this could become quite expensive. Additionally, in
some particular contexts, like when using the web features of Evennia, you must have the result as a
queryset in order to use it in operations, such as in Django's admin interface when creating list
filters.
Enter [F objects](https://docs.djangoproject.com/en/1.11/ref/models/expressions/#f-expressions) and
*annotations*. So-called F expressions allow you to do a query that looks at a value of each object
in the database, while annotations allow you to calculate and attach a value to a query. So, let's
do the same example as before directly in the database:
```python
from typeclasses.rooms import Room
from django.db.models import Count
room_count = Room.objects.annotate(num_objects=Count('locations_set'))
queryset = room_count.filter(num_objects__gte=5)
rooms = (Room.objects.annotate(num_objects=Count('locations_set'))
.filter(num_objects__gte=5))
rooms = list(rooms)
```
Here we first create an annotation `num_objects` of type `Count`, which is a Django class. Note that
use of `location_set` in that `Count`. The `*_set` is a back-reference automatically created by
Django. In this case it allows you to find all objects that *has the current object as location*.
Once we have those, they are counted.
Next we filter on this annotation, using the name `num_objects` as something we can filter for. We
use `num_objects__gte=5` which means that `num_objects` should be greater than 5. This is a little
harder to get one's head around but much more efficient than lopping over all objects in Python.
What if we wanted to compare two parameters against one another in a query? For example, what if
instead of having 5 or more objects, we only wanted objects that had a bigger inventory than they
had tags? Here an F-object comes in handy:
```python
from django.db.models import Count, F
from typeclasses.rooms import Room
result = (Room.objects.annotate(num_objects=Count('locations_set'),
num_tags=Count('db_tags'))
.filter(num_objects__gt=F('num_tags')))
```
F-objects allows for wrapping an annotated structure on the right-hand-side of the expression. It
will be evaluated on-the-fly as needed.
### Grouping By and Values
Suppose you used tags to mark someone belonging an organization. Now you want to make a list and
need to get the membership count of every organization all at once. That's where annotations and the
`.values_list` queryset method come in. Values/Values Lists are an alternate way of returning a
queryset - instead of objects, you get a list of dicts or tuples that hold selected properties from
the the matches. It also allows you a way to 'group up' queries for returning information. For
example, to get a display about each tag per Character and the names of the tag:
```python
result = (Character.objects.filter(db_tags__db_category="organization")
.values_list('db_tags__db_key')
.annotate(cnt=Count('id'))
.order_by('-cnt'))
```
The result queryset will be a list of tuples ordered in descending order by the number of matches,
in a format like the following:
```
[('Griatch Fanclub', 3872), ("Chainsol's Ainneve Testers", 2076), ("Blaufeuer's Whitespace Fixers",
1903),
("Volund's Bikeshed Design Crew", 1764), ("Tehom's Misanthropes", 1)]
[prev lesson](Creating-Things) | [next lesson]()

View file

@ -132,7 +132,7 @@ Talker-type game you *will* have to bite the bullet and code your game (or find
do it for you).
Even if you won't code anything yourself, as a designer you need to at least understand the basic
paradigms of Evennia, such as [Objects](../../Component/Objects), [Commands](../../Component/Commands) and [Scripts](../../Component/Scripts) and
paradigms of Evennia, such as [Objects](../../../Component/Objects), [Commands](../../../Component/Commands) and [Scripts](../../../Component/Scripts) and
how they hang together. We recommend you go through the [Tutorial World](Tutorial-World-
Introduction) in detail (as well as glancing at its code) to get at least a feel for what is
involved behind the scenes. You could also look through the tutorial for [building a game from
@ -144,7 +144,7 @@ The earlier you revise problems, the easier they will be to fix.
A good idea is to host your code online (publicly or privately) using version control. Not only will
this make it easy for multiple coders to collaborate (and have a bug-tracker etc), it also means
your work is backed up at all times. The [Version Control](../../Coding/Version-Control) tutorial has
your work is backed up at all times. The [Version Control](../../../Coding/Version-Control) tutorial has
instructions for setting up a sane developer environment with proper version control.
### "Tech Demo" Building
@ -199,7 +199,7 @@ flag and let people try it! Call upon your alpha-players to try everything - the
to break your game in ways that you never could have imagined. In Alpha you might be best off to
focus on inviting friends and maybe other MUD developers, people who you can pester to give proper
feedback and bug reports (there *will* be bugs, there is no way around it). Follow the quick
instructions for [Online Setup](../../Setup/Online-Setup) to make your game visible online. If you hadn't
instructions for [Online Setup](../../../Setup/Online-Setup) to make your game visible online. If you hadn't
already, make sure to put up your game on the [Evennia game index](http://games.evennia.com/) so
people know it's in the works (actually, even pre-alpha games are allowed in the index so don't be
shy)!

View file

@ -27,11 +27,12 @@ own first little game in Evennia. Let's get started!
1. [Python basics](Part1/Python-basic-introduction)
1. [Game dir overview](Part1/Gamedir-Overview)
1. [Python classes and objects](Part1/Python-classes-and-objects)
1. [Persistent objects](Part1/Learning-Typeclasses)
1. [Accessing the Evennia library](Part1/Evennia-Library-Overview)
1. [Typeclasses - Persistent objects](Part1/Learning-Typeclasses)
1. [Making our first own commands](Part1/Adding-Commands)
1. [Parsing and replacing default Commands](Part1/More-on-Commands)
1. [Searching and creating things](Tutorial-Searching-For-Objects)
1. [A walkthrough of the API](Walkthrough-of-API)
1. [Creating things](Part1/Creating-Things)
1. [Searching for things](Part1/Searching-Things)
In this first part we'll focus on what we get out of the box in Evennia - we'll get used to the tools,
where things are and how we find things we are looking for. We will also dive into some of things you'll