mirror of
https://github.com/evennia/evennia.git
synced 2026-03-25 09:16:32 +01:00
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):
|
||||
|
||||
#
|
||||
# ObjectManager Get methods
|
||||
#
|
||||
|
||||
def num_total_players(self):
|
||||
"""
|
||||
Returns the total number of registered players.
|
||||
|
|
@ -50,6 +54,35 @@ class ObjectManager(models.Manager):
|
|||
start_date = end_date - tdelta
|
||||
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):
|
||||
"""
|
||||
Figure out what our next free database reference number is.
|
||||
|
|
@ -68,6 +101,38 @@ class ObjectManager(models.Manager):
|
|||
# for our next free.
|
||||
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):
|
||||
"""
|
||||
Searches through all objects for a name match.
|
||||
|
|
@ -86,65 +151,97 @@ class ObjectManager(models.Manager):
|
|||
"""
|
||||
o_query = self.filter(script_parent__exact=script_parent)
|
||||
return o_query.exclude(type__in=[defines_global.OTYPE_GARBAGE,
|
||||
defines_global.OTYPE_GOING])
|
||||
|
||||
defines_global.OTYPE_GOING])
|
||||
|
||||
def list_search_object_namestr(self, searchlist, ostring, dbref_only=False,
|
||||
limit_types=False, match_type="fuzzy"):
|
||||
|
||||
"""
|
||||
Iterates through a list of objects and returns a list of
|
||||
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.
|
||||
ostring: (string) The string to match against.
|
||||
dbref_only: (bool) Only compare dbrefs.
|
||||
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
|
||||
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'
|
||||
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.
|
||||
|
||||
Uses two helper functions, _list_search_helper1/2.
|
||||
"""
|
||||
if dbref_only:
|
||||
#search by dbref - these must always be unique.
|
||||
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]
|
||||
else:
|
||||
return [prospect for prospect in searchlist if prospect.dbref_match(ostring)]
|
||||
else:
|
||||
if limit_types:
|
||||
results = [prospect for prospect in searchlist
|
||||
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":
|
||||
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
|
||||
|
||||
return [prospect for prospect in searchlist
|
||||
if prospect.dbref_match(ostring)]
|
||||
|
||||
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
|
||||
#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:
|
||||
return [prospect for prospect in searchlist
|
||||
if prospect.name_match(ostring, match_type=match_type)]
|
||||
|
||||
def _list_search_helper2(self, ostring):
|
||||
"""
|
||||
Hhelper function for list_search_object_namestr -
|
||||
strips eventual keyword-N endings from a search criterion
|
||||
"""
|
||||
if not '-' in ostring:
|
||||
return False, ostring
|
||||
try:
|
||||
il = ostring.find('-')
|
||||
number = int(ostring[:il])-1
|
||||
return number, ostring[il+1:]
|
||||
except ValueError:
|
||||
#not a number; this is not an identifier.
|
||||
return None, ostring
|
||||
except IndexError:
|
||||
return None, ostring
|
||||
|
||||
def player_alias_search(self, searcher, ostring):
|
||||
"""
|
||||
|
|
@ -186,32 +283,6 @@ class ObjectManager(models.Manager):
|
|||
except IndexError:
|
||||
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,
|
||||
search_location=True, dbref_only=False,
|
||||
|
|
@ -249,34 +320,21 @@ class ObjectManager(models.Manager):
|
|||
player_match = self.player_name_search(search_target)
|
||||
if player_match is not None:
|
||||
return [player_match]
|
||||
|
||||
local_matches = []
|
||||
|
||||
# Handle our location/contents searches. list_search_object_namestr() does
|
||||
# name and dbref comparisons against search_query.
|
||||
local_objs = []
|
||||
if search_contents:
|
||||
local_matches += self.list_search_object_namestr(searcher.get_contents(),
|
||||
search_query, limit_types)
|
||||
local_objs.extend(searcher.get_contents())
|
||||
if search_location:
|
||||
local_matches += \
|
||||
self.list_search_object_namestr(searcher.get_location().get_contents(),
|
||||
search_query, limit_types=limit_types)
|
||||
return local_matches
|
||||
|
||||
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)
|
||||
local_objs.extend(searcher.get_location().get_contents())
|
||||
return self.list_search_object_namestr(local_objs, search_query,
|
||||
limit_types=limit_types)
|
||||
|
||||
#
|
||||
# ObjectManager Create methods
|
||||
#
|
||||
|
||||
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):
|
||||
"""
|
||||
Create a new object
|
||||
|
|
|
|||
|
|
@ -180,9 +180,9 @@ class Object(models.Model):
|
|||
limit_types=limit_types)
|
||||
|
||||
if len(results) > 1:
|
||||
emit_to_obj.emit_to("More than one match found (please narrow target):")
|
||||
for result in results:
|
||||
emit_to_obj.emit_to(" %s" % (result.get_name(),))
|
||||
emit_to_obj.emit_to("More than one match for '%s' (please narrow target):" % ostring)
|
||||
for num, result in enumerate(results):
|
||||
emit_to_obj.emit_to(" %i-%s" % (num+1,result.get_name(show_dbref=False)))
|
||||
return False
|
||||
elif len(results) == 0:
|
||||
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
|
||||
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):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue