diff --git a/evennia/utils/olc/olc_storage.py b/evennia/utils/olc/olc_storage.py index f96a79edb2..a10c0d508b 100644 --- a/evennia/utils/olc/olc_storage.py +++ b/evennia/utils/olc/olc_storage.py @@ -6,13 +6,41 @@ available in a repository for buildiers to use. Each prototype is stored in a Script so that it can be tagged for quick sorting/finding and locked for limiting access. +This system also takes into consideration prototypes defined and stored in modules. +Such prototypes are considered 'read-only' to the system and can only be modified +in code. To replace a default prototype, add the same-name prototype in a +custom module read later in the settings.PROTOTYPE_MODULES list. To remove a default +prototype, override its name with an empty dict. + """ +from django.conf import settings from evennia.scripts.scripts import DefaultScript from evennia.utils.create import create_script -from evennia.utils.utils import make_iter +from evennia.utils.utils import make_iter, all_from_module from evennia.utils.evtable import EvTable +# prepare the available prototypes defined in modules + +_READONLY_PROTOTYPES = {} +_READONLY_PROTOTYPE_MODULES = {} + +for mod in settings.PROTOTYPE_MODULES: + # to remove a default prototype, override it with an empty dict. + # internally we store as (key, desc, locks, tags, prototype_dict) + prots = [(key, prot) for key, prot in all_from_module(mod).items() + if prot and isinstance(prot, dict)] + _READONLY_PROTOTYPES.update( + {key.lower(): + (key.lower(), + prot['prototype_desc'] if 'prototype_desc' in prot else mod, + prot['prototype_lock'] if 'prototype_lock' in prot else "use:all()", + set(make_iter( + prot['prototype_tags']) if 'prototype_tags' in prot else ["base-prototype"]), + prot) + for key, prot in prots}) + _READONLY_PROTOTYPE_MODULES.update({tup[0]: mod for tup in prots}) + class PersistentPrototype(DefaultScript): """ @@ -46,17 +74,25 @@ def store_prototype(caller, key, prototype, desc="", tags=None, locks="", delete Raises: PermissionError: If edit lock was not passed by caller. + """ + key_orig = key key = key.lower() locks = locks if locks else "use:all();edit:id({}) or edit:perm(Admin)".format(caller.id) tags = [(tag, "persistent_prototype") for tag in make_iter(tags)] + if key in _READONLY_PROTOTYPES: + mod = _READONLY_PROTOTYPE_MODULES.get(key, "N/A") + raise PermissionError("{} is a read-only prototype " + "(defined as code in {}).".format(key_orig, mod)) + stored_prototype = PersistentPrototype.objects.filter(db_key=key) if stored_prototype: stored_prototype = stored_prototype[0] if not stored_prototype.access(caller, 'edit'): - PermissionError("{} does not have permission to edit prototype {}".format(caller, key)) + raise PermissionError("{} does not have permission to " + "edit prototype {}".format(caller, key)) if delete: stored_prototype.delete() @@ -77,9 +113,9 @@ def store_prototype(caller, key, prototype, desc="", tags=None, locks="", delete return stored_prototype -def search_prototype(key=None, tags=None): +def search_persistent_prototype(key=None, tags=None): """ - Find prototypes based on key and/or tags. + Find persistent (database-stored) prototypes based on key and/or tags. Kwargs: key (str): An exact or partial key to query for. @@ -87,13 +123,10 @@ def search_prototype(key=None, tags=None): will always be applied with the 'persistent_protototype' tag category. Return: - matches (queryset): All found PersistentPrototypes. This will - be all prototypes if no arguments are given. + matches (queryset): All found PersistentPrototypes Note: - This will use the tags to make a subselection before attempting - to match on the key. So if key/tags don't match up nothing will - be found. + This will not include read-only prototypes defined in modules. """ if tags: @@ -109,6 +142,68 @@ def search_prototype(key=None, tags=None): return matches +def search_readonly_prototype(key=None, tags=None): + """ + Find read-only prototypes, defined in modules. + + Kwargs: + key (str): An exact or partial key to query for. + tags (str or list): Tag key to query for. + + Return: + matches (list): List of prototype tuples that includes + prototype metadata, on the form + `(key, desc, lockstring, taglist, prototypedict)` + + """ + matches = [] + if tags: + # use tags to limit selection + tagset = set(tags) + matches = {key: tup for key, tup in _READONLY_PROTOTYPES.items() + if tagset.intersection(tup[3])} + else: + matches = _READONLY_PROTOTYPES + + if key: + if key in matches: + # exact match + return matches[key] + else: + # fuzzy matching + return [tup for pkey, tup in matches.items() if key in pkey] + return matches + + +def search_prototype(key=None, tags=None): + """ + Find prototypes based on key and/or tags. + + Kwargs: + key (str): An exact or partial key to query for. + tags (str or list): Tag key or keys to query for. These + will always be applied with the 'persistent_protototype' + tag category. + Return: + matches (list): All found prototype dicts. + + Note: + The available prototypes is a combination of those supplied in + PROTOTYPE_MODULES and those stored from in-game. For the latter, + this will use the tags to make a subselection before attempting + to match on the key. So if key/tags don't match up nothing will + be found. + + """ + matches = [] + if key and key in _READONLY_PROTOTYPES: + matches.append(_READONLY_PROTOTYPES[key][3]) + else: + matches.extend([prot.attributes.get("prototype") + for prot in search_persistent_prototype(key, tags)]) + return matches + + def get_prototype_list(caller, key=None, tags=None, show_non_use=False, show_non_edit=True): """ Collate a list of found prototypes based on search criteria and access. @@ -124,20 +219,39 @@ def get_prototype_list(caller, key=None, tags=None, show_non_use=False, show_non if no prototypes were found. """ - prototypes = search_prototype(key, tags) + # handle read-only prototypes separately + if key and key in _READONLY_PROTOTYPES: + readonly_prototypes = _READONLY_PROTOTYPES[key] + else: + readonly_prototypes = _READONLY_PROTOTYPES.values() + + # get use-permissions of readonly attributes (edit is always False) + readonly_prototypes = [ + (tup[0], + tup[1], + ("{}/N".format('Y' + if caller.locks.check_lockstring(caller, tup[2], access_type='use') else 'N')), + ",".join(tup[3])) for tup in readonly_prototypes] + + # next, handle db-stored prototypes + prototypes = search_persistent_prototype(key, tags) if not prototypes: return None - # gather access permissions as (key, desc, can_use, can_edit) + # gather access permissions as (key, desc, tags, can_use, can_edit) prototypes = [(prototype.key, prototype.desc, - prototype.access(caller, "use"), prototype.access(caller, "edit")) + "{}/{}".format('Y' if prototype.access(caller, "use") else 'N', + 'Y' if prototype.access(caller, "edit") else 'N'), + ",".join(prototype.tags.get(category="persistent_prototype"))) for prototype in prototypes] + prototypes = prototypes + readonly_prototypes + if not show_non_use: - prototypes = [tup for tup in prototypes if tup[2]] + prototypes = [tup for tup in sorted(prototypes, key=lambda o: o[0]) if tup[2]] if not show_non_edit: - prototypes = [tup for tup in prototypes if tup[3]] + prototypes = [tup for tup in sorted(prototypes, key=lambda o: o[0]) if tup[3]] if not prototypes: return None @@ -145,9 +259,9 @@ def get_prototype_list(caller, key=None, tags=None, show_non_use=False, show_non table = [] for i in range(len(prototypes[0])): table.append([str(tup[i]) for tup in prototypes]) - table = EvTable("Key", "Desc", "Use", "Edit", table=table, crop=True, width=78) + table = EvTable("Key", "Desc", "Use/Edit", "Tags", table=table, crop=True, width=78) table.reformat_column(0, width=28) table.reformat_column(1, width=40) - table.reformat_column(2, width=5) - table.reformat_column(3, width=5) + table.reformat_column(2, width=11, align='r') + table.reformat_column(3, width=20) return table