Add lock-handling to FileHelp, add warnings to sethelp

This commit is contained in:
Griatch 2021-05-30 20:09:51 +02:00
parent 0fb29073b2
commit b050656319
7 changed files with 377 additions and 187 deletions

View file

@ -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

View file

@ -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

View file

@ -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):

View file

@ -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."
) )

View file

@ -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())

View file

@ -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

View file

@ -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