Further fuzzy matching improvement, with integer list selection of multiple matches.
This uses exact-match-first fuzzy matching as discussed in previous commits. It also use the match order to present a list of options to narrow the selection down - the user can then specify the choice by appending the correct number to the query. Example: objects: [box,box]; searching for "box" gives a multiple match error, which presents a list looking like "1-box, 2-box". The user can now just write "2-box" to choose the second box. Showing dbrefs is perhaps even more universal, but revealing the underlying data structure to the normal user is not really good practice - dbrefs is only something builders and admins should have to know about ... (IMHO). /Griatch
This commit is contained in:
parent
2aae4a0105
commit
41365074fd
2 changed files with 147 additions and 92 deletions
|
|
@ -18,6 +18,10 @@ from src import logger
|
||||||
|
|
||||||
class ObjectManager(models.Manager):
|
class ObjectManager(models.Manager):
|
||||||
|
|
||||||
|
#
|
||||||
|
# ObjectManager Get methods
|
||||||
|
#
|
||||||
|
|
||||||
def num_total_players(self):
|
def num_total_players(self):
|
||||||
"""
|
"""
|
||||||
Returns the total number of registered players.
|
Returns the total number of registered players.
|
||||||
|
|
@ -50,6 +54,35 @@ class ObjectManager(models.Manager):
|
||||||
start_date = end_date - tdelta
|
start_date = end_date - tdelta
|
||||||
return User.objects.filter(last_login__range=(start_date, end_date)).order_by('-last_login')
|
return User.objects.filter(last_login__range=(start_date, end_date)).order_by('-last_login')
|
||||||
|
|
||||||
|
def get_user_from_email(self, uemail):
|
||||||
|
"""
|
||||||
|
Returns a player's User object when given an email address.
|
||||||
|
"""
|
||||||
|
return User.objects.filter(email__iexact=uemail)
|
||||||
|
|
||||||
|
def get_object_from_dbref(self, dbref):
|
||||||
|
"""
|
||||||
|
Returns an object when given a dbref.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return self.get(id=dbref)
|
||||||
|
except self.model.DoesNotExist:
|
||||||
|
raise ObjectNotExist(dbref)
|
||||||
|
|
||||||
|
def object_totals(self):
|
||||||
|
"""
|
||||||
|
Returns a dictionary with database object totals.
|
||||||
|
"""
|
||||||
|
dbtotals = {
|
||||||
|
"objects": self.count(),
|
||||||
|
"things": self.filter(type=defines_global.OTYPE_THING).count(),
|
||||||
|
"exits": self.filter(type=defines_global.OTYPE_EXIT).count(),
|
||||||
|
"rooms": self.filter(type=defines_global.OTYPE_ROOM).count(),
|
||||||
|
"garbage": self.filter(type=defines_global.OTYPE_GARBAGE).count(),
|
||||||
|
"players": self.filter(type=defines_global.OTYPE_PLAYER).count(),
|
||||||
|
}
|
||||||
|
return dbtotals
|
||||||
|
|
||||||
def get_nextfree_dbnum(self):
|
def get_nextfree_dbnum(self):
|
||||||
"""
|
"""
|
||||||
Figure out what our next free database reference number is.
|
Figure out what our next free database reference number is.
|
||||||
|
|
@ -68,6 +101,38 @@ class ObjectManager(models.Manager):
|
||||||
# for our next free.
|
# for our next free.
|
||||||
return int(self.order_by('-id')[0].id + 1)
|
return int(self.order_by('-id')[0].id + 1)
|
||||||
|
|
||||||
|
def is_dbref(self, dbstring, require_pound=True):
|
||||||
|
"""
|
||||||
|
Is the input a well-formed dbref number?
|
||||||
|
"""
|
||||||
|
return util_object.is_dbref(dbstring, require_pound=require_pound)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# ObjectManager Search methods
|
||||||
|
#
|
||||||
|
|
||||||
|
def dbref_search(self, dbref_string, limit_types=False):
|
||||||
|
"""
|
||||||
|
Searches for a given dbref.
|
||||||
|
|
||||||
|
dbref_number: (string) The dbref to search for. With # sign.
|
||||||
|
limit_types: (list of int) A list of Object type numbers to filter by.
|
||||||
|
"""
|
||||||
|
if not util_object.is_dbref(dbref_string):
|
||||||
|
return None
|
||||||
|
dbref_string = dbref_string[1:]
|
||||||
|
dbref_matches = self.filter(id=dbref_string).exclude(
|
||||||
|
type=defines_global.OTYPE_GARBAGE)
|
||||||
|
# Check for limiters
|
||||||
|
if limit_types is not False:
|
||||||
|
for limiter in limit_types:
|
||||||
|
dbref_matches.filter(type=limiter)
|
||||||
|
try:
|
||||||
|
return dbref_matches[0]
|
||||||
|
except IndexError:
|
||||||
|
return None
|
||||||
|
|
||||||
def global_object_name_search(self, ostring, exact_match=False):
|
def global_object_name_search(self, ostring, exact_match=False):
|
||||||
"""
|
"""
|
||||||
Searches through all objects for a name match.
|
Searches through all objects for a name match.
|
||||||
|
|
@ -90,61 +155,93 @@ class ObjectManager(models.Manager):
|
||||||
|
|
||||||
def list_search_object_namestr(self, searchlist, ostring, dbref_only=False,
|
def list_search_object_namestr(self, searchlist, ostring, dbref_only=False,
|
||||||
limit_types=False, match_type="fuzzy"):
|
limit_types=False, match_type="fuzzy"):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Iterates through a list of objects and returns a list of
|
Iterates through a list of objects and returns a list of
|
||||||
name matches.
|
name matches.
|
||||||
|
|
||||||
|
This version handles search criteria of the type keyword-N, this is used
|
||||||
|
to differentiate several objects of the exact same name, e.g. box-1, box-2 etc.
|
||||||
|
|
||||||
searchlist: (List of Objects) The objects to perform name comparisons on.
|
searchlist: (List of Objects) The objects to perform name comparisons on.
|
||||||
ostring: (string) The string to match against.
|
ostring: (string) The string to match against.
|
||||||
dbref_only: (bool) Only compare dbrefs.
|
dbref_only: (bool) Only compare dbrefs.
|
||||||
limit_types: (list of int) A list of Object type numbers to filter by.
|
limit_types: (list of int) A list of Object type numbers to filter by.
|
||||||
|
match_type: (string) 'exact' or 'fuzzy' matching.
|
||||||
|
|
||||||
Note that the fuzzy matching gives precedence to exact matches; so if your
|
Note that the fuzzy matching gives precedence to exact matches; so if your
|
||||||
search query matches an object in the list exactly, it will be the only result.
|
search query matches an object in the list exactly, it will be the only result.
|
||||||
This means that if the list contains [box,box11,box12], the search string 'box'
|
This means that if the list contains [box,box11,box12], the search string 'box'
|
||||||
will only match the first entry since it is exact. The search 'box1' will however
|
will only match the first entry since it is exact. The search 'box1' will however
|
||||||
match both box11 and box12 since neither is an exact match.
|
match both box11 and box12 since neither is an exact match.
|
||||||
|
|
||||||
|
Uses two helper functions, _list_search_helper1/2.
|
||||||
"""
|
"""
|
||||||
if dbref_only:
|
if dbref_only:
|
||||||
|
#search by dbref - these must always be unique.
|
||||||
if limit_types:
|
if limit_types:
|
||||||
return [prospect for prospect in searchlist if prospect.dbref_match(ostring)
|
return [prospect for prospect in searchlist
|
||||||
|
if prospect.dbref_match(ostring)
|
||||||
and prospect.type in limit_types]
|
and prospect.type in limit_types]
|
||||||
else:
|
else:
|
||||||
return [prospect for prospect in searchlist if prospect.dbref_match(ostring)]
|
return [prospect for prospect in searchlist
|
||||||
|
if prospect.dbref_match(ostring)]
|
||||||
|
|
||||||
|
#search by name - this may return multiple matches.
|
||||||
|
results = self._list_search_helper1(searchlist,ostring,dbref_only,
|
||||||
|
limit_types, match_type)
|
||||||
|
match_number = None
|
||||||
|
if not results:
|
||||||
|
#if we have no match, check if we are dealing
|
||||||
|
#with a "keyword-N" query - if so, strip it and run again.
|
||||||
|
match_number, ostring = self._list_search_helper2(ostring)
|
||||||
|
if match_number != None and ostring:
|
||||||
|
results = self._list_search_helper1(searchlist,ostring,dbref_only,
|
||||||
|
limit_types, match_type)
|
||||||
|
if match_type == "fuzzy":
|
||||||
|
#fuzzy matching; run second sweep to catch exact matches
|
||||||
|
exact_results = [prospect for prospect in results
|
||||||
|
if prospect.name_match(ostring, match_type="exact")]
|
||||||
|
if exact_results:
|
||||||
|
results = exact_results
|
||||||
|
if len(results) > 1 and match_number != None:
|
||||||
|
#select a particular match using the "keyword-N" markup.
|
||||||
|
try:
|
||||||
|
results = [results[match_number]]
|
||||||
|
except IndexError:
|
||||||
|
pass
|
||||||
|
return results
|
||||||
|
|
||||||
|
def _list_search_helper1(self,searchlist,ostring,dbref_only,
|
||||||
|
limit_types,match_type):
|
||||||
|
"""
|
||||||
|
Helper function for list_search_object_namestr -
|
||||||
|
does name matching through a list of objects.
|
||||||
|
"""
|
||||||
|
if limit_types:
|
||||||
|
return [prospect for prospect in searchlist
|
||||||
|
if prospect.name_match(ostring, match_type=match_type)
|
||||||
|
and prospect.type in limit_types]
|
||||||
else:
|
else:
|
||||||
if limit_types:
|
return [prospect for prospect in searchlist
|
||||||
results = [prospect for prospect in searchlist
|
if prospect.name_match(ostring, match_type=match_type)]
|
||||||
if prospect.name_match(ostring, match_type=match_type)
|
|
||||||
and prospect.type in limit_types]
|
|
||||||
else:
|
|
||||||
results = [prospect for prospect in searchlist
|
|
||||||
if prospect.name_match(ostring, match_type=match_type)]
|
|
||||||
|
|
||||||
if match_type == "exact":
|
def _list_search_helper2(self, ostring):
|
||||||
return results
|
|
||||||
else:
|
|
||||||
#fuzzy matching; run second sweep to catch exact matches
|
|
||||||
exact_results = [prospect for prospect in results
|
|
||||||
if prospect.name_match(ostring, match_type="exact")]
|
|
||||||
if exact_results:
|
|
||||||
return exact_results
|
|
||||||
else:
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
def object_totals(self):
|
|
||||||
"""
|
"""
|
||||||
Returns a dictionary with database object totals.
|
Hhelper function for list_search_object_namestr -
|
||||||
|
strips eventual keyword-N endings from a search criterion
|
||||||
"""
|
"""
|
||||||
dbtotals = {
|
if not '-' in ostring:
|
||||||
"objects": self.count(),
|
return False, ostring
|
||||||
"things": self.filter(type=defines_global.OTYPE_THING).count(),
|
try:
|
||||||
"exits": self.filter(type=defines_global.OTYPE_EXIT).count(),
|
il = ostring.find('-')
|
||||||
"rooms": self.filter(type=defines_global.OTYPE_ROOM).count(),
|
number = int(ostring[:il])-1
|
||||||
"garbage": self.filter(type=defines_global.OTYPE_GARBAGE).count(),
|
return number, ostring[il+1:]
|
||||||
"players": self.filter(type=defines_global.OTYPE_PLAYER).count(),
|
except ValueError:
|
||||||
}
|
#not a number; this is not an identifier.
|
||||||
return dbtotals
|
return None, ostring
|
||||||
|
except IndexError:
|
||||||
|
return None, ostring
|
||||||
|
|
||||||
def player_alias_search(self, searcher, ostring):
|
def player_alias_search(self, searcher, ostring):
|
||||||
"""
|
"""
|
||||||
|
|
@ -186,32 +283,6 @@ class ObjectManager(models.Manager):
|
||||||
except IndexError:
|
except IndexError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def is_dbref(self, dbstring, require_pound=True):
|
|
||||||
"""
|
|
||||||
Is the input a well-formed dbref number?
|
|
||||||
"""
|
|
||||||
return util_object.is_dbref(dbstring, require_pound=require_pound)
|
|
||||||
|
|
||||||
def dbref_search(self, dbref_string, limit_types=False):
|
|
||||||
"""
|
|
||||||
Searches for a given dbref.
|
|
||||||
|
|
||||||
dbref_number: (string) The dbref to search for. With # sign.
|
|
||||||
limit_types: (list of int) A list of Object type numbers to filter by.
|
|
||||||
"""
|
|
||||||
if not util_object.is_dbref(dbref_string):
|
|
||||||
return None
|
|
||||||
dbref_string = dbref_string[1:]
|
|
||||||
dbref_matches = self.filter(id=dbref_string).exclude(
|
|
||||||
type=defines_global.OTYPE_GARBAGE)
|
|
||||||
# Check for limiters
|
|
||||||
if limit_types is not False:
|
|
||||||
for limiter in limit_types:
|
|
||||||
dbref_matches.filter(type=limiter)
|
|
||||||
try:
|
|
||||||
return dbref_matches[0]
|
|
||||||
except IndexError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def local_and_global_search(self, searcher, ostring, search_contents=True,
|
def local_and_global_search(self, searcher, ostring, search_contents=True,
|
||||||
search_location=True, dbref_only=False,
|
search_location=True, dbref_only=False,
|
||||||
|
|
@ -250,32 +321,19 @@ class ObjectManager(models.Manager):
|
||||||
if player_match is not None:
|
if player_match is not None:
|
||||||
return [player_match]
|
return [player_match]
|
||||||
|
|
||||||
local_matches = []
|
|
||||||
# Handle our location/contents searches. list_search_object_namestr() does
|
# Handle our location/contents searches. list_search_object_namestr() does
|
||||||
# name and dbref comparisons against search_query.
|
# name and dbref comparisons against search_query.
|
||||||
|
local_objs = []
|
||||||
if search_contents:
|
if search_contents:
|
||||||
local_matches += self.list_search_object_namestr(searcher.get_contents(),
|
local_objs.extend(searcher.get_contents())
|
||||||
search_query, limit_types)
|
|
||||||
if search_location:
|
if search_location:
|
||||||
local_matches += \
|
local_objs.extend(searcher.get_location().get_contents())
|
||||||
self.list_search_object_namestr(searcher.get_location().get_contents(),
|
return self.list_search_object_namestr(local_objs, search_query,
|
||||||
search_query, limit_types=limit_types)
|
limit_types=limit_types)
|
||||||
return local_matches
|
|
||||||
|
|
||||||
def get_user_from_email(self, uemail):
|
#
|
||||||
"""
|
# ObjectManager Create methods
|
||||||
Returns a player's User object when given an email address.
|
#
|
||||||
"""
|
|
||||||
return User.objects.filter(email__iexact=uemail)
|
|
||||||
|
|
||||||
def get_object_from_dbref(self, dbref):
|
|
||||||
"""
|
|
||||||
Returns an object when given a dbref.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
return self.get(id=dbref)
|
|
||||||
except self.model.DoesNotExist:
|
|
||||||
raise ObjectNotExist(dbref)
|
|
||||||
|
|
||||||
def create_object(self, name, otype, location, owner, home=None):
|
def create_object(self, name, otype, location, owner, home=None):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -180,9 +180,9 @@ class Object(models.Model):
|
||||||
limit_types=limit_types)
|
limit_types=limit_types)
|
||||||
|
|
||||||
if len(results) > 1:
|
if len(results) > 1:
|
||||||
emit_to_obj.emit_to("More than one match found (please narrow target):")
|
emit_to_obj.emit_to("More than one match for '%s' (please narrow target):" % ostring)
|
||||||
for result in results:
|
for num, result in enumerate(results):
|
||||||
emit_to_obj.emit_to(" %s" % (result.get_name(),))
|
emit_to_obj.emit_to(" %i-%s" % (num+1,result.get_name(show_dbref=False)))
|
||||||
return False
|
return False
|
||||||
elif len(results) == 0:
|
elif len(results) == 0:
|
||||||
emit_to_obj.emit_to("I don't see that here.")
|
emit_to_obj.emit_to("I don't see that here.")
|
||||||
|
|
@ -952,9 +952,6 @@ class Object(models.Model):
|
||||||
|
|
||||||
NOTE: A 'name' can be a dbref or the actual name of the object. See
|
NOTE: A 'name' can be a dbref or the actual name of the object. See
|
||||||
dbref_match for an exclusively name-based match.
|
dbref_match for an exclusively name-based match.
|
||||||
|
|
||||||
The fuzzy match gives precedence to exact matches by raising the
|
|
||||||
UniqueMatch Exception.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if util_object.is_dbref(oname):
|
if util_object.is_dbref(oname):
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue