Adds better help functionality

- expanded help command, allowing adding, deleting and appending to help the help database
 - auto-doc functionality using the __doc__ property of commands
 - markup in help text for creation of multiple help subtopics at once
 - dynamic help index (use 'help topic' to get the normal mux topic list)
 /Starkiel
This commit is contained in:
Griatch 2009-04-11 23:17:44 +00:00
parent 46f35bc574
commit 4cc8e57774
3 changed files with 424 additions and 34 deletions

View file

@ -15,6 +15,9 @@ settings.CUSTOM_COMMAND_MODULES. Each module imports cmdtable.py and runs
add_command on the command table each command belongs to.
"""
from src.helpsys.management.commands.edit_helpfiles import add_help
from src.ansi import ansi
class CommandTable(object):
"""
Stores command tables and performs lookups.
@ -26,7 +29,7 @@ class CommandTable(object):
self.ctable = {}
def add_command(self, command_string, function, priv_tuple=None,
extra_vals=None):
extra_vals=None, auto_help=False, staff_help=False):
"""
Adds a command to the command table.
@ -34,9 +37,30 @@ class CommandTable(object):
function: (reference) The command's function.
priv_tuple: (tuple) String tuple of permissions required for command.
extra_vals: (dict) Dictionary to add to the Command object.
Auto-help system:
auto_help (bool): If true, automatically creates/replaces a help topic with the
same name as the command_string, using the functions's __doc__ property
for help text.
staff_help (bool): Only relevant if help_auto is activated; It True, makes the
help topic (and all eventual subtopics) only visible to staff.
Note: the auto_help system also supports limited markup. If you divide your __doc__
with markers of the form <<TOPIC:MyTopic>>, the system will automatically create
separate help topics for each topic. Your initial text (if you define no TOPIC)
will still default to the name of your command.
You can also custon-set the staff_only flag for individual subtopics by
using the markup <<TOPIC:STAFF:MyTopic>> and <<TOPIC:NOSTAFF:MyTopic>>.
"""
self.ctable[command_string] = (function, priv_tuple, extra_vals)
if auto_help:
#add automatic help text from the command's doc string
topicstr = command_string
entrytext = function.__doc__
add_help(topicstr, entrytext, staff_only=staff_help,
force_create=True, auto_help=True)
def get_command_tuple(self, func_name):
"""
Returns a reference to the command's tuple. If there are no matches,
@ -50,4 +74,4 @@ Command tables
# Global command table, for authenticated users.
GLOBAL_CMD_TABLE = CommandTable()
# Global unconnected command table, for unauthenticated users.
GLOBAL_UNCON_CMD_TABLE = CommandTable()
GLOBAL_UNCON_CMD_TABLE = CommandTable()

View file

@ -11,6 +11,8 @@ from src import defines_global
from src import session_mgr
from src import ansi
from src.util import functions_general
import src.helpsys.management.commands.edit_helpfiles as edit_help
from src.cmdtable import GLOBAL_CMD_TABLE
def cmd_password(command):
@ -138,7 +140,7 @@ def cmd_look(command):
# SCRIPT: Get the item's appearance from the scriptlink.
source_object.emit_to(target_obj.scriptlink.return_appearance(pobject=source_object))
# SCRIPT: Call the object's script's a_desc() method.
# SCRIPT: Call the object's script's at_desc() method.
target_obj.scriptlink.at_desc(pobject=source_object)
GLOBAL_CMD_TABLE.add_command("look", cmd_look)
@ -273,17 +275,20 @@ def cmd_examine(command):
Player is examining an object. Return a full readout of attributes,
along with detailed information about said object.
"""
s = ""
newl = "\r\n"
# Format the examine header area with general flag/type info.
source_object.emit_to(target_obj.get_name(fullname=True))
source_object.emit_to("Type: %s Flags: %s" % (target_obj.get_type(),
target_obj.get_flags()))
source_object.emit_to("Desc: %s" % target_obj.get_description(no_parsing=True))
source_object.emit_to("Owner: %s " % (target_obj.get_owner(),))
source_object.emit_to("Zone: %s" % (target_obj.get_zone(),))
source_object.emit_to("Parent: %s " % target_obj.get_script_parent())
for attribute in target_obj.get_all_attributes():
source_object.emit_to(attribute.get_attrline())
s += str(target_obj.get_name(fullname=True)) + newl
s += str("Type: %s Flags: %s" % (target_obj.get_type(),
target_obj.get_flags())) + newl
s += str("Desc: %s" % target_obj.get_description(no_parsing=True)) + newl
s += str("Owner: %s " % target_obj.get_owner()) + newl
s += str("Zone: %s" % target_obj.get_zone()) + newl
s += str("Parent: %s " % target_obj.get_script_parent()) + newl
for attribute in target_obj.get_all_attributes():
s += str(attribute.get_attrline()) + newl
# Contents container lists for sorting by type.
con_players = []
@ -301,30 +306,32 @@ def cmd_examine(command):
# Render Contents display.
if con_players or con_things:
source_object.emit_to("%sContents:%s" % (ansi.ansi["hilite"],
ansi.ansi["normal"],))
s += str("%sContents:%s" % (ansi.ansi["hilite"],
ansi.ansi["normal"])) + newl
for player in con_players:
source_object.emit_to('%s' % (player.get_name(fullname=True),))
s += str(' %s' % player.get_name(fullname=True)) + newl
for thing in con_things:
source_object.emit_to('%s' % (thing.get_name(fullname=True),))
s += str(' %s' % thing.get_name(fullname=True)) + newl
# Render Exists display.
if con_exits:
source_object.emit_to("%sExits:%s" % (ansi.ansi["hilite"],
ansi.ansi["normal"],))
s += str("%sExits:%s" % (ansi.ansi["hilite"],
ansi.ansi["normal"])) + newl
for exit in con_exits:
source_object.emit_to('%s' %(exit.get_name(fullname=True),))
s += str(' %s' % exit.get_name(fullname=True)) + newl
# Render the object's home or destination (for exits).
if not target_obj.is_room():
if target_obj.is_exit():
# The Home attribute on an exit is really its destination.
source_object.emit_to("Destination: %s" % (target_obj.get_home(),))
s += str("Destination: %s" % target_obj.get_home()) + newl
else:
# For everything else, home is home.
source_object.emit_to("Home: %s" % (target_obj.get_home(),))
s += str("Home: %s" % target_obj.get_home()) + newl
# This obviously isn't valid for rooms.
source_object.emit_to("Location: %s" % (target_obj.get_location(),))
s += str("Location: %s" % target_obj.get_location()) + newl
source_object.emit_to(s)
GLOBAL_CMD_TABLE.add_command("examine", cmd_examine)
def cmd_quit(command):
@ -441,22 +448,151 @@ def cmd_pose(command):
source_object.get_location().emit_to_contents(sent_msg)
GLOBAL_CMD_TABLE.add_command("pose", cmd_pose)
def cmd_help(command):
"""
Help system commands.
Help command
Usage: help <topic>
Examples: help index
help topic
help 2
Shows the available help on <topic>. Use without <topic> to
get the help index. If more than one topic match your query, you will get a
list of topics to choose between. You can also supply a help entry number
directly if you know it.
<<TOPIC:STAFF:help_staff>>
Help command extra functions for staff:
help index - the normal index
help index_staff - show only help files unique to staff
help index_player - show only help files visible to all players
The help command has a range of staff-only switches for manipulating the
help data base:
help/add <topic>:<text> - add/replace help topic with text (staff only)
help/append <topic>:<text> - add text to the end of a topic (staff only)
(use the /newline switch to add a new paragraph
to your help entry.)
help/delete <topic> - delete help topic (staff only)
Note: further switches are /force and /staff. /force is used together with /add to
always create a help entry, also when they partially match a previous entry. /staff
makes the help file visible to staff only. The /append switch can be used to change the
/staff setting of an existing help file if required.
The <text> entry supports markup to automatically divide the help text into
sub-entries. These are started by the markup < <TOPIC:MyTopic> > (with no spaces
between the << >>), which will create a new subsectioned entry 'MyTopic' for all
text to follow it. All subsections to be added this way are automatically
referred to in the footer of each help entry. Normally the subsections inherit the
staff_only flag from the main entry (so if this is a staff-only help, all subentries
will also be staff-only and vice versa). You can override this behaviour using the
alternate forms < <TOPIC:STAFF:MyTopic> > and < <TOPIC:ALL:MyTopic> >.
"""
source_object = command.source_object
topicstr = command.command_argument
switches = command.command_switches
if not command.command_argument:
topicstr = "Help Index"
#display topic index if just help command is given
if not switches:
topicstr = "index"
else:
#avoid applying things to "topic" by mistake
source_object.emit_to("You have to supply a topic.")
return
elif len(topicstr) < 2 and not topicstr.isdigit():
source_object.emit_to("Your search query is too short. It must be at least three letters long.")
#check valid query
source_object.emit_to("Your search query must be at least two letters long.")
return
#speciel help index names. These entries are dynamically
#created upon request.
if topicstr == 'index':
#the normal index, affected by permissions
edit_help.get_help_index(source_object)
return
elif topicstr == 'index_staff':
#allows staff to view only staff-specific help
edit_help.get_help_index(source_object,filter='staff')
return
elif topicstr == 'index_player':
#allows staff to view only the help files a player sees
edit_help.get_help_index(source_object,filter='player')
return
#handle special switches
force_create = 'for' in switches or 'force' in switches
staff_only = 'sta' in switches or 'staff' in switches
if 'add' in switches:
#try to add/replace help text for a command
if not source_object.is_staff():
source_object.emit_to("Only staff can add new help entries.")
return
spl = (topicstr.split(':',1))
if len(spl) != 2:
source_object.emit_to("Format is help/add <topic>:<helptext>")
return
topicstr = spl[0]
text = spl[1]
topics = edit_help.add_help(topicstr,text,staff_only,force_create,source_object)
if not topics:
source_object.emit_to("No topic(s) added due to errors. Check syntax and that you don't have duplicate subtopics with the same name defined.")
return
elif len(topics)>1:
source_object.emit_to("Added or replaced multiple help entries.")
else:
source_object.emit_to("Added or replaced help entry for %s." % topicstr )
elif 'append' in switches or 'app' in switches:
#append text to a help entry
if not source_object.is_staff():
source_object.emit_to("Only staff can append to help entries.")
return
spl = (topicstr.split(':',1))
if len(spl) != 2:
source_object.emit_to("""Format is help/append <topic>:<text to add>
Use the /newline switch to make a new paragraph.""")
return
topicstr = spl[0]
text = spl[1]
topics = HelpEntry.objects.find_topicmatch(source_object, topicstr)
if len(topics) == 1:
newtext = topics[0].get_entrytext_ingame()
if 'newl' in switches or 'newline' in switches:
newtext += "\n\r\n\r%s" % text
else:
newtext += "\n\r%s" % text
topics = edit_help.add_help(topicstr,newtext,staff_only,force_create,source_object)
if topics:
source_object.emit_to("Appended text to help entry for %s." % topicstr)
elif 'del' in switches or 'delete' in switches:
#delete a help entry
if not source_object.is_staff():
source_object.emit_to("Only staff can add delete help entries.")
return
topics = edit_help.del_help(source_object,topicstr)
if type(topics) != type(list()):
source_object.emit_to("Help entry '%s' deleted." % topicstr)
return
else:
#no switch; just try to get the help as normal
topics = HelpEntry.objects.find_topicmatch(source_object, topicstr)
topics = HelpEntry.objects.find_topicmatch(source_object, topicstr)
#display help entry or handle no/multiple matches
if len(topics) == 0:
source_object.emit_to("No matching topics found, please refine your search.")
suggestions = HelpEntry.objects.find_topicsuggestions(source_object,
@ -464,14 +600,14 @@ def cmd_help(command):
if len(suggestions) > 0:
source_object.emit_to("Matching similarly named topics:")
for result in suggestions:
source_object.emit_to(" %s" % (result,))
source_object.emit_to("You may type 'help <#>' to see any of these topics.")
source_object.emit_to(" %s" % (result,))
source_object.emit_to("You may type 'help <#>' to see any of these topics.")
elif len(topics) > 1:
source_object.emit_to("More than one match found:")
for result in topics:
source_object.emit_to("%3d. %s" % (result.id, result.get_topicname()))
source_object.emit_to(" %3d. %s" % (result.id, result.get_topicname()))
source_object.emit_to("You may type 'help <#>' to see any of these topics.")
else:
topic = topics[0]
source_object.emit_to("\n\r"+ topic.get_entrytext_ingame())
GLOBAL_CMD_TABLE.add_command("help", cmd_help)
source_object.emit_to("\n\r "+ topic.get_entrytext_ingame())
GLOBAL_CMD_TABLE.add_command("help", cmd_help, auto_help=True)

View file

@ -0,0 +1,230 @@
"""
Support commands for a more advanced 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.
"""
from src.helpsys.models import HelpEntry
from src.ansi import ansi
#
# Helper functions
#
def _privileged_help_search(topicstr):
"""
searches the topic data base without needing to know who calls it. Needed
for autohelp functionality. Will show all help entries, also those set to staff
only.
"""
if topicstr.isdigit():
t_query = HelpEntry.objects.filter(id=topicstr)
else:
exact_match = HelpEntry.objects.filter(topicname__iexact=topicstr)
if exact_match:
t_query = exact_match
else:
t_query = HelpEntry.objects.filter(topicname__istartswith=topicstr)
return t_query
def _create_help(topicstr, entrytext, staff_only=False, force_create=False,
pobject=None, noauth=False):
"""
Add a help entry to the database, replace an old one if it exists.
Note - noauth=True will bypass permission checks, so do not use this from
inside mud, it is needed by the autohelp system only.
"""
if noauth:
#do not check permissions (for autohelp)
topic = _privileged_help_search(topicstr)
elif pobject:
#checks access rights before searching (this should have been
#done already at the command level)
if not pobject.is_staff(): return []
topic = HelpEntry.objects.find_topicmatch(pobject, topicstr)
else:
return []
if len(topic) == 1:
#replace an old help file
topic = topic[0]
topic.entrytext = entrytext
topic.staff_only = staff_only
topic.save()
return [topic]
elif len(topic) > 1 and not force_create:
#a partial match, return it for inspection.
return topic
else:
#we have a new topic - create a new help object
new_entry = HelpEntry(topicname=topicstr,
entrytext=entrytext,
staff_only=staff_only)
new_entry.save()
return [new_entry]
def _handle_help_markup(topicstr, entrytext, staff_only, identifier="<<TOPIC:"):
"""
Handle help markup in order to split help into subsections.
Handles markup of the form <<TOPIC:STAFF:TopicTitle>> and
<<TOPIC:ALL:TopicTitle>> to override the staff_only flag on a per-subtopic
basis.
"""
topic_list = entrytext.split(identifier)
topic_dict = {}
staff_dict = {}
for txt in topic_list:
txt = txt.strip()
if txt.count('>>'):
topic, text = txt.split('>>',1)
text = text.strip()
topic = topic.lower()
if topic in topic_dict.keys():
#do not allow multiple topics of the same name
return {}, []
if 'all:' in topic:
topic = topic[4:]
staff_dict[topic] = False
elif 'staff:' in topic:
topic = topic[6:]
staff_dict[topic] = True
else:
staff_dict[topic] = staff_only
topic_dict[topic] = text
else:
#no markup, just add the entry as-is
topic = topicstr.lower()
topic_dict[topic] = '\r\n' + txt
staff_dict[topic] = staff_only
return topic_dict, staff_dict
def _format_footer(top, text, topic_dict, staff_dict):
"""
Formats the subtopic with a 'Related Topics:' footer. If mixed
staff-only flags are set, those help entries without the staff-only flag
will not see staff-only help files recommended in the footer. This allows
to separate out the staff-only help switches etc into a separate
help file so as not to confuse normal players.
"""
if text:
#only include non-staff related footers to non-staff commands
if staff_dict[top]:
other_topics = other_topics = filter(lambda o: o != top, topic_dict.keys())
else:
other_topics = other_topics = filter(lambda o: o != top and not staff_dict[o],
topic_dict.keys())
if other_topics:
footer = ansi['normal'] + "\n\r Related Topics: "
for t in other_topics:
footer += t + ', '
footer = footer[:-2] + '.'
return text + footer
else:
return text
else:
return False
#
# Access functions
#
def add_help(topicstr, entrytext, staff_only=False, force_create=False,
pobject=None, auto_help=False):
"""
Add a help topic to the database. This is also usable by autohelp, with auto=True.
Allows <<TOPIC:TopicTitle>> markup in the help text, to automatically spawn
subtopics. For creating mixed staff/ordinary subtopics, the <<TOPIC:STAFF:TopicTitle>> and
<<TOPIC:ALL:TopicTitle>> commands can override the overall staff_only setting for
that entry only.
"""
identifier = '<<TOPIC:'
if identifier in entrytext:
#There is markup in the entry, so we should split the doc into separate subtopics
topic_dict, staff_dict = _handle_help_markup(topicstr, entrytext,
staff_only, identifier)
topics = []
for topic, text in topic_dict.items():
#format with nice footer
entry = _format_footer(topic, text, topic_dict, staff_dict)
if entry:
#create the subtopic
newtopic = _create_help(topic, entry,staff_only=staff_dict[topic],
force_create=force_create,pobject=pobject,noauth=auto_help)
topics.extend(newtopic)
return topics
elif entrytext:
#if there were no topic sections, just create the help entry as normal
return _create_help(topicstr.lower(),entrytext,staff_only=staff_only,
force_create=force_create,pobject=pobject,noauth=auto_help)
def del_help(pobject,topicstr):
"""
Delete a help entry from the data base.
Note that it makes no sense to delete auto-added help entries this way since
they will just be re-added on the next @reload. Delete such entries by turning
off their auto-help functionality first.
"""
#find topic with permission checks
if not pobject.is_staff(): return []
topic = HelpEntry.objects.find_topicmatch(pobject, topicstr)
if topic:
if len(topic) == 1:
#delete topic
topic.delete()
return True
else:
return topic
else:
return []
def get_help_index(pobject,filter=None):
"""
Dynamically builds a help index depending on who asks for it, so
normal players won't see staff-only help files, for example.
The filter parameter allows staff to limit their view of the help index
no filter (default) - view all help files, staff and non-staff
filter='staff' - view only staff-specific help files
filter='player' - view only those files visible to all
"""
if pobject.is_staff():
if filter == 'staff':
helpentries = HelpEntry.objects.filter(staff_only=True).order_by('topicname')
elif filter == 'player':
helpentries = HelpEntry.objects.filter(staff_only=False).order_by('topicname')
else:
helpentries = HelpEntry.objects.all().order_by('topicname')
else:
helpentries = HelpEntry.objects.filter(staff_only=False).order_by('topicname')
if not helpentries:
pobject.emit_to("No help entries found.")
return
topics = [entry.topicname for entry in helpentries]
#format help entries into suitable alphabetized collumns.
percollumn = 8
s = ""
i = 0
while True:
i += 1
try:
top = topics.pop(0)
s+= " %s " % top
if i%percollumn == 0: s += '\n\r'
except IndexError:
break
s += " (%i entries)" % (i-1)
pobject.emit_to(s)