diff --git a/src/cmdtable.py b/src/cmdtable.py index 6e2322ae26..8352394f8b 100644 --- a/src/cmdtable.py +++ b/src/cmdtable.py @@ -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 <>, 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 <> and <>. """ 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() \ No newline at end of file +GLOBAL_UNCON_CMD_TABLE = CommandTable() diff --git a/src/commands/general.py b/src/commands/general.py index 4f71dbffcc..ace18ff354 100644 --- a/src/commands/general.py +++ b/src/commands/general.py @@ -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 + + Examples: help index + help topic + help 2 + + Shows the available help on . Use without 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. + + <> + 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 : - add/replace help topic with text (staff only) + help/append : - 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 - 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 entry supports markup to automatically divide the help text into + sub-entries. These are started by the markup < > (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 < > and < >. + """ + 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 :") + 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 : + 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) \ No newline at end of file + source_object.emit_to("\n\r "+ topic.get_entrytext_ingame()) +GLOBAL_CMD_TABLE.add_command("help", cmd_help, auto_help=True) diff --git a/src/helpsys/management/commands/edit_helpfiles.py b/src/helpsys/management/commands/edit_helpfiles.py new file mode 100644 index 0000000000..cdd5b6dec4 --- /dev/null +++ b/src/helpsys/management/commands/edit_helpfiles.py @@ -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="<> and + <> 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 <> markup in the help text, to automatically spawn + subtopics. For creating mixed staff/ordinary subtopics, the <> and + <> commands can override the overall staff_only setting for + that entry only. + """ + identifier = '<