415 lines
16 KiB
Python
415 lines
16 KiB
Python
"""
|
|
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 helptext:
|
|
return 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)
|