Add lock-handling to FileHelp, add warnings to sethelp
This commit is contained in:
parent
0fb29073b2
commit
b050656319
7 changed files with 377 additions and 187 deletions
|
|
@ -51,6 +51,11 @@ Up requirements to Django 3.2+
|
||||||
to the user and respond to their input. This complements the existing `get_input` helper.
|
to the user and respond to their input. This complements the existing `get_input` helper.
|
||||||
- Allow sending messages with `page/tell` without a `=` if target name contains no spaces.
|
- Allow sending messages with `page/tell` without a `=` if target name contains no spaces.
|
||||||
- New FileHelpStorage system allows adding help entries via external files.
|
- New FileHelpStorage system allows adding help entries via external files.
|
||||||
|
- `sethelp` command now warns if shadowing other help-types when creating a new
|
||||||
|
entry.
|
||||||
|
- Help command now uses `view` lock to determine if cmd/entry shows in index and
|
||||||
|
`read` lock to determine if it can be read. It used to be `view` in the role
|
||||||
|
of the latter. Migration swaps these around.
|
||||||
- In modules given by `settings.PROTOTYPE_MODULES`, spawner will now first look for a global
|
- In modules given by `settings.PROTOTYPE_MODULES`, spawner will now first look for a global
|
||||||
list `PROTOTYPE_LIST` of dicts before loading all dicts in the module as prototypes.
|
list `PROTOTYPE_LIST` of dicts before loading all dicts in the module as prototypes.
|
||||||
- New Channel-System using the `channel` command and nicks. Removed the `ChannelHandler` and the
|
- New Channel-System using the `channel` command and nicks. Removed the `ChannelHandler` and the
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ match it will provide suggestsions, first from alternative topics and then by
|
||||||
finding mentions of the search term in help entries.
|
finding mentions of the search term in help entries.
|
||||||
|
|
||||||
|
|
||||||
|
help theatre
|
||||||
|
|
||||||
```
|
```
|
||||||
------------------------------------------------------------------------------
|
------------------------------------------------------------------------------
|
||||||
Help for The theatre (aliases: the hub, curtains)
|
Help for The theatre (aliases: the hub, curtains)
|
||||||
|
|
@ -19,6 +21,9 @@ Subtopics:
|
||||||
theatre/dramatis personae
|
theatre/dramatis personae
|
||||||
------------------------------------------------------------------------------
|
------------------------------------------------------------------------------
|
||||||
```
|
```
|
||||||
|
|
||||||
|
help evennia
|
||||||
|
|
||||||
```
|
```
|
||||||
------------------------------------------------------------------------------
|
------------------------------------------------------------------------------
|
||||||
No help found
|
No help found
|
||||||
|
|
@ -51,43 +56,48 @@ Use the `/edit` switch to open the EvEditor for more convenient in-game writing
|
||||||
(but note that devs can also create help entries outside the game using their
|
(but note that devs can also create help entries outside the game using their
|
||||||
regular code editor, see below).
|
regular code editor, see below).
|
||||||
|
|
||||||
|
> You can also create help entries as Python modules, outside of the game. These
|
||||||
|
> can not be modified from in-game.
|
||||||
|
|
||||||
## Sources of help entries
|
## Sources of help entries
|
||||||
|
|
||||||
Evennia collects help entries from three sources:
|
Evennia collects help entries from three sources:
|
||||||
|
|
||||||
- _Auto-generated command help_ - this is literally the doc-strings of the [Command classes](./Commands).
|
- _Auto-generated command help_ - this is literally the doc-strings of
|
||||||
The idea is that the command docs are easier to maintain and keep up-to-date if
|
the [Command classes](./Commands). The idea is that the command docs are
|
||||||
the developer can change them at the same time as they do the code.
|
easier to maintain and keep up-to-date if the developer can change them at the
|
||||||
- _Database-stored help entries_ - These are created in-game (using the default `sethelp` command
|
same time as they do the code.
|
||||||
as exemplified in the previous section).
|
- _Database-stored help entries_ - These are created in-game (using the
|
||||||
|
default `sethelp` command as exemplified in the previous section).
|
||||||
- _File-stored help entries_ - These are created outside the game, as dicts in
|
- _File-stored help entries_ - These are created outside the game, as dicts in
|
||||||
normal Python modules. They allows developers to write and maintain their help files using
|
normal Python modules. They allows developers to write and maintain their help
|
||||||
a proper text editor.
|
files using a proper text editor.
|
||||||
|
|
||||||
### The Help Entry
|
### The Help Entry
|
||||||
|
|
||||||
All help entries (no matter the source) have the following properties:
|
All help entries (no matter the source) have the following properties:
|
||||||
|
|
||||||
- `key` - This is the main topic-name. For Commands, this is literally the command's `key`.
|
- `key` - This is the main topic-name. For Commands, this is literally the
|
||||||
- `aliases` - Alternate names for the help entry. This can be useful if the main name is hard to remember.
|
command's `key`.
|
||||||
- `help_category` - The general grouping of the entry. This is optional. If not given it will use the
|
- `aliases` - Alternate names for the help entry. This can be useful if the main
|
||||||
default category given by `settings.COMMAND_DEFAULT_HELP_CATEGORY` for Commands and `settings.DEFAULT_HELP_CATEGORY`
|
name is hard to remember.
|
||||||
for file+db help entries.
|
- `help_category` - The general grouping of the entry. This is optional. If not
|
||||||
- `locks` - This defines who may read this entry. The locktype checked by the `help` command is `view`. In the
|
given it will use the default category given by
|
||||||
case of Commands, it's more common that the `cmd` lock fails - in that case the command is not loaded
|
`settings.COMMAND_DEFAULT_HELP_CATEGORY` for Commands and
|
||||||
into the help parser at all.
|
`settings.DEFAULT_HELP_CATEGORY` for file+db help entries.
|
||||||
- `tags` - This is not used by default, but could be used to further organize help entries.
|
- `locks` - Lock string (for commands) or LockHandler (all help entries).
|
||||||
|
This defines who may read this entry. See the next section.
|
||||||
|
- `tags` - This is not used by default, but could be used to further organize
|
||||||
|
help entries.
|
||||||
- `text` - The actual help entry text. This will be dedented and stripped of
|
- `text` - The actual help entry text. This will be dedented and stripped of
|
||||||
extra space at beginning and end.
|
extra space at beginning and end.
|
||||||
|
|
||||||
|
|
||||||
A `text` that scrolls off the screen will automatically be paginated by
|
A `text` that scrolls off the screen will automatically be paginated by
|
||||||
the [EvMore](./EvMore) pager (you can control this with
|
the [EvMore](./EvMore) pager (you can control this with
|
||||||
`settings.HELP_MORE_ENABLED=False`). If you use EvMore and want to control
|
`settings.HELP_MORE_ENABLED=False`). If you use EvMore and want to control
|
||||||
exactly where the pager should break the page, mark the break with the control
|
exactly where the pager should break the page, mark the break with the control
|
||||||
character `\f`.
|
character `\f`.
|
||||||
|
|
||||||
|
|
||||||
#### Subtopics
|
#### Subtopics
|
||||||
|
|
||||||
```versionadded:: 1.0
|
```versionadded:: 1.0
|
||||||
|
|
@ -108,7 +118,6 @@ it all out exactly.
|
||||||
|
|
||||||
Below is an example of a `text` with sub topics.
|
Below is an example of a `text` with sub topics.
|
||||||
|
|
||||||
|
|
||||||
```
|
```
|
||||||
The theatre is the heart of the city, here you can find ...
|
The theatre is the heart of the city, here you can find ...
|
||||||
(This is the main help text, what you get with `help theatre`)
|
(This is the main help text, what you get with `help theatre`)
|
||||||
|
|
@ -152,7 +161,6 @@ He always keeps an eye on the door and ...
|
||||||
(`help theatre/drama/gate`)
|
(`help theatre/drama/gate`)
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Command Auto-help system
|
### Command Auto-help system
|
||||||
|
|
||||||
The auto-help system uses the `__doc__` strings of your command classes and
|
The auto-help system uses the `__doc__` strings of your command classes and
|
||||||
|
|
@ -179,30 +187,38 @@ Example (from a module with command definitions):
|
||||||
"""
|
"""
|
||||||
# [...]
|
# [...]
|
||||||
|
|
||||||
help_category = "General" # default
|
locks = "cmd:all();read:all()" # default
|
||||||
auto_help = True # default
|
help_category = "General" # default
|
||||||
|
auto_help = True # default
|
||||||
|
|
||||||
# [...]
|
# [...]
|
||||||
```
|
```
|
||||||
|
|
||||||
The text at the very top of the command class definition is the class' `__doc__`-string and will be
|
The text at the very top of the command class definition is the class'
|
||||||
shown to users looking for help. Try to use a consistent format - all default commands are using the
|
`__doc__`-string and will be shown to users looking for help. Try to use a
|
||||||
structure shown above.
|
consistent format - all default commands are using the structure shown above.
|
||||||
|
|
||||||
You should also supply the `help_category` class property if you can; this helps to group help
|
You can limit access to the help entry by the `view` and/or `read` locks on the
|
||||||
entries together for people to more easily find them. See the `help` command in-game to see the
|
Command. See [the section below](#Locking-help-entries) for details.
|
||||||
default categories. If you don't specify the category, `settings.COMMAND_DEFAULT_HELP_CATEGORY`
|
|
||||||
(default is "General") is used.
|
|
||||||
|
|
||||||
If you don't want your command to be picked up by the auto-help system at all (like if you want to
|
You should also supply the `help_category` class property if you can; this helps
|
||||||
write its docs manually using the info in the next section or you use a [cmdset](./Command-Sets) that
|
to group help entries together for people to more easily find them. See the
|
||||||
has its own help functionality) you can explicitly set `auto_help` class property to `False` in your
|
`help` command in-game to see the default categories. If you don't specify the
|
||||||
command definition.
|
category, `settings.COMMAND_DEFAULT_HELP_CATEGORY` (default is "General") is
|
||||||
|
used.
|
||||||
|
|
||||||
Alternatively, you can keep the advantages of *auto-help* in commands, but control the display of
|
If you don't want your command to be picked up by the auto-help system at all
|
||||||
command helps. You can do so by overriding the command's `get_help(caller, cmdset)` method. By default, this
|
(like if you want to write its docs manually using the info in the next section
|
||||||
method will return the class docstring. You could modify it to add custom behavior: the text
|
or you use a [cmdset](./Command-Sets) that has its own help functionality) you
|
||||||
returned by this method will be displayed to the character asking for help in this command.
|
can explicitly set `auto_help` class property to `False` in your command
|
||||||
|
definition.
|
||||||
|
|
||||||
|
Alternatively, you can keep the advantages of *auto-help* in commands, but
|
||||||
|
control the display of command helps. You can do so by overriding the command's
|
||||||
|
`get_help(caller, cmdset)` method. By default, this method will return the
|
||||||
|
class docstring. You could modify it to add custom behavior: the text returned
|
||||||
|
by this method will be displayed to the character asking for help in this
|
||||||
|
command.
|
||||||
|
|
||||||
### Database-help entries
|
### Database-help entries
|
||||||
|
|
||||||
|
|
@ -223,9 +239,8 @@ be modified to any great degree. It holds the properties listed earlier. The
|
||||||
text is stored in a field `entrytext`. It does not provide a `get_help` method
|
text is stored in a field `entrytext`. It does not provide a `get_help` method
|
||||||
like commands, stores and returns the `entrytext` directly.
|
like commands, stores and returns the `entrytext` directly.
|
||||||
|
|
||||||
You can search for `HelpEntry` objects using `evennia.search_help` but note
|
You can search for (db-)-`HelpEntry` objects using `evennia.search_help` but note that
|
||||||
that this will not return the two other types of help entries.
|
this will not return the two other types of help entries.
|
||||||
|
|
||||||
|
|
||||||
### File-help entries
|
### File-help entries
|
||||||
|
|
||||||
|
|
@ -237,16 +252,17 @@ help entries are defined in normal Python modules (`.py` file ending) containing
|
||||||
a `dict` to represent each entry. They require a server `reload` before any changes
|
a `dict` to represent each entry. They require a server `reload` before any changes
|
||||||
apply.
|
apply.
|
||||||
|
|
||||||
- Evennia will look through all modules given by `settings.FILE_HELP_ENTRY_MODULES`. This
|
- Evennia will look through all modules given by
|
||||||
should be a list of python-paths for Evennia to import.
|
`settings.FILE_HELP_ENTRY_MODULES`. This should be a list of python-paths for
|
||||||
- If this module contains a top-level variable `HELP_ENTRY_DICTS`, this will be imported
|
Evennia to import.
|
||||||
and must be a `list` of help-entry dicts.
|
- If this module contains a top-level variable `HELP_ENTRY_DICTS`, this will be
|
||||||
|
imported and must be a `list` of help-entry dicts.
|
||||||
- If no `HELP_ENTRY_DICTS` list is found, _every_ top-level variable in the
|
- If no `HELP_ENTRY_DICTS` list is found, _every_ top-level variable in the
|
||||||
module that is a `dict` will be read as a help entry. The variable-names will
|
module that is a `dict` will be read as a help entry. The variable-names will
|
||||||
be ignored in this case.
|
be ignored in this case.
|
||||||
|
|
||||||
If you add multiple modules to be read, same-keyed help entries added later in the list
|
If you add multiple modules to be read, same-keyed help entries added later in
|
||||||
will override coming before.
|
the list will override coming before.
|
||||||
|
|
||||||
Each entry dict must define keys to match that needed by all help entries.
|
Each entry dict must define keys to match that needed by all help entries.
|
||||||
Here's an example of a help module:
|
Here's an example of a help module:
|
||||||
|
|
@ -260,6 +276,7 @@ HELP_ENTRY_DICTS = [
|
||||||
"key": "The Gods", # case-insensitive, can be searched by 'gods' too
|
"key": "The Gods", # case-insensitive, can be searched by 'gods' too
|
||||||
"aliases": ['pantheon', 'religion']
|
"aliases": ['pantheon', 'religion']
|
||||||
"category": "Lore",
|
"category": "Lore",
|
||||||
|
"locks": "read:all()", # optional
|
||||||
"text": '''
|
"text": '''
|
||||||
The gods formed the world ...
|
The gods formed the world ...
|
||||||
|
|
||||||
|
|
@ -291,6 +308,47 @@ The help entry text will be dedented and will retain paragraphs. You should try
|
||||||
to keep your strings a reasonable width (it will look better). Just reload the
|
to keep your strings a reasonable width (it will look better). Just reload the
|
||||||
server and the file-based help entries will be available to view.
|
server and the file-based help entries will be available to view.
|
||||||
|
|
||||||
|
## Locking help entries
|
||||||
|
|
||||||
|
The default `help` command gather all available commands and help entries
|
||||||
|
together so they can be searched or listed. By setting locks on the command/help
|
||||||
|
entry one can limit who can read help about it.
|
||||||
|
|
||||||
|
- Commands failing the normal `cmd`-lock will be removed before even getting
|
||||||
|
to the help command. In this case the other two lock types below are ignored.
|
||||||
|
- The `view` access type determines if the command/help entry should be visible in
|
||||||
|
the main help index. If not given, it is assumed everyone can view.
|
||||||
|
- The `read` access type determines if the command/help entry can be actually read.
|
||||||
|
If a `read` lock is given and `view` is not, the `read`-lock is assumed to
|
||||||
|
apply to `view`-access as well (so if you can't read the help entry it will
|
||||||
|
also not show up in the index). If `read`-lock is not given, it's assume
|
||||||
|
everyone can read the help entry.
|
||||||
|
|
||||||
|
For Commands you set the help-related locks the same way you would any lock:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class MyCommand(Command):
|
||||||
|
"""
|
||||||
|
<docstring for command>
|
||||||
|
"""
|
||||||
|
key = "mycommand"
|
||||||
|
# everyone can use the command, builders can view it in the help index
|
||||||
|
# but only devs can actually read the help (a weird setup for sure!)
|
||||||
|
locks = "cmd:all();view:perm(Builders);read:perm(Developers)
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Db-help entries and File-Help entries work the same way (except the `cmd`-type
|
||||||
|
lock is not used. A file-help example:
|
||||||
|
|
||||||
|
```python
|
||||||
|
help_entry = {
|
||||||
|
# ...
|
||||||
|
locks = "read:perm(Developer)",
|
||||||
|
# ...
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
## Customizing the look of the help system
|
## Customizing the look of the help system
|
||||||
|
|
||||||
|
|
@ -303,10 +361,9 @@ together and search through them on the fly. It also does all the formatting of
|
||||||
the output.
|
the output.
|
||||||
|
|
||||||
To make it easier to tweak the look, the parts of the code that changes the
|
To make it easier to tweak the look, the parts of the code that changes the
|
||||||
visual presentation has been broken out into separate methods `format_help_entry` and
|
visual presentation has been broken out into separate methods
|
||||||
`format_help_index` - override these in your version of `help` to change the display
|
`format_help_entry` and `format_help_index` - override these in your version of
|
||||||
as you please. See the api link above for details.
|
`help` to change the display as you please. See the api link above for details.
|
||||||
|
|
||||||
|
|
||||||
## Technical notes
|
## Technical notes
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ General Character commands usually available to all characters
|
||||||
"""
|
"""
|
||||||
import re
|
import re
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from evennia.utils import utils, evtable
|
from evennia.utils import utils
|
||||||
from evennia.typeclasses.attributes import NickTemplateInvalid
|
from evennia.typeclasses.attributes import NickTemplateInvalid
|
||||||
|
|
||||||
COMMAND_DEFAULT_CLASS = utils.class_from_module(settings.COMMAND_DEFAULT_CLASS)
|
COMMAND_DEFAULT_CLASS = utils.class_from_module(settings.COMMAND_DEFAULT_CLASS)
|
||||||
|
|
@ -400,7 +400,7 @@ class CmdGet(COMMAND_DEFAULT_CLASS):
|
||||||
|
|
||||||
key = "get"
|
key = "get"
|
||||||
aliases = "grab"
|
aliases = "grab"
|
||||||
locks = "cmd:all()"
|
locks = "cmd:all();view:perm(Developer);read:perm(Developer)"
|
||||||
arg_regex = r"\s|$"
|
arg_regex = r"\s|$"
|
||||||
|
|
||||||
def func(self):
|
def func(self):
|
||||||
|
|
|
||||||
|
|
@ -8,16 +8,16 @@ outside the game in modules given by ``settings.FILE_HELP_ENTRY_MODULES``.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import re
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from evennia.utils.utils import fill, dedent
|
from evennia.utils.utils import dedent
|
||||||
from evennia.help.models import HelpEntry
|
from evennia.help.models import HelpEntry
|
||||||
from evennia.utils import create, evmore
|
from evennia.utils import create, evmore
|
||||||
from evennia.utils.ansi import ANSIString
|
from evennia.utils.ansi import ANSIString
|
||||||
from evennia.help.filehelp import FILE_HELP_ENTRIES
|
from evennia.help.filehelp import FILE_HELP_ENTRIES
|
||||||
from evennia.utils.eveditor import EvEditor
|
from evennia.utils.eveditor import EvEditor
|
||||||
|
from evennia.utils.evmenu import ask_yes_no
|
||||||
from evennia.utils.utils import (
|
from evennia.utils.utils import (
|
||||||
class_from_module,
|
class_from_module,
|
||||||
inherits_from,
|
inherits_from,
|
||||||
|
|
@ -259,7 +259,7 @@ class CmdHelp(COMMAND_DEFAULT_CLASS):
|
||||||
|
|
||||||
return help_index
|
return help_index
|
||||||
|
|
||||||
def check_show_help(self, cmd_or_topic, caller):
|
def can_read_topic(self, cmd_or_topic, caller):
|
||||||
"""
|
"""
|
||||||
Helper method. If this return True, the given help topic
|
Helper method. If this return True, the given help topic
|
||||||
be viewable in the help listing. Note that even if this returns False,
|
be viewable in the help listing. Note that even if this returns False,
|
||||||
|
|
@ -273,18 +273,22 @@ class CmdHelp(COMMAND_DEFAULT_CLASS):
|
||||||
Returns:
|
Returns:
|
||||||
bool: If command can be viewed or not.
|
bool: If command can be viewed or not.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
This uses the 'read' lock. If no 'read' lock is defined, the topic is assumed readable
|
||||||
|
by all.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if inherits_from(cmd_or_topic, "evennia.commands.command.Command"):
|
if inherits_from(cmd_or_topic, "evennia.commands.command.Command"):
|
||||||
return cmd_or_topic.auto_help and cmd_or_topic.access(caller)
|
return cmd_or_topic.auto_help and cmd_or_topic.access(caller, 'read', default=True)
|
||||||
else:
|
else:
|
||||||
return cmd_or_topic.access(caller, 'read', default=True)
|
return cmd_or_topic.access(caller, 'read', default=True)
|
||||||
|
|
||||||
def should_list_topic(self, cmd_or_topic, caller):
|
def can_list_topic(self, cmd_or_topic, caller):
|
||||||
"""
|
"""
|
||||||
Should the specified command appear in the help table?
|
Should the specified command appear in the help table?
|
||||||
|
|
||||||
This method only checks whether a specified command should appear in the table of
|
This method only checks whether a specified command should appear in the table of
|
||||||
topics/commands. The command can be used by the caller (see the 'check_show_help' method)
|
topics/commands. The command can be used by the caller (see the 'should_show_help' method)
|
||||||
and the command will still be available, for instance, if a character type 'help name of the
|
and the command will still be available, for instance, if a character type 'help name of the
|
||||||
command'. However, if you return False, the specified command will not appear in the table.
|
command'. However, if you return False, the specified command will not appear in the table.
|
||||||
This is sometimes useful to "hide" commands in the table, but still access them through the
|
This is sometimes useful to "hide" commands in the table, but still access them through the
|
||||||
|
|
@ -297,8 +301,123 @@ class CmdHelp(COMMAND_DEFAULT_CLASS):
|
||||||
Returns:
|
Returns:
|
||||||
bool: If command should be listed or not.
|
bool: If command should be listed or not.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
By default, the 'view' lock will be checked, and if no such lock is defined, the 'read'
|
||||||
|
lock will be used. If neither lock is defined, the help entry is assumed to be
|
||||||
|
accessible to all.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return cmd_or_topic.access(caller, 'view', default=True)
|
has_view = (
|
||||||
|
"view:" in cmd_or_topic.locks
|
||||||
|
if inherits_from(cmd_or_topic, "evennia.commands.command.Command")
|
||||||
|
else cmd_or_topic.locks.get("view")
|
||||||
|
)
|
||||||
|
|
||||||
|
if has_view:
|
||||||
|
return cmd_or_topic.access(caller, 'view', default=True)
|
||||||
|
else:
|
||||||
|
# no explicit 'view' lock - use the 'read' lock
|
||||||
|
return cmd_or_topic.access(caller, 'read', default=True)
|
||||||
|
|
||||||
|
def collect_topics(self, caller, mode='list'):
|
||||||
|
"""
|
||||||
|
Collect help topics from all sources (cmd/db/file).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
caller (Object or Account): The user of the Command.
|
||||||
|
mode (str): One of 'list' or 'query', where the first means we are collecting to view
|
||||||
|
the help index and the second because of wanting to search for a specific help
|
||||||
|
entry/cmd to read. This determines which access should be checked.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: A tuple of three dicts containing the different types of help entries
|
||||||
|
in the order cmd-help, db-help, file-help:
|
||||||
|
`({key: cmd,...}, {key: dbentry,...}, {key: fileentry,...}`
|
||||||
|
|
||||||
|
"""
|
||||||
|
# start with cmd-help
|
||||||
|
cmdset = self.cmdset
|
||||||
|
# removing doublets in cmdset, caused by cmdhandler
|
||||||
|
# having to allow doublet commands to manage exits etc.
|
||||||
|
cmdset.make_unique(caller)
|
||||||
|
# retrieve all available commands and database / file-help topics.
|
||||||
|
# also check the 'cmd:' lock here
|
||||||
|
cmd_help_topics = [cmd for cmd in cmdset if cmd and cmd.access(caller, 'cmd')]
|
||||||
|
# get all file-based help entries, checking perms
|
||||||
|
file_help_topics = {
|
||||||
|
topic.key.lower().strip(): topic
|
||||||
|
for topic in FILE_HELP_ENTRIES.all()
|
||||||
|
}
|
||||||
|
# get db-based help entries, checking perms
|
||||||
|
db_help_topics = {
|
||||||
|
topic.key.lower().strip(): topic
|
||||||
|
for topic in HelpEntry.objects.all()
|
||||||
|
}
|
||||||
|
if mode == 'list':
|
||||||
|
# check the view lock for all help entries/commands and determine key
|
||||||
|
cmd_help_topics = {
|
||||||
|
cmd.auto_help_display_key
|
||||||
|
if hasattr(cmd, "auto_help_display_key") else cmd.key: cmd
|
||||||
|
for cmd in cmd_help_topics if self.can_list_topic(cmd, caller)}
|
||||||
|
db_help_topics = {
|
||||||
|
key: entry for key, entry in db_help_topics.items()
|
||||||
|
if self.can_list_topic(entry, caller)
|
||||||
|
}
|
||||||
|
file_help_topics = {
|
||||||
|
key: entry for key, entry in file_help_topics.items()
|
||||||
|
if self.can_list_topic(entry, caller)}
|
||||||
|
else:
|
||||||
|
# query
|
||||||
|
cmd_help_topics = {
|
||||||
|
cmd.auto_help_display_key
|
||||||
|
if hasattr(cmd, "auto_help_display_key") else cmd.key: cmd
|
||||||
|
for cmd in cmd_help_topics if self.can_read_topic(cmd, caller)}
|
||||||
|
db_help_topics = {
|
||||||
|
key: entry for key, entry in db_help_topics.items()
|
||||||
|
if self.can_read_topic(entry, caller)
|
||||||
|
}
|
||||||
|
file_help_topics = {
|
||||||
|
key: entry for key, entry in file_help_topics.items()
|
||||||
|
if self.can_read_topic(entry, caller)}
|
||||||
|
|
||||||
|
return cmd_help_topics, db_help_topics, file_help_topics
|
||||||
|
|
||||||
|
def do_search(self, query, entries, search_fields=None):
|
||||||
|
"""
|
||||||
|
Perform a help-query search, default using Lunr search engine.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query (str): The help entry to search for.
|
||||||
|
entries (list): All possibilities. A mix of commands, HelpEntries and FileHelpEntries.
|
||||||
|
search_fields (list): A list of dicts defining how Lunr will find the
|
||||||
|
search data on the elements. If not given, will use a default.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: A tuple (match, suggestions).
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not search_fields:
|
||||||
|
# lunr search fields/boosts
|
||||||
|
search_fields = [
|
||||||
|
{"field_name": "key", "boost": 10},
|
||||||
|
{"field_name": "aliases", "boost": 9},
|
||||||
|
{"field_name": "category", "boost": 8},
|
||||||
|
{"field_name": "tags", "boost": 1}, # tags are not used by default
|
||||||
|
]
|
||||||
|
match, suggestions = None, None
|
||||||
|
for match_query in (query, f"{query}*"):
|
||||||
|
# We first do an exact word-match followed by a start-by query. The
|
||||||
|
# return of this will either be a HelpCategory, a Command or a
|
||||||
|
# HelpEntry/FileHelpEntry.
|
||||||
|
matches, suggestions = help_search_with_index(
|
||||||
|
match_query, entries,
|
||||||
|
suggestion_maxnum=self.suggestion_maxnum,
|
||||||
|
fields=search_fields
|
||||||
|
)
|
||||||
|
if matches:
|
||||||
|
match = matches[0]
|
||||||
|
break
|
||||||
|
return match, suggestions
|
||||||
|
|
||||||
def parse(self):
|
def parse(self):
|
||||||
"""
|
"""
|
||||||
|
|
@ -331,85 +450,52 @@ class CmdHelp(COMMAND_DEFAULT_CLASS):
|
||||||
caller = self.caller
|
caller = self.caller
|
||||||
query, subtopics, cmdset = self.topic, self.subtopics, self.cmdset
|
query, subtopics, cmdset = self.topic, self.subtopics, self.cmdset
|
||||||
|
|
||||||
# removing doublets in cmdset, caused by cmdhandler
|
|
||||||
# having to allow doublet commands to manage exits etc.
|
|
||||||
cmdset.make_unique(caller)
|
|
||||||
|
|
||||||
# retrieve all available commands and database / file-help topics
|
|
||||||
all_cmd_topics = [cmd for cmd in cmdset if self.check_show_help(cmd, caller) if cmd]
|
|
||||||
|
|
||||||
# get all file-based help entries, checking perms
|
|
||||||
file_help_topics = {
|
|
||||||
topic.key.lower().strip(): topic
|
|
||||||
for topic in FILE_HELP_ENTRIES.all()
|
|
||||||
if topic.access(caller)
|
|
||||||
}
|
|
||||||
# get db-based help entries, checking perms
|
|
||||||
db_topics = {
|
|
||||||
topic.key.lower().strip(): topic
|
|
||||||
for topic in HelpEntry.objects.all()
|
|
||||||
if topic.access(caller)
|
|
||||||
}
|
|
||||||
# merge so db topics override file topics with same key
|
|
||||||
all_file_db_topics = list({**file_help_topics, **db_topics}.values())
|
|
||||||
|
|
||||||
# get all categories
|
|
||||||
all_categories = list(set(
|
|
||||||
[HelpCategory(cmd.help_category) for cmd in all_cmd_topics]
|
|
||||||
+ [HelpCategory(topic.help_category) for topic in all_file_db_topics]
|
|
||||||
))
|
|
||||||
|
|
||||||
if not query:
|
if not query:
|
||||||
# list all available help entries, grouped by category. We want to
|
# list all available help entries, grouped by category. We want to
|
||||||
# build dictionaries {category: [topic, topic, ...], ...}
|
# build dictionaries {category: [topic, topic, ...], ...}
|
||||||
cmd_help_dict = defaultdict(list)
|
|
||||||
file_db_help_dict = defaultdict(list)
|
|
||||||
|
|
||||||
# Filter commands/topics that should be reached by the help
|
cmd_help_topics, db_help_topics, file_help_topics = \
|
||||||
# system, but not be displayed in the table, or be displayed differently.
|
self.collect_topics(caller, mode='list')
|
||||||
for cmd in all_cmd_topics:
|
|
||||||
if self.should_list_topic(cmd, caller):
|
|
||||||
key = (
|
|
||||||
cmd.auto_help_display_key
|
|
||||||
if hasattr(cmd, "auto_help_display_key") else cmd.key
|
|
||||||
)
|
|
||||||
cmd_help_dict[cmd.help_category].append(key)
|
|
||||||
for topic in all_file_db_topics:
|
|
||||||
if self.should_list_topic(topic, caller):
|
|
||||||
file_db_help_dict[topic.help_category].append(topic.key)
|
|
||||||
|
|
||||||
output = self.format_help_index(cmd_help_dict, file_db_help_dict)
|
# db-topics override file-based ones
|
||||||
|
file_db_help_topics = {**file_help_topics, **db_help_topics}
|
||||||
|
|
||||||
|
# group by category (cmds are listed separately)
|
||||||
|
cmd_help_by_category = defaultdict(list)
|
||||||
|
file_db_help_by_category = defaultdict(list)
|
||||||
|
for key, cmd in cmd_help_topics.items():
|
||||||
|
cmd_help_by_category[cmd.help_category].append(key)
|
||||||
|
for key, entry in file_db_help_topics.items():
|
||||||
|
file_db_help_by_category[entry.help_category].append(key)
|
||||||
|
|
||||||
|
# generate the index and display
|
||||||
|
output = self.format_help_index(cmd_help_by_category,
|
||||||
|
file_db_help_by_category)
|
||||||
self.msg_help(output)
|
self.msg_help(output)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# We have a query - try to find a specific topic/category using the
|
# search for a specific entry. We need to check for 'read' access here before # building the
|
||||||
# Lunr search engine
|
# set of possibilities.
|
||||||
|
cmd_help_topics, db_help_topics, file_help_topics = \
|
||||||
|
self.collect_topics(caller, mode='query')
|
||||||
|
|
||||||
# all available help options - will be searched in order
|
# db-help topics takes priority over file-help
|
||||||
entries = all_cmd_topics + all_file_db_topics + all_categories
|
file_db_help_topics = {**file_help_topics, **db_help_topics}
|
||||||
|
|
||||||
|
# commands take priority over the other types
|
||||||
|
all_topics = {**file_db_help_topics, **cmd_help_topics}
|
||||||
|
|
||||||
|
# get all categories
|
||||||
|
all_categories = list(set(
|
||||||
|
HelpCategory(topic.help_category) for topic in all_topics.values()))
|
||||||
|
|
||||||
|
# all available help options - will be searched in order. We also check # the
|
||||||
|
# read-permission here.
|
||||||
|
entries = list(all_topics.values()) + all_categories
|
||||||
|
|
||||||
# lunr search fields/boosts
|
# lunr search fields/boosts
|
||||||
search_fields = [
|
match, suggestions = self.do_search(query, entries)
|
||||||
{"field_name": "key", "boost": 10},
|
|
||||||
{"field_name": "aliases", "boost": 9},
|
|
||||||
{"field_name": "category", "boost": 8},
|
|
||||||
{"field_name": "tags", "boost": 1}, # tags are not used by default
|
|
||||||
]
|
|
||||||
match, suggestions = None, None
|
|
||||||
|
|
||||||
for match_query in (query, f"{query}*"):
|
|
||||||
# We first do an exact word-match followed by a start-by query. The
|
|
||||||
# return of this will either be a HelpCategory, a Command or a
|
|
||||||
# HelpEntry/FileHelpEntry.
|
|
||||||
matches, suggestions = help_search_with_index(
|
|
||||||
match_query, entries,
|
|
||||||
suggestion_maxnum=self.suggestion_maxnum,
|
|
||||||
fields=search_fields
|
|
||||||
)
|
|
||||||
if matches:
|
|
||||||
match = matches[0]
|
|
||||||
break
|
|
||||||
|
|
||||||
if not match:
|
if not match:
|
||||||
# no topic matches found. Only give suggestions.
|
# no topic matches found. Only give suggestions.
|
||||||
|
|
@ -448,24 +534,15 @@ class CmdHelp(COMMAND_DEFAULT_CLASS):
|
||||||
|
|
||||||
if isinstance(match, HelpCategory):
|
if isinstance(match, HelpCategory):
|
||||||
# no subtopics for categories - these are just lists of topics
|
# no subtopics for categories - these are just lists of topics
|
||||||
|
category = match.key
|
||||||
output = self.format_help_index(
|
category_lower = category.lower()
|
||||||
{
|
cmds_in_category = [key for key, cmd in cmd_help_topics.items()
|
||||||
match.key: [
|
if category_lower == cmd.help_category]
|
||||||
cmd.key
|
topics_in_category = [key for key, topic in file_db_help_topics.items()
|
||||||
for cmd in all_cmd_topics
|
if category_lower == topic.help_category]
|
||||||
if match.key.lower() == cmd.help_category
|
output = self.format_help_index({category: cmds_in_category},
|
||||||
]
|
{category: topics_in_category},
|
||||||
},
|
title_lone_category=True)
|
||||||
{
|
|
||||||
match.key: [
|
|
||||||
topic.key
|
|
||||||
for topic in all_file_db_topics
|
|
||||||
if match.key.lower() == topic.help_category
|
|
||||||
]
|
|
||||||
},
|
|
||||||
title_lone_category=True
|
|
||||||
)
|
|
||||||
self.msg_help(output)
|
self.msg_help(output)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -565,7 +642,7 @@ def _quithelp(caller):
|
||||||
del caller.db._editing_help
|
del caller.db._editing_help
|
||||||
|
|
||||||
|
|
||||||
class CmdSetHelp(COMMAND_DEFAULT_CLASS):
|
class CmdSetHelp(CmdHelp):
|
||||||
"""
|
"""
|
||||||
Edit the help database.
|
Edit the help database.
|
||||||
|
|
||||||
|
|
@ -629,9 +706,15 @@ class CmdSetHelp(COMMAND_DEFAULT_CLASS):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
key = "sethelp"
|
key = "sethelp"
|
||||||
|
aliases = []
|
||||||
switch_options = ("edit", "replace", "append", "extend", "delete")
|
switch_options = ("edit", "replace", "append", "extend", "delete")
|
||||||
locks = "cmd:perm(Helper)"
|
locks = "cmd:perm(Helper)"
|
||||||
help_category = "Building"
|
help_category = "Building"
|
||||||
|
arg_regex = None
|
||||||
|
|
||||||
|
def parse(self):
|
||||||
|
"""We want to use the default parser rather than the CmdHelp.parse"""
|
||||||
|
return COMMAND_DEFAULT_CLASS.parse(self)
|
||||||
|
|
||||||
def func(self):
|
def func(self):
|
||||||
"""Implement the function"""
|
"""Implement the function"""
|
||||||
|
|
@ -659,18 +742,63 @@ class CmdSetHelp(COMMAND_DEFAULT_CLASS):
|
||||||
old_entry = None
|
old_entry = None
|
||||||
|
|
||||||
# check if we have an old entry with the same name
|
# check if we have an old entry with the same name
|
||||||
try:
|
|
||||||
for querystr in topicstrlist:
|
cmd_help_topics, db_help_topics, file_help_topics = \
|
||||||
old_entry = HelpEntry.objects.find_topicmatch(querystr) # also search by alias
|
self.collect_topics(self.caller, mode='query')
|
||||||
if old_entry:
|
# db-help topics takes priority over file-help
|
||||||
old_entry = list(old_entry)[0]
|
file_db_help_topics = {**file_help_topics, **db_help_topics}
|
||||||
|
# commands take priority over the other types
|
||||||
|
all_topics = {**file_db_help_topics, **cmd_help_topics}
|
||||||
|
# get all categories
|
||||||
|
all_categories = list(set(
|
||||||
|
HelpCategory(topic.help_category) for topic in all_topics.values()))
|
||||||
|
# all available help options - will be searched in order. We also check # the
|
||||||
|
# read-permission here.
|
||||||
|
entries = list(all_topics.values()) + all_categories
|
||||||
|
|
||||||
|
# default setup
|
||||||
|
category = lhslist[1] if nlist > 1 else DEFAULT_HELP_CATEGORY
|
||||||
|
lockstring = ",".join(lhslist[2:]) if nlist > 2 else "read:all()"
|
||||||
|
|
||||||
|
# search for existing entries of this or other types
|
||||||
|
old_entry = None
|
||||||
|
for querystr in topicstrlist:
|
||||||
|
match, _ = self.do_search(querystr, entries)
|
||||||
|
if match:
|
||||||
|
warning = None
|
||||||
|
if isinstance(match, HelpCategory):
|
||||||
|
warning = (f"'{querystr}' matches (or partially matches) the name of "
|
||||||
|
"help-category '{match.key}'. If you continue, your help entry will "
|
||||||
|
"take precedence and the category (or part of its name) *may* not "
|
||||||
|
"be usable for grouping help entries anymore.")
|
||||||
|
elif inherits_from(match, "evennia.commands.command.Command"):
|
||||||
|
warning = (f"'{querystr}' matches (or partially matches) the key/alias of "
|
||||||
|
"Command '{match.key}'. Command-help take precedence over other "
|
||||||
|
"help entries so your help *may* be impossible to reach for those "
|
||||||
|
"with access to that command.")
|
||||||
|
elif inherits_from(match, "evennia.help.filehelp.FileHelpEntry"):
|
||||||
|
warning = (f"'{querystr}' matches (or partially matches) the name/alias of the "
|
||||||
|
"file-based help file '{match.key}'. File-help entries cannot be "
|
||||||
|
"modified from in-game (they are files on-disk). If you continue, "
|
||||||
|
"your help entry *may* shadow the file-based one's name partly or "
|
||||||
|
"completely.")
|
||||||
|
if warning:
|
||||||
|
# show a warning for a clashing help-entry type. Even if user accepts this
|
||||||
|
# we don't break here since we may need to show warnings for other inputs.
|
||||||
|
# We don't count this as an old-entry hit because we can't edit these
|
||||||
|
# types of entries.
|
||||||
|
self.msg(f"|rWarning:\n|r{warning}|n")
|
||||||
|
repl = yield("|wDo you still want to continue? Y/[N]?|n")
|
||||||
|
if repl.lower() not in ('y', 'yes'):
|
||||||
|
self.msg("Aborted.")
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
# a db-based help entry - this is OK
|
||||||
|
old_entry = match
|
||||||
|
category = lhslist[1] if nlist > 1 else old_entry.help_category
|
||||||
|
lockstring = ",".join(lhslist[2:]) if nlist > 2 else old_entry.locks.get()
|
||||||
break
|
break
|
||||||
category = lhslist[1] if nlist > 1 else old_entry.help_category
|
|
||||||
lockstring = ",".join(lhslist[2:]) if nlist > 2 else old_entry.locks.get()
|
|
||||||
except Exception:
|
|
||||||
old_entry = None
|
|
||||||
category = lhslist[1] if nlist > 1 else DEFAULT_HELP_CATEGORY
|
|
||||||
lockstring = ",".join(lhslist[2:]) if nlist > 2 else "view:all()"
|
|
||||||
category = category.lower()
|
category = category.lower()
|
||||||
|
|
||||||
if "edit" in switches:
|
if "edit" in switches:
|
||||||
|
|
@ -739,8 +867,8 @@ class CmdSetHelp(COMMAND_DEFAULT_CLASS):
|
||||||
self.msg("Overwrote the old topic '%s'%s." % (topicstr, aliastxt))
|
self.msg("Overwrote the old topic '%s'%s." % (topicstr, aliastxt))
|
||||||
else:
|
else:
|
||||||
self.msg(
|
self.msg(
|
||||||
"Topic '%s'%s already exists. Use /replace to overwrite "
|
f"Topic '{topicstr}'{aliastxt} already exists. Use /edit to open in editor, or "
|
||||||
"or /append or /merge to add text to it." % (topicstr, aliastxt)
|
"/replace, /append and /merge to modify it directly."
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# no old entry. Create a new one.
|
# no old entry. Create a new one.
|
||||||
|
|
@ -748,7 +876,7 @@ class CmdSetHelp(COMMAND_DEFAULT_CLASS):
|
||||||
topicstr, self.rhs, category=category, locks=lockstring, aliases=aliases
|
topicstr, self.rhs, category=category, locks=lockstring, aliases=aliases
|
||||||
)
|
)
|
||||||
if new_entry:
|
if new_entry:
|
||||||
self.msg("Topic '%s'%s was successfully created." % (topicstr, aliastxt))
|
self.msg(f"Topic '{topicstr}'{aliastxt} was successfully created.")
|
||||||
if "edit" in switches:
|
if "edit" in switches:
|
||||||
# open the line editor to edit the helptext
|
# open the line editor to edit the helptext
|
||||||
self.caller.db._editing_help = new_entry
|
self.caller.db._editing_help = new_entry
|
||||||
|
|
@ -763,5 +891,5 @@ class CmdSetHelp(COMMAND_DEFAULT_CLASS):
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
self.msg(
|
self.msg(
|
||||||
"Error when creating topic '%s'%s! Contact an admin." % (topicstr, aliastxt)
|
f"Error when creating topic '{topicstr}'{aliastxt}! Contact an admin."
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -431,6 +431,7 @@ class TestHelp(CommandTest):
|
||||||
help_module.CmdSetHelp(),
|
help_module.CmdSetHelp(),
|
||||||
"testhelp, General = This is a test",
|
"testhelp, General = This is a test",
|
||||||
"Topic 'testhelp' was successfully created.",
|
"Topic 'testhelp' was successfully created.",
|
||||||
|
cmdset=CharacterCmdSet()
|
||||||
)
|
)
|
||||||
self.call(help_module.CmdHelp(), "testhelp", "Help for testhelp", cmdset=CharacterCmdSet())
|
self.call(help_module.CmdHelp(), "testhelp", "Help for testhelp", cmdset=CharacterCmdSet())
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ forms of help that do not concern commands, like information about the
|
||||||
game world, policy info, rules and similar.
|
game world, policy info, rules and similar.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from datetime import datetime
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,9 @@ import re
|
||||||
|
|
||||||
# these are words that Lunr normally ignores but which we want to find
|
# these are words that Lunr normally ignores but which we want to find
|
||||||
# since we use them (e.g. as command names).
|
# since we use them (e.g. as command names).
|
||||||
# Lunr's default word list is found here:
|
# Lunr's default ignore-word list is found here:
|
||||||
# https://github.com/yeraydiazdiaz/lunr.py/blob/master/lunr/stop_word_filter.py
|
# https://github.com/yeraydiazdiaz/lunr.py/blob/master/lunr/stop_word_filter.py
|
||||||
_LUNR_STOP_WORD_FILTER_EXCEPTIONS = ("about", "might")
|
_LUNR_STOP_WORD_FILTER_EXCEPTIONS = ("about", "might", "get")
|
||||||
|
|
||||||
_LUNR = None
|
_LUNR = None
|
||||||
_LUNR_EXCEPTION = None
|
_LUNR_EXCEPTION = None
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue