Merge branch 'cmd_and_file_help_web_support' of https://github.com/davewiththenicehat/evennia into davewiththenicehat-cmd_and_file_help_web_support
This commit is contained in:
commit
d00915d092
7 changed files with 437 additions and 82 deletions
|
|
@ -9,6 +9,8 @@ import math
|
||||||
import inspect
|
import inspect
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils.text import slugify
|
||||||
|
|
||||||
from evennia.locks.lockhandler import LockHandler
|
from evennia.locks.lockhandler import LockHandler
|
||||||
from evennia.utils.utils import is_iter, fill, lazy_property, make_iter
|
from evennia.utils.utils import is_iter, fill, lazy_property, make_iter
|
||||||
|
|
@ -514,6 +516,53 @@ Command {self} has no defined `func()` - showing on-command variables:
|
||||||
"""
|
"""
|
||||||
return self.__doc__
|
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<slug>[\w\d\-]+)/(?P<pk>[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:
|
||||||
|
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):
|
def client_width(self):
|
||||||
"""
|
"""
|
||||||
Get the client screenwidth for the session using this command.
|
Get the client screenwidth for the session using this command.
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,8 @@ An example of the contents of a module:
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils.text import slugify
|
||||||
from evennia.utils.utils import (
|
from evennia.utils.utils import (
|
||||||
variable_from_module, make_iter, all_from_module)
|
variable_from_module, make_iter, all_from_module)
|
||||||
from evennia.utils import logger
|
from evennia.utils import logger
|
||||||
|
|
@ -115,6 +117,53 @@ class FileHelpEntry:
|
||||||
def locks(self):
|
def locks(self):
|
||||||
return LockHandler(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<slug>[\w\d\-]+)/(?P<pk>[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:
|
||||||
|
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):
|
def access(self, accessing_obj, access_type="view", default=True):
|
||||||
"""
|
"""
|
||||||
Determines if another object has permission to access this help entry.
|
Determines if another object has permission to access this help entry.
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@ class HelpEntry(SharedMemoryModel):
|
||||||
db_key = models.CharField(
|
db_key = models.CharField(
|
||||||
"help key", max_length=255, unique=True, help_text="key to search for"
|
"help key", max_length=255, unique=True, help_text="key to search for"
|
||||||
)
|
)
|
||||||
|
|
||||||
# help category
|
# help category
|
||||||
db_help_category = models.CharField(
|
db_help_category = models.CharField(
|
||||||
"help category",
|
"help category",
|
||||||
|
|
@ -63,6 +64,7 @@ class HelpEntry(SharedMemoryModel):
|
||||||
default="General",
|
default="General",
|
||||||
help_text="organizes help entries in lists",
|
help_text="organizes help entries in lists",
|
||||||
)
|
)
|
||||||
|
|
||||||
# the actual help entry text, in any formatting.
|
# the actual help entry text, in any formatting.
|
||||||
db_entrytext = models.TextField(
|
db_entrytext = models.TextField(
|
||||||
"help entry", blank=True, help_text="the main body of help text"
|
"help entry", blank=True, help_text="the main body of help text"
|
||||||
|
|
@ -221,6 +223,7 @@ class HelpEntry(SharedMemoryModel):
|
||||||
path (str): URI path to object detail page, if defined.
|
path (str): URI path to object detail page, if defined.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return reverse(
|
return reverse(
|
||||||
"%s-detail" % slugify(self._meta.verbose_name),
|
"%s-detail" % slugify(self._meta.verbose_name),
|
||||||
|
|
|
||||||
|
|
@ -17,15 +17,15 @@
|
||||||
<hr />
|
<hr />
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
<li class="breadcrumb-item"><a href="{% url 'help' %}">Help Index</a></li>
|
<li class="breadcrumb-item"><a href="{% url 'help' %}">Help Index</a></li>
|
||||||
<li class="breadcrumb-item"><a href="{% url 'help' %}#{{ object.db_help_category }}">{{ object.db_help_category|title }}</a></li>
|
<li class="breadcrumb-item"><a href="{% url 'help' %}#{{ object.web_help_category }}">{{ object.help_category|title }}</a></li>
|
||||||
<li class="breadcrumb-item active" aria-current="page">{{ object.db_key|title }}</li>
|
<li class="breadcrumb-item active" aria-current="page">{{ object.web_help_key|title }}</li>
|
||||||
</ol>
|
</ol>
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<!-- left column -->
|
<!-- left column -->
|
||||||
<div class="col-lg-9 col-sm-12">
|
<div class="col-lg-9 col-sm-12">
|
||||||
<p>{{ entry_text }}</p>
|
<pre>{{ entry_text }}</pre>
|
||||||
|
|
||||||
{% if topic_previous or topic_next %}
|
{% if topic_previous or topic_next %}
|
||||||
<hr />
|
<hr />
|
||||||
|
|
@ -62,7 +62,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="card mb-3">
|
<div class="card mb-3">
|
||||||
<div class="card-header">{{ object.db_help_category|title }}</div>
|
<div class="card-header">{{ object.web_help_category|title }}</div>
|
||||||
|
|
||||||
<ul class="list-group list-group-flush">
|
<ul class="list-group list-group-flush">
|
||||||
{% for topic in topic_list %}
|
{% for topic in topic_list %}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
</ol>
|
</ol>
|
||||||
<hr />
|
<hr />
|
||||||
<div class="row">
|
<div class="row">
|
||||||
{% regroup object_list by help_category as category_list %}
|
{% regroup object_list by web_help_category as category_list %}
|
||||||
|
|
||||||
{% if category_list %}
|
{% if category_list %}
|
||||||
<!-- left column -->
|
<!-- left column -->
|
||||||
|
|
@ -36,11 +36,18 @@
|
||||||
|
|
||||||
<!-- index list -->
|
<!-- index list -->
|
||||||
<div class="mx-3">
|
<div class="mx-3">
|
||||||
{% for help_category in category_list %}
|
{% for web_help_category in category_list %}
|
||||||
<h5><a id="{{ help_category.grouper }}"></a>{{ help_category.grouper|title }}</h5>
|
<h5><a id="{{ web_help_category.grouper }}"></a>{{ web_help_category.grouper|title }}</h5>
|
||||||
<ul>
|
<ul>
|
||||||
{% for object in help_category.list %}
|
{% for object in web_help_category.list %}
|
||||||
<li><a href="{{ object.web_get_detail_url }}">{{ object|title }}</a></li>
|
<li>
|
||||||
|
<a href="{{ object.web_get_detail_url }}">{{ object|title }}</a>
|
||||||
|
{% if object.web_get_admin_url %}
|
||||||
|
{% if user.is_staff %}
|
||||||
|
<a href="{{ object.web_get_admin_url }}">-edit-</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,11 @@ from django.utils.text import slugify
|
||||||
from django.test import Client, override_settings
|
from django.test import Client, override_settings
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from evennia.utils import class_from_module
|
from evennia.utils import class_from_module
|
||||||
|
from evennia.utils.create import create_help_entry
|
||||||
from evennia.utils.test_resources import EvenniaTest
|
from evennia.utils.test_resources import EvenniaTest
|
||||||
|
from evennia.help import filehelp
|
||||||
|
|
||||||
|
_FILE_HELP_ENTRIES = None
|
||||||
|
|
||||||
|
|
||||||
class EvenniaWebTest(EvenniaTest):
|
class EvenniaWebTest(EvenniaTest):
|
||||||
|
|
@ -135,6 +139,79 @@ class ChannelDetailTest(EvenniaWebTest):
|
||||||
return {"slug": slugify("demo")}
|
return {"slug": slugify("demo")}
|
||||||
|
|
||||||
|
|
||||||
|
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 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 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 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):
|
||||||
|
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):
|
class CharacterCreateView(EvenniaWebTest):
|
||||||
url_name = "character-create"
|
url_name = "character-create"
|
||||||
unauthenticated_response = 302
|
unauthenticated_response = 302
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,161 @@
|
||||||
"""
|
"""
|
||||||
Views to manipulate help entries.
|
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
|
||||||
|
from evennia.utils.utils import inherits_from
|
||||||
|
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 evennia.utils.ansi import strip_ansi
|
||||||
|
|
||||||
|
DEFAULT_HELP_CATEGORY = settings.DEFAULT_HELP_CATEGORY
|
||||||
|
|
||||||
|
|
||||||
|
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 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 = 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
|
||||||
|
|
||||||
|
|
||||||
|
def get_help_topic(help_entry):
|
||||||
|
"""Get the topic of the help entry.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
help_entry (HelpEntry, FileHelpEntry or Command): Help entry instance.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
help_topic (str): The topic of the help entry. Default is '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):
|
||||||
|
"""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.
|
||||||
|
account: the account or puppet 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.
|
||||||
|
Even if this returns False, the entry will still be visible in the help index unless
|
||||||
|
`can_list_topic` is also returning False.
|
||||||
|
"""
|
||||||
|
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:
|
||||||
|
return cmd_or_topic.access(account, 'read', default=True)
|
||||||
|
|
||||||
|
|
||||||
|
def collect_topics(account):
|
||||||
|
"""Collect help topics from all sources (cmd/db/file).
|
||||||
|
|
||||||
|
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
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from django.utils.text import slugify
|
# collect commands of account and all puppets
|
||||||
from django.views.generic import ListView
|
# skip a command if an entry is recorded with the same topics, category and help entry
|
||||||
from django.http import HttpResponseBadRequest
|
cmd_help_topics = []
|
||||||
from django.db.models.functions import Lower
|
if not str(account) == 'AnonymousUser':
|
||||||
from evennia.help.models import HelpEntry
|
# create list of account and account's puppets
|
||||||
from .mixins import TypeclassMixin, EvenniaDetailView
|
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.
|
This is a "mixin", a modifier of sorts.
|
||||||
|
|
||||||
|
|
@ -20,9 +164,6 @@ class HelpMixin(TypeclassMixin):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# -- Django constructs --
|
|
||||||
model = HelpEntry
|
|
||||||
|
|
||||||
# -- Evennia constructs --
|
# -- Evennia constructs --
|
||||||
page_title = "Help"
|
page_title = "Help"
|
||||||
|
|
||||||
|
|
@ -32,25 +173,24 @@ class HelpMixin(TypeclassMixin):
|
||||||
and other documentation that the current user is allowed to see.
|
and other documentation that the current user is allowed to see.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
queryset (QuerySet): List of Help entries available to the user.
|
queryset (list): List of Help entries available to the user.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
account = self.request.user
|
account = self.request.user
|
||||||
|
|
||||||
# Get list of all HelpEntries
|
# collect all help entries
|
||||||
entries = self.typeclass.objects.all().iterator()
|
cmd_help_topics, db_help_topics, file_help_topics = collect_topics(account)
|
||||||
|
|
||||||
# Now figure out which ones the current user is allowed to see
|
# merge the entries
|
||||||
bucket = [entry.id for entry in entries if entry.access(account, "view")]
|
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())
|
||||||
|
|
||||||
# Re-query and set a sorted list
|
# sort the entries
|
||||||
filtered = (
|
all_entries = sorted(all_entries, key=get_help_topic) # sort alphabetically
|
||||||
self.typeclass.objects.filter(id__in=bucket)
|
all_entries.sort(key=get_help_category) # group by categories
|
||||||
.order_by(Lower("db_key"))
|
|
||||||
.order_by(Lower("db_help_category"))
|
|
||||||
)
|
|
||||||
|
|
||||||
return filtered
|
return all_entries
|
||||||
|
|
||||||
|
|
||||||
class HelpListView(HelpMixin, ListView):
|
class HelpListView(HelpMixin, ListView):
|
||||||
|
|
@ -68,15 +208,23 @@ class HelpListView(HelpMixin, ListView):
|
||||||
page_title = "Help Index"
|
page_title = "Help Index"
|
||||||
|
|
||||||
|
|
||||||
class HelpDetailView(HelpMixin, EvenniaDetailView):
|
class HelpDetailView(HelpMixin, DetailView):
|
||||||
"""
|
"""
|
||||||
Returns the detail page for a given help entry.
|
Returns the detail page for a given help entry.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# -- Django constructs --
|
# -- Django constructs --
|
||||||
|
# the html template to use when contructing the detail page for a help topic
|
||||||
template_name = "website/help_detail.html"
|
template_name = "website/help_detail.html"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def page_title(self):
|
||||||
|
# Makes sure the page has a sensible title.
|
||||||
|
obj = self.get_object()
|
||||||
|
topic = get_help_topic(obj)
|
||||||
|
return f'{topic} detail'
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
Adds navigational data to the template to let browsers go to the next
|
Adds navigational data to the template to let browsers go to the next
|
||||||
|
|
@ -88,19 +236,23 @@ class HelpDetailView(HelpMixin, EvenniaDetailView):
|
||||||
"""
|
"""
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
# get a full query set
|
||||||
|
full_set = self.get_queryset()
|
||||||
|
|
||||||
# Get the object in question
|
# Get the object in question
|
||||||
obj = self.get_object()
|
obj = self.get_object(full_set)
|
||||||
|
|
||||||
# Get queryset and filter out non-related categories
|
# filter non related categories from the query set
|
||||||
queryset = (
|
obj_category = get_help_category(obj)
|
||||||
self.get_queryset()
|
category_set = []
|
||||||
.filter(db_help_category=obj.db_help_category)
|
for entry in full_set:
|
||||||
.order_by(Lower("db_key"))
|
entry_category = get_help_category(entry)
|
||||||
)
|
if entry_category.lower() == obj_category.lower():
|
||||||
context["topic_list"] = queryset
|
category_set.append(entry)
|
||||||
|
context["topic_list"] = 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(queryset)
|
objs = list(category_set)
|
||||||
for i, x in enumerate(objs):
|
for i, x in enumerate(objs):
|
||||||
if obj is x:
|
if obj is x:
|
||||||
break
|
break
|
||||||
|
|
@ -118,12 +270,16 @@ class HelpDetailView(HelpMixin, EvenniaDetailView):
|
||||||
except:
|
except:
|
||||||
context["topic_previous"] = None
|
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__
|
||||||
|
elif inherits_from(obj, "evennia.help.models.HelpEntry"):
|
||||||
text = obj.db_entrytext
|
text = obj.db_entrytext
|
||||||
text = text.replace("\r\n\r\n", "\n\n")
|
elif inherits_from(obj, "evennia.help.filehelp.FileHelpEntry"):
|
||||||
text = text.replace("\r\n", "\n")
|
text = obj.entrytext
|
||||||
text = text.replace("\n", "<br />")
|
text = strip_ansi(text) # remove ansii markups
|
||||||
context["entry_text"] = text
|
context["entry_text"] = text.strip()
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
@ -132,31 +288,45 @@ class HelpDetailView(HelpMixin, EvenniaDetailView):
|
||||||
Override of Django hook that retrieves an object by category and topic
|
Override of Django hook that retrieves an object by category and topic
|
||||||
instead of pk and slug.
|
instead of pk and slug.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
queryset (list): A list of help entry objects.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
entry (HelpEntry): HelpEntry requested in the URL.
|
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
|
# Get the queryset for the help entries the user can access
|
||||||
if not queryset:
|
if not queryset:
|
||||||
queryset = self.get_queryset()
|
queryset = self.get_queryset()
|
||||||
|
|
||||||
# Find the object in the queryset
|
# get the category and topic requested
|
||||||
category = slugify(self.kwargs.get("category", ""))
|
category = slugify(self.kwargs.get("category", ""))
|
||||||
topic = slugify(self.kwargs.get("topic", ""))
|
topic = slugify(self.kwargs.get("topic", ""))
|
||||||
obj = next(
|
|
||||||
(
|
# Find the object in the queryset
|
||||||
x
|
obj = None
|
||||||
for x in queryset
|
for entry in queryset:
|
||||||
if slugify(x.db_help_category) == category and slugify(x.db_key) == topic
|
# continue to next entry if the topics do not match
|
||||||
),
|
entry_topic = get_help_topic(entry)
|
||||||
None,
|
if not slugify(entry_topic) == topic:
|
||||||
)
|
continue
|
||||||
|
# if the category also matches, object requested is found
|
||||||
|
entry_category = get_help_category(entry)
|
||||||
|
if slugify(entry_category) == category:
|
||||||
|
obj = entry
|
||||||
|
break
|
||||||
|
|
||||||
# Check if this object was requested in a valid manner
|
# Check if this object was requested in a valid manner
|
||||||
if not obj:
|
if not obj:
|
||||||
return HttpResponseBadRequest(
|
return HttpResponseBadRequest(
|
||||||
"No %(verbose_name)s found matching the query"
|
f"No ({category}/{topic})s found matching the query."
|
||||||
% {"verbose_name": queryset.model._meta.verbose_name}
|
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
# cache the object if one was found
|
||||||
|
self.obj = obj
|
||||||
|
|
||||||
return obj
|
return obj
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue