Largely rewrote and refactored the help system.
The help entry database structure has changed! You have to resync or purge your database or your will get problems! New features: * Help entry access now fully controlled by evennia permissions * Categories for each help entry * All entries are created dynamically, with a See also: footer calculated after the current state of the database. * Indexes and topic list calculated on the fly (alphabetically/after category) * Added auto-help help entries for all default commands. * Only shows commands _actually implemented_ - MUX help db moved into 'MUX' category which is not shown by default. * More powerful auto-help markup - supports categories and permissions (and inheritance). * Global on/off switch for auto-help, when entering production * Auto_help_override switch for selectively activating auto-help when developing new commands (like the old system). * Refactored State help system; no more risk of overwriting global help entries. * State help now defers to main help db when no match found; makes system more transparent. * State help entries also support categories/permissions (state categories are not used much though). Other updates: * Added more commands to the batch processor * Many bug-fixes. /Griatch
This commit is contained in:
parent
46e2cd3ecb
commit
8074617285
27 changed files with 1995 additions and 1072 deletions
411
src/helpsys/helpsystem.py
Normal file
411
src/helpsys/helpsystem.py
Normal file
|
|
@ -0,0 +1,411 @@
|
|||
"""
|
||||
Support functions for the help system.
|
||||
Allows adding help to the data base from inside the mud as
|
||||
well as creating auto-docs of commands based on their doc strings.
|
||||
The system supports help-markup for multiple help entries as well
|
||||
as a dynamically updating help index.
|
||||
"""
|
||||
import textwrap
|
||||
from django.conf import settings
|
||||
from src.helpsys.models import HelpEntry
|
||||
from src import logger
|
||||
from src import defines_global
|
||||
|
||||
|
||||
class EditHelp(object):
|
||||
"""
|
||||
This sets up an object able to perform normal editing
|
||||
operations on the help database.
|
||||
"""
|
||||
def __init__(self, indent=4, width=70):
|
||||
"""
|
||||
We check if auto-help is active or not and
|
||||
set some formatting options.
|
||||
"""
|
||||
self.indent = indent # indentation of help text
|
||||
self.width = width # width of help text
|
||||
|
||||
def format_help_text(self, help_text):
|
||||
"""
|
||||
This formats the help entry text for proper left-side indentation.
|
||||
|
||||
The first line is adjusted to the proper indentation and the
|
||||
subsequent lines are then adjusted proportionally to the first;
|
||||
so indentation relative this first line remains intact.
|
||||
"""
|
||||
lines = help_text.expandtabs().splitlines()
|
||||
|
||||
# strip empty lines above and below the text
|
||||
while True:
|
||||
if lines and not lines[0].strip():
|
||||
lines.pop(0)
|
||||
else:
|
||||
break
|
||||
while True:
|
||||
if lines and not lines[-1].strip():
|
||||
lines.pop()
|
||||
else:
|
||||
break
|
||||
if not lines:
|
||||
return ""
|
||||
|
||||
# produce a list of the indentations of each line initially
|
||||
indentlist = [len(line) - len(line.lstrip()) for line in lines]
|
||||
|
||||
# use the first line to set the shift
|
||||
lineshift = indentlist[0] - self.indent
|
||||
|
||||
# shift everything to the left
|
||||
indentlist = [max(self.indent, indent-lineshift) for indent in indentlist]
|
||||
trimmed = []
|
||||
for il, line in enumerate(lines):
|
||||
indentstr = " " * indentlist[il]
|
||||
trimmed.append("%s%s" % (indentstr, line.strip()))
|
||||
return "\n".join(trimmed)
|
||||
|
||||
def parse_markup_header(self, subtopic_header):
|
||||
"""
|
||||
The possible markup headers for splitting the help into sections are:
|
||||
[[TopicTitle]]
|
||||
[[TopicTitle,category]]
|
||||
[[TopicTitle(perm1,perm2)]]
|
||||
[[TopicTitle,category(perm1,perm2)]]
|
||||
"""
|
||||
subtitle = ""
|
||||
subcategory = ""
|
||||
subpermissions = ()
|
||||
#identifying the header parts. The header can max have three parts:
|
||||
# topicname, category (perm1,perm2,...)
|
||||
try:
|
||||
# find the permission tuple
|
||||
lindex = subtopic_header.index('(')
|
||||
rindex = subtopic_header.index(')')
|
||||
if lindex < rindex:
|
||||
permtuple = subtopic_header[lindex+1:rindex]
|
||||
subpermissions = tuple([p.strip()
|
||||
for p in permtuple.split(',')])
|
||||
subtopic_header = subtopic_header[:lindex]
|
||||
except ValueError:
|
||||
# no permission tuple found
|
||||
pass
|
||||
# see if we have a name, category pair.
|
||||
try:
|
||||
subtitle, subcategory = subtopic_header.split(',')
|
||||
subtitle, subcategory = subtitle.strip(), subcategory.strip()
|
||||
except ValueError:
|
||||
subtitle = subtopic_header.strip()
|
||||
# we are done, return a tuple with the results
|
||||
return ( subtitle, subcategory, subpermissions )
|
||||
|
||||
def format_help_entry(self, helptopic, category, helptext, permissions=None):
|
||||
"""
|
||||
helptopic (string) - name of the full help entry
|
||||
helptext (string) - the help entry (may contain sections)
|
||||
permissions (tuple) - tuple with permission/group names
|
||||
defined for the entire help entry.
|
||||
(markup permissions override those)
|
||||
Handles help markup in order to split help into subsections.
|
||||
|
||||
These markup markers will be assumed to start a new line, regardless
|
||||
of where they are located in the help entry. If no permission string
|
||||
tuple and/or category is given, the overall permission/category of
|
||||
the entire help entry is used.
|
||||
"""
|
||||
# sanitize input
|
||||
topics = []
|
||||
if '[[' not in helptext:
|
||||
formatted_text = self.format_help_text(helptext)
|
||||
topics.append((helptopic, category,
|
||||
formatted_text, permissions))
|
||||
return topics
|
||||
|
||||
subtopics = helptext.split('[[')
|
||||
|
||||
if subtopics[0]:
|
||||
# the very first entry (before any markup) is the normal
|
||||
# help entry for the helptopic at hand.
|
||||
formatted_text = self.format_help_text(subtopics[0])
|
||||
topics.append((helptopic, category, formatted_text, permissions))
|
||||
|
||||
for subtopic in subtopics[1:]:
|
||||
# handle all extra topics designated with markup
|
||||
try:
|
||||
subtopic_header, subtopic_text = subtopic.split(']]', 1)
|
||||
except ValueError:
|
||||
# if we have no ending, the entry is malformed and
|
||||
# we ignore this entry (better than overwriting
|
||||
# something in the database).
|
||||
logger.log_errmsg("Malformed help markup in %s: '%s'\n (missing end ']]' )" % \
|
||||
(helptopic, subtopic))
|
||||
continue
|
||||
# parse and format the help entry parts
|
||||
subtopic_header = self.parse_markup_header(subtopic_header)
|
||||
if not subtopic_header[0]:
|
||||
# we require a topic title.
|
||||
logger.log_errmsg("Malformed help markup in '%s': Missing title." % subtopic_header)
|
||||
return
|
||||
# parse the header and use defaults
|
||||
subtopic_name = subtopic_header[0]
|
||||
subtopic_category = subtopic_header[1]
|
||||
subtopic_text = self.format_help_text(subtopic_text)
|
||||
subtopic_permissions = subtopic_header[2]
|
||||
if not subtopic_category:
|
||||
# no category set; inherit from main topic
|
||||
subtopic_category = category
|
||||
if not subtopic_permissions:
|
||||
# no permissions set; inherit from main topic
|
||||
subtopic_permissions = permissions
|
||||
|
||||
# We have a finished topic, add it to the list as a topic tuple.
|
||||
topics.append((subtopic_name, subtopic_category,
|
||||
subtopic_text, subtopic_permissions))
|
||||
return topics
|
||||
|
||||
def create_help(self, newtopic):
|
||||
"""
|
||||
Add a help entry to the database, replace an old one if it exists.
|
||||
topic (tuple) - this is a formatted tuple of data as prepared
|
||||
by format_help_entry, on the form (title, category, text, (perm_tuple))
|
||||
"""
|
||||
#sanity checks;
|
||||
topicname = newtopic[0]
|
||||
category = newtopic[1]
|
||||
entrytext = newtopic[2]
|
||||
permissions = newtopic[3]
|
||||
|
||||
if not (topicname or entrytext):
|
||||
# don't create anything if there we
|
||||
# are missing vital parts
|
||||
return
|
||||
if not category:
|
||||
# this will force the default
|
||||
category = "General"
|
||||
if permissions:
|
||||
# the permissions tuple might be mangled;
|
||||
# make sure we build a string properly.
|
||||
if type(permissions) != type(tuple()):
|
||||
permissions = "%s" % permissions
|
||||
else:
|
||||
permissions = ", ".join(permissions)
|
||||
else:
|
||||
permissions = ""
|
||||
|
||||
# check if the help topic already exist.
|
||||
oldtopic = HelpEntry.objects.filter(topicname__iexact=newtopic[0])
|
||||
if oldtopic:
|
||||
#replace an old help file
|
||||
topic = oldtopic[0]
|
||||
topic.category = category
|
||||
topic.entrytext = entrytext
|
||||
topic.canview = permissions
|
||||
topic.save()
|
||||
else:
|
||||
#we have a new topic - create a new help object
|
||||
new_entry = HelpEntry(topicname=topicname,
|
||||
category=category,
|
||||
entrytext=entrytext,
|
||||
canview=permissions)
|
||||
new_entry.save()
|
||||
|
||||
def add_help_auto(self, topicstr, category, entrytext, permissions=()):
|
||||
"""
|
||||
This is used by the auto_help system to add help one or more
|
||||
help entries to the system.
|
||||
"""
|
||||
# sanity checks
|
||||
if permissions and type(permissions) != type(tuple()):
|
||||
string = "Auto-Help: malformed perm_tuple %s: %s -> %s (fixed)" % \
|
||||
(topicstr,permissions, (permissions,))
|
||||
logger.log_errmsg(string)
|
||||
permissions = (permissions,)
|
||||
|
||||
# identify markup and do nice formatting as well as eventual
|
||||
# related entries to the help entries.
|
||||
logger.log_infomsg("auto-help in: %s %s %s %s" % (topicstr, category, entrytext, permissions))
|
||||
topics = self.format_help_entry(topicstr, category,
|
||||
entrytext, permissions)
|
||||
logger.log_infomsg("auto-help: %s -> %s" % (topicstr,topics))
|
||||
# create the help entries:
|
||||
if topics:
|
||||
for topic in topics:
|
||||
self.create_help(topic)
|
||||
|
||||
def add_help_manual(self, pobject, topicstr, category,
|
||||
entrytext, permissions=(), force=False):
|
||||
"""
|
||||
This is used when a player wants to add a help entry to the database
|
||||
manually (most often from inside the game)
|
||||
|
||||
force - this is given by the player and forces an overwrite also if the
|
||||
entry already exists or there are multiple similar matches to
|
||||
the entry.
|
||||
"""
|
||||
# permission check:
|
||||
if not (pobject.is_superuser() or pobject.has_perm("helpsys.add_help")):
|
||||
pobject.emit_to(defines_global.NOPERMS_MSG)
|
||||
return None
|
||||
# do a more fuzzy search to warn in case in case we are misspelling.
|
||||
topic = HelpEntry.objects.find_topicmatch(pobject, topicstr)
|
||||
if topic and not force:
|
||||
return topic
|
||||
self.add_help_auto(topicstr, category, entrytext, permissions)
|
||||
pobject.emit_to("Added/appended help topic '%s'." % topicstr)
|
||||
|
||||
def del_help_auto(self, topicstr):
|
||||
"""
|
||||
Delete a help entry from the data base. Automatic version.
|
||||
"""
|
||||
topic = HelpEntry.objects.filter(topicname__iexact=topicstr)
|
||||
if topic:
|
||||
topic[0].delete()
|
||||
|
||||
def del_help_manual(self, pobject, topicstr):
|
||||
"""
|
||||
Deletes an entry from the database. Interactive version.
|
||||
Note that it makes no sense to delete auto-added help entries this way since
|
||||
they will be re-added on the next @reload. This is mostly useful for cleaning
|
||||
the database from doublet or orphaned entries, or when auto-help is turned off.
|
||||
"""
|
||||
# find topic with permission checks
|
||||
if not (pobject.is_superuser() or pobject.has_perm("helpsys.del_help")):
|
||||
pobject.emit_to(defines_global.NOPERMS_MSG)
|
||||
return None
|
||||
topic = HelpEntry.objects.find_topicmatch(pobject, topicstr)
|
||||
if not topic or len(topic) > 1:
|
||||
return topic
|
||||
# we have an exact match. Delete topic.
|
||||
topic[0].delete()
|
||||
pobject.emit_to("Help entry '%s' deleted." % topicstr)
|
||||
|
||||
def homogenize_database(self, category):
|
||||
"""
|
||||
This sets the entire help database to one category.
|
||||
It can be used to mark an initially loaded help database
|
||||
in a particular category, for later filtering.
|
||||
|
||||
In evennia dev version, this is done with MUX help database.
|
||||
"""
|
||||
entries = HelpEntry.objects.all()
|
||||
for entry in entries:
|
||||
entry.category = category
|
||||
entry.save()
|
||||
logger.log_infomsg("Help database homogenized to category %s" % category)
|
||||
|
||||
def autoclean_database(self, topiclist):
|
||||
"""
|
||||
This syncs the entire help database against a reference topic
|
||||
list, deleting non-used or duplicate help entries that can be
|
||||
the result of auto-help misspellings etc.
|
||||
"""
|
||||
pass
|
||||
|
||||
class ViewHelp(object):
|
||||
"""
|
||||
This class contains ways to view the
|
||||
help database in a dynamical fashion.
|
||||
"""
|
||||
def __init__(self, indent=4, width=78, category_cols=4, entry_cols=6):
|
||||
"""
|
||||
indent (int) - number of spaces to indent tables with
|
||||
width (int) - width of index tables
|
||||
category_cols (int) - number of collumns per row for
|
||||
category tables
|
||||
entry_cols (int) - number of collumns per row for help entries
|
||||
"""
|
||||
self.width = width
|
||||
self.indent = indent
|
||||
self.category_cols = category_cols
|
||||
self.entry_cols = entry_cols
|
||||
self.show_related = settings.HELP_SHOW_RELATED
|
||||
|
||||
def make_table(self, items, cols):
|
||||
"""
|
||||
This takes a list of string items and displays them in collumn order,
|
||||
(sorted horizontally-first), ie
|
||||
A A A A
|
||||
A B B B
|
||||
B B C C
|
||||
C C
|
||||
cols is the number of collumns to format.
|
||||
"""
|
||||
items.sort()
|
||||
if not items or not cols:
|
||||
return []
|
||||
length = len(items)
|
||||
# split the list into sublists of length cols
|
||||
rows = [items[i:i+cols] for i in xrange(0, length, cols)]
|
||||
# build the table
|
||||
string = ""
|
||||
for row in rows:
|
||||
string += self.indent * " " + ", ".join(row) + "\n"
|
||||
return string
|
||||
|
||||
def index_full(self, pobject):
|
||||
"""
|
||||
This lists all available topics in the help index,
|
||||
ordered after category.
|
||||
|
||||
The MUX category is not shown, it is for development
|
||||
reference only.
|
||||
"""
|
||||
entries = HelpEntry.objects.all()
|
||||
|
||||
categories = [e.category for e in entries if e.category != 'MUX']
|
||||
categories = list(set(categories)) # make list unique
|
||||
categories.sort()
|
||||
table = ""
|
||||
for category in categories:
|
||||
topics = [e.topicname.lower() for e in entries.filter(category__iexact=category)
|
||||
if e.can_view(pobject)]
|
||||
|
||||
# pretty-printing the list
|
||||
header = "--- Topics in category %s:" % category
|
||||
nl = self.width - len(header)
|
||||
if not topics:
|
||||
text = self.indent*" " + "[There are no topics relevant to you in this category.]\n\r"
|
||||
else:
|
||||
text = self.make_table(topics, self.entry_cols)
|
||||
table += "\r\n%s%s\n\r\n\r%s" % (header, "-"*nl, text)
|
||||
return table
|
||||
|
||||
def index_categories(self):
|
||||
"""
|
||||
This lists all categories defined in the help index.
|
||||
"""
|
||||
entries = HelpEntry.objects.all()
|
||||
categories = [e.category for e in entries]
|
||||
categories = list(set(categories)) # make list unique
|
||||
return self.make_table(categories, self.category_cols)
|
||||
|
||||
def index_category(self, pobject, category):
|
||||
"""
|
||||
List the help entries within a certain category
|
||||
"""
|
||||
entries = HelpEntry.objects.find_topics_with_category(pobject, category)
|
||||
if not entries:
|
||||
return []
|
||||
# filter out those we can actually view
|
||||
helptopics = [e.topicname.lower() for e in entries if e.can_view(pobject)]
|
||||
if not helptopics:
|
||||
# we don't have permission to view anything in this category
|
||||
return " [There are no topics relevant to you in this category.]\n\r"
|
||||
return self.make_table(helptopics, self.entry_cols)
|
||||
|
||||
def suggest_help(self, pobject, topic):
|
||||
"""
|
||||
This goes through the help database, searching for relatively
|
||||
close matches to this topic. If those are found, they are
|
||||
added as a nice footer to the end of the topic entry.
|
||||
"""
|
||||
if not self.show_related:
|
||||
return None
|
||||
topicname = topic.topicname
|
||||
return HelpEntry.objects.find_topicsuggestions(pobject, topicname)
|
||||
|
||||
# Object instances
|
||||
edithelp = EditHelp(indent=3,
|
||||
width=80)
|
||||
viewhelp = ViewHelp(indent=3,
|
||||
width=80,
|
||||
category_cols=4,
|
||||
entry_cols=4)
|
||||
Loading…
Add table
Add a link
Reference in a new issue