diff --git a/evennia/commands/default/help.py b/evennia/commands/default/help.py index 0332686a96..4da7c39afb 100644 --- a/evennia/commands/default/help.py +++ b/evennia/commands/default/help.py @@ -259,46 +259,46 @@ class CmdHelp(COMMAND_DEFAULT_CLASS): return help_index - def check_show_help(self, cmd, caller): + def check_show_help(self, cmd_or_topic, caller): """ - Helper method. If this return True, the given cmd - auto-help will be viewable in the help listing. - Override this to easily select what is shown to - the account. Note that only commands available - in the caller's merged cmdset are available. + Helper method. If this return True, the given help topic + be viewable in the help listing. Note that even if this returns False, + the entry will still be visible in the help index unless `should_list_topic` + is also returning False. Args: - cmd (Command): Command class from the merged cmdset - caller (Character, Account or Session): The current caller - executing the help command. + cmd_or_topic (Command, HelpEntry or FileHelpEntry): The topic/command to test. + caller: the caller checking for access. + + Returns: + bool: If command can be viewed or not. """ - # return only those with auto_help set and passing the cmd: lock - return cmd.auto_help and cmd.access(caller) + if inherits_from(cmd_or_topic, "evennia.commands.command.Command"): + return cmd_or_topic.auto_help and cmd_or_topic.access(caller) + else: + return cmd_or_topic.access(caller, 'read', default=True) - def should_list_cmd(self, cmd, caller): + def should_list_topic(self, cmd_or_topic, caller): """ Should the specified command appear in the help table? - 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) 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. This is sometimes useful to "hide" commands in - the table, but still access them through the help system. + 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) + 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. + This is sometimes useful to "hide" commands in the table, but still access them through the + help system. Args: - cmd: the command to be tested. - caller: the caller of the help system. + cmd_or_topic (Command, HelpEntry or FileHelpEntry): The topic/command to test. + caller: the caller checking for access. - Return: - True: the command should appear in the table. - False: the command shouldn't appear in the table. + Returns: + bool: If command should be listed or not. """ - return True + return cmd_or_topic.access(caller, 'view', default=True) def parse(self): """ @@ -336,39 +336,49 @@ class CmdHelp(COMMAND_DEFAULT_CLASS): cmdset.make_unique(caller) # retrieve all available commands and database / file-help topics - all_cmds = [cmd for cmd in cmdset if self.check_show_help(cmd, caller)] + all_cmd_topics = [cmd for cmd in cmdset if self.check_show_help(cmd, caller) if cmd] - # we group the file-help topics with the db ones, giving the db ones priority - file_help_topics = FILE_HELP_ENTRIES.all(return_dict=True) - db_topics = { - topic.key.lower().strip(): topic for topic in HelpEntry.objects.all() - if topic.access(caller, "view", default=True) + # 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) } - all_db_topics = list({**file_help_topics, **db_topics}.values()) + # 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_cmds] - + [HelpCategory(topic.help_category) for topic in all_db_topics] + [HelpCategory(cmd.help_category) for cmd in all_cmd_topics] + + [HelpCategory(topic.help_category) for topic in all_file_db_topics] )) if not query: # list all available help entries, grouped by category. We want to # build dictionaries {category: [topic, topic, ...], ...} cmd_help_dict = defaultdict(list) - db_help_dict = defaultdict(list) + file_db_help_dict = defaultdict(list) - # Filter commands that should be reached by the help + # Filter commands/topics that should be reached by the help # system, but not be displayed in the table, or be displayed differently. - for cmd in all_cmds: - if self.should_list_cmd(cmd, caller): - key = (cmd.auto_help_display_key - if hasattr(cmd, "auto_help_display_key") else cmd.key) + 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) - for db_topic in all_db_topics: - db_help_dict[db_topic.help_category].append(db_topic.key) - - output = self.format_help_index(cmd_help_dict, db_help_dict) + output = self.format_help_index(cmd_help_dict, file_db_help_dict) self.msg_help(output) return @@ -376,8 +386,8 @@ class CmdHelp(COMMAND_DEFAULT_CLASS): # We have a query - try to find a specific topic/category using the # Lunr search engine - # all available options - entries = [cmd for cmd in all_cmds if cmd] + all_db_topics + all_categories + # all available help options - will be searched in order + entries = all_cmd_topics + all_file_db_topics + all_categories # lunr search fields/boosts search_fields = [ @@ -443,14 +453,14 @@ class CmdHelp(COMMAND_DEFAULT_CLASS): { match.key: [ cmd.key - for cmd in all_cmds + for cmd in all_cmd_topics if match.key.lower() == cmd.help_category ] }, { match.key: [ topic.key - for topic in all_db_topics + for topic in all_file_db_topics if match.key.lower() == topic.help_category ] }, diff --git a/evennia/help/filehelp.py b/evennia/help/filehelp.py index 602aee1016..772d804fcc 100644 --- a/evennia/help/filehelp.py +++ b/evennia/help/filehelp.py @@ -13,12 +13,13 @@ Each help-entry dict is on the form :: {'key': , + 'text': , 'category': , # optional, otherwise settings.DEFAULT_HELP_CATEGORY 'aliases': , # optional - 'text': } + 'locks': } # optional, use access-type 'view'. Default is view:all() -where the `category` is optional and the `text`` should be formatted on the -same form as other help entry-texts and contain ``# subtopics`` as normal. +The `text`` should be formatted on the same form as other help entry-texts and +can contain ``# subtopics`` as normal. New help-entry modules are added to the system by providing the python-path to the module to `settings.FILE_HELP_ENTRY_MODULES`. Note that if same-key entries are @@ -33,6 +34,7 @@ An example of the contents of a module: "key": "The Gods", # case-insensitive, also partial-matching ('gods') works "aliases": ['pantheon', 'religion'], "category": "Lore", + "locks": "view:all()", # this is optional unless restricting access "text": ''' The gods formed the world ... @@ -68,6 +70,8 @@ from django.conf import settings from evennia.utils.utils import ( variable_from_module, make_iter, all_from_module) from evennia.utils import logger +from evennia.utils.utils import lazy_property +from evennia.locks.lockhandler import LockHandler _DEFAULT_HELP_CATEGORY = settings.DEFAULT_HELP_CATEGORY @@ -84,6 +88,7 @@ class FileHelpEntry: aliases: list help_category: str entrytext: str + lock_storage: str @property def search_index_entry(self): @@ -96,6 +101,7 @@ class FileHelpEntry: "aliases": " ".join(self.aliases), "category": self.help_category, "tags": "", + "locks": "", "text": self.entrytext, } @@ -105,6 +111,22 @@ class FileHelpEntry: def __repr__(self): return f"" + @lazy_property + def locks(self): + return LockHandler(self) + + def access(self, accessing_obj, access_type="view", default=True): + """ + Determines if another object has permission to access this help entry. + + Args: + accessing_obj (Object or Account): Entity trying to access this one. + access_type (str): type of access sought. + default (bool): What to return if no lock of `access_type` was found. + + """ + return self.locks.check(accessing_obj, access_type=access_type, default=default) + class FileHelpStorageHandler: """ @@ -154,14 +176,15 @@ class FileHelpStorageHandler: key = dct.get('key').lower().strip() category = dct.get('category', _DEFAULT_HELP_CATEGORY).strip() aliases = list(dct.get('aliases', [])) - entrytext = dct.get('text') + entrytext = dct.get('text', '') + locks = dct.get('locks', '') if not key and entrytext: logger.error(f"Cannot load file-help-entry (missing key or text): {dct}") continue unique_help_entries[key] = FileHelpEntry( - key=key, help_category=category, aliases=aliases, + key=key, help_category=category, aliases=aliases, lock_storage=locks, entrytext=entrytext) self.help_entries_dict = unique_help_entries diff --git a/evennia/help/models.py b/evennia/help/models.py index e9214857e8..eb84d83542 100644 --- a/evennia/help/models.py +++ b/evennia/help/models.py @@ -114,12 +114,19 @@ class HelpEntry(SharedMemoryModel): def __repr__(self): return f"" - def access(self, accessing_obj, access_type="read", default=False): + def access(self, accessing_obj, access_type="read", default=True): """ - Determines if another object has permission to access. - accessing_obj - object trying to access this one - access_type - type of access sought - default - what to return if no lock of access_type was found + Determines if another object has permission to access this help entry. + + Accesses used by default: + 'read' - read the help entry itself. + 'view' - see help entry in help index. + + Args: + accessing_obj (Object or Account): Entity trying to access this one. + access_type (str): type of access sought. + default (bool): What to return if no lock of `access_type` was found. + """ return self.locks.check(accessing_obj, access_type=access_type, default=default)