From 9eb1c0532f17df15a522fd740d7172bf20ad9a13 Mon Sep 17 00:00:00 2001 From: davewiththenicehat <54369722+davewiththenicehat@users.noreply.github.com> Date: Fri, 4 Jun 2021 16:24:51 -0400 Subject: [PATCH 01/18] file and command help web support, proof of concept Not fleshed enough for a prototype. Is a working proof of concept. Adds command help and file help entries to the website. --- evennia/commands/command.py | 39 +++++ evennia/help/filehelp.py | 42 +++++ evennia/help/models.py | 6 +- evennia/web/website/tests.py | 11 ++ evennia/web/website/views/help.py | 249 +++++++++++++++++++++++++----- 5 files changed, 310 insertions(+), 37 deletions(-) diff --git a/evennia/commands/command.py b/evennia/commands/command.py index b4519fc8a..4cc764cc6 100644 --- a/evennia/commands/command.py +++ b/evennia/commands/command.py @@ -9,11 +9,14 @@ import math import inspect from django.conf import settings +from django.urls import reverse +from django.utils.text import slugify from evennia.locks.lockhandler import LockHandler from evennia.utils.utils import is_iter, fill, lazy_property, make_iter from evennia.utils.evtable import EvTable from evennia.utils.ansi import ANSIString +from evennia.utils.logger import log_info class InterruptCommand(Exception): @@ -514,6 +517,42 @@ Command {self} has no defined `func()` - showing on-command variables: """ return self.__doc__ + def web_get_detail_url(self): + """ + Returns the URI path for a View that allows users to view details for + this object. + + ex. Oscar (Character) = '/characters/oscar/1/' + + For this to work, the developer must have defined a named view somewhere + in urls.py that follows the format 'modelname-action', so in this case + a named view of 'character-detail' would be referenced by this method. + + ex. + :: + url(r'characters/(?P[\w\d\-]+)/(?P[0-9]+)/$', + CharDetailView.as_view(), name='character-detail') + + If no View has been created and defined in urls.py, returns an + HTML anchor. + + This method is naive and simply returns a path. Securing access to + the actual view and limiting who can view this object is the developer's + responsibility. + + Returns: + path (str): URI path to object detail page, if defined. + + """ + try: + return reverse( + 'help-entry-detail', + kwargs={"category": slugify(self.help_category), "topic": slugify(self.key)}, + ) + except Exception as e: + log_info(f'Exception: {getattr(e, "message", repr(e))}') + return "#" + def client_width(self): """ Get the client screenwidth for the session using this command. diff --git a/evennia/help/filehelp.py b/evennia/help/filehelp.py index 772d804fc..c5752ca1e 100644 --- a/evennia/help/filehelp.py +++ b/evennia/help/filehelp.py @@ -67,11 +67,14 @@ An example of the contents of a module: from dataclasses import dataclass from django.conf import settings +from django.urls import reverse +from django.utils.text import slugify 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 +from evennia.utils.logger import log_info _DEFAULT_HELP_CATEGORY = settings.DEFAULT_HELP_CATEGORY @@ -115,6 +118,43 @@ class FileHelpEntry: def locks(self): return LockHandler(self) + def web_get_detail_url(self): + """ + Returns the URI path for a View that allows users to view details for + this object. + + ex. Oscar (Character) = '/characters/oscar/1/' + + For this to work, the developer must have defined a named view somewhere + in urls.py that follows the format 'modelname-action', so in this case + a named view of 'character-detail' would be referenced by this method. + + ex. + :: + url(r'characters/(?P[\w\d\-]+)/(?P[0-9]+)/$', + CharDetailView.as_view(), name='character-detail') + + If no View has been created and defined in urls.py, returns an + HTML anchor. + + This method is naive and simply returns a path. Securing access to + the actual view and limiting who can view this object is the developer's + responsibility. + + Returns: + path (str): URI path to object detail page, if defined. + + """ + # log_info('filehelp web_get_detail_url start') + try: + return reverse( + 'help-entry-detail', + kwargs={"category": slugify(self.help_category), "topic": slugify(self.key)}, + ) + except Exception as e: + log_info(f'Exception: {getattr(e, "message", repr(e))}') + return "#" + def access(self, accessing_obj, access_type="view", default=True): """ Determines if another object has permission to access this help entry. @@ -207,3 +247,5 @@ class FileHelpStorageHandler: # singleton to hold the loaded help entries FILE_HELP_ENTRIES = FileHelpStorageHandler() +# Used by Django Sites/Admin +#get_absolute_url = web_get_detail_url diff --git a/evennia/help/models.py b/evennia/help/models.py index 2671687b3..f7285aed7 100644 --- a/evennia/help/models.py +++ b/evennia/help/models.py @@ -19,6 +19,7 @@ from evennia.help.manager import HelpEntryManager from evennia.typeclasses.models import Tag, TagHandler, AliasHandler from evennia.locks.lockhandler import LockHandler from evennia.utils.utils import lazy_property +from evennia.utils.logger import log_info __all__ = ("HelpEntry",) @@ -221,11 +222,14 @@ class HelpEntry(SharedMemoryModel): path (str): URI path to object detail page, if defined. """ + try: - return reverse( + url = reverse( "%s-detail" % slugify(self._meta.verbose_name), kwargs={"category": slugify(self.db_help_category), "topic": slugify(self.db_key)}, ) + # log_info(f'HelpEntry web_get_detail_url url: {url}') + return url except Exception: return "#" diff --git a/evennia/web/website/tests.py b/evennia/web/website/tests.py index a4633f51f..7f0b41df9 100644 --- a/evennia/web/website/tests.py +++ b/evennia/web/website/tests.py @@ -135,6 +135,17 @@ class ChannelDetailTest(EvenniaWebTest): return {"slug": slugify("demo")} +class HelpListTest(EvenniaWebTest): + url_name = "help" + + +class HelpDetailTest(EvenniaWebTest): + url_name = "help-entry-detail" + + def get_kwargs(self): + return {"category": slugify("general"), + "topic": slugify("test-key")} + class CharacterCreateView(EvenniaWebTest): url_name = "character-create" unauthenticated_response = 302 diff --git a/evennia/web/website/views/help.py b/evennia/web/website/views/help.py index afa439064..3f1e6f460 100644 --- a/evennia/web/website/views/help.py +++ b/evennia/web/website/views/help.py @@ -2,14 +2,152 @@ Views to manipulate help entries. """ - +from dataclasses import dataclass from django.utils.text import slugify +from django.conf import settings +from evennia.utils.utils import inherits_from from django.views.generic import ListView from django.http import HttpResponseBadRequest from django.db.models.functions import Lower from evennia.help.models import HelpEntry +from evennia.help.filehelp import FILE_HELP_ENTRIES from .mixins import TypeclassMixin, EvenniaDetailView +from django.views.generic import DetailView +from evennia.utils.logger import log_info +DEFAULT_HELP_CATEGORY = settings.DEFAULT_HELP_CATEGORY + +def get_help_category(help_entry): + if hasattr(help_entry, 'help_category'): + return help_entry.help_category + elif hasattr(help_entry, 'category'): + return help_entry.category + elif hasattr(help_entry, 'db_help_category'): + return help_entry.db_help_category + else: + return 'unsorted' + +def get_help_topic(help_entry): + topic = getattr(help_entry, 'key', False) + if not topic: + getattr(help_entry, 'db_key', False) + # log_info(f'get_help_topic returning: {topic}') + return topic + +def can_read_topic(cmd_or_topic, caller): + """ + 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_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. + 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"): + return cmd_or_topic.auto_help and cmd_or_topic.access(caller, 'read', default=True) + else: + return cmd_or_topic.access(caller, 'read', default=True) + +def can_list_topic(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 'should_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_or_topic (Command, HelpEntry or FileHelpEntry): The topic/command to test. + caller: the caller checking for access. + Returns: + 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. + """ + 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(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 + + # get Character's primary command set. + cmdset = caller.cmdset.get()[0] + + # 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 can_list_topic(cmd, caller)} + db_help_topics = { + key: entry for key, entry in db_help_topics.items() + if can_list_topic(entry, caller) + } + file_help_topics = { + key: entry for key, entry in file_help_topics.items() + if 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 can_read_topic(cmd, caller)} + db_help_topics = { + key: entry for key, entry in db_help_topics.items() + if can_read_topic(entry, caller) + } + file_help_topics = { + key: entry for key, entry in file_help_topics.items() + if can_read_topic(entry, caller)} + + return cmd_help_topics, db_help_topics, file_help_topics class HelpMixin(TypeclassMixin): """ @@ -35,22 +173,36 @@ class HelpMixin(TypeclassMixin): queryset (QuerySet): List of Help entries available to the user. """ + log_info('get_queryset') account = self.request.user + all_entries = [] + if not str(account) == 'AnonymousUser': + # collect all help entries + cmd_help_topics, db_help_topics, file_help_topics = \ + collect_topics(account.db._playable_characters[0], mode='query') + # combine and sort all the help entries + file_db_help_topics = {**file_help_topics, **db_help_topics} + all_topics = {**file_db_help_topics, **cmd_help_topics} + all_entries = list(all_topics.values()) + all_entries.sort(key=get_help_category) + # log_info(f'{all_entries}') + log_info('get_queryset success') + return all_entries - # Get list of all HelpEntries - entries = self.typeclass.objects.all().iterator() - - # Now figure out which ones the current user is allowed to see - bucket = [entry.id for entry in entries if entry.access(account, "view")] - - # Re-query and set a sorted list - filtered = ( - self.typeclass.objects.filter(id__in=bucket) - .order_by(Lower("db_key")) - .order_by(Lower("db_help_category")) - ) - - return filtered + def get_entries(self): + account = self.request.user + all_entries = [] + if not str(account) == 'AnonymousUser': + # collect all help entries + cmd_help_topics, db_help_topics, file_help_topics = \ + collect_topics(account.db._playable_characters[0], mode='query') + # combine and sort all the help entries + file_db_help_topics = {**file_help_topics, **db_help_topics} + all_topics = {**file_db_help_topics, **cmd_help_topics} + all_entries = list(all_topics.values()) + all_entries.sort(key=get_help_category) + # log_info(f'{all_entries}') + return all_entries class HelpListView(HelpMixin, ListView): @@ -68,7 +220,7 @@ class HelpListView(HelpMixin, ListView): page_title = "Help Index" -class HelpDetailView(HelpMixin, EvenniaDetailView): +class HelpDetailView(HelpMixin, DetailView): """ Returns the detail page for a given help entry. @@ -77,6 +229,14 @@ class HelpDetailView(HelpMixin, EvenniaDetailView): # -- Django constructs -- template_name = "website/help_detail.html" + @property + def page_title(self): + # Makes sure the page has a sensible title. + #return "%s Detail" % self.typeclass._meta.verbose_name.title() + obj = self.get_object() + topic = get_help_topic(obj) + return f'{topic} detail' + def get_context_data(self, **kwargs): """ Adds navigational data to the template to let browsers go to the next @@ -86,21 +246,26 @@ class HelpDetailView(HelpMixin, EvenniaDetailView): context (dict): Django context object """ + log_info('get_context_data') context = super().get_context_data(**kwargs) # Get the object in question obj = self.get_object() # Get queryset and filter out non-related categories - queryset = ( - self.get_queryset() - .filter(db_help_category=obj.db_help_category) - .order_by(Lower("db_key")) - ) - context["topic_list"] = queryset + full_set = self.get_queryset() + obj_topic = get_help_category(obj) + topic_set = [] + for entry in full_set: + entry_topic = get_help_category(entry) + if entry_topic.lower() == obj_topic.lower(): + topic_set.append(entry) + context["topic_list"] = topic_set + + # log_info(f'topic_set: {topic_set}') # Find the index position of the given obj in the queryset - objs = list(queryset) + objs = list(topic_set) for i, x in enumerate(objs): if obj is x: break @@ -119,12 +284,18 @@ class HelpDetailView(HelpMixin, EvenniaDetailView): context["topic_previous"] = None # Format the help entry using HTML instead of newlines - text = obj.db_entrytext + text = 'Failed to find entry.' + if inherits_from(obj, "evennia.commands.command.Command"): + text = obj.__doc__ + elif inherits_from(obj, "evennia.help.models.HelpEntry"): + text = obj.db_entrytext + elif inherits_from(obj, "evennia.help.filehelp.FileHelpEntry"): + text = obj.entrytext text = text.replace("\r\n\r\n", "\n\n") text = text.replace("\r\n", "\n") text = text.replace("\n", "
") context["entry_text"] = text - + log_info('get_context_data success') return context def get_object(self, queryset=None): @@ -136,27 +307,33 @@ class HelpDetailView(HelpMixin, EvenniaDetailView): entry (HelpEntry): HelpEntry requested in the URL. """ + log_info('get_object start') # Get the queryset for the help entries the user can access if not queryset: queryset = self.get_queryset() - # Find the object in the queryset + # get the category and topic requested category = slugify(self.kwargs.get("category", "")) topic = slugify(self.kwargs.get("topic", "")) - obj = next( - ( - x - for x in queryset - if slugify(x.db_help_category) == category and slugify(x.db_key) == topic - ), - None, - ) + + # Find the object in the queryset + obj = None + for entry in queryset: + # continue to next entry if the topics do not match + entry_topic = get_help_topic(entry) + if not entry_topic.lower() == topic.replace('-', ' '): + continue + # if the category also matches, object requested is found + entry_category = get_help_category(entry) + if entry_category.lower() == category.replace('-', ' '): + obj = entry + break # Check if this object was requested in a valid manner if not obj: return HttpResponseBadRequest( - "No %(verbose_name)s found matching the query" - % {"verbose_name": queryset.model._meta.verbose_name} + f"No ({category}/{topic})s found matching the query" ) + log_info(f'get_obj returning {obj}') return obj From 86bd79d7ec35c5c0d68e4711eb5dca7b88755bfe Mon Sep 17 00:00:00 2001 From: davewiththenicehat <54369722+davewiththenicehat@users.noreply.github.com> Date: Fri, 4 Jun 2021 17:35:50 -0400 Subject: [PATCH 02/18] DetailView sub title bread crumbs addded DetailView sub title bread crumbs addded for command and file help --- evennia/help/models.py | 6 ++++ .../web/templates/website/help_detail.html | 30 +++++++++---------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/evennia/help/models.py b/evennia/help/models.py index f7285aed7..6577dd7c8 100644 --- a/evennia/help/models.py +++ b/evennia/help/models.py @@ -57,6 +57,9 @@ class HelpEntry(SharedMemoryModel): db_key = models.CharField( "help key", max_length=255, unique=True, help_text="key to search for" ) + @lazy_property + def key(self): + return self.db_key # help category db_help_category = models.CharField( "help category", @@ -64,6 +67,9 @@ class HelpEntry(SharedMemoryModel): default="General", help_text="organizes help entries in lists", ) + @lazy_property + def help_category(self): + return self.db_help_category # the actual help entry text, in any formatting. db_entrytext = models.TextField( "help entry", blank=True, help_text="the main body of help text" diff --git a/evennia/web/templates/website/help_detail.html b/evennia/web/templates/website/help_detail.html index dd2a7d677..b91162f87 100644 --- a/evennia/web/templates/website/help_detail.html +++ b/evennia/web/templates/website/help_detail.html @@ -9,16 +9,16 @@ {% load addclass %}
- - + +

{{ view.page_title }} ({{ object|title }})



@@ -26,7 +26,7 @@

{{ entry_text }}

- + {% if topic_previous or topic_next %}
@@ -37,7 +37,7 @@ Previous ({{ topic_previous|title }}) {% endif %} - + {% if topic_next %}
  • Next ({{ topic_next|title }}) @@ -47,40 +47,40 @@ {% endif %} - +
  • - +
    - + {% if request.user.is_staff %} Admin
    {% endif %} - +
    {{ object.db_help_category|title }}
    - + - +
    - +

    - +
    - +
    {% endblock %} From 6d7755976e3010a38f294aac2a994989e10fc83b Mon Sep 17 00:00:00 2001 From: davewiththenicehat <54369722+davewiththenicehat@users.noreply.github.com> Date: Sat, 5 Jun 2021 08:32:38 -0400 Subject: [PATCH 03/18] card header fix card header fix --- evennia/web/templates/website/help_detail.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/web/templates/website/help_detail.html b/evennia/web/templates/website/help_detail.html index b91162f87..4c1f51d0c 100644 --- a/evennia/web/templates/website/help_detail.html +++ b/evennia/web/templates/website/help_detail.html @@ -62,7 +62,7 @@ {% endif %}
    -
    {{ object.db_help_category|title }}
    +
    {{ object.help_category|title }}
      {% for topic in topic_list %} From 2e351b7fab636a7df8e09a31adac6570bbb412e9 Mon Sep 17 00:00:00 2001 From: davewiththenicehat <54369722+davewiththenicehat@users.noreply.github.com> Date: Sat, 5 Jun 2021 10:06:33 -0400 Subject: [PATCH 04/18] ListView category grouping fix --- evennia/web/templates/website/help_list.html | 30 +++++----- evennia/web/website/views/help.py | 62 ++++++++++++-------- 2 files changed, 53 insertions(+), 39 deletions(-) diff --git a/evennia/web/templates/website/help_list.html b/evennia/web/templates/website/help_list.html index b8264cead..c5dbf9284 100644 --- a/evennia/web/templates/website/help_list.html +++ b/evennia/web/templates/website/help_list.html @@ -18,12 +18,12 @@
      - {% regroup object_list by help_category as category_list %} - + {% regroup object_list by web_help_category as category_list %} + {% if category_list %}
      - +
      @@ -33,23 +33,23 @@

      - +
      - {% for help_category in category_list %} -
      {{ help_category.grouper|title }}
      + {% for web_help_category in category_list %} +
      {{ web_help_category.grouper|title }}
        - {% for object in help_category.list %} + {% for object in web_help_category.list %}
      • {{ object|title }}
      • {% endfor %}
      {% endfor %}
      - +
      - +
      {% if user.is_staff %} @@ -58,16 +58,16 @@
      {% endif %} - +
      Category Index
      - + - +
      @@ -81,14 +81,14 @@

      You're missing out on an opportunity to attract visitors (and potentially new players) to {{ game_name }}!

      Use Evennia's Help System to tell the world about the universe you've created, its lore and legends, its people and creatures, and their customs and conflicts!

      You don't even need coding skills-- writing Help Entries is no more complicated than writing an email or blog post. Once you publish your first entry, these ugly boxes go away and this page will turn into an index of everything you've written about {{ game_name }}.

      -

      The documentation you write is eventually picked up by search engines, so the more you write about how {{ game_name }} works, the larger your web presence will be-- and the more traffic you'll attract. +

      The documentation you write is eventually picked up by search engines, so the more you write about how {{ game_name }} works, the larger your web presence will be-- and the more traffic you'll attract.

      Everything you write can be viewed either on this site or within the game itself, using the in-game help commands.


      Click here to start writing about {{ game_name }}!

      {% endif %} - +
      {% endif %} - +

    diff --git a/evennia/web/website/views/help.py b/evennia/web/website/views/help.py index 3f1e6f460..e2ceae23c 100644 --- a/evennia/web/website/views/help.py +++ b/evennia/web/website/views/help.py @@ -2,30 +2,38 @@ Views to manipulate help entries. """ -from dataclasses import dataclass from django.utils.text import slugify from django.conf import settings from evennia.utils.utils import inherits_from -from django.views.generic import ListView +from django.views.generic import ListView, DetailView from django.http import HttpResponseBadRequest -from django.db.models.functions import Lower from evennia.help.models import HelpEntry from evennia.help.filehelp import FILE_HELP_ENTRIES -from .mixins import TypeclassMixin, EvenniaDetailView -from django.views.generic import DetailView +from .mixins import TypeclassMixin from evennia.utils.logger import log_info DEFAULT_HELP_CATEGORY = settings.DEFAULT_HELP_CATEGORY -def get_help_category(help_entry): - if hasattr(help_entry, 'help_category'): - return help_entry.help_category - elif hasattr(help_entry, 'category'): - return help_entry.category - elif hasattr(help_entry, 'db_help_category'): - return help_entry.db_help_category - else: - return 'unsorted' + +def get_help_category(help_entry, slugify_cat=True): + """Returns help category. + + Args: + help_entry (HelpEntry, FileHelpEntry or Command): Help entry instance. + slugify_cat (bool): If true the return string is slugified. Default is True. + + Notes: + If the entry does not have attribute 'web_help_entries'. One is created with + a slugified copy of the attribute help_category. + This attribute is used for sorting the entries for the help index (ListView) page. + + Returns: + help_category (str): The category for the help entry. + """ + if not hasattr(help_entry, 'web_help_category'): + setattr(help_entry, 'web_help_category', slugify(help_entry.help_category)) + return slugify(help_entry.help_category) if slugify_cat else help_entry.help_category + def get_help_topic(help_entry): topic = getattr(help_entry, 'key', False) @@ -34,6 +42,7 @@ def get_help_topic(help_entry): # log_info(f'get_help_topic returning: {topic}') return topic + def can_read_topic(cmd_or_topic, caller): """ Helper method. If this return True, the given help topic @@ -54,6 +63,7 @@ def can_read_topic(cmd_or_topic, caller): else: return cmd_or_topic.access(caller, 'read', default=True) + def can_list_topic(cmd_or_topic, caller): """ Should the specified command appear in the help table? @@ -85,6 +95,7 @@ def can_list_topic(cmd_or_topic, caller): # no explicit 'view' lock - use the 'read' lock return cmd_or_topic.access(caller, 'read', default=True) + def collect_topics(caller, mode='list'): """ Collect help topics from all sources (cmd/db/file). @@ -149,6 +160,7 @@ def collect_topics(caller, mode='list'): return cmd_help_topics, db_help_topics, file_help_topics + class HelpMixin(TypeclassMixin): """ This is a "mixin", a modifier of sorts. @@ -180,11 +192,13 @@ class HelpMixin(TypeclassMixin): # collect all help entries cmd_help_topics, db_help_topics, file_help_topics = \ collect_topics(account.db._playable_characters[0], mode='query') - # combine and sort all the help entries + # merge the entries file_db_help_topics = {**file_help_topics, **db_help_topics} all_topics = {**file_db_help_topics, **cmd_help_topics} all_entries = list(all_topics.values()) - all_entries.sort(key=get_help_category) + # sort the entries + all_entries = sorted(all_entries, key=get_help_topic) # sort alphabetically + all_entries.sort(key=get_help_category) # group by categories # log_info(f'{all_entries}') log_info('get_queryset success') return all_entries @@ -254,18 +268,18 @@ class HelpDetailView(HelpMixin, DetailView): # Get queryset and filter out non-related categories full_set = self.get_queryset() - obj_topic = get_help_category(obj) - topic_set = [] + obj_category = get_help_category(obj) + category_set = [] for entry in full_set: - entry_topic = get_help_category(entry) - if entry_topic.lower() == obj_topic.lower(): - topic_set.append(entry) - context["topic_list"] = topic_set + entry_category = get_help_category(entry) + if entry_category.lower() == obj_category.lower(): + category_set.append(entry) + context["topic_list"] = category_set - # log_info(f'topic_set: {topic_set}') + # log_info(f'category_set: {category_set}') # Find the index position of the given obj in the queryset - objs = list(topic_set) + objs = list(category_set) for i, x in enumerate(objs): if obj is x: break From 4c0fcfbaf679e1747eef90e20f89b50687bb3b75 Mon Sep 17 00:00:00 2001 From: davewiththenicehat <54369722+davewiththenicehat@users.noreply.github.com> Date: Sat, 5 Jun 2021 10:42:09 -0400 Subject: [PATCH 05/18] DetailView entry text spacing & ascii fix --- evennia/web/templates/website/help_detail.html | 2 +- evennia/web/website/views/help.py | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/evennia/web/templates/website/help_detail.html b/evennia/web/templates/website/help_detail.html index 4c1f51d0c..9da5ad3a1 100644 --- a/evennia/web/templates/website/help_detail.html +++ b/evennia/web/templates/website/help_detail.html @@ -25,7 +25,7 @@
    -

    {{ entry_text }}

    +
    {{ entry_text }}
    {% if topic_previous or topic_next %}
    diff --git a/evennia/web/website/views/help.py b/evennia/web/website/views/help.py index e2ceae23c..e9dbb8be4 100644 --- a/evennia/web/website/views/help.py +++ b/evennia/web/website/views/help.py @@ -1,6 +1,8 @@ """ Views to manipulate help entries. +Multi entry object type supported added by DaveWithTheNiceHat 2021 + Pull Request #2429 """ from django.utils.text import slugify from django.conf import settings @@ -11,6 +13,7 @@ from evennia.help.models import HelpEntry from evennia.help.filehelp import FILE_HELP_ENTRIES from .mixins import TypeclassMixin from evennia.utils.logger import log_info +from evennia.utils.ansi import strip_ansi DEFAULT_HELP_CATEGORY = settings.DEFAULT_HELP_CATEGORY @@ -305,10 +308,8 @@ class HelpDetailView(HelpMixin, DetailView): text = obj.db_entrytext elif inherits_from(obj, "evennia.help.filehelp.FileHelpEntry"): text = obj.entrytext - text = text.replace("\r\n\r\n", "\n\n") - text = text.replace("\r\n", "\n") - text = text.replace("\n", "
    ") - context["entry_text"] = text + text = strip_ansi(text) # remove ansii markups + context["entry_text"] = text.strip() log_info('get_context_data success') return context From 5caaf149f43cce1b138d75de166ce2a13565e88d Mon Sep 17 00:00:00 2001 From: davewiththenicehat <54369722+davewiththenicehat@users.noreply.github.com> Date: Sat, 5 Jun 2021 19:24:27 -0400 Subject: [PATCH 06/18] account plus all puppets commands help entries --- evennia/web/website/views/help.py | 94 +++++++++++++++---------------- 1 file changed, 46 insertions(+), 48 deletions(-) diff --git a/evennia/web/website/views/help.py b/evennia/web/website/views/help.py index e9dbb8be4..3b8c1faa3 100644 --- a/evennia/web/website/views/help.py +++ b/evennia/web/website/views/help.py @@ -99,11 +99,11 @@ def can_list_topic(cmd_or_topic, caller): return cmd_or_topic.access(caller, 'read', default=True) -def collect_topics(caller, mode='list'): +def collect_topics(account, mode='list'): """ Collect help topics from all sources (cmd/db/file). Args: - caller (Object or Account): The user of the Command. + account (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. @@ -113,17 +113,34 @@ def collect_topics(caller, mode='list'): `({key: cmd,...}, {key: dbentry,...}, {key: fileentry,...}` """ # start with cmd-help - - # get Character's primary command set. - cmdset = caller.cmdset.get()[0] - - # 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')] + cmd_help_topics = [] + if not str(account) == 'AnonymousUser': + # create list of account and account's puppets + puppets = account.db._playable_characters + [account] + # add the account's and puppets' commands to cmd_help_topics list + for puppet in puppets: + for cmdset in puppet.cmdset.get(): + # removing doublets in cmdset, caused by cmdhandler + # having to allow doublet commands to manage exits etc. + cmdset.make_unique(puppet) + # retrieve all available commands and database / file-help topics. + # also check the 'cmd:' lock here + for cmd in cmdset: + # skip the command if the puppet does not have access + if not cmd.access(puppet, 'cmd'): + continue + # skip the command if it's help entry already exists in the topic list + entry_exists = False + for verify_cmd in cmd_help_topics: + if verify_cmd.key and cmd.key and \ + verify_cmd.help_category == cmd.help_category and \ + verify_cmd.__doc__ == cmd.__doc__: + entry_exists = True + break + if entry_exists: + continue + # add this command to the list + cmd_help_topics.append(cmd) # get all file-based help entries, checking perms file_help_topics = { topic.key.lower().strip(): topic @@ -139,27 +156,27 @@ def collect_topics(caller, mode='list'): 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 can_list_topic(cmd, caller)} + for cmd in cmd_help_topics if can_list_topic(cmd, account)} db_help_topics = { key: entry for key, entry in db_help_topics.items() - if can_list_topic(entry, caller) + if can_list_topic(entry, account) } file_help_topics = { key: entry for key, entry in file_help_topics.items() - if can_list_topic(entry, caller)} + if can_list_topic(entry, account)} 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 can_read_topic(cmd, caller)} + for cmd in cmd_help_topics if can_read_topic(cmd, account)} db_help_topics = { key: entry for key, entry in db_help_topics.items() - if can_read_topic(entry, caller) + if can_read_topic(entry, account) } file_help_topics = { key: entry for key, entry in file_help_topics.items() - if can_read_topic(entry, caller)} + if can_read_topic(entry, account)} return cmd_help_topics, db_help_topics, file_help_topics @@ -190,37 +207,18 @@ class HelpMixin(TypeclassMixin): """ log_info('get_queryset') account = self.request.user - all_entries = [] - if not str(account) == 'AnonymousUser': - # collect all help entries - cmd_help_topics, db_help_topics, file_help_topics = \ - collect_topics(account.db._playable_characters[0], mode='query') - # merge the entries - file_db_help_topics = {**file_help_topics, **db_help_topics} - all_topics = {**file_db_help_topics, **cmd_help_topics} - all_entries = list(all_topics.values()) - # sort the entries - all_entries = sorted(all_entries, key=get_help_topic) # sort alphabetically - all_entries.sort(key=get_help_category) # group by categories - # log_info(f'{all_entries}') + # collect all help entries + cmd_help_topics, db_help_topics, file_help_topics = collect_topics(account, mode='query') + # merge the entries + file_db_help_topics = {**file_help_topics, **db_help_topics} + all_topics = {**file_db_help_topics, **cmd_help_topics} + all_entries = list(all_topics.values()) + # sort the entries + all_entries = sorted(all_entries, key=get_help_topic) # sort alphabetically + all_entries.sort(key=get_help_category) # group by categories log_info('get_queryset success') return all_entries - def get_entries(self): - account = self.request.user - all_entries = [] - if not str(account) == 'AnonymousUser': - # collect all help entries - cmd_help_topics, db_help_topics, file_help_topics = \ - collect_topics(account.db._playable_characters[0], mode='query') - # combine and sort all the help entries - file_db_help_topics = {**file_help_topics, **db_help_topics} - all_topics = {**file_db_help_topics, **cmd_help_topics} - all_entries = list(all_topics.values()) - all_entries.sort(key=get_help_category) - # log_info(f'{all_entries}') - return all_entries - class HelpListView(HelpMixin, ListView): """ @@ -249,7 +247,7 @@ class HelpDetailView(HelpMixin, DetailView): @property def page_title(self): # Makes sure the page has a sensible title. - #return "%s Detail" % self.typeclass._meta.verbose_name.title() + # return "%s Detail" % self.typeclass._meta.verbose_name.title() obj = self.get_object() topic = get_help_topic(obj) return f'{topic} detail' From 4d9308b1d0636b1dfcaadefbdd0bd74d547b3cb3 Mon Sep 17 00:00:00 2001 From: davewiththenicehat <54369722+davewiththenicehat@users.noreply.github.com> Date: Sat, 5 Jun 2021 21:00:19 -0400 Subject: [PATCH 07/18] cleanup A lot of misc cleanup. Docstring updates. Removed code that is not required. --- evennia/web/website/views/help.py | 278 +++++++++++++----------------- 1 file changed, 121 insertions(+), 157 deletions(-) diff --git a/evennia/web/website/views/help.py b/evennia/web/website/views/help.py index 3b8c1faa3..c6a5b6c9f 100644 --- a/evennia/web/website/views/help.py +++ b/evennia/web/website/views/help.py @@ -11,8 +11,6 @@ from django.views.generic import ListView, DetailView from django.http import HttpResponseBadRequest from evennia.help.models import HelpEntry from evennia.help.filehelp import FILE_HELP_ENTRIES -from .mixins import TypeclassMixin -from evennia.utils.logger import log_info from evennia.utils.ansi import strip_ansi DEFAULT_HELP_CATEGORY = settings.DEFAULT_HELP_CATEGORY @@ -26,162 +24,126 @@ def get_help_category(help_entry, slugify_cat=True): slugify_cat (bool): If true the return string is slugified. Default is True. Notes: - If the entry does not have attribute 'web_help_entries'. One is created with - a slugified copy of the attribute help_category. - This attribute is used for sorting the entries for the help index (ListView) page. + If an entry does not have a `help_category` attribute, DEFAULT_HELP_CATEGORY from the + settings file is used. + If the entry does not have attribute 'web_help_entries'. One is created with a slugified + copy of the attribute help_category. This attribute is used for sorting the entries for the + help index (ListView) page. Returns: help_category (str): The category for the help entry. """ + help_category = help_entry.help_category if help_entry.help_category else DEFAULT_HELP_CATEGORY if not hasattr(help_entry, 'web_help_category'): - setattr(help_entry, 'web_help_category', slugify(help_entry.help_category)) - return slugify(help_entry.help_category) if slugify_cat else help_entry.help_category + setattr(help_entry, 'web_help_category', slugify(help_category)) + return slugify(help_category) if slugify_cat else help_category def get_help_topic(help_entry): - topic = getattr(help_entry, 'key', False) - if not topic: - getattr(help_entry, 'db_key', False) - # log_info(f'get_help_topic returning: {topic}') - return topic + """Get the topic of the help entry. + Args: + help_entry (HelpEntry, FileHelpEntry or Command): Help entry instance. -def can_read_topic(cmd_or_topic, caller): - """ - 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_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. - 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"): - return cmd_or_topic.auto_help and cmd_or_topic.access(caller, 'read', default=True) - else: - return cmd_or_topic.access(caller, 'read', default=True) - - -def can_list_topic(cmd_or_topic, caller): + Returns: + topic (str): The topic of the help entry. Default is 'unknown_topic'. """ - 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 'should_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. + return getattr(help_entry, 'key', 'unknown_topic') + + +def can_read_topic(cmd_or_topic, account): + """Check if an account or puppet has read access to a help entry. + Args: cmd_or_topic (Command, HelpEntry or FileHelpEntry): The topic/command to test. - caller: the caller checking for access. + account: the account or puppet checking for access. + Returns: - bool: If command should be listed or not. + bool: If command can be viewed 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. + This uses the 'read' lock. If no 'read' lock is defined, the topic is assumed readable + by all. + Even if this returns False, the entry will still be visible in the help index unless + `can_list_topic` is also returning False. """ - 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) + if inherits_from(cmd_or_topic, "evennia.commands.command.Command"): + return cmd_or_topic.auto_help and cmd_or_topic.access(account, 'read', default=True) else: - # no explicit 'view' lock - use the 'read' lock - return cmd_or_topic.access(caller, 'read', default=True) + return cmd_or_topic.access(account, 'read', default=True) -def collect_topics(account, mode='list'): - """ - Collect help topics from all sources (cmd/db/file). - Args: - account (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 - cmd_help_topics = [] - if not str(account) == 'AnonymousUser': - # create list of account and account's puppets - puppets = account.db._playable_characters + [account] - # add the account's and puppets' commands to cmd_help_topics list - for puppet in puppets: - for cmdset in puppet.cmdset.get(): - # removing doublets in cmdset, caused by cmdhandler - # having to allow doublet commands to manage exits etc. - cmdset.make_unique(puppet) - # retrieve all available commands and database / file-help topics. - # also check the 'cmd:' lock here - for cmd in cmdset: - # skip the command if the puppet does not have access - if not cmd.access(puppet, 'cmd'): - continue - # skip the command if it's help entry already exists in the topic list - entry_exists = False - for verify_cmd in cmd_help_topics: - if verify_cmd.key and cmd.key and \ - verify_cmd.help_category == cmd.help_category and \ - verify_cmd.__doc__ == cmd.__doc__: - entry_exists = True - break - if entry_exists: - continue - # add this command to the list - cmd_help_topics.append(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 can_list_topic(cmd, account)} - db_help_topics = { - key: entry for key, entry in db_help_topics.items() - if can_list_topic(entry, account) - } - file_help_topics = { - key: entry for key, entry in file_help_topics.items() - if can_list_topic(entry, account)} - 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 can_read_topic(cmd, account)} - db_help_topics = { - key: entry for key, entry in db_help_topics.items() - if can_read_topic(entry, account) - } - file_help_topics = { - key: entry for key, entry in file_help_topics.items() - if can_read_topic(entry, account)} +def collect_topics(account): + """Collect help topics from all sources (cmd/db/file). - return cmd_help_topics, db_help_topics, file_help_topics + Args: + account (Character or Account): Account or Character to collect help topics from. + + Returns: + cmd_help_topics (dict): contains Command instances. + db_help_topics (dict): contains HelpEntry instances. + file_help_topics (dict): contains FileHelpEntry instances + """ + + # collect commands of account and all puppets + # skip a command if an entry is recorded with the same topics, category and help entry + cmd_help_topics = [] + if not str(account) == 'AnonymousUser': + # create list of account and account's puppets + puppets = account.db._playable_characters + [account] + # add the account's and puppets' commands to cmd_help_topics list + for puppet in puppets: + for cmdset in puppet.cmdset.get(): + # removing doublets in cmdset, caused by cmdhandler + # having to allow doublet commands to manage exits etc. + cmdset.make_unique(puppet) + # retrieve all available commands and database / file-help topics. + # also check the 'cmd:' lock here + for cmd in cmdset: + # skip the command if the puppet does not have access + if not cmd.access(puppet, 'cmd'): + continue + # skip the command if the puppet does not have read access + if not can_read_topic(cmd, puppet): + continue + # skip the command if it's help entry already exists in the topic list + entry_exists = False + for verify_cmd in cmd_help_topics: + if verify_cmd.key and cmd.key and \ + verify_cmd.help_category == cmd.help_category and \ + verify_cmd.__doc__ == cmd.__doc__: + entry_exists = True + break + if entry_exists: + continue + # add this command to the list + cmd_help_topics.append(cmd) + + # Verify account has read access to filehelp entries and gather them into a dictionary + file_help_topics = { + topic.key.lower().strip(): topic + for topic in FILE_HELP_ENTRIES.all() + if can_read_topic(topic, account) + } + + # Verify account has read access to database entries and gather them into a dictionary + db_help_topics = { + topic.key.lower().strip(): topic + for topic in HelpEntry.objects.all() + if can_read_topic(topic, account) + } + + # Collect commands into a dictionary, read access verified at puppet level + 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 + } + + return cmd_help_topics, db_help_topics, file_help_topics -class HelpMixin(TypeclassMixin): +class HelpMixin(): """ This is a "mixin", a modifier of sorts. @@ -202,21 +164,23 @@ class HelpMixin(TypeclassMixin): and other documentation that the current user is allowed to see. Returns: - queryset (QuerySet): List of Help entries available to the user. + queryset (list): List of Help entries available to the user. """ - log_info('get_queryset') account = self.request.user + # collect all help entries - cmd_help_topics, db_help_topics, file_help_topics = collect_topics(account, mode='query') + cmd_help_topics, db_help_topics, file_help_topics = collect_topics(account) + # merge the entries file_db_help_topics = {**file_help_topics, **db_help_topics} all_topics = {**file_db_help_topics, **cmd_help_topics} all_entries = list(all_topics.values()) + # sort the entries all_entries = sorted(all_entries, key=get_help_topic) # sort alphabetically all_entries.sort(key=get_help_category) # group by categories - log_info('get_queryset success') + return all_entries @@ -242,12 +206,12 @@ class HelpDetailView(HelpMixin, DetailView): """ # -- Django constructs -- + # the html template to use when contructing the detail page for a help topic template_name = "website/help_detail.html" @property def page_title(self): # Makes sure the page has a sensible title. - # return "%s Detail" % self.typeclass._meta.verbose_name.title() obj = self.get_object() topic = get_help_topic(obj) return f'{topic} detail' @@ -261,14 +225,15 @@ class HelpDetailView(HelpMixin, DetailView): context (dict): Django context object """ - log_info('get_context_data') context = super().get_context_data(**kwargs) - # Get the object in question - obj = self.get_object() - - # Get queryset and filter out non-related categories + # get a full query set full_set = self.get_queryset() + + # Get the object in question + obj = self.get_object(full_set) + + # filter non related caegories from the query set obj_category = get_help_category(obj) category_set = [] for entry in full_set: @@ -277,9 +242,7 @@ class HelpDetailView(HelpMixin, DetailView): category_set.append(entry) context["topic_list"] = category_set - # log_info(f'category_set: {category_set}') - - # Find the index position of the given obj in the queryset + # Find the index position of the given obj in the category set objs = list(category_set) for i, x in enumerate(objs): if obj is x: @@ -298,7 +261,7 @@ class HelpDetailView(HelpMixin, DetailView): except: context["topic_previous"] = None - # Format the help entry using HTML instead of newlines + # Get the help entry text text = 'Failed to find entry.' if inherits_from(obj, "evennia.commands.command.Command"): text = obj.__doc__ @@ -308,7 +271,7 @@ class HelpDetailView(HelpMixin, DetailView): text = obj.entrytext text = strip_ansi(text) # remove ansii markups context["entry_text"] = text.strip() - log_info('get_context_data success') + return context def get_object(self, queryset=None): @@ -316,11 +279,13 @@ class HelpDetailView(HelpMixin, DetailView): Override of Django hook that retrieves an object by category and topic instead of pk and slug. + Args: + queryset (list): A list of help entry objects. + Returns: - entry (HelpEntry): HelpEntry requested in the URL. + entry (HelpEntry, FileHelpEntry or Command): HelpEntry requested in the URL. """ - log_info('get_object start') # Get the queryset for the help entries the user can access if not queryset: queryset = self.get_queryset() @@ -334,19 +299,18 @@ class HelpDetailView(HelpMixin, DetailView): for entry in queryset: # continue to next entry if the topics do not match entry_topic = get_help_topic(entry) - if not entry_topic.lower() == topic.replace('-', ' '): + if not slugify(entry_topic) == topic: continue # if the category also matches, object requested is found entry_category = get_help_category(entry) - if entry_category.lower() == category.replace('-', ' '): + if slugify(entry_category) == category: obj = entry break # Check if this object was requested in a valid manner if not obj: return HttpResponseBadRequest( - f"No ({category}/{topic})s found matching the query" + f"No ({category}/{topic})s found matching the query." ) - log_info(f'get_obj returning {obj}') return obj From 3bcdff2c3457febca88264b5f5b561fe287f5f4b Mon Sep 17 00:00:00 2001 From: davewiththenicehat <54369722+davewiththenicehat@users.noreply.github.com> Date: Sat, 5 Jun 2021 21:10:39 -0400 Subject: [PATCH 08/18] get_help_category bugfix --- evennia/web/website/views/help.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/web/website/views/help.py b/evennia/web/website/views/help.py index c6a5b6c9f..f7d596a14 100644 --- a/evennia/web/website/views/help.py +++ b/evennia/web/website/views/help.py @@ -33,7 +33,7 @@ def get_help_category(help_entry, slugify_cat=True): Returns: help_category (str): The category for the help entry. """ - help_category = help_entry.help_category if help_entry.help_category else DEFAULT_HELP_CATEGORY + help_category = getattr(help_entry, 'help_category', DEFAULT_HELP_CATEGORY) if not hasattr(help_entry, 'web_help_category'): setattr(help_entry, 'web_help_category', slugify(help_category)) return slugify(help_category) if slugify_cat else help_category From 4cd721ad79ee40e11e719b659677c3583fc45e4d Mon Sep 17 00:00:00 2001 From: davewiththenicehat <54369722+davewiththenicehat@users.noreply.github.com> Date: Sat, 5 Jun 2021 21:18:49 -0400 Subject: [PATCH 09/18] cleanup --- evennia/help/filehelp.py | 5 +---- evennia/help/models.py | 5 ++++- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/evennia/help/filehelp.py b/evennia/help/filehelp.py index c5752ca1e..c4d2d8b71 100644 --- a/evennia/help/filehelp.py +++ b/evennia/help/filehelp.py @@ -74,7 +74,6 @@ from evennia.utils.utils import ( from evennia.utils import logger from evennia.utils.utils import lazy_property from evennia.locks.lockhandler import LockHandler -from evennia.utils.logger import log_info _DEFAULT_HELP_CATEGORY = settings.DEFAULT_HELP_CATEGORY @@ -145,14 +144,12 @@ class FileHelpEntry: path (str): URI path to object detail page, if defined. """ - # log_info('filehelp web_get_detail_url start') try: return reverse( 'help-entry-detail', kwargs={"category": slugify(self.help_category), "topic": slugify(self.key)}, ) - except Exception as e: - log_info(f'Exception: {getattr(e, "message", repr(e))}') + except Exception: return "#" def access(self, accessing_obj, access_type="view", default=True): diff --git a/evennia/help/models.py b/evennia/help/models.py index 6577dd7c8..88b54e340 100644 --- a/evennia/help/models.py +++ b/evennia/help/models.py @@ -19,7 +19,6 @@ from evennia.help.manager import HelpEntryManager from evennia.typeclasses.models import Tag, TagHandler, AliasHandler from evennia.locks.lockhandler import LockHandler from evennia.utils.utils import lazy_property -from evennia.utils.logger import log_info __all__ = ("HelpEntry",) @@ -57,9 +56,11 @@ class HelpEntry(SharedMemoryModel): db_key = models.CharField( "help key", max_length=255, unique=True, help_text="key to search for" ) + @lazy_property def key(self): return self.db_key + # help category db_help_category = models.CharField( "help category", @@ -67,9 +68,11 @@ class HelpEntry(SharedMemoryModel): default="General", help_text="organizes help entries in lists", ) + @lazy_property def help_category(self): return self.db_help_category + # the actual help entry text, in any formatting. db_entrytext = models.TextField( "help entry", blank=True, help_text="the main body of help text" From b92ebcafe1e1b083f618edfb4123e99365ca4016 Mon Sep 17 00:00:00 2001 From: davewiththenicehat <54369722+davewiththenicehat@users.noreply.github.com> Date: Tue, 8 Jun 2021 08:40:31 -0400 Subject: [PATCH 10/18] HelpDetailView unit test --- evennia/web/website/tests.py | 39 +++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/evennia/web/website/tests.py b/evennia/web/website/tests.py index 7f0b41df9..3afb842d5 100644 --- a/evennia/web/website/tests.py +++ b/evennia/web/website/tests.py @@ -3,6 +3,7 @@ from django.utils.text import slugify from django.test import Client, override_settings from django.urls import reverse from evennia.utils import class_from_module +from evennia.utils.create import create_help_entry from evennia.utils.test_resources import EvenniaTest @@ -142,9 +143,45 @@ class HelpListTest(EvenniaWebTest): class HelpDetailTest(EvenniaWebTest): url_name = "help-entry-detail" + def setUp(self): + super().setUp() + create_help_entry('unit test entry', 'unit test entry text', category="General") + def get_kwargs(self): return {"category": slugify("general"), - "topic": slugify("test-key")} + "topic": slugify('unit test entry')} + + def test_view(self): + response = self.client.get(reverse(self.url_name, kwargs=self.get_kwargs()), follow=True) + self.assertEqual(response.context["entry_text"], 'unit test entry text') + + +class HelpLockedDetailTest(EvenniaWebTest): + url_name = "help-entry-detail" + + def setUp(self): + super(HelpLockedDetailTest, self).setUp() + + # create a db entry with a lock + self.db_help_entry = create_help_entry('unit test locked topic', 'unit test locked entrytext', + category="General", locks='read:perm(Developer)') + + def get_kwargs(self): + return {"category": slugify("general"), + "topic": slugify('unit test locked topic')} + + def test_locked_entry(self): + # request access to an entry for permission the account does not have + response = self.client.get(reverse(self.url_name, kwargs=self.get_kwargs()), follow=True) + self.assertEqual(response.context["entry_text"], 'Failed to find entry.') + + def test_lock_with_perm(self): + # log TestAccount in, grant permission required, read the entry + self.login() + self.account.permissions.add("Developer") + response = self.client.get(reverse(self.url_name, kwargs=self.get_kwargs()), follow=True) + self.assertEqual(response.context["entry_text"], 'unit test locked entrytext') + class CharacterCreateView(EvenniaWebTest): url_name = "character-create" From b42456c83cdbd6b6476dea0da3115653d66a9fb1 Mon Sep 17 00:00:00 2001 From: davewiththenicehat <54369722+davewiththenicehat@users.noreply.github.com> Date: Tue, 8 Jun 2021 08:45:06 -0400 Subject: [PATCH 11/18] HelpMixin, model attr no longer required --- evennia/web/website/views/help.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/evennia/web/website/views/help.py b/evennia/web/website/views/help.py index f7d596a14..187515635 100644 --- a/evennia/web/website/views/help.py +++ b/evennia/web/website/views/help.py @@ -152,9 +152,6 @@ class HelpMixin(): """ - # -- Django constructs -- - model = HelpEntry - # -- Evennia constructs -- page_title = "Help" From 7336957e9d8aa62a65dfc6f76bd6125e3290842c Mon Sep 17 00:00:00 2001 From: davewiththenicehat <54369722+davewiththenicehat@users.noreply.github.com> Date: Fri, 11 Jun 2021 09:27:37 -0400 Subject: [PATCH 12/18] HelpDetailView object cache Created a cache for the object (target of the help request) in a HelpDetailView. This reduces the call of DetailView.get_object by three or four times. Which also reduces the number of DetailView.get_queryset calls by the same amount. Unit test created. To verify requesting a new object does not return the result from a previous request. --- evennia/web/website/tests.py | 35 ++++++++++++++++++++++++++++--- evennia/web/website/views/help.py | 7 +++++++ 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/evennia/web/website/tests.py b/evennia/web/website/tests.py index 3afb842d5..5e52c16f9 100644 --- a/evennia/web/website/tests.py +++ b/evennia/web/website/tests.py @@ -5,6 +5,9 @@ from django.urls import reverse from evennia.utils import class_from_module from evennia.utils.create import create_help_entry from evennia.utils.test_resources import EvenniaTest +from evennia.help import filehelp + +_FILE_HELP_ENTRIES = None class EvenniaWebTest(EvenniaTest): @@ -140,20 +143,46 @@ class HelpListTest(EvenniaWebTest): url_name = "help" +HELP_ENTRY_DICTS = [ + { + "key": "unit test file entry", + "category": "General", + "text": "cache test file entry text" + } +] + class HelpDetailTest(EvenniaWebTest): url_name = "help-entry-detail" def setUp(self): super().setUp() - create_help_entry('unit test entry', 'unit test entry text', category="General") + + # create a db help entry + create_help_entry('unit test db entry', 'unit test db entry text', category="General") def get_kwargs(self): return {"category": slugify("general"), - "topic": slugify('unit test entry')} + "topic": slugify('unit test db entry')} def test_view(self): response = self.client.get(reverse(self.url_name, kwargs=self.get_kwargs()), follow=True) - self.assertEqual(response.context["entry_text"], 'unit test entry text') + self.assertEqual(response.context["entry_text"], 'unit test db entry text') + + def test_object_cache(self): + # clear file help entries, use local HELP_ENTRY_DICTS to recreate new entries + global _FILE_HELP_ENTRIES + if _FILE_HELP_ENTRIES is None: + from evennia.help.filehelp import FILE_HELP_ENTRIES as _FILE_HELP_ENTRIES + help_module = 'evennia.web.website.tests' + self.file_help_store = _FILE_HELP_ENTRIES.__init__(help_file_modules=[help_module]) + + # request access to an entry + response = self.client.get(reverse(self.url_name, kwargs=self.get_kwargs()), follow=True) + self.assertEqual(response.context["entry_text"], 'unit test db entry text') + # request a second entry, verifing the cached object is not provided on a new topic request + entry_two_args = {"category": slugify("general"), "topic": slugify('unit test file entry')} + response = self.client.get(reverse(self.url_name, kwargs=entry_two_args), follow=True) + self.assertEqual(response.context["entry_text"], 'cache test file entry text') class HelpLockedDetailTest(EvenniaWebTest): diff --git a/evennia/web/website/views/help.py b/evennia/web/website/views/help.py index 187515635..e0923ec24 100644 --- a/evennia/web/website/views/help.py +++ b/evennia/web/website/views/help.py @@ -283,6 +283,10 @@ class HelpDetailView(HelpMixin, DetailView): entry (HelpEntry, FileHelpEntry or Command): HelpEntry requested in the URL. """ + + if hasattr(self, 'obj'): + return getattr(self, 'obj', None) + # Get the queryset for the help entries the user can access if not queryset: queryset = self.get_queryset() @@ -309,5 +313,8 @@ class HelpDetailView(HelpMixin, DetailView): return HttpResponseBadRequest( f"No ({category}/{topic})s found matching the query." ) + else: + # cache the object if one was found + self.obj = obj return obj From 787ef8c068fc30881f14741ef811fc17a62b677c Mon Sep 17 00:00:00 2001 From: davewiththenicehat <54369722+davewiththenicehat@users.noreply.github.com> Date: Tue, 15 Jun 2021 16:54:41 -0400 Subject: [PATCH 13/18] text cleanup All evennia.web.website.tests pass. --- evennia/commands/command.py | 2 -- evennia/help/filehelp.py | 2 -- evennia/help/models.py | 1 - evennia/web/website/views/help.py | 2 +- 4 files changed, 1 insertion(+), 6 deletions(-) diff --git a/evennia/commands/command.py b/evennia/commands/command.py index 4cc764cc6..0adf89bd1 100644 --- a/evennia/commands/command.py +++ b/evennia/commands/command.py @@ -16,7 +16,6 @@ from evennia.locks.lockhandler import LockHandler from evennia.utils.utils import is_iter, fill, lazy_property, make_iter from evennia.utils.evtable import EvTable from evennia.utils.ansi import ANSIString -from evennia.utils.logger import log_info class InterruptCommand(Exception): @@ -550,7 +549,6 @@ Command {self} has no defined `func()` - showing on-command variables: kwargs={"category": slugify(self.help_category), "topic": slugify(self.key)}, ) except Exception as e: - log_info(f'Exception: {getattr(e, "message", repr(e))}') return "#" def client_width(self): diff --git a/evennia/help/filehelp.py b/evennia/help/filehelp.py index c4d2d8b71..bb28fd63e 100644 --- a/evennia/help/filehelp.py +++ b/evennia/help/filehelp.py @@ -244,5 +244,3 @@ class FileHelpStorageHandler: # singleton to hold the loaded help entries FILE_HELP_ENTRIES = FileHelpStorageHandler() -# Used by Django Sites/Admin -#get_absolute_url = web_get_detail_url diff --git a/evennia/help/models.py b/evennia/help/models.py index 88b54e340..d3323266c 100644 --- a/evennia/help/models.py +++ b/evennia/help/models.py @@ -237,7 +237,6 @@ class HelpEntry(SharedMemoryModel): "%s-detail" % slugify(self._meta.verbose_name), kwargs={"category": slugify(self.db_help_category), "topic": slugify(self.db_key)}, ) - # log_info(f'HelpEntry web_get_detail_url url: {url}') return url except Exception: return "#" diff --git a/evennia/web/website/views/help.py b/evennia/web/website/views/help.py index e0923ec24..23e6bcd27 100644 --- a/evennia/web/website/views/help.py +++ b/evennia/web/website/views/help.py @@ -230,7 +230,7 @@ class HelpDetailView(HelpMixin, DetailView): # Get the object in question obj = self.get_object(full_set) - # filter non related caegories from the query set + # filter non related categories from the query set obj_category = get_help_category(obj) category_set = [] for entry in full_set: From a661e4801b154878fb1282263b26a4d3742fb39b Mon Sep 17 00:00:00 2001 From: davewiththenicehat <54369722+davewiththenicehat@users.noreply.github.com> Date: Tue, 15 Jun 2021 17:11:51 -0400 Subject: [PATCH 14/18] text update All evennia.web.website.tests pass. --- evennia/web/website/views/help.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/evennia/web/website/views/help.py b/evennia/web/website/views/help.py index 23e6bcd27..04a293367 100644 --- a/evennia/web/website/views/help.py +++ b/evennia/web/website/views/help.py @@ -109,9 +109,11 @@ def collect_topics(account): # skip the command if it's help entry already exists in the topic list entry_exists = False for verify_cmd in cmd_help_topics: - if verify_cmd.key and cmd.key and \ - verify_cmd.help_category == cmd.help_category and \ - verify_cmd.__doc__ == cmd.__doc__: + if ( + verify_cmd.key and cmd.key and + verify_cmd.help_category == cmd.help_category and + verify_cmd.__doc__ == cmd.__doc__ + ): entry_exists = True break if entry_exists: From 8eb7ffa70bdd52507234afdda423b9a7363b1f31 Mon Sep 17 00:00:00 2001 From: davewiththenicehat <54369722+davewiththenicehat@users.noreply.github.com> Date: Wed, 16 Jun 2021 08:59:44 -0400 Subject: [PATCH 15/18] sethelp db help entry creation fix, @laxy_property removed evennia.web.website.tests all pass evennia.commands.default.tests.TestHelp all pass (sethelp command test is in that unit test) --- evennia/help/models.py | 11 +---------- evennia/web/templates/website/help_detail.html | 6 +++--- evennia/web/website/views/help.py | 16 +++++++++++++--- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/evennia/help/models.py b/evennia/help/models.py index d3323266c..76391699e 100644 --- a/evennia/help/models.py +++ b/evennia/help/models.py @@ -57,10 +57,6 @@ class HelpEntry(SharedMemoryModel): "help key", max_length=255, unique=True, help_text="key to search for" ) - @lazy_property - def key(self): - return self.db_key - # help category db_help_category = models.CharField( "help category", @@ -69,10 +65,6 @@ class HelpEntry(SharedMemoryModel): help_text="organizes help entries in lists", ) - @lazy_property - def help_category(self): - return self.db_help_category - # the actual help entry text, in any formatting. db_entrytext = models.TextField( "help entry", blank=True, help_text="the main body of help text" @@ -233,11 +225,10 @@ class HelpEntry(SharedMemoryModel): """ try: - url = reverse( + return reverse( "%s-detail" % slugify(self._meta.verbose_name), kwargs={"category": slugify(self.db_help_category), "topic": slugify(self.db_key)}, ) - return url except Exception: return "#" diff --git a/evennia/web/templates/website/help_detail.html b/evennia/web/templates/website/help_detail.html index 9da5ad3a1..a5ed6045e 100644 --- a/evennia/web/templates/website/help_detail.html +++ b/evennia/web/templates/website/help_detail.html @@ -17,8 +17,8 @@

    @@ -62,7 +62,7 @@ {% endif %}
    -
    {{ object.help_category|title }}
    +
    {{ object.web_help_category|title }}
      {% for topic in topic_list %} diff --git a/evennia/web/website/views/help.py b/evennia/web/website/views/help.py index 04a293367..caa1fd911 100644 --- a/evennia/web/website/views/help.py +++ b/evennia/web/website/views/help.py @@ -33,7 +33,10 @@ def get_help_category(help_entry, slugify_cat=True): Returns: help_category (str): The category for the help entry. """ - help_category = getattr(help_entry, 'help_category', DEFAULT_HELP_CATEGORY) + help_category = getattr(help_entry, 'help_category', None) + if not help_category: + help_category = getattr(help_entry, 'db_help_category', DEFAULT_HELP_CATEGORY) + # if one does not exist, create a category for ease of use with web views html templates if not hasattr(help_entry, 'web_help_category'): setattr(help_entry, 'web_help_category', slugify(help_category)) return slugify(help_category) if slugify_cat else help_category @@ -46,9 +49,16 @@ def get_help_topic(help_entry): help_entry (HelpEntry, FileHelpEntry or Command): Help entry instance. Returns: - topic (str): The topic of the help entry. Default is 'unknown_topic'. + help_topic (str): The topic of the help entry. Default is 'unknown_topic'. """ - return getattr(help_entry, 'key', 'unknown_topic') + help_topic = getattr(help_entry, 'key', None) + # if object has no key, assume it is a db help entry. + if not help_topic: + help_topic = getattr(help_entry, 'db_key', 'unknown_topic') + # if one does not exist, create a key for ease of use with web views html templates + if not hasattr(help_entry, 'web_help_key'): + setattr(help_entry, 'web_help_key', slugify(help_topic)) + return help_topic def can_read_topic(cmd_or_topic, account): From c5043c3a222a796cf93070de76527f6c28616d8b Mon Sep 17 00:00:00 2001 From: davewiththenicehat <54369722+davewiththenicehat@users.noreply.github.com> Date: Sat, 19 Jun 2021 18:13:13 -0400 Subject: [PATCH 16/18] edit link Provided an edit link if the help entry can be edited on the website and the user is staff. --- evennia/web/templates/website/help_list.html | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/evennia/web/templates/website/help_list.html b/evennia/web/templates/website/help_list.html index c5dbf9284..378b938d3 100644 --- a/evennia/web/templates/website/help_list.html +++ b/evennia/web/templates/website/help_list.html @@ -40,7 +40,14 @@
      {{ web_help_category.grouper|title }}
      {% endfor %} From 9b793fc845a960db182d66b89d0c95bf7faeeb9a Mon Sep 17 00:00:00 2001 From: davewiththenicehat <54369722+davewiththenicehat@users.noreply.github.com> Date: Sat, 19 Jun 2021 19:31:04 -0400 Subject: [PATCH 17/18] edit link goes to admin url for entry edit link goes to admin url for entry --- evennia/web/templates/website/help_list.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/evennia/web/templates/website/help_list.html b/evennia/web/templates/website/help_list.html index 378b938d3..ec9c07b2b 100644 --- a/evennia/web/templates/website/help_list.html +++ b/evennia/web/templates/website/help_list.html @@ -42,9 +42,9 @@ {% for object in web_help_category.list %}
    • {{ object|title }} - {% if object.web_get_update_url %} + {% if object.web_get_admin_url %} {% if user.is_staff %} - -edit- + -edit- {% endif %} {% endif %}
    • From 0866f54d7e6486c36407bfd0f898870497a68443 Mon Sep 17 00:00:00 2001 From: davewiththenicehat <54369722+davewiththenicehat@users.noreply.github.com> Date: Sun, 20 Jun 2021 09:16:23 -0400 Subject: [PATCH 18/18] command and filehelp addded web_get_admin_rul It was a very easy add. --- evennia/commands/command.py | 12 ++++++++++++ evennia/help/filehelp.py | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/evennia/commands/command.py b/evennia/commands/command.py index 0adf89bd1..a81f8b6ef 100644 --- a/evennia/commands/command.py +++ b/evennia/commands/command.py @@ -551,6 +551,18 @@ Command {self} has no defined `func()` - showing on-command variables: except Exception as e: return "#" + def web_get_admin_url(self): + """ + Returns the URI path for the Django Admin page for this object. + + ex. Account#1 = '/admin/accounts/accountdb/1/change/' + + Returns: + path (str): URI path to Django Admin page for object. + + """ + return False + def client_width(self): """ Get the client screenwidth for the session using this command. diff --git a/evennia/help/filehelp.py b/evennia/help/filehelp.py index bb28fd63e..4e0a9dac0 100644 --- a/evennia/help/filehelp.py +++ b/evennia/help/filehelp.py @@ -152,6 +152,18 @@ class FileHelpEntry: except Exception: return "#" + def web_get_admin_url(self): + """ + Returns the URI path for the Django Admin page for this object. + + ex. Account#1 = '/admin/accounts/accountdb/1/change/' + + Returns: + path (str): URI path to Django Admin page for object. + + """ + return False + def access(self, accessing_obj, access_type="view", default=True): """ Determines if another object has permission to access this help entry.