Changed how Objects are searched, using proper Django Q objects instead of hack-y evals to build queries. This has lead to a number of changes to the ObjectDB manager search. Notably there is now no way to supply a "location" to either of the manager search methods anymore. Instead you can now supply the keyword "candidates", a list of objects which should be used to limit the search. This is much more generic than giving location. The higher-level search (like caller.search, reached from commands) have not changed its API, so commands should work the same unless you are using the manager backbone directly. This search function is now using location to create the "candidates" list. Some other things, like matching for "me" and "here" have also been moved up to a level were it can be easily overloaded. "me" and "here" etc were also moved under i18n.

As part of this overhaul I implemented the partial_matching algorithm originally asked for by user "Adam_ASE" over IRC. This will allow for (local-only) partial matching of objects. So "big black sword" will now be matched by "bi", "sword", "bi bla" and so on. The partial matcher sits in src.utils.utils.py if one wants to use it for something else.
This commit is contained in:
Griatch 2012-09-17 15:31:50 +02:00
parent cc6fa079b6
commit c53a9b5770
7 changed files with 236 additions and 193 deletions

View file

@ -1,17 +1,17 @@
"""
Custom manager for Objects.
"""
from django.db.models import Q
from django.conf import settings
#from django.contrib.auth.models import User
from django.db.models.fields import exceptions
from src.typeclasses.managers import TypedObjectManager
from src.typeclasses.managers import returns_typeclass, returns_typeclass_list
from src.utils import utils
from src.utils.utils import to_unicode, make_iter
_ObjAttribute = None
from src.utils.utils import to_unicode, make_iter, string_partial_matching
__all__ = ("ObjectManager",)
_GA = object.__getattribute__
# Try to use a custom way to parse id-tagged multimatches.
@ -73,7 +73,7 @@ class ObjectManager(TypedObjectManager):
# This returns typeclass since get_object_with_user and get_dbref does.
@returns_typeclass
def get_object_with_player(self, search_string):
def get_object_with_player(self, ostring, exact=True, candidates=None):
"""
Search for an object based on its player's name or dbref.
This search
@ -81,127 +81,138 @@ class ObjectManager(TypedObjectManager):
the search criterion (e.g. in local_and_global_search).
search_string: (string) The name or dbref to search for.
"""
dbref = self.dbref(search_string)
if not dbref:
# not a dbref. Search by name.
search_string = to_unicode(search_string).lstrip('*')
return self.filter(db_player__user__username__iexact=search_string)
return self.get_id(dbref)
ostring = to_unicode(ostring).lstrip('*')
# simplest case - search by dbref
dbref = self.dbref(ostring)
if dbref:
return dbref
# not a dbref. Search by name.
cand_restriction = candidates and Q(pk__in=[_GA(obj, "id") for obj in make_iter(candidates) if obj]) or Q()
if exact:
return self.filter(cand_restriction & Q(db_player__user__username__iexact=ostring))
else: # fuzzy matching
ply_cands = self.filter(cand_restriction & Q(playerdb__user__username__istartswith=ostring)).values_list("db_key", flat=True)
if candidates:
index_matches = string_partial_matching(ply_cands, ostring, ret_index=True)
return [obj for ind, obj in enumerate(make_iter(candidates)) if ind in index_matches]
else:
return string_partial_matching(ply_cands, ostring, ret_index=False)
@returns_typeclass_list
def get_objs_with_key_and_typeclass(self, oname, otypeclass_path):
def get_objs_with_key_and_typeclass(self, oname, otypeclass_path, candidates=None):
"""
Returns objects based on simultaneous key and typeclass match.
"""
return self.filter(db_key__iexact=oname, db_typeclass_path__exact=otypeclass_path)
cand_restriction = candidates and Q(pk__in=[_GA(obj, "id") for obj in make_iter(candidates) if obj]) or Q()
return self.filter(cand_restriction & Q(db_key__iexact=oname, db_typeclass_path__exact=otypeclass_path))
# attr/property related
@returns_typeclass_list
def get_objs_with_attr(self, attribute_name, location=None):
def get_objs_with_attr(self, attribute_name, candidates=None):
"""
Returns all objects having the given attribute_name defined at all. Location
should be a valid location object.
"""
global _ObjAttribute
if not _ObjAttribute:
from src.objects.models import ObjAttribute as _ObjAttribute
if location:
attrs = _ObjAttribute.objects.select_related("db_obj").filter(db_key=attribute_name, db_obj__db_location=location)
else:
attrs = _ObjAttribute.objects.select_related("db_obj").filter(db_key=attribute_name)
return [attr.db_obj for attr in attrs]
cand_restriction = candidates and Q(objattribute__db_obj__pk__in=[_GA(obj, "id") for obj in make_iter(candidates) if obj]) or Q()
return self.filter(cand_restriction & Q(objattribute__db_key=attribute_name))
@returns_typeclass_list
def get_objs_with_attr_match(self, attribute_name, attribute_value, location=None, exact=False):
def get_objs_with_attr_value(self, attribute_name, attribute_value, candidates=None):
"""
Returns all objects having the valid
attrname set to the given value. Note that no conversion is made
to attribute_value, and so it can accept also non-strings.
to attribute_value, and so it can accept also non-strings. For this reason it does
not make sense to offer an "exact" type matching for this.
"""
global _ObjAttribute
if not _ObjAttribute:
from src.objects.models import ObjAttribute as _ObjAttribute
if location:
attrs = _ObjAttribute.objects.select_related("db_value").filter(db_key=attribute_name, db_obj__db_location=location)
else:
attrs = _ObjAttribute.objects.select_related("db_value").filter(db_key=attribute_name)
if exact:
return [attr.obj for attr in attrs if attribute_value == attr.value]
else:
return [attr.obj for attr in attrs if to_unicode(attribute_value) in str(attr.value)]
cand_restriction = candidates and Q(db_obj__pk__in=[_GA(obj, "id") for obj in make_iter(candidates) if obj]) or Q()
attrs= self.model.objattribute_set.related.model.objects.select_related("db_obj").filter(cand_restriction & Q(db_key=attribute_name))
return [attr.db_obj for attr in attrs if attribute_value == attr.value]
@returns_typeclass_list
def get_objs_with_db_property(self, property_name, location=None):
def get_objs_with_db_property(self, property_name, candidates=None):
"""
Returns all objects having a given db field property.
property_name = search string
location - actual location object to restrict to
candidates - list of candidate objects to search
"""
lstring = ""
if location:
lstring = ".filter(db_location=location)"
property_name = "db_%s" % property_name.lstrip('db_')
cand_restriction = candidates and Q(pk__in=[_GA(obj, "in") for obj in make_iter(candidates) if obj]) or Q()
try:
return eval("self.exclude(db_%s=None)%s" % (property_name, lstring))
return self.filter(cand_restriction).exclude(Q(property_name=None))
except exceptions.FieldError:
return []
@returns_typeclass_list
def get_objs_with_db_property_match(self, property_name, property_value, location, exact=False):
def get_objs_with_db_property_value(self, property_name, property_value, candidates=None):
"""
Returns all objects having a given db field property
"""
lstring = ""
if location:
lstring = ", db_location=location"
if isinstance(property_value, basestring):
property_value = to_unicode(property_value)
property_name = "db_%s" % property_name.lstrip('db_')
cand_restriction = candidates and Q(pk__in=[_GA(obj, "in") for obj in make_iter(candidates) if obj]) or Q()
try:
if exact:
return eval("self.filter(db_%s__iexact=property_value%s)" % (property_name, lstring))
else:
return eval("self.filter(db_%s__icontains=property_value%s)" % (property_name, lstring))
return self.filter(cand_restriction & Q(property_name=property_value))
except exceptions.FieldError:
return []
@returns_typeclass_list
def get_objs_with_key_or_alias(self, ostring, location=None, exact=False):
"""
Returns objects based on key or alias match
"""
lstring_key, lstring_alias, estring = "", "", "icontains"
if location:
lstring_key = ", db_location=location"
lstring_alias = ", db_obj__db_location=location"
if exact:
estring = "__iexact"
else:
estring = "__istartswith"
matches = eval("self.filter(db_key%s=ostring%s)" % (estring, lstring_key))
if not matches:
alias_matches = eval("self.model.alias_set.related.model.objects.filter(db_key%s=ostring%s)" % (estring, lstring_alias))
matches = [alias.db_obj for alias in alias_matches]
return matches
# main search methods and helper functions
@returns_typeclass_list
def get_contents(self, location, excludeobj=None):
"""
Get all objects that has a location
set to this one.
excludeobjs - one or more object keys to exclude from the match
excludeobj - one or more object keys to exclude from the match
"""
query = self.filter(db_location__id=location.id)
for objkey in make_iter(excludeobj):
query = query.exclude(db_key=objkey)
return query
exclude_restriction = excludeobj and Q(pk__in=[_GA(obj, "in") for obj in make_iter(excludeobj)]) or Q()
return self.filter(db_location=location).exclude(exclude_restriction)
@returns_typeclass_list
def get_objs_with_key_or_alias(self, ostring, exact=True, candidates=None):
"""
Returns objects based on key or alias match. Will also do fuzzy matching based on
the utils.string_partial_matching function.
"""
# build query objects
candidates_id = [_GA(obj, "id") for obj in make_iter(candidates) if obj]
cand_restriction = candidates and Q(pk__in=candidates_id) or Q()
if exact:
return self.filter(cand_restriction & (Q(db_key__iexact=ostring) | Q(alias__db_key__iexact=ostring)))
else:
if candidates:
# fuzzy matching - only check the candidates
key_candidates = self.filter(cand_restriction)
key_strings = key_candidates.values_list("db_key", flat=True)
index_matches = string_partial_matching(key_strings, ostring, ret_index=True)
if index_matches:
return [obj for ind, obj in enumerate(key_candidates) if ind in index_matches]
else:
alias_candidates = self.model.alias_set.related.model.objects.filter(db_obj__pk__in=candidates_id)
alias_strings = alias_candidates.values_list("db_key", flat=True)
index_matches = string_partial_matching(alias_strings, ostring, ret_index=True)
if index_matches:
return [alias.db_obj for ind, alias in enumerate(alias_candidates) if ind in index_matches]
return []
else:
# fuzzy matching - first check with keys, then with aliases
key_candidates = self.filter(Q(db_key__istartswith=ostring) | Q(alias__db_key__istartswith=ostring))
key_strings = key_candidates.values_list("db_key", flat=True)
matches = string_partial_matching(key_candidates, ostring, reg_index=False)
if matches:
return matches
alias_candidates = self.model.alias_set.related.model.objects.filter(db_obj__pk__in=candidates_id).values_list("db_key", flat=True)
return string_partial_matching(alias_candidates, ostring, ret_index=False)
# main search methods and helper functions
@returns_typeclass_list
def object_search(self, ostring, caller=None,
global_search=False,
attribute_name=None, location=None, single_result=False):
attribute_name=None,
candidates=None,
exact=True):
"""
Search as an object and return results. The result is always an Object.
If * is appended (player search, a Character controlled by this Player
@ -212,19 +223,40 @@ class ObjectManager(TypedObjectManager):
ostring: (string) The string to compare names against.
Can be a dbref. If name is appended by *, a player is searched for.
caller: (Object) The optional object performing the search.
global_search (bool). Defaults to False. If a caller is defined, search will
be restricted to the contents of caller.location unless global_search
is True. If no caller is given (or the caller has no location), a
global search is assumed automatically.
attribute_name: (string) Which object attribute to match ostring against. If not
set, the "key" and "aliases" properties are searched in order.
location (Object): If set, this location's contents will be used to limit the search instead
of the callers. global_search will override this argument
candidates (list obj ObjectDBs): If objlist is supplied, global_search keyword is ignored
and search will only be performed among the candidates in this list. A common list
of candidates is the contents of the current location searched.
exact (bool): Match names/aliases exactly or partially. Partial matching matches the
beginning of words in the names/aliases, using a matching routine to separate
multiple matches in names with multiple components (so "bi sw" will match
"Big sword"). Since this is more expensive than exact matching, it is
recommended to be used together with the objlist keyword to limit the number
of possibilities. This value has no meaning if searching for attributes/properties.
Returns:
A list of matching objects (or a list with one unique match)
"""
def _searcher(ostring, exact=False):
"Helper method for searching objects"
# Handle player
if ostring.startswith("*"):
# Player search - try to find obj by its player's name
player_match = self.get_object_with_player(ostring, candidates=candidates)
if player_match is not None:
return [player_match]
if attribute_name:
# attribute/property search (always exact).
matches = self.get_objs_with_db_property_value(attribute_name, ostring, candidates=candidates)
if not matches:
return self.get_objs_with_attr_value(attribute_name, ostring, candidates=candidates)
else:
# normal key/alias search
return self.get_objs_with_key_or_alias(ostring, exact=exact, candidates=candidates)
ostring = to_unicode(ostring, force_string=True)
if not ostring and ostring != 0:
@ -237,81 +269,25 @@ class ObjectManager(TypedObjectManager):
if dbref_match:
return [dbref_match]
if not location and caller and hasattr(caller, "location"):
location = caller.location
# Test some common self-references
if location and ostring == 'here':
return [location]
if caller and ostring in ('me', 'self'):
return [caller]
if caller and ostring in ('*me', '*self'):
return [caller]
# Test if we are looking for an object controlled by a
# specific player
#logger.log_infomsg(str(type(ostring)))
if ostring.startswith("*"):
# Player search - try to find obj by its player's name
player_match = self.get_object_with_player(ostring)
if player_match is not None:
return [player_match]
# Search for keys, aliases or other attributes
search_locations = [None] # this means a global search
if not global_search and location:
# Test if we are referring to the current room
if location and (ostring.lower() == location.key.lower()
or ostring.lower() in [alias.lower() for alias in location.aliases]):
return [location]
# otherwise, setup the locations to search in
search_locations = [location]
if caller:
search_locations.append(caller)
def local_and_global_search(ostring, exact=False):
"Helper method for searching objects"
matches = []
for location in search_locations:
if attribute_name:
# Attribute/property search. First, search for db_<attrname> matches on the model
matches.extend(self.get_objs_with_db_property_match(attribute_name, ostring, location, exact))
if not matches:
# Next, try Attribute matches
matches.extend(self.get_objs_with_attr_match(attribute_name, ostring, location, exact))
else:
# No attribute/property named. Do a normal key/alias-search
matches.extend(self.get_objs_with_key_or_alias(ostring, location, exact))
return matches
# Search through all possibilities.
match_number = None
matches = local_and_global_search(ostring, exact=True)
# always run first check exact - we don't want partial matches if on the form of 1-keyword etc.
matches = _searcher(ostring, exact=True)
if not matches:
# if we have no match, check if we are dealing with an "N-keyword" query - if so, strip it.
# no matches found - check if we are dealing with N-keyword query - if so, strip it.
match_number, ostring = _AT_MULTIMATCH_INPUT(ostring)
if match_number != None and ostring:
# Run search again, without match number:
matches = local_and_global_search(ostring, exact=True)
if ostring and (len(matches) > 1 or not matches):
# Already multimatch or no matches. Run a fuzzy matching.
matches = local_and_global_search(ostring, exact=False)
elif len(matches) > 1:
# multiple matches already. Run a fuzzy search. This catches partial matches (suggestions)
matches = local_and_global_search(ostring, exact=False)
# run search again, with the exactness set by caller
matches = _searcher(ostring, exact=exact)
# deal with the result
# deal with result
if len(matches) > 1 and match_number != None:
# We have multiple matches, but a N-type match number is available to separate them.
# multiple matches, but a number was given to separate them
try:
matches = [matches[match_number]]
except IndexError:
pass
# We always have a (possibly empty) list at this point.
# return a list (possibly empty)
return matches
#